feat(secret-sync): rework GitLab secret-sync to add group variables

This commit is contained in:
carlosmonastyrski
2025-06-24 18:01:32 -03:00
parent 43e0d400f9
commit d4bf8a33dc
20 changed files with 504 additions and 460 deletions
@@ -68,7 +68,6 @@ export const GitLabSyncFields = () => {
const connectionId = useWatch({ name: "connection.id", control });
const scope = useWatch({ name: "destinationConfig.scope", control });
const selectedGroup = useWatch({ name: "destinationConfig.groupId", control });
const shouldMaskSecrets = useWatch({ name: "destinationConfig.shouldMaskSecrets", control });
const { data: groups, isLoading: isGroupsLoading } = useGitlabConnectionListGroups(connectionId, {
@@ -77,7 +76,6 @@ export const GitLabSyncFields = () => {
const { data: projects, isLoading: isProjectsLoading } = useGitlabConnectionListProjects(
connectionId,
selectedGroup,
{
enabled: Boolean(connectionId)
}
@@ -90,14 +88,15 @@ export const GitLabSyncFields = () => {
setValue("destinationConfig.projectId", "");
setValue("destinationConfig.projectName", "");
setValue("destinationConfig.groupId", "");
setValue("destinationConfig.scope", GitlabSyncScope.Individual);
setValue("destinationConfig.groupName", "");
setValue("destinationConfig.scope", GitlabSyncScope.Project);
}}
/>
<Controller
name="destinationConfig.scope"
control={control}
defaultValue={GitlabSyncScope.Individual}
defaultValue={GitlabSyncScope.Project}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Scope">
<Select
@@ -107,6 +106,7 @@ export const GitLabSyncFields = () => {
setValue("destinationConfig.projectId", "");
setValue("destinationConfig.projectName", "");
setValue("destinationConfig.groupId", "");
setValue("destinationConfig.groupName", "");
}}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
@@ -151,6 +151,10 @@ export const GitLabSyncFields = () => {
value={groups?.find((group) => group.id === value) ?? null}
onChange={(option) => {
onChange((option as SingleValue<TGitLabGroup>)?.id ?? "");
setValue(
"destinationConfig.groupName",
(option as SingleValue<TGitLabGroup>)?.name ?? ""
);
}}
options={groups}
placeholder="Select a group..."
@@ -162,46 +166,48 @@ export const GitLabSyncFields = () => {
/>
)}
<Controller
name="destinationConfig.projectId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="GitLab Project"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the project exists in the connection's GitLab instance URL and the connection has access to it."
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId || (scope === GitlabSyncScope.Group && !selectedGroup)}
value={projects?.find((project) => project.id === value) ?? null}
onChange={(option) => {
onChange((option as SingleValue<TGitLabProject>)?.id ?? "");
setValue(
"destinationConfig.projectName",
(option as SingleValue<TGitLabProject>)?.name ?? ""
);
}}
options={projects}
placeholder="Select a project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
{scope === GitlabSyncScope.Project && (
<Controller
name="destinationConfig.projectId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="GitLab Project"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the project exists in the connection's GitLab instance URL and the connection has access to it."
>
<div>
<span>Don&#39;t see the project you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isProjectsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={projects?.find((project) => project.id === value) ?? null}
onChange={(option) => {
onChange((option as SingleValue<TGitLabProject>)?.id ?? "");
setValue(
"destinationConfig.projectName",
(option as SingleValue<TGitLabProject>)?.name ?? ""
);
}}
options={projects}
placeholder="Select a project..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
)}
<Controller
control={control}
@@ -9,7 +9,7 @@ export const GitlabSyncDestinationSchema = BaseSecretSyncSchema().merge(
destination: z.literal(SecretSync.GitLab),
destinationConfig: z.discriminatedUnion("scope", [
z.object({
scope: z.literal(GitlabSyncScope.Individual),
scope: z.literal(GitlabSyncScope.Project),
projectId: z.string().trim().min(1, "Project ID required"),
projectName: z.string().trim().min(1, "Project name required"),
targetEnvironment: z.string().optional(),
@@ -19,10 +19,9 @@ export const GitlabSyncDestinationSchema = BaseSecretSyncSchema().merge(
}),
z.object({
scope: z.literal(GitlabSyncScope.Group),
projectId: z.string().trim().min(1, "Project ID required"),
projectName: z.string().trim().min(1, "Project name required"),
targetEnvironment: z.string().optional(),
groupId: z.string().trim().min(1, "Group ID required"),
groupName: z.string().trim().min(1, "Group name required"),
shouldProtectSecrets: z.boolean().optional().default(false),
shouldMaskSecrets: z.boolean().optional().default(false),
shouldHideSecrets: z.boolean().optional().default(false)
@@ -7,15 +7,14 @@ import { TGitLabGroup, TGitLabProject } from "./types";
const gitlabConnectionKeys = {
all: [...appConnectionKeys.all, "gitlab"] as const,
listProjects: (connectionId: string, group?: string) =>
[...gitlabConnectionKeys.all, "projects", connectionId, group] as const,
listProjects: (connectionId: string) =>
[...gitlabConnectionKeys.all, "projects", connectionId] as const,
listGroups: (connectionId: string) =>
[...gitlabConnectionKeys.all, "groups", connectionId] as const
};
export const useGitlabConnectionListProjects = (
connectionId: string,
group?: string,
options?: Omit<
UseQueryOptions<
TGitLabProject[],
@@ -27,10 +26,10 @@ export const useGitlabConnectionListProjects = (
>
) => {
return useQuery({
queryKey: gitlabConnectionKeys.listProjects(connectionId, group),
queryKey: gitlabConnectionKeys.listProjects(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TGitLabProject[]>(
`/api/v1/app-connections/gitlab/${connectionId}/projects${group ? `?group=${group}` : ""}`
`/api/v1/app-connections/gitlab/${connectionId}/projects`
);
return data;
@@ -3,7 +3,7 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export enum GitlabSyncScope {
Individual = "individual",
Project = "project",
Group = "group"
}
@@ -11,7 +11,7 @@ export type TGitlabSync = TRootSecretSync & {
destination: SecretSync.GitLab;
destinationConfig:
| {
scope: GitlabSyncScope.Individual;
scope: GitlabSyncScope.Project;
projectId: string;
projectName: string;
targetEnvironment?: string;
@@ -22,8 +22,7 @@ export type TGitlabSync = TRootSecretSync & {
| {
scope: GitlabSyncScope.Group;
groupId: string;
projectId: string;
projectName: string;
groupName: string;
targetEnvironment?: string;
shouldProtectSecrets?: boolean;
shouldMaskSecrets?: boolean;
@@ -145,7 +145,7 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
oauthUrl.searchParams.set("client_id", oauthClientId);
oauthUrl.searchParams.set(
"redirect_uri",
`${window.location.origin}/integrations/gitlab/oauth2/callback`
`${window.location.origin}/organization/app-connections/gitlab/oauth/callback`
);
oauthUrl.searchParams.set("response_type", "code");
oauthUrl.searchParams.set("state", state);
@@ -11,12 +11,14 @@ import {
AzureDevOpsConnectionMethod,
AzureKeyVaultConnectionMethod,
GitHubConnectionMethod,
GitlabConnectionMethod,
TAzureAppConfigurationConnection,
TAzureClientSecretsConnection,
TAzureDevOpsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
TGitlabConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
@@ -25,6 +27,7 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
type BaseFormData = {
returnUrl?: string;
connectionId?: string;
isUpdate?: boolean;
};
type GithubFormData = BaseFormData & Pick<TGitHubConnection, "name" | "method" | "description">;
@@ -32,6 +35,8 @@ type GithubFormData = BaseFormData & Pick<TGitHubConnection, "name" | "method" |
type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
type GitlabFormData = BaseFormData & Pick<TGitlabConnection, "name" | "method" | "description">;
type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
@@ -60,6 +65,7 @@ type AzureDevOpsFormData = BaseFormData &
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.Gitlab]: GitlabFormData & { app: AppConnection.Gitlab };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
@@ -75,6 +81,7 @@ type FormDataMap = {
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
[AppConnection.GitHubRadar]: "githubRadarConnectionFormData",
[AppConnection.Gitlab]: "gitlabConnectionFormData",
[AppConnection.AzureKeyVault]: "azureKeyVaultConnectionFormData",
[AppConnection.AzureAppConfiguration]: "azureAppConfigurationConnectionFormData",
[AppConnection.AzureClientSecrets]: "azureClientSecretsConnectionFormData",
@@ -133,6 +140,57 @@ export const OAuthCallbackPage = () => {
}
};
const handleGitlab = useCallback(async () => {
const formData = getFormData(AppConnection.Gitlab);
if (formData === null) return null;
clearState(AppConnection.Gitlab);
const { connectionId, name, description, returnUrl, isUpdate } = formData;
try {
if (isUpdate && connectionId) {
await updateAppConnection.mutateAsync({
app: AppConnection.Gitlab,
connectionId,
credentials: {
code: code as string
}
});
} else {
await createAppConnection.mutateAsync({
app: AppConnection.Gitlab,
name,
description,
method: GitlabConnectionMethod.OAuth,
credentials: {
code: code as string
}
});
}
navigate({
to: returnUrl ?? "/organization/app-connections"
});
return {
connectionId,
returnUrl,
appConnectionName: formData.app
};
} catch (err: any) {
createNotification({
title: `Failed to ${connectionId ? "update" : "add"} GitLab Connection`,
text: err?.message,
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
});
return null;
}
}, []);
const handleAzureKeyVault = useCallback(async () => {
const formData = getFormData(AppConnection.AzureKeyVault);
if (formData === null) return null;
@@ -458,11 +516,14 @@ export const OAuthCallbackPage = () => {
(async () => {
let data: { connectionId?: string; returnUrl?: string; appConnectionName?: string } | null =
null;
console.log("appConnection", appConnection);
if (appConnection === AppConnection.GitHub) {
data = await handleGithub();
} else if (appConnection === AppConnection.GitHubRadar) {
data = await handleGithubRadar();
} else if (appConnection === AppConnection.Gitlab) {
data = await handleGitlab();
} else if (appConnection === AppConnection.AzureKeyVault) {
data = await handleAzureKeyVault();
} else if (appConnection === AppConnection.AzureAppConfiguration) {
@@ -5,6 +5,7 @@ import {
GitHubSyncScope,
GitHubSyncVisibility
} from "@app/hooks/api/secretSyncs/types/github-sync";
import { GitlabSyncScope } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
import { HumanitecSyncScope } from "@app/hooks/api/secretSyncs/types/humanitec-sync";
// This functional ensures parity across what is displayed in the destination column
@@ -129,8 +130,15 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
secondaryText = "App ID";
break;
case SecretSync.GitLab:
primaryText = destinationConfig.projectName;
secondaryText = destinationConfig.projectId;
if (destinationConfig.scope === GitlabSyncScope.Project) {
primaryText = destinationConfig.projectName;
secondaryText = destinationConfig.projectId;
} else if (destinationConfig.scope === GitlabSyncScope.Group) {
primaryText = destinationConfig.groupName;
secondaryText = destinationConfig.groupId;
} else {
throw new Error(`Unhandled GitLab Scope Destination Col Values ${destination}`);
}
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
@@ -1,5 +1,5 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TGitlabSync } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
import { GitlabSyncScope, TGitlabSync } from "@app/hooks/api/secretSyncs/types/gitlab-sync";
type Props = {
secretSync: TGitlabSync;
@@ -8,8 +8,6 @@ type Props = {
export const GitLabSyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: {
projectName,
projectId,
targetEnvironment,
shouldProtectSecrets,
shouldMaskSecrets,
@@ -19,8 +17,26 @@ export const GitLabSyncDestinationSection = ({ secretSync }: Props) => {
return (
<>
<GenericFieldLabel label="Project Name">{projectName}</GenericFieldLabel>
<GenericFieldLabel label="Project ID">{projectId}</GenericFieldLabel>
{secretSync.destinationConfig.scope === GitlabSyncScope.Project && (
<>
<GenericFieldLabel label="Project Name">
{secretSync.destinationConfig.projectName}
</GenericFieldLabel>
<GenericFieldLabel label="Project ID">
{secretSync.destinationConfig.projectId}
</GenericFieldLabel>
</>
)}
{secretSync.destinationConfig.scope === GitlabSyncScope.Group && (
<>
<GenericFieldLabel label="Group Name">
{secretSync.destinationConfig.groupName}
</GenericFieldLabel>
<GenericFieldLabel label="Group ID">
{secretSync.destinationConfig.groupId}
</GenericFieldLabel>
</>
)}
{targetEnvironment && (
<GenericFieldLabel label="Environment">{targetEnvironment}</GenericFieldLabel>
)}