From 9a622f60fe7a73cdccaf0cc626aa715109e3ba68 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 30 Jul 2025 15:55:40 +0200 Subject: [PATCH] feat(providers): add GitHub provider support with credential types (#8405) --- ui/CHANGELOG.md | 2 +- .../add-credentials/page.tsx | 43 +++++----- .../update-credentials/page.tsx | 42 +++++----- .../providers-badge/github-provider-badge.tsx | 29 +++++++ ui/components/icons/providers-badge/index.ts | 1 + .../providers/credentials-update-info.tsx | 4 + .../providers/radio-group-provider.tsx | 7 ++ .../workflow/forms/base-credentials-form.tsx | 41 +++++---- .../workflow/forms/connect-account-form.tsx | 18 +++- .../credentials-type/github-app-form.tsx | 47 +++++++++++ .../github-oauth-app-form.tsx | 34 ++++++++ .../github-personal-access-token-form.tsx | 40 +++++++++ .../github/credentials-type/index.ts | 3 + .../select-credentials-type/github/index.ts | 3 + ...group-github-via-credentials-type-form.tsx | 84 +++++++++++++++++++ .../github/select-via-github.tsx | 38 +++++++++ .../github-credentials-form.tsx | 30 +++++++ .../workflow/forms/via-credentials/index.ts | 1 + .../ui/entities/get-provider-logo.tsx | 5 ++ ui/hooks/use-credentials-form.ts | 26 ++++++ ui/lib/external-urls.ts | 5 ++ .../build-crendentials.ts | 54 ++++++++++++ .../provider-credential-fields.ts | 11 +++ ui/lib/provider-helpers.ts | 65 ++++++++++++++ ui/types/formSchemas.ts | 67 ++++++++++++++- ui/types/providers.ts | 11 ++- 26 files changed, 638 insertions(+), 73 deletions(-) create mode 100644 ui/components/icons/providers-badge/github-provider-badge.tsx create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-app-form.tsx create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-oauth-app-form.tsx create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-personal-access-token-form.tsx create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/index.ts create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/github/index.ts create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/github/select-via-github.tsx create mode 100644 ui/components/providers/workflow/forms/via-credentials/github-credentials-form.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index bd8bf042e5..96c26cad86 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Lighthouse banner [(#8259)](https://github.com/prowler-cloud/prowler/pull/8259) - Integration with Amazon S3, enabling storage and retrieval of scan data via S3 buckets [(#8056)](https://github.com/prowler-cloud/prowler/pull/8056) - +- Github provider support [(#8405)](https://github.com/prowler-cloud/prowler/pull/8405) ___ ## [v1.9.3] (Prowler v5.9.3) diff --git a/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx index b867e919d0..723854ade3 100644 --- a/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx +++ b/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -9,6 +9,8 @@ import { AddViaServiceAccountForm, SelectViaGCP, } from "@/components/providers/workflow/forms/select-credentials-type/gcp"; +import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github"; +import { getProviderFormType } from "@/lib/provider-helpers"; import { ProviderType } from "@/types/providers"; interface Props { @@ -16,30 +18,27 @@ interface Props { } export default function AddCredentialsPage({ searchParams }: Props) { - return ( - <> - {searchParams.type === "aws" && !searchParams.via && ( - - )} + const { type: providerType, via } = searchParams; + const formType = getProviderFormType(providerType, via); - {searchParams.type === "gcp" && !searchParams.via && ( - - )} + switch (formType) { + case "selector": + if (providerType === "aws") return ; + if (providerType === "gcp") return ; + if (providerType === "github") + return ; + return null; - {((searchParams.type === "aws" && searchParams.via === "credentials") || - (searchParams.type === "gcp" && searchParams.via === "credentials") || - (searchParams.type !== "aws" && searchParams.type !== "gcp")) && ( - - )} + case "credentials": + return ; - {searchParams.type === "aws" && searchParams.via === "role" && ( - - )} + case "role": + return ; - {searchParams.type === "gcp" && - searchParams.via === "service-account" && ( - - )} - - ); + case "service-account": + return ; + + default: + return null; + } } diff --git a/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx index 185a107244..84e15108a9 100644 --- a/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx +++ b/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx @@ -6,6 +6,7 @@ import { UpdateViaRoleForm, } from "@/components/providers/workflow/forms"; import { UpdateViaServiceAccountForm } from "@/components/providers/workflow/forms/update-via-service-account-key-form"; +import { getProviderFormType } from "@/lib/provider-helpers"; import { ProviderType } from "@/types/providers"; interface Props { @@ -18,30 +19,25 @@ interface Props { } export default function UpdateCredentialsPage({ searchParams }: Props) { - return ( - <> - {(searchParams.type === "aws" || searchParams.type === "gcp") && - !searchParams.via && ( - - )} + const { type: providerType, via } = searchParams; + const formType = getProviderFormType(providerType, via); - {((searchParams.type === "aws" && searchParams.via === "credentials") || - (searchParams.type === "gcp" && searchParams.via === "credentials") || - (searchParams.type !== "aws" && searchParams.type !== "gcp")) && ( - - )} + switch (formType) { + case "selector": + return ( + + ); - {searchParams.type === "aws" && searchParams.via === "role" && ( - - )} + case "credentials": + return ; - {searchParams.type === "gcp" && - searchParams.via === "service-account" && ( - - )} - - ); + case "role": + return ; + + case "service-account": + return ; + + default: + return null; + } } diff --git a/ui/components/icons/providers-badge/github-provider-badge.tsx b/ui/components/icons/providers-badge/github-provider-badge.tsx new file mode 100644 index 0000000000..667785de03 --- /dev/null +++ b/ui/components/icons/providers-badge/github-provider-badge.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; + +import { IconSvgProps } from "@/types"; + +export const GitHubProviderBadge: React.FC = ({ + size, + width, + height, + ...props +}) => ( + +); diff --git a/ui/components/icons/providers-badge/index.ts b/ui/components/icons/providers-badge/index.ts index 86641d82ad..07b45d3cb0 100644 --- a/ui/components/icons/providers-badge/index.ts +++ b/ui/components/icons/providers-badge/index.ts @@ -1,5 +1,6 @@ export * from "./aws-provider-badge"; export * from "./azure-provider-badge"; export * from "./gcp-provider-badge"; +export * from "./github-provider-badge"; export * from "./ks8-provider-badge"; export * from "./m365-provider-badge"; diff --git a/ui/components/providers/credentials-update-info.tsx b/ui/components/providers/credentials-update-info.tsx index 2715a1ea6b..0fe75f0cb2 100644 --- a/ui/components/providers/credentials-update-info.tsx +++ b/ui/components/providers/credentials-update-info.tsx @@ -2,6 +2,7 @@ import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws"; import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp"; +import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github"; import { ProviderType } from "@/types/providers"; interface UpdateCredentialsInfoProps { @@ -20,6 +21,9 @@ export const CredentialsUpdateInfo = ({ if (providerType === "gcp") { return ; } + if (providerType === "github") { + return ; + } return null; }; diff --git a/ui/components/providers/radio-group-provider.tsx b/ui/components/providers/radio-group-provider.tsx index ec77a148db..fa5021a8ad 100644 --- a/ui/components/providers/radio-group-provider.tsx +++ b/ui/components/providers/radio-group-provider.tsx @@ -11,6 +11,7 @@ import { AWSProviderBadge, AzureProviderBadge, GCPProviderBadge, + GitHubProviderBadge, KS8ProviderBadge, M365ProviderBadge, } from "../icons/providers-badge"; @@ -71,6 +72,12 @@ export const RadioGroupProvider: React.FC = ({ Kubernetes + +
+ + GitHub +
+
{errorMessage && ( diff --git a/ui/components/providers/workflow/forms/base-credentials-form.tsx b/ui/components/providers/workflow/forms/base-credentials-form.tsx index 39ab8ff6b3..3762532594 100644 --- a/ui/components/providers/workflow/forms/base-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/base-credentials-form.tsx @@ -8,6 +8,7 @@ import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { useCredentialsForm } from "@/hooks/use-credentials-form"; import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { requiresBackButton } from "@/lib/provider-helpers"; import { AWSCredentials, AWSCredentialsRole, @@ -25,6 +26,7 @@ import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credential import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type"; import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form"; import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form"; +import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form"; import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form"; import { M365CredentialsForm } from "./via-credentials/m365-credentials-form"; @@ -121,26 +123,29 @@ export const BaseCredentialsForm = ({ control={form.control as unknown as Control} /> )} + {providerType === "github" && ( + + )}
- {showBackButton && - (searchParamsObj.get("via") === "credentials" || - searchParamsObj.get("via") === "role" || - searchParamsObj.get("via") === "service-account") && ( - } - isDisabled={isLoading} - > - Back - - )} + {showBackButton && requiresBackButton(searchParamsObj.get("via")) && ( + } + isDisabled={isLoading} + > + Back + + )} ; // Helper function for labels and placeholders -const getProviderFieldDetails = (providerType?: string) => { +const getProviderFieldDetails = (providerType?: ProviderType) => { switch (providerType) { case "aws": return { @@ -45,6 +46,11 @@ const getProviderFieldDetails = (providerType?: string) => { label: "Domain ID", placeholder: "e.g. your-domain.onmicrosoft.com", }; + case "github": + return { + label: "Username", + placeholder: "e.g. your-github-username", + }; default: return { label: "Provider UID", @@ -142,6 +148,10 @@ export const ConnectAccountForm = () => { const handleBackStep = () => { setPrevStep((prev) => prev - 1); + //Deselect the providerType if the user is going back to the first step + if (prevStep === 2) { + form.setValue("providerType", undefined as unknown as ProviderType); + } // Reset the providerUid and providerAlias fields when going back form.setValue("providerUid", ""); form.setValue("providerAlias", ""); diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-app-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-app-form.tsx new file mode 100644 index 0000000000..1d9f77f1bf --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-app-form.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { Control } from "react-hook-form"; + +import { CustomInput, CustomTextarea } from "@/components/ui/custom"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; + +export const GitHubAppForm = ({ control }: { control: Control }) => { + return ( + <> +
+
+ Connect via GitHub App +
+
+ Please provide your GitHub App ID and private key. +
+
+ + + + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-oauth-app-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-oauth-app-form.tsx new file mode 100644 index 0000000000..115f46bebf --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-oauth-app-form.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { Control } from "react-hook-form"; + +import { CustomInput } from "@/components/ui/custom"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; + +export const GitHubOAuthAppForm = ({ control }: { control: Control }) => { + return ( + <> +
+
+ Connect via OAuth App +
+
+ Please provide your GitHub OAuth App token. +
+
+ + + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-personal-access-token-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-personal-access-token-form.tsx new file mode 100644 index 0000000000..460ae47d81 --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-personal-access-token-form.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Control } from "react-hook-form"; + +import { CustomInput } from "@/components/ui/custom"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; + +export const GitHubPersonalAccessTokenForm = ({ + control, +}: { + control: Control; +}) => { + return ( + <> +
+
+ Connect via Personal Access Token +
+
+ Please provide your GitHub personal access token. +
+
+ + + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/index.ts b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/index.ts new file mode 100644 index 0000000000..e359315250 --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/index.ts @@ -0,0 +1,3 @@ +export * from "./github-app-form"; +export * from "./github-oauth-app-form"; +export * from "./github-personal-access-token-form"; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/index.ts b/ui/components/providers/workflow/forms/select-credentials-type/github/index.ts new file mode 100644 index 0000000000..3377bb1fd6 --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/github/index.ts @@ -0,0 +1,3 @@ +export * from "./credentials-type"; +export * from "./radio-group-github-via-credentials-type-form"; +export * from "./select-via-github"; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx new file mode 100644 index 0000000000..0b24da9bf0 --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { RadioGroup } from "@nextui-org/react"; +import React from "react"; +import { Control, Controller } from "react-hook-form"; + +import { CustomRadio } from "@/components/ui/custom"; +import { FormMessage } from "@/components/ui/form"; + +type RadioGroupGitHubViaCredentialsFormProps = { + control: Control; + isInvalid: boolean; + errorMessage?: string; + onChange?: (value: string) => void; +}; + +export const RadioGroupGitHubViaCredentialsTypeForm = ({ + control, + isInvalid, + errorMessage, + onChange, +}: RadioGroupGitHubViaCredentialsFormProps) => { + return ( + ( + <> + { + field.onChange(value); + if (onChange) { + onChange(value); + } + }} + > +
+ + Personal Access Token + + +
+ Personal Access Token +
+
+ + OAuth App + +
+ OAuth App Token +
+
+ + GitHub App + +
+ GitHub App +
+
+
+
+ {errorMessage && ( + + {errorMessage} + + )} + + )} + /> + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/select-via-github.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/select-via-github.tsx new file mode 100644 index 0000000000..9fe491f4ca --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/github/select-via-github.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; + +import { Form } from "@/components/ui/form"; + +import { RadioGroupGitHubViaCredentialsTypeForm } from "./radio-group-github-via-credentials-type-form"; + +interface SelectViaGitHubProps { + initialVia?: string; +} + +export const SelectViaGitHub = ({ initialVia }: SelectViaGitHubProps) => { + const router = useRouter(); + const form = useForm({ + defaultValues: { + githubCredentialsType: initialVia || "", + }, + }); + + const handleSelectionChange = (value: string) => { + const url = new URL(window.location.href); + url.searchParams.set("via", value); + router.push(url.toString()); + }; + + return ( +
+ + + ); +}; diff --git a/ui/components/providers/workflow/forms/via-credentials/github-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/github-credentials-form.tsx new file mode 100644 index 0000000000..94d3443cb7 --- /dev/null +++ b/ui/components/providers/workflow/forms/via-credentials/github-credentials-form.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { Control } from "react-hook-form"; + +import { + GitHubAppForm, + GitHubOAuthAppForm, + GitHubPersonalAccessTokenForm, +} from "../select-credentials-type/github"; + +interface GitHubCredentialsFormProps { + control: Control; + credentialsType?: string; +} + +export const GitHubCredentialsForm = ({ + control, + credentialsType, +}: GitHubCredentialsFormProps) => { + switch (credentialsType) { + case "personal_access_token": + return ; + case "oauth_app": + return ; + case "github_app": + return ; + default: + return null; + } +}; diff --git a/ui/components/providers/workflow/forms/via-credentials/index.ts b/ui/components/providers/workflow/forms/via-credentials/index.ts index 9058fc7362..d020b9715f 100644 --- a/ui/components/providers/workflow/forms/via-credentials/index.ts +++ b/ui/components/providers/workflow/forms/via-credentials/index.ts @@ -1,3 +1,4 @@ export * from "./azure-credentials-form"; +export * from "./github-credentials-form"; export * from "./k8s-credentials-form"; export * from "./m365-credentials-form"; diff --git a/ui/components/ui/entities/get-provider-logo.tsx b/ui/components/ui/entities/get-provider-logo.tsx index 17ab559988..047652ffdc 100644 --- a/ui/components/ui/entities/get-provider-logo.tsx +++ b/ui/components/ui/entities/get-provider-logo.tsx @@ -4,6 +4,7 @@ import { AWSProviderBadge, AzureProviderBadge, GCPProviderBadge, + GitHubProviderBadge, KS8ProviderBadge, M365ProviderBadge, } from "@/components/icons/providers-badge"; @@ -21,6 +22,8 @@ export const getProviderLogo = (provider: ProviderType) => { return ; case "m365": return ; + case "github": + return ; default: return null; } @@ -38,6 +41,8 @@ export const getProviderName = (provider: ProviderType): string => { return "Kubernetes"; case "m365": return "Microsoft 365"; + case "github": + return "GitHub"; default: return "Unknown Provider"; } diff --git a/ui/hooks/use-credentials-form.ts b/ui/hooks/use-credentials-form.ts index df6ffa9517..317d1749ae 100644 --- a/ui/hooks/use-credentials-form.ts +++ b/ui/hooks/use-credentials-form.ts @@ -46,6 +46,10 @@ export const useCredentialsForm = ({ if (providerType === "gcp" && via === "service-account") { return addCredentialsServiceAccountFormSchema(providerType); } + // For GitHub, we need to pass the via parameter to determine which fields are required + if (providerType === "github") { + return addCredentialsFormSchema(providerType, via); + } return addCredentialsFormSchema(providerType); }; @@ -117,6 +121,28 @@ export const useCredentialsForm = ({ ...baseDefaults, [ProviderCredentialFields.KUBECONFIG_CONTENT]: "", }; + case "github": + // GitHub credentials based on via parameter + if (via === "personal_access_token") { + return { + ...baseDefaults, + [ProviderCredentialFields.PERSONAL_ACCESS_TOKEN]: "", + }; + } + if (via === "oauth_app") { + return { + ...baseDefaults, + [ProviderCredentialFields.OAUTH_APP_TOKEN]: "", + }; + } + if (via === "github_app") { + return { + ...baseDefaults, + [ProviderCredentialFields.GITHUB_APP_ID]: "", + [ProviderCredentialFields.GITHUB_APP_KEY]: "", + }; + } + return baseDefaults; default: return baseDefaults; } diff --git a/ui/lib/external-urls.ts b/ui/lib/external-urls.ts index cea7a694ae..1e8210a116 100644 --- a/ui/lib/external-urls.ts +++ b/ui/lib/external-urls.ts @@ -25,6 +25,11 @@ export const getProviderHelpText = (provider: string) => { text: "Need help connecting your Kubernetes cluster?", link: "https://goto.prowler.com/provider-k8s", }; + case "github": + return { + text: "Need help connecting your GitHub account?", + link: "https://goto.prowler.com/provider-github", + }; default: return { text: "How to setup a provider?", diff --git a/ui/lib/provider-credentials/build-crendentials.ts b/ui/lib/provider-credentials/build-crendentials.ts index 791a209c8c..0e2c96f990 100644 --- a/ui/lib/provider-credentials/build-crendentials.ts +++ b/ui/lib/provider-credentials/build-crendentials.ts @@ -147,6 +147,56 @@ export const buildKubernetesSecret = (formData: FormData) => { return filterEmptyValues(secret); }; +export const buildGitHubSecret = (formData: FormData) => { + // Check which authentication method is being used + const hasPersonalToken = + formData.get(ProviderCredentialFields.PERSONAL_ACCESS_TOKEN) !== null && + formData.get(ProviderCredentialFields.PERSONAL_ACCESS_TOKEN) !== ""; + const hasOAuthToken = + formData.get(ProviderCredentialFields.OAUTH_APP_TOKEN) !== null && + formData.get(ProviderCredentialFields.OAUTH_APP_TOKEN) !== ""; + const hasGitHubApp = + formData.get(ProviderCredentialFields.GITHUB_APP_ID) !== null && + formData.get(ProviderCredentialFields.GITHUB_APP_ID) !== ""; + + if (hasPersonalToken) { + const secret = { + [ProviderCredentialFields.PERSONAL_ACCESS_TOKEN]: getFormValue( + formData, + ProviderCredentialFields.PERSONAL_ACCESS_TOKEN, + ), + }; + return filterEmptyValues(secret); + } + + if (hasOAuthToken) { + const secret = { + [ProviderCredentialFields.OAUTH_APP_TOKEN]: getFormValue( + formData, + ProviderCredentialFields.OAUTH_APP_TOKEN, + ), + }; + return filterEmptyValues(secret); + } + + if (hasGitHubApp) { + const secret = { + [ProviderCredentialFields.GITHUB_APP_ID]: getFormValue( + formData, + ProviderCredentialFields.GITHUB_APP_ID, + ), + [ProviderCredentialFields.GITHUB_APP_KEY]: getFormValue( + formData, + ProviderCredentialFields.GITHUB_APP_KEY, + ), + }; + return filterEmptyValues(secret); + } + + // If no credentials are provided, return empty object + return {}; +}; + // Main function to build secret configuration export const buildSecretConfig = ( formData: FormData, @@ -177,6 +227,10 @@ export const buildSecretConfig = ( secretType: "static", secret: buildKubernetesSecret(formData), }), + github: () => ({ + secretType: "static", + secret: buildGitHubSecret(formData), + }), }; const builder = secretBuilders[providerType]; diff --git a/ui/lib/provider-credentials/provider-credential-fields.ts b/ui/lib/provider-credentials/provider-credential-fields.ts index 5cbd6605b9..32dc184292 100644 --- a/ui/lib/provider-credentials/provider-credential-fields.ts +++ b/ui/lib/provider-credentials/provider-credential-fields.ts @@ -8,6 +8,7 @@ export const ProviderCredentialFields = { CREDENTIALS_TYPE: "credentials_type", CREDENTIALS_TYPE_AWS: "aws-sdk-default", CREDENTIALS_TYPE_ACCESS_SECRET_KEY: "access-secret-key", + // Base fields for all providers PROVIDER_ID: "providerId", PROVIDER_TYPE: "providerType", @@ -35,6 +36,12 @@ export const ProviderCredentialFields = { // Kubernetes fields KUBECONFIG_CONTENT: "kubeconfig_content", + + // GitHub fields + PERSONAL_ACCESS_TOKEN: "personal_access_token", + OAUTH_APP_TOKEN: "oauth_app_token", + GITHUB_APP_ID: "github_app_id", + GITHUB_APP_KEY: "github_app_key_content", } as const; // Type for credential field values @@ -59,6 +66,10 @@ export const ErrorPointers = { SESSION_DURATION: "/data/attributes/secret/session_duration", ROLE_SESSION_NAME: "/data/attributes/secret/role_session_name", SERVICE_ACCOUNT_KEY: "/data/attributes/secret/service_account_key", + PERSONAL_ACCESS_TOKEN: "/data/attributes/secret/personal_access_token", + OAUTH_APP_TOKEN: "/data/attributes/secret/oauth_app_token", + GITHUB_APP_ID: "/data/attributes/secret/github_app_id", + GITHUB_APP_KEY: "/data/attributes/secret/github_app_key_content", } as const; export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers]; diff --git a/ui/lib/provider-helpers.ts b/ui/lib/provider-helpers.ts index d1247c3eb4..becd9bc4f2 100644 --- a/ui/lib/provider-helpers.ts +++ b/ui/lib/provider-helpers.ts @@ -2,6 +2,7 @@ import { ProviderEntity, ProviderProps, ProvidersApiResponse, + ProviderType, } from "@/types/providers"; export const extractProviderUIDs = ( @@ -38,3 +39,67 @@ export const createProviderDetailsMapping = ( }; }); }; + +// Helper function to determine which form type to show +export type ProviderFormType = + | "selector" + | "credentials" + | "role" + | "service-account" + | null; + +export const getProviderFormType = ( + providerType: ProviderType, + via?: string, +): ProviderFormType => { + // Providers that need credential type selection + const needsSelector = ["aws", "gcp", "github"].includes(providerType); + + // Show selector if no via parameter and provider needs it + if (needsSelector && !via) { + return "selector"; + } + + // AWS specific forms + if (providerType === "aws") { + if (via === "role") return "role"; + if (via === "credentials") return "credentials"; + } + + // GCP specific forms + if (providerType === "gcp") { + if (via === "service-account") return "service-account"; + if (via === "credentials") return "credentials"; + } + + // GitHub credential types + if ( + providerType === "github" && + ["personal_access_token", "oauth_app", "github_app"].includes(via || "") + ) { + return "credentials"; + } + + // Other providers go directly to credentials form + if (!needsSelector) { + return "credentials"; + } + + return null; +}; + +// Helper to check if back button should be shown based on via parameter +export const requiresBackButton = (via?: string | null): boolean => { + if (!via) return false; + + const validViaTypes = [ + "credentials", + "role", + "service-account", + "personal_access_token", + "oauth_app", + "github_app", + ]; + + return validViaTypes.includes(via); +}; diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 2211a7a2cb..b1deadf489 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; import { validateMutelistYaml, validateYaml } from "@/lib/yaml"; -import { ProviderType } from "./providers"; +import { PROVIDER_TYPES, ProviderType } from "./providers"; export const addRoleFormSchema = z.object({ name: z.string().min(1, "Name is required"), @@ -71,7 +71,7 @@ export const awsCredentialsTypeSchema = z.object({ export const addProviderFormSchema = z .object({ - providerType: z.enum(["aws", "azure", "gcp", "kubernetes", "m365"], { + providerType: z.enum(PROVIDER_TYPES, { required_error: "Please select a provider type", }), }) @@ -105,10 +105,18 @@ export const addProviderFormSchema = z providerUid: z.string(), awsCredentialsType: z.string().optional(), }), + z.object({ + providerType: z.literal("github"), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), + providerUid: z.string(), + }), ]), ); -export const addCredentialsFormSchema = (providerType: string) => +export const addCredentialsFormSchema = ( + providerType: ProviderType, + via?: string | null, +) => z .object({ [ProviderCredentialFields.PROVIDER_ID]: z.string(), @@ -167,7 +175,22 @@ export const addCredentialsFormSchema = (providerType: string) => [ProviderCredentialFields.USER]: z.string().optional(), [ProviderCredentialFields.PASSWORD]: z.string().optional(), } - : {}), + : providerType === "github" + ? { + [ProviderCredentialFields.PERSONAL_ACCESS_TOKEN]: z + .string() + .optional(), + [ProviderCredentialFields.OAUTH_APP_TOKEN]: z + .string() + .optional(), + [ProviderCredentialFields.GITHUB_APP_ID]: z + .string() + .optional(), + [ProviderCredentialFields.GITHUB_APP_KEY]: z + .string() + .optional(), + } + : {}), }) .superRefine((data: Record, ctx) => { if (providerType === "m365") { @@ -190,6 +213,42 @@ export const addCredentialsFormSchema = (providerType: string) => }); } } + + if (providerType === "github") { + // For GitHub, validation depends on the 'via' parameter + if (via === "personal_access_token") { + if (!data[ProviderCredentialFields.PERSONAL_ACCESS_TOKEN]) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Personal Access Token is required", + path: [ProviderCredentialFields.PERSONAL_ACCESS_TOKEN], + }); + } + } else if (via === "oauth_app") { + if (!data[ProviderCredentialFields.OAUTH_APP_TOKEN]) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "OAuth App Token is required", + path: [ProviderCredentialFields.OAUTH_APP_TOKEN], + }); + } + } else if (via === "github_app") { + if (!data[ProviderCredentialFields.GITHUB_APP_ID]) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "GitHub App ID is required", + path: [ProviderCredentialFields.GITHUB_APP_ID], + }); + } + if (!data[ProviderCredentialFields.GITHUB_APP_KEY]) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "GitHub App Private Key is required", + path: [ProviderCredentialFields.GITHUB_APP_KEY], + }); + } + } + } }); export const addCredentialsRoleFormSchema = (providerType: string) => diff --git a/ui/types/providers.ts b/ui/types/providers.ts index b7aae5b944..0967d3304e 100644 --- a/ui/types/providers.ts +++ b/ui/types/providers.ts @@ -1,4 +1,13 @@ -export type ProviderType = "aws" | "azure" | "m365" | "gcp" | "kubernetes"; +export const PROVIDER_TYPES = [ + "aws", + "azure", + "gcp", + "kubernetes", + "m365", + "github", +] as const; + +export type ProviderType = (typeof PROVIDER_TYPES)[number]; export interface ProviderProps { id: string;