feat(m365): add provider credential forms and update credentials handling

This commit is contained in:
Alan Buscaglia
2025-10-17 14:35:11 +02:00
parent df856e30d9
commit 81ec745af8
6 changed files with 357 additions and 200 deletions

View File

@@ -4,44 +4,44 @@ import {
AddViaCredentialsForm,
AddViaRoleForm,
} from "@/components/providers/workflow/forms";
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
import { AddViaServiceAccountForm } from "@/components/providers/workflow/forms/select-credentials-type/gcp";
import {
AddViaServiceAccountForm,
SelectViaGCP,
} from "@/components/providers/workflow/forms/select-credentials-type/gcp";
import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github";
import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365";
import { getProviderFormType } from "@/lib/provider-helpers";
getProviderFormType,
getSelectorComponentKey,
PROVIDER_SELECTOR_COMPONENTS,
} from "@/lib/provider-helpers";
import { ProviderType } from "@/types/providers";
interface Props {
searchParams: Promise<{ type: ProviderType; id: string; via?: string }>;
}
// Form type components mapping
const FORM_COMPONENTS = {
credentials: AddViaCredentialsForm,
role: AddViaRoleForm,
"service-account": AddViaServiceAccountForm,
} as const;
type FormType = keyof typeof FORM_COMPONENTS;
export default async function AddCredentialsPage({ searchParams }: Props) {
const resolvedSearchParams = await searchParams;
const { type: providerType, via } = resolvedSearchParams;
const formType = getProviderFormType(providerType, via);
switch (formType) {
case "selector":
if (providerType === "aws") return <SelectViaAWS initialVia={via} />;
if (providerType === "gcp") return <SelectViaGCP initialVia={via} />;
if (providerType === "github")
return <SelectViaGitHub initialVia={via} />;
if (providerType === "m365") return <SelectViaM365 initialVia={via} />;
return null;
// Handle selector form type
if (formType === "selector") {
const componentKey = getSelectorComponentKey(providerType);
if (!componentKey) return null;
case "credentials":
return <AddViaCredentialsForm searchParams={resolvedSearchParams} />;
case "role":
return <AddViaRoleForm searchParams={resolvedSearchParams} />;
case "service-account":
return <AddViaServiceAccountForm searchParams={resolvedSearchParams} />;
default:
return null;
const SelectorComponent = PROVIDER_SELECTOR_COMPONENTS[componentKey];
return <SelectorComponent initialVia={via} />;
}
// Handle other form types
const FormComponent = FORM_COMPONENTS[formType as FormType];
if (!FormComponent) return null;
return <FormComponent searchParams={resolvedSearchParams} />;
}

View File

@@ -1,9 +1,9 @@
"use client";
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 { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365";
import {
getSelectorComponentKey,
PROVIDER_SELECTOR_COMPONENTS,
} from "@/lib/provider-helpers";
import { ProviderType } from "@/types/providers";
interface UpdateCredentialsInfoProps {
@@ -15,21 +15,15 @@ export const CredentialsUpdateInfo = ({
providerType,
initialVia,
}: UpdateCredentialsInfoProps) => {
const renderSelectComponent = () => {
if (providerType === "aws") {
return <SelectViaAWS initialVia={initialVia} />;
}
if (providerType === "gcp") {
return <SelectViaGCP initialVia={initialVia} />;
}
if (providerType === "github") {
return <SelectViaGitHub initialVia={initialVia} />;
}
if (providerType === "m365") {
return <SelectViaM365 initialVia={initialVia} />;
}
return null;
};
const componentKey = getSelectorComponentKey(providerType);
return <div className="flex flex-col gap-4">{renderSelectComponent()}</div>;
if (!componentKey) return null;
const SelectorComponent = PROVIDER_SELECTOR_COMPONENTS[componentKey];
return (
<div className="flex flex-col gap-4">
<SelectorComponent initialVia={initialVia} />
</div>
);
};

View File

@@ -8,32 +8,12 @@ import { CustomButton } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { useCredentialsForm } from "@/hooks/use-credentials-form";
import { getAWSCredentialsTemplateLinks } from "@/lib";
import { getCredentialFormComponent } from "@/lib/provider-credential-forms";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { requiresBackButton } from "@/lib/provider-helpers";
import {
AWSCredentials,
AWSCredentialsRole,
AzureCredentials,
GCPDefaultCredentials,
GCPServiceAccountKey,
KubernetesCredentials,
M365CertificateCredentials,
M365ClientSecretCredentials,
ProviderType,
} from "@/types";
import { AWSCredentialsRole, ProviderType } from "@/types";
import { ProviderTitleDocs } from "../provider-title-docs";
import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credentials-type";
import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type/aws-role-credentials-form";
import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type";
import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
import {
M365CertificateCredentialsForm,
M365ClientSecretCredentialsForm,
} from "./select-credentials-type/m365";
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form";
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
type BaseCredentialsFormProps = {
providerType: ProviderType;
@@ -67,6 +47,12 @@ export const BaseCredentialsForm = ({
});
const templateLinks = getAWSCredentialsTemplateLinks(externalId);
const credentialFormInfo = getCredentialFormComponent(
providerType,
searchParamsObj.get("via"),
);
if (!credentialFormInfo) return null;
return (
<Form {...form}>
@@ -89,65 +75,27 @@ export const BaseCredentialsForm = ({
<Divider />
{providerType === "aws" && searchParamsObj.get("via") === "role" && (
<AWSRoleCredentialsForm
control={form.control as unknown as Control<AWSCredentialsRole>}
setValue={form.setValue as any}
externalId={externalId}
templateLinks={templateLinks}
/>
)}
{providerType === "aws" && searchParamsObj.get("via") !== "role" && (
<AWSStaticCredentialsForm
control={form.control as unknown as Control<AWSCredentials>}
/>
)}
{providerType === "azure" && (
<AzureCredentialsForm
control={form.control as unknown as Control<AzureCredentials>}
/>
)}
{providerType === "m365" &&
searchParamsObj.get("via") === "app_client_secret" && (
<M365ClientSecretCredentialsForm
control={
form.control as unknown as Control<M365ClientSecretCredentials>
}
{credentialFormInfo.requiresExtendedProps &&
credentialFormInfo.passesCredentialsType === false &&
providerType === "aws" && (
<credentialFormInfo.component
control={form.control as unknown as Control<AWSCredentialsRole>}
setValue={form.setValue as any}
externalId={externalId}
templateLinks={templateLinks}
/>
)}
{providerType === "m365" &&
searchParamsObj.get("via") === "app_certificate" && (
<M365CertificateCredentialsForm
control={
form.control as unknown as Control<M365CertificateCredentials>
}
{!credentialFormInfo.requiresExtendedProps &&
credentialFormInfo.passesCredentialsType === true && (
<credentialFormInfo.component
control={form.control as unknown as Control}
credentialsType={searchParamsObj.get("via") || undefined}
/>
)}
{providerType === "gcp" &&
searchParamsObj.get("via") === "service-account" && (
<GCPServiceAccountKeyForm
control={form.control as unknown as Control<GCPServiceAccountKey>}
/>
{!credentialFormInfo.requiresExtendedProps &&
credentialFormInfo.passesCredentialsType === false && (
<credentialFormInfo.component control={form.control as any} />
)}
{providerType === "gcp" &&
searchParamsObj.get("via") !== "service-account" && (
<GCPDefaultCredentialsForm
control={
form.control as unknown as Control<GCPDefaultCredentials>
}
/>
)}
{providerType === "kubernetes" && (
<KubernetesCredentialsForm
control={form.control as unknown as Control<KubernetesCredentials>}
/>
)}
{providerType === "github" && (
<GitHubCredentialsForm
control={form.control}
credentialsType={searchParamsObj.get("via") || undefined}
/>
)}
<div className="flex w-full justify-end sm:gap-6">
{showBackButton && requiresBackButton(searchParamsObj.get("via")) && (

View File

@@ -0,0 +1,154 @@
"use client";
import { AWSStaticCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type";
import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form";
import { GCPDefaultCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type";
import { GCPServiceAccountKeyForm } from "@/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
import {
M365CertificateCredentialsForm,
M365ClientSecretCredentialsForm,
} from "@/components/providers/workflow/forms/select-credentials-type/m365";
import { AzureCredentialsForm } from "@/components/providers/workflow/forms/via-credentials/azure-credentials-form";
import { GitHubCredentialsForm } from "@/components/providers/workflow/forms/via-credentials/github-credentials-form";
import { KubernetesCredentialsForm } from "@/components/providers/workflow/forms/via-credentials/k8s-credentials-form";
import { ProviderType } from "@/types/providers";
// Type definitions for different credential form configurations
export type CredentialFormConfigExtended = {
component: typeof AWSRoleCredentialsForm;
requiresExtendedProps: true;
passesCredentialsType: false;
};
export type CredentialFormConfigPassesCredentialsType = {
component: typeof GitHubCredentialsForm;
requiresExtendedProps: false;
passesCredentialsType: true;
};
export type CredentialFormConfigBasic = {
component:
| typeof AWSStaticCredentialsForm
| typeof GCPServiceAccountKeyForm
| typeof GCPDefaultCredentialsForm
| typeof M365ClientSecretCredentialsForm
| typeof M365CertificateCredentialsForm
| typeof AzureCredentialsForm
| typeof KubernetesCredentialsForm;
requiresExtendedProps: false;
passesCredentialsType: false;
};
export type CredentialFormConfig =
| CredentialFormConfigExtended
| CredentialFormConfigPassesCredentialsType
| CredentialFormConfigBasic;
// Provider credential form components mapping
const PROVIDER_CREDENTIAL_FORMS = {
aws: {
role: AWSRoleCredentialsForm,
credentials: AWSStaticCredentialsForm,
},
gcp: {
"service-account": GCPServiceAccountKeyForm,
credentials: GCPDefaultCredentialsForm,
},
m365: {
app_client_secret: M365ClientSecretCredentialsForm,
app_certificate: M365CertificateCredentialsForm,
},
azure: {
default: AzureCredentialsForm,
},
kubernetes: {
default: KubernetesCredentialsForm,
},
github: {
default: GitHubCredentialsForm,
},
} as const;
// Strategy dictionary: maps (provider + via) to credential form config
type ProviderViaKey =
| "aws:role"
| "aws"
| "gcp:service-account"
| "gcp"
| "m365:app_client_secret"
| "m365:app_certificate"
| "github"
| "azure"
| "kubernetes";
const PROVIDER_VIA_STRATEGIES = {
// AWS strategies
"aws:role": () => ({
component: PROVIDER_CREDENTIAL_FORMS.aws.role,
requiresExtendedProps: true,
passesCredentialsType: false,
}),
aws: () => ({
component: PROVIDER_CREDENTIAL_FORMS.aws.credentials,
requiresExtendedProps: false,
passesCredentialsType: false,
}),
// GCP strategies
"gcp:service-account": () => ({
component: PROVIDER_CREDENTIAL_FORMS.gcp["service-account"],
requiresExtendedProps: false,
passesCredentialsType: false,
}),
gcp: () => ({
component: PROVIDER_CREDENTIAL_FORMS.gcp.credentials,
requiresExtendedProps: false,
passesCredentialsType: false,
}),
// M365 strategies
"m365:app_client_secret": () => ({
component: PROVIDER_CREDENTIAL_FORMS.m365.app_client_secret,
requiresExtendedProps: false,
passesCredentialsType: false,
}),
"m365:app_certificate": () => ({
component: PROVIDER_CREDENTIAL_FORMS.m365.app_certificate,
requiresExtendedProps: false,
passesCredentialsType: false,
}),
// GitHub strategy
github: () => ({
component: PROVIDER_CREDENTIAL_FORMS.github.default,
requiresExtendedProps: false,
passesCredentialsType: true,
}),
// Azure strategy
azure: () => ({
component: PROVIDER_CREDENTIAL_FORMS.azure.default,
requiresExtendedProps: false,
passesCredentialsType: false,
}),
// Kubernetes strategy
kubernetes: () => ({
component: PROVIDER_CREDENTIAL_FORMS.kubernetes.default,
requiresExtendedProps: false,
passesCredentialsType: false,
}),
} satisfies Record<ProviderViaKey, () => CredentialFormConfig>;
// Helper to get credential form component based on provider and via parameter
export const getCredentialFormComponent = (
provider: ProviderType,
via?: string | null,
): CredentialFormConfig | null => {
// Build strategy key: use "provider:via" if via exists, otherwise just "provider"
const strategyKey = via ? `${provider}:${via}` : provider;
const strategy = PROVIDER_VIA_STRATEGIES[strategyKey as ProviderViaKey];
return strategy ? strategy() : null;
};

View File

@@ -1,3 +1,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 { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365";
import {
ProviderEntity,
ProviderProps,
@@ -113,3 +117,28 @@ export const requiresBackButton = (via?: string | null): boolean => {
return validViaTypes.includes(via);
};
// Provider selector components mapping
export const PROVIDER_SELECTOR_COMPONENTS = {
AWS: SelectViaAWS,
GCP: SelectViaGCP,
GITHUB: SelectViaGitHub,
M365: SelectViaM365,
} as const;
export type SelectorProvider = keyof typeof PROVIDER_SELECTOR_COMPONENTS;
// Helper to map ProviderType to SelectorProvider key
export const getSelectorComponentKey = (
provider: ProviderType,
): SelectorProvider | null => {
const keyMap: Record<ProviderType, SelectorProvider | null> = {
aws: "AWS",
azure: null,
gcp: "GCP",
github: "GITHUB",
kubernetes: null,
m365: "M365",
};
return keyMap[provider] ?? null;
};

View File

@@ -9,53 +9,67 @@ export const PROVIDER_TYPES = [
export type ProviderType = (typeof PROVIDER_TYPES)[number];
interface ProviderConnection {
connected: boolean;
last_checked_at: string;
}
interface ScannerArgs {
only_logs: boolean;
excluded_checks: string[];
aws_retries_max_attempts: number;
}
interface CreatedBy {
object: string;
id: string;
}
interface ProviderAttributes {
provider: ProviderType;
uid: string;
alias: string;
status: "completed" | "pending" | "cancelled";
resources: number;
connection: ProviderConnection;
scanner_args: ScannerArgs;
inserted_at: string;
updated_at: string;
created_by: CreatedBy;
}
interface ApiEntity {
type: string;
id: string;
}
interface RelationshipData {
data: ApiEntity | null;
}
interface ProviderGroupMeta {
count: number;
}
interface ProviderGroupRelationship {
meta: ProviderGroupMeta;
data: ApiEntity[];
}
interface ProviderRelationships {
secret: RelationshipData;
provider_groups: ProviderGroupRelationship;
}
export interface ProviderProps {
id: string;
type: "providers";
attributes: {
provider: ProviderType;
uid: string;
alias: string;
status: "completed" | "pending" | "cancelled";
resources: number;
connection: {
connected: boolean;
last_checked_at: string;
};
scanner_args: {
only_logs: boolean;
excluded_checks: string[];
aws_retries_max_attempts: number;
};
inserted_at: string;
updated_at: string;
created_by: {
object: string;
id: string;
};
};
relationships: {
secret: {
data: {
type: string;
id: string;
} | null;
};
provider_groups: {
meta: {
count: number;
};
data: Array<{
type: string;
id: string;
}>;
};
};
attributes: ProviderAttributes;
relationships: ProviderRelationships;
groupNames?: string[];
}
export interface ProviderEntity {
provider: ProviderType;
uid: string;
alias: string | null;
}
@@ -65,47 +79,65 @@ export interface ProviderConnectionStatus {
value: string;
}
interface ProviderFinding {
pass: number;
fail: number;
manual: number;
total: number;
}
interface ProviderResources {
total: number;
}
interface ProviderOverviewAttributes {
findings: ProviderFinding;
resources: ProviderResources;
}
interface ProviderOverviewData {
type: "provider-overviews";
id: ProviderType;
attributes: ProviderOverviewAttributes;
}
interface ResponseMeta {
version: string;
}
export interface ProviderOverviewProps {
data: {
type: "provider-overviews";
id: ProviderType;
attributes: {
findings: {
pass: number;
fail: number;
manual: number;
total: number;
};
resources: {
total: number;
};
};
}[];
meta: {
version: string;
};
data: ProviderOverviewData[];
meta: ResponseMeta;
}
interface PaginationMeta {
page: number;
pages: number;
count: number;
}
interface ApiResponseMeta {
pagination: PaginationMeta;
version: string;
}
interface ApiLinks {
first: string;
last: string;
next: string | null;
prev: string | null;
}
interface IncludedEntity {
type: string;
id: string;
attributes?: any;
relationships?: any;
}
export interface ProvidersApiResponse {
links: {
first: string;
last: string;
next: string | null;
prev: string | null;
};
links: ApiLinks;
data: ProviderProps[];
included?: Array<{
type: string;
id: string;
attributes: any;
relationships?: any;
}>;
meta: {
pagination: {
page: number;
pages: number;
count: number;
};
version: string;
};
included?: IncludedEntity[];
meta: ApiResponseMeta;
}