feature: secret sync base + aws parameter store & github

This commit is contained in:
Scott Wilson
2025-01-15 21:52:50 -08:00
parent b440e918ac
commit 473efa91f0
214 changed files with 10212 additions and 623 deletions
@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";
@@ -0,0 +1,105 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "@app/hooks/api/appConnections";
import {
TGitHubConnectionEnvironment,
TGitHubConnectionListEnvironmentsResponse,
TGitHubConnectionListOrganizationsResponse,
TGitHubConnectionListRepositoriesResponse,
TGitHubConnectionOrganization,
TGitHubConnectionRepository,
TListGitHubConnectionEnvironments
} from "./types";
const githubConnectionKeys = {
all: [...appConnectionKeys.all, "github"] as const,
listRepositories: (connectionId: string) =>
[...githubConnectionKeys.all, "repositories", connectionId] as const,
listOrganizations: (connectionId: string) =>
[...githubConnectionKeys.all, "organizations", connectionId] as const,
listEnvironments: ({ connectionId, repo, owner }: TListGitHubConnectionEnvironments) =>
[...githubConnectionKeys.all, "environments", connectionId, repo, owner] as const
};
export const useGitHubConnectionListRepositories = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TGitHubConnectionRepository[],
unknown,
TGitHubConnectionRepository[],
ReturnType<typeof githubConnectionKeys.listRepositories>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: githubConnectionKeys.listRepositories(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGitHubConnectionListRepositoriesResponse>(
`/api/v1/app-connections/github/${connectionId}/repositories`
);
return data.repositories;
},
...options
});
};
export const useGitHubConnectionListOrganizations = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TGitHubConnectionOrganization[],
unknown,
TGitHubConnectionOrganization[],
ReturnType<typeof githubConnectionKeys.listOrganizations>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: githubConnectionKeys.listOrganizations(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGitHubConnectionListOrganizationsResponse>(
`/api/v1/app-connections/github/${connectionId}/organizations`
);
return data.organizations;
},
...options
});
};
export const useGitHubConnectionListEnvironments = (
{ connectionId, repo, owner }: TListGitHubConnectionEnvironments,
options?: Omit<
UseQueryOptions<
TGitHubConnectionEnvironment[],
unknown,
TGitHubConnectionEnvironment[],
ReturnType<typeof githubConnectionKeys.listEnvironments>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: githubConnectionKeys.listEnvironments({ connectionId, repo, owner }),
queryFn: async () => {
const { data } = await apiRequest.get<TGitHubConnectionListEnvironmentsResponse>(
`/api/v1/app-connections/github/${connectionId}/environments`,
{
params: {
repo,
owner
}
}
);
return data.environments;
},
...options
});
};
@@ -0,0 +1,33 @@
export type TGitHubConnectionOrganization = {
login: string;
id: number;
};
export type TGitHubConnectionRepository = {
id: number;
name: string;
owner: TGitHubConnectionOrganization;
};
export type TGitHubConnectionEnvironment = {
id: number;
name: string;
};
export type TGitHubConnectionListRepositoriesResponse = {
repositories: TGitHubConnectionRepository[];
};
export type TGitHubConnectionListOrganizationsResponse = {
organizations: TGitHubConnectionOrganization[];
};
export type TGitHubConnectionListEnvironmentsResponse = {
environments: TGitHubConnectionEnvironment[];
};
export type TListGitHubConnectionEnvironments = {
connectionId: string;
repo: string;
owner: string;
};
@@ -7,6 +7,8 @@ import {
TAppConnection,
TAppConnectionMap,
TAppConnectionOptions,
TAvailableAppConnection,
TAvailableAppConnectionsResponse,
TGetAppConnection,
TListAppConnections
} from "@app/hooks/api/appConnections/types";
@@ -19,9 +21,10 @@ export const appConnectionKeys = {
all: ["app-connection"] as const,
options: () => [...appConnectionKeys.all, "options"] as const,
list: () => [...appConnectionKeys.all, "list"] as const,
listAvailable: (app: AppConnection) => [...appConnectionKeys.all, app, "list-available"] as const,
listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app],
byId: (app: AppConnection, templateId: string) =>
[...appConnectionKeys.all, app, "by-id", templateId] as const
byId: (app: AppConnection, connectionId: string) =>
[...appConnectionKeys.all, app, "by-id", connectionId] as const
};
export const useAppConnectionOptions = (
@@ -83,6 +86,31 @@ export const useListAppConnections = (
});
};
export const useListAvailableAppConnections = (
app: AppConnection,
options?: Omit<
UseQueryOptions<
TAvailableAppConnection[],
unknown,
TAvailableAppConnection[],
ReturnType<typeof appConnectionKeys.listAvailable>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: appConnectionKeys.listAvailable(app),
queryFn: async () => {
const { data } = await apiRequest.get<TAvailableAppConnectionsResponse>(
`/api/v1/app-connections/${app}/available`
);
return data.appConnections;
},
...options
});
};
export const useListAppConnectionsByApp = <T extends AppConnection>(
app: T,
options?: Omit<
@@ -8,10 +8,13 @@ export * from "./github-connection";
export type TAppConnection = TAwsConnection | TGitHubConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "app" | "id">;
export type TListAppConnections<T extends TAppConnection> = { appConnections: T[] };
export type TGetAppConnection<T extends TAppConnection> = { appConnection: T };
export type TAppConnectionOptions = { appConnectionOptions: TAppConnectionOption[] };
export type TAppConnectionResponse = { appConnection: TAppConnection };
export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppConnection[] };
export type TCreateAppConnectionDTO = Pick<
TAppConnection,
+30 -1
View File
@@ -85,7 +85,36 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.INTEGRATION_SYNCED]: "Integration sync",
[EventType.CREATE_SHARED_SECRET]: "Create shared secret",
[EventType.DELETE_SHARED_SECRET]: "Delete shared secret",
[EventType.READ_SHARED_SECRET]: "Read shared secret"
[EventType.READ_SHARED_SECRET]: "Read shared secret",
[EventType.CREATE_CMEK]: "Create KMS key",
[EventType.UPDATE_CMEK]: "Update KMS key",
[EventType.DELETE_CMEK]: "Delete KMS key",
[EventType.GET_CMEKS]: "List KMS keys",
[EventType.CMEK_ENCRYPT]: "Encrypt with KMS key",
[EventType.CMEK_DECRYPT]: "Decrypt with KMS key",
[EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]:
"Update SSO group to organization role mapping",
[EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS]: "List SSO group to organization role mapping",
[EventType.GET_PROJECT_TEMPLATES]: "List project templates",
[EventType.GET_PROJECT_TEMPLATE]: "Get project template",
[EventType.CREATE_PROJECT_TEMPLATE]: "Create project template",
[EventType.UPDATE_PROJECT_TEMPLATE]: "Update project template",
[EventType.DELETE_PROJECT_TEMPLATE]: "Delete project template",
[EventType.APPLY_PROJECT_TEMPLATE]: "Apply project template",
[EventType.GET_APP_CONNECTIONS]: "List App Connections",
[EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS]: "List App Connections Details",
[EventType.GET_APP_CONNECTION]: "Get App Connection",
[EventType.CREATE_APP_CONNECTION]: "Create App Connection",
[EventType.UPDATE_APP_CONNECTION]: "Update App Connection",
[EventType.DELETE_APP_CONNECTION]: "Delete App Connection",
[EventType.GET_SECRET_SYNCS]: "List Secret Syncs",
[EventType.GET_SECRET_SYNC]: "Get Secret Sync",
[EventType.CREATE_SECRET_SYNC]: "Create Secret Sync",
[EventType.UPDATE_SECRET_SYNC]: "Update Secret Sync",
[EventType.DELETE_SECRET_SYNC]: "Delete Secret Sync",
[EventType.SECRET_SYNC_SYNC_SECRETS]: "Secret Sync synced secrets",
[EventType.SECRET_SYNC_IMPORT_SECRETS]: "Secret Sync imported secrets",
[EventType.SECRET_SYNC_REMOVE_SECRETS]: "Secret Sync removed secrets"
};
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
+29 -1
View File
@@ -99,5 +99,33 @@ export enum EventType {
INTEGRATION_SYNCED = "integration-synced",
CREATE_SHARED_SECRET = "create-shared-secret",
DELETE_SHARED_SECRET = "delete-shared-secret",
READ_SHARED_SECRET = "read-shared-secret"
READ_SHARED_SECRET = "read-shared-secret",
CREATE_CMEK = "create-cmek",
UPDATE_CMEK = "update-cmek",
DELETE_CMEK = "delete-cmek",
GET_CMEKS = "get-cmeks",
CMEK_ENCRYPT = "cmek-encrypt",
CMEK_DECRYPT = "cmek-decrypt",
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping",
GET_PROJECT_TEMPLATES = "get-project-templates",
GET_PROJECT_TEMPLATE = "get-project-template",
CREATE_PROJECT_TEMPLATE = "create-project-template",
UPDATE_PROJECT_TEMPLATE = "update-project-template",
DELETE_PROJECT_TEMPLATE = "delete-project-template",
APPLY_PROJECT_TEMPLATE = "apply-project-template",
GET_APP_CONNECTIONS = "get-app-connections",
GET_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details",
GET_APP_CONNECTION = "get-app-connection",
CREATE_APP_CONNECTION = "create-app-connection",
UPDATE_APP_CONNECTION = "update-app-connection",
DELETE_APP_CONNECTION = "delete-app-connection",
GET_SECRET_SYNCS = "get-secret-syncs",
GET_SECRET_SYNC = "get-secret-sync",
CREATE_SECRET_SYNC = "create-secret-sync",
UPDATE_SECRET_SYNC = "update-secret-sync",
DELETE_SECRET_SYNC = "delete-secret-sync",
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets"
}
+1 -1
View File
@@ -182,7 +182,7 @@ export const queryClient = new QueryClient({
createNotification({
title: "Bad Request",
type: "error",
text: `${serverResponse.message}.`,
text: `${serverResponse.message}${serverResponse.message.endsWith(".") ? "" : "."}`,
copyActions: [
{
value: serverResponse.reqId,
@@ -0,0 +1,22 @@
export enum SecretSync {
AWSParameterStore = "aws-parameter-store",
GitHub = "github"
}
export enum SecretSyncStatus {
Pending = "pending",
Running = "running",
Succeeded = "succeeded",
Failed = "failed"
}
export enum SecretSyncInitialSyncBehavior {
OverwriteDestination = "overwrite-destination",
ImportPrioritizeSource = "import-prioritize-source",
ImportPrioritizeDestination = "import-prioritize-destination"
}
export enum SecretSyncImportBehavior {
PrioritizeSource = "prioritize-source",
PrioritizeDestination = "prioritize-destination"
}
@@ -0,0 +1,4 @@
export * from "./enums";
export * from "./mutations";
export * from "./queries";
export * from "./types";
@@ -0,0 +1,116 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { secretSyncKeys } from "@app/hooks/api/secretSyncs/queries";
import {
TCreateSecretSyncDTO,
TDeleteSecretSyncDTO,
TSecretSyncResponse,
TTriggerSecretSyncImportSecretsDTO,
TTriggerSecretSyncRemoveSecretsDTO,
TTriggerSecretSyncSyncSecretsDTO,
TUpdateSecretSyncDTO
} from "@app/hooks/api/secretSyncs/types";
export const useCreateSecretSync = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ destination, ...params }: TCreateSecretSyncDTO) => {
const { data } = await apiRequest.post<TSecretSyncResponse>(
`/api/v1/secret-syncs/${destination}`,
params
);
return data.secretSync;
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() })
});
};
export const useUpdateSecretSync = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, destination, ...params }: TUpdateSecretSyncDTO) => {
const { data } = await apiRequest.patch<TSecretSyncResponse>(
`/api/v1/secret-syncs/${destination}/${syncId}`,
params
);
return data.secretSync;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
};
export const useDeleteSecretSync = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, destination }: TDeleteSecretSyncDTO) => {
const { data } = await apiRequest.delete(`/api/v1/secret-syncs/${destination}/${syncId}`);
return data;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
};
export const useTriggerSecretSyncSyncSecrets = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, destination }: TTriggerSecretSyncSyncSecretsDTO) => {
const { data } = await apiRequest.post(
`/api/v1/secret-syncs/${destination}/${syncId}/sync-secrets`
);
return data;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
};
export const useTriggerSecretSyncImportSecrets = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
syncId,
destination,
importBehavior
}: TTriggerSecretSyncImportSecretsDTO) => {
const { data } = await apiRequest.post(
`/api/v1/secret-syncs/${destination}/${syncId}/import-secrets?importBehavior=${importBehavior}`
);
return data;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
};
export const useTriggerSecretSyncRemoveSecrets = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ syncId, destination }: TTriggerSecretSyncRemoveSecretsDTO) => {
const { data } = await apiRequest.post(
`/api/v1/secret-syncs/${destination}/${syncId}/remove-secrets`
);
return data;
},
onSuccess: (_, { syncId, destination }) => {
queryClient.invalidateQueries({ queryKey: secretSyncKeys.list() });
queryClient.invalidateQueries({ queryKey: secretSyncKeys.byId(destination, syncId) });
}
});
};
@@ -0,0 +1,88 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { SecretSync, TSecretSyncOption } from "@app/hooks/api/secretSyncs";
import {
TListSecretSyncOptions,
TListSecretSyncs,
TSecretSync,
TSecretSyncResponse
} from "@app/hooks/api/secretSyncs/types";
export const secretSyncKeys = {
all: ["secret-sync"] as const,
options: () => [...secretSyncKeys.all, "options"] as const,
list: () => [...secretSyncKeys.all, "list"] as const,
byId: (destination: SecretSync, syncId: string) =>
[...secretSyncKeys.all, destination, "by-id", syncId] as const
};
export const useSecretSyncOptions = (
options?: Omit<
UseQueryOptions<
TSecretSyncOption[],
unknown,
TSecretSyncOption[],
ReturnType<typeof secretSyncKeys.options>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretSyncKeys.options(),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretSyncOptions>("/api/v1/secret-syncs/options");
return data.secretSyncOptions;
},
...options
});
};
export const useSecretSyncOption = (destination: SecretSync) => {
const { data: syncOptions, isPending } = useSecretSyncOptions();
const syncOption = syncOptions?.find((option) => option.destination === destination);
return { syncOption, isPending };
};
export const useListSecretSyncs = (
projectId: string,
options?: Omit<
UseQueryOptions<TSecretSync[], unknown, TSecretSync[], ReturnType<typeof secretSyncKeys.list>>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretSyncKeys.list(),
queryFn: async () => {
const { data } = await apiRequest.get<TListSecretSyncs>("/api/v1/secret-syncs", {
params: { projectId }
});
return data.secretSyncs;
},
...options
});
};
export const useGetSecretSync = (
destination: SecretSync,
syncId: string,
options?: Omit<
UseQueryOptions<TSecretSync, unknown, TSecretSync, ReturnType<typeof secretSyncKeys.byId>>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: secretSyncKeys.byId(destination, syncId),
queryFn: async () => {
const { data } = await apiRequest.get<TSecretSyncResponse>(
`/api/v1/secret-syncs/${destination}/${syncId}`
);
return data.secretSync;
},
...options
});
};
@@ -0,0 +1,16 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TAwsParameterStoreSync = TRootSecretSync & {
destination: SecretSync.AWSParameterStore;
destinationConfig: {
path: string;
region: string;
};
connection: {
app: AppConnection.AWS;
name: string;
id: string;
};
};
@@ -0,0 +1,42 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export enum GitHubSyncScope {
Organization = "organization",
Repository = "repository",
RepositoryEnvironment = "repository-environment"
}
export enum GitHubSyncVisibility {
All = "all",
Private = "private",
Selected = "selected"
}
export type TGitHubSync = TRootSecretSync & {
destination: SecretSync.GitHub;
destinationConfig:
| {
scope: GitHubSyncScope.Organization;
org: string;
visibility: GitHubSyncVisibility;
selectedRepositoryIds?: number[];
}
| {
scope: GitHubSyncScope.Repository;
owner: string;
repo: string;
}
| {
scope: GitHubSyncScope.RepositoryEnvironment;
owner: string;
repo: string;
env: string;
};
connection: {
app: AppConnection.GitHub;
name: string;
id: string;
};
};
@@ -0,0 +1,56 @@
import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs";
import { TAwsParameterStoreSync } from "@app/hooks/api/secretSyncs/types/aws-parameter-store-sync";
import { TGitHubSync } from "@app/hooks/api/secretSyncs/types/github-sync";
import { DiscriminativePick } from "@app/types";
export type TSecretSyncOption = {
name: string;
destination: SecretSync;
canImportSecrets: boolean;
};
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
export type TListSecretSyncOptions = { secretSyncOptions: TSecretSyncOption[] };
export type TSecretSyncResponse = { secretSync: TSecretSync };
export type TCreateSecretSyncDTO = DiscriminativePick<
TSecretSync,
| "name"
| "destinationConfig"
| "description"
| "connectionId"
| "syncOptions"
| "destination"
| "isEnabled"
> & { environment: string; secretPath: string; projectId: string };
export type TUpdateSecretSyncDTO = Partial<
Omit<TCreateSecretSyncDTO, "connectionId" | "destination" | "projectId">
> & {
destination: SecretSync;
syncId: string;
};
export type TDeleteSecretSyncDTO = {
destination: SecretSync;
syncId: string;
};
export type TTriggerSecretSyncSyncSecretsDTO = {
destination: SecretSync;
syncId: string;
};
export type TTriggerSecretSyncImportSecretsDTO = {
destination: SecretSync;
syncId: string;
importBehavior: SecretSyncImportBehavior;
};
export type TTriggerSecretSyncRemoveSecretsDTO = {
destination: SecretSync;
syncId: string;
};
@@ -0,0 +1,46 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/secretSyncs";
export type TRootSecretSync = {
id: string;
name: string;
description?: string | null;
version: number;
folderId: string;
connectionId: string;
createdAt: string;
updatedAt: string;
isEnabled: boolean;
projectId: string;
syncStatus: SecretSyncStatus | null;
lastSyncJobId: string | null;
lastSyncedAt: Date | null;
lastSyncMessage: string | null;
importStatus: SecretSyncStatus | null;
lastImportJobId: string | null;
lastImportedAt: Date | null;
lastImportMessage: string | null;
removeStatus: SecretSyncStatus | null;
lastRemoveJobId: string | null;
lastRemovedAt: Date | null;
lastRemoveMessage: string | null;
syncOptions: {
initialSyncBehavior: SecretSyncInitialSyncBehavior;
prependPrefix?: string;
appendSuffix?: string;
};
connection: {
app: AppConnection;
id: string;
name: string;
};
environment: {
id: string;
name: string;
slug: string;
};
folder: {
id: string;
path: string;
};
};
@@ -46,5 +46,4 @@ export type SubscriptionPlan = {
pkiEst: boolean;
enforceMfa: boolean;
projectTemplates: boolean;
appConnections: boolean;
};