feat(ui): add Google Workspace provider integration (#10333)

Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
lydiavilchez
2026-03-17 13:28:28 +01:00
committed by GitHub
parent 88ce188103
commit 4f93a89d1b
22 changed files with 488 additions and 12 deletions

View File

@@ -2,10 +2,11 @@
All notable changes to the **Prowler UI** are documented in this file.
## [1.21.0] (Prowler UNRELEASED)
## [1.21.0] (Prowler v5.21.0 UNRELEASED)
### 🚀 Added
- Google Workspace provider support [(#10333)](https://github.com/prowler-cloud/prowler/pull/10333)
- Image (Container Registry) provider support in UI: badge icon, credentials form, and provider-type filtering [(#10167)](https://github.com/prowler-cloud/prowler/pull/10167)
- Organization and organizational unit row actions (Edit Name, Update Credentials, Test Connections, Delete) in providers table dropdown [(#10317)](https://github.com/prowler-cloud/prowler/pull/10317)
- Events tab in Findings and Resource detail cards showing an AWS CloudTrail timeline with expandable event rows, actor info, request/response JSON payloads, and error details [(#10320)](https://github.com/prowler-cloud/prowler/pull/10320)

View File

@@ -10,6 +10,7 @@ import {
CloudflareProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
GoogleWorkspaceProviderBadge,
IacProviderBadge,
ImageProviderBadge,
KS8ProviderBadge,
@@ -35,6 +36,7 @@ const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
kubernetes: <KS8ProviderBadge width={18} height={18} />,
m365: <M365ProviderBadge width={18} height={18} />,
github: <GitHubProviderBadge width={18} height={18} />,
googleworkspace: <GoogleWorkspaceProviderBadge width={18} height={18} />,
iac: <IacProviderBadge width={18} height={18} />,
image: <ImageProviderBadge width={18} height={18} />,
oraclecloud: <OracleCloudProviderBadge width={18} height={18} />,

View File

@@ -78,6 +78,11 @@ const OpenStackProviderBadge = lazy(() =>
default: m.OpenStackProviderBadge,
})),
);
const GoogleWorkspaceProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.GoogleWorkspaceProviderBadge,
})),
);
type IconProps = { width: number; height: number };
@@ -113,6 +118,10 @@ const PROVIDER_DATA: Record<
label: "GitHub",
icon: GitHubProviderBadge,
},
googleworkspace: {
label: "Google Workspace",
icon: GoogleWorkspaceProviderBadge,
},
iac: {
label: "Infrastructure as Code",
icon: IacProviderBadge,

View File

@@ -5,6 +5,7 @@ import {
CloudflareProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
GoogleWorkspaceProviderBadge,
IacProviderBadge,
ImageProviderBadge,
KS8ProviderBadge,
@@ -22,6 +23,7 @@ export const PROVIDER_ICONS = {
kubernetes: KS8ProviderBadge,
m365: M365ProviderBadge,
github: GitHubProviderBadge,
googleworkspace: GoogleWorkspaceProviderBadge,
iac: IacProviderBadge,
image: ImageProviderBadge,
oraclecloud: OracleCloudProviderBadge,

View File

@@ -0,0 +1,44 @@
import { type FC } from "react";
import { IconSvgProps } from "@/types";
export const GoogleWorkspaceProviderBadge: FC<IconSvgProps> = ({
size,
width,
height,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="none"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 256 256"
width={size || width}
{...props}
>
<g fill="none">
<rect width="256" height="256" fill="#f4f2ed" rx="60" />
<g transform="translate(64 64) scale(2.0)">
<path
d="M62.3 30.9c0-2.5-.2-4.9-.6-7.2H32v13.6h17c-.7 3.9-3 7.2-6.3 9.4v8.3h10.2c6-5.5 9.4-13.6 9.4-23.1z"
fill="#4285F4"
/>
<path
d="M32 64c8.5 0 15.6-2.8 20.8-7.6l-10.2-7.9c-2.8 1.9-6.4 3-10.6 3-8.1 0-15-5.5-17.4-12.9H4.1v8.1C9.3 57.3 19.9 64 32 64z"
fill="#34A853"
/>
<path
d="M14.6 38.6c-.6-1.9-1-3.9-1-6s.3-4.1 1-6v-8.1H4.1C1.5 22.4 0 27 0 32s1.5 9.6 4.1 13.5l10.5-7.9z"
fill="#FBBC05"
/>
<path
d="M32 12.7c4.6 0 8.7 1.6 11.9 4.7l8.9-8.9C47.6 3.4 40.5 0 32 0 19.9 0 9.3 6.7 4.1 17.5l10.5 8.1C17 18.2 23.9 12.7 32 12.7z"
fill="#EA4335"
/>
</g>
</g>
</svg>
);

View File

@@ -8,6 +8,7 @@ import { AzureProviderBadge } from "./azure-provider-badge";
import { CloudflareProviderBadge } from "./cloudflare-provider-badge";
import { GCPProviderBadge } from "./gcp-provider-badge";
import { GitHubProviderBadge } from "./github-provider-badge";
import { GoogleWorkspaceProviderBadge } from "./googleworkspace-provider-badge";
import { IacProviderBadge } from "./iac-provider-badge";
import { ImageProviderBadge } from "./image-provider-badge";
import { KS8ProviderBadge } from "./ks8-provider-badge";
@@ -23,6 +24,7 @@ export {
CloudflareProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
GoogleWorkspaceProviderBadge,
IacProviderBadge,
ImageProviderBadge,
KS8ProviderBadge,
@@ -40,6 +42,7 @@ export const PROVIDER_BADGE_BY_NAME: Record<string, FC<IconSvgProps>> = {
Kubernetes: KS8ProviderBadge,
"Microsoft 365": M365ProviderBadge,
GitHub: GitHubProviderBadge,
"Google Workspace": GoogleWorkspaceProviderBadge,
"Infrastructure as Code": IacProviderBadge,
"Container Registry": ImageProviderBadge,
"Oracle Cloud Infrastructure": OracleCloudProviderBadge,

View File

@@ -15,6 +15,7 @@ import {
CloudflareProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
GoogleWorkspaceProviderBadge,
IacProviderBadge,
ImageProviderBadge,
KS8ProviderBadge,
@@ -61,6 +62,11 @@ const PROVIDERS = [
label: "GitHub",
badge: GitHubProviderBadge,
},
{
value: "googleworkspace",
label: "Google Workspace",
badge: GoogleWorkspaceProviderBadge,
},
{
value: "iac",
label: "Infrastructure as Code",

View File

@@ -22,6 +22,7 @@ import {
CloudflareTokenCredentials,
GCPDefaultCredentials,
GCPServiceAccountKey,
GoogleWorkspaceCredentials,
IacCredentials,
ImageCredentials,
KubernetesCredentials,
@@ -52,6 +53,7 @@ import {
} from "./select-credentials-type/m365";
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form";
import { GoogleWorkspaceCredentialsForm } from "./via-credentials/googleworkspace-credentials-form";
import { IacCredentialsForm } from "./via-credentials/iac-credentials-form";
import { ImageCredentialsForm } from "./via-credentials/image-credentials-form";
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
@@ -263,6 +265,13 @@ export const BaseCredentialsForm = ({
control={form.control as unknown as Control<OpenStackCredentials>}
/>
)}
{providerType === "googleworkspace" && (
<GoogleWorkspaceCredentialsForm
control={
form.control as unknown as Control<GoogleWorkspaceCredentials>
}
/>
)}
{!hideActions && (
<div className="flex w-full justify-end gap-4">

View File

@@ -111,6 +111,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
label: "Project ID",
placeholder: "e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890",
};
case "googleworkspace":
return {
label: "Customer ID",
placeholder: "e.g. C01234abc",
};
default:
return {
label: "Provider UID",

View File

@@ -0,0 +1,58 @@
import { Control, Controller } from "react-hook-form";
import {
WizardInputField,
WizardTextareaField,
} from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { GoogleWorkspaceCredentials } from "@/types";
export const GoogleWorkspaceCredentialsForm = ({
control,
}: {
control: Control<GoogleWorkspaceCredentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md text-default-foreground leading-9 font-bold">
Connect via Service Account
</div>
<div className="text-default-500 text-sm">
Provide your Service Account JSON and the admin email to impersonate.
</div>
</div>
{/* Hidden input for customer_id - auto-populated from provider UID */}
<Controller
control={control}
name={ProviderCredentialFields.GOOGLEWORKSPACE_CUSTOMER_ID}
render={({ field }) => <input type="hidden" {...field} />}
/>
<WizardTextareaField
control={control}
name={ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT}
label="Service Account JSON"
labelPlacement="inside"
placeholder="Paste your Service Account JSON here"
variant="bordered"
minRows={10}
isRequired
/>
<WizardInputField
control={control}
name={ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER}
type="email"
label="Delegated User Email"
labelPlacement="inside"
placeholder="admin@example.com"
variant="bordered"
isRequired
/>
<div className="text-default-400 text-xs">
Credentials never leave your browser unencrypted and are stored as
secrets in the backend. You can revoke the Service Account from the
Google Cloud Console anytime if needed.
</div>
</>
);
};

View File

@@ -5,6 +5,7 @@ import {
CloudflareProviderBadge,
GCPProviderBadge,
GitHubProviderBadge,
GoogleWorkspaceProviderBadge,
IacProviderBadge,
ImageProviderBadge,
KS8ProviderBadge,
@@ -29,6 +30,8 @@ export const getProviderLogo = (provider: ProviderType) => {
return <M365ProviderBadge width={35} height={35} />;
case "github":
return <GitHubProviderBadge width={35} height={35} />;
case "googleworkspace":
return <GoogleWorkspaceProviderBadge width={35} height={35} />;
case "iac":
return <IacProviderBadge width={35} height={35} />;
case "image":
@@ -62,6 +65,8 @@ export const getProviderName = (provider: ProviderType): string => {
return "Microsoft 365";
case "github":
return "GitHub";
case "googleworkspace":
return "Google Workspace";
case "iac":
return "Infrastructure as Code";
case "image":

View File

@@ -226,6 +226,14 @@ export const useCredentialsForm = ({
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: "",
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: "",
};
case "googleworkspace":
return {
...baseDefaults,
[ProviderCredentialFields.GOOGLEWORKSPACE_CUSTOMER_ID]:
providerUid || "",
[ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]: "",
[ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER]: "",
};
case "image":
return {
...baseDefaults,

View File

@@ -40,4 +40,10 @@ export const PROVIDER_CREDENTIALS_ERROR_MAPPING: Record<string, string> = {
ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT,
[ErrorPointers.OPENSTACK_CLOUDS_YAML_CLOUD]:
ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD,
[ErrorPointers.GOOGLEWORKSPACE_CUSTOMER_ID]:
ProviderCredentialFields.GOOGLEWORKSPACE_CUSTOMER_ID,
[ErrorPointers.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]:
ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT,
[ErrorPointers.GOOGLEWORKSPACE_DELEGATED_USER]:
ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER,
};

View File

@@ -87,6 +87,11 @@ export const getProviderHelpText = (provider: string) => {
text: "Need help connecting your OpenStack cloud?",
link: "https://goto.prowler.com/provider-openstack",
};
case "googleworkspace":
return {
text: "Need help connecting your Google Workspace account?",
link: "https://goto.prowler.com/provider-googleworkspace",
};
default:
return {
text: "How to setup a provider?",

View File

@@ -264,6 +264,21 @@ export const buildOpenStackSecret = (formData: FormData) => {
return filterEmptyValues(secret);
};
export const buildGoogleWorkspaceSecret = (formData: FormData) => {
const secret = {
[ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]:
getFormValue(
formData,
ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT,
),
[ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER]: getFormValue(
formData,
ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER,
),
};
return filterEmptyValues(secret);
};
export const buildIacSecret = (formData: FormData) => {
const secret = {
[ProviderCredentialFields.REPOSITORY_URL]: getFormValue(
@@ -480,6 +495,10 @@ export const buildSecretConfig = (
secretType: "static",
secret: buildOpenStackSecret(formData),
}),
googleworkspace: () => ({
secretType: "static",
secret: buildGoogleWorkspaceSecret(formData),
}),
};
const builder = secretBuilders[providerType];

View File

@@ -83,6 +83,11 @@ export const ProviderCredentialFields = {
// OpenStack fields
OPENSTACK_CLOUDS_YAML_CONTENT: "clouds_yaml_content",
OPENSTACK_CLOUDS_YAML_CLOUD: "clouds_yaml_cloud",
// Google Workspace fields
GOOGLEWORKSPACE_CUSTOMER_ID: "customer_id",
GOOGLEWORKSPACE_CREDENTIALS_CONTENT: "credentials_content",
GOOGLEWORKSPACE_DELEGATED_USER: "delegated_user",
} as const;
// Type for credential field values
@@ -137,6 +142,10 @@ export const ErrorPointers = {
CLOUDFLARE_API_EMAIL: "/data/attributes/secret/api_email",
OPENSTACK_CLOUDS_YAML_CONTENT: "/data/attributes/secret/clouds_yaml_content",
OPENSTACK_CLOUDS_YAML_CLOUD: "/data/attributes/secret/clouds_yaml_cloud",
GOOGLEWORKSPACE_CUSTOMER_ID: "/data/attributes/secret/customer_id",
GOOGLEWORKSPACE_CREDENTIALS_CONTENT:
"/data/attributes/secret/credentials_content",
GOOGLEWORKSPACE_DELEGATED_USER: "/data/attributes/secret/delegated_user",
} as const;
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];

View File

@@ -59,6 +59,28 @@ export interface AlibabaCloudProviderData {
alias?: string;
}
// Google Workspace provider data
export interface GoogleWorkspaceProviderData {
customerId: string;
alias?: string;
}
// Google Workspace credential options
export const GOOGLEWORKSPACE_CREDENTIAL_OPTIONS = {
GOOGLEWORKSPACE_SERVICE_ACCOUNT: "service_account",
} as const;
// Google Workspace credential type
type GoogleWorkspaceCredentialType =
(typeof GOOGLEWORKSPACE_CREDENTIAL_OPTIONS)[keyof typeof GOOGLEWORKSPACE_CREDENTIAL_OPTIONS];
// Google Workspace provider credential
export interface GoogleWorkspaceProviderCredential {
type: GoogleWorkspaceCredentialType;
serviceAccountJson: string;
delegatedUser: string;
}
// AWS credential options
export const AWS_CREDENTIAL_OPTIONS = {
AWS_ROLE_ARN: "role",
@@ -223,6 +245,12 @@ export class ProvidersPage extends BasePage {
readonly githubProviderRadio: Locator;
readonly ociProviderRadio: Locator;
readonly alibabacloudProviderRadio: Locator;
readonly googleworkspaceProviderRadio: Locator;
// Google Workspace provider form elements
readonly googleworkspaceCustomerIdInput: Locator;
readonly googleworkspaceServiceAccountJsonInput: Locator;
readonly googleworkspaceDelegatedUserInput: Locator;
// AWS provider form elements
readonly accountIdInput: Locator;
@@ -449,6 +477,20 @@ export class ProvidersPage extends BasePage {
name: /Connect assuming RAM Role/i,
});
// Google Workspace
this.googleworkspaceProviderRadio = page.getByRole("option", {
name: /Google Workspace/i,
});
this.googleworkspaceCustomerIdInput = page.getByRole("textbox", {
name: "Customer ID",
});
this.googleworkspaceServiceAccountJsonInput = page.getByRole("textbox", {
name: /Service Account JSON/i,
});
this.googleworkspaceDelegatedUserInput = page.getByRole("textbox", {
name: /Delegated User Email/i,
});
// Alias input
this.aliasInput = page.getByRole("textbox", {
name: "Provider alias (optional)",
@@ -1208,6 +1250,41 @@ export class ProvidersPage extends BasePage {
await expect(this.alibabacloudAccessKeySecretInput).toBeVisible();
}
async selectGoogleWorkspaceProvider(): Promise<void> {
await this.selectProviderRadio(this.googleworkspaceProviderRadio);
}
async fillGoogleWorkspaceProviderDetails(
data: GoogleWorkspaceProviderData,
): Promise<void> {
await this.googleworkspaceCustomerIdInput.fill(data.customerId);
if (data.alias) {
await this.aliasInput.fill(data.alias);
}
}
async fillGoogleWorkspaceCredentials(
credentials: GoogleWorkspaceProviderCredential,
): Promise<void> {
if (credentials.serviceAccountJson) {
await this.googleworkspaceServiceAccountJsonInput.fill(
credentials.serviceAccountJson,
);
}
if (credentials.delegatedUser) {
await this.googleworkspaceDelegatedUserInput.fill(
credentials.delegatedUser,
);
}
}
async verifyGoogleWorkspaceCredentialsPageLoaded(): Promise<void> {
await this.verifyPageHasProwlerTitle();
await expect(this.googleworkspaceServiceAccountJsonInput).toBeVisible();
await expect(this.googleworkspaceDelegatedUserInput).toBeVisible();
}
async verifyPageLoaded(): Promise<void> {
// Verify the providers page is loaded
@@ -1228,6 +1305,7 @@ export class ProvidersPage extends BasePage {
await expect(this.kubernetesProviderRadio).toBeVisible();
await expect(this.githubProviderRadio).toBeVisible();
await expect(this.alibabacloudProviderRadio).toBeVisible();
await expect(this.googleworkspaceProviderRadio).toBeVisible();
}
async verifyCredentialsPageLoaded(): Promise<void> {

View File

@@ -948,3 +948,67 @@
- Organization ID must follow AWS format (e.g., o-abc123def4)
- Role ARN must belong to the StackSet deployment for Organizations flow
- Provider cleanup is executed before test run to avoid unique constraint conflicts
---
## Test Case: `PROVIDER-E2E-017` - Add Google Workspace Provider with Service Account Credentials
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @googleworkspace
**Description/Objective:** Validates the complete flow of adding a new Google Workspace provider using Service Account authentication with Customer ID, Service Account JSON, and delegated user email.
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_GOOGLEWORKSPACE_CUSTOMER_ID, E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON, E2E_GOOGLEWORKSPACE_DELEGATED_USER
- Remove any existing provider with the same Customer ID before starting the test
- This test must be run serially and never in parallel with other tests, as it requires the Customer ID not to be already registered beforehand.
### Flow Steps:
1. Navigate to providers page
2. Click "Add Provider" button
3. Select Google Workspace provider type
4. Fill provider details (customer ID and alias)
5. Verify Google Workspace credentials page is loaded
6. Fill Google Workspace credentials (customer ID, service account JSON, delegated user email)
7. Launch initial scan
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- Google Workspace provider successfully added with Service Account credentials
- Initial scan launched successfully
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
- Provider page loads correctly
- Connect account page displays Google Workspace option
- Provider details form accepts customer ID (format: C[0-9a-zA-Z]+) and alias
- Credentials page loads with customer ID, service account JSON textarea, and delegated user email fields
- Customer ID help text is visible with instructions on finding the Customer ID
- Service account JSON field accepts multi-line formatted JSON
- Delegated user email field validates email format
- Launch scan page appears
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by customer ID)
- Scan name field contains "scheduled scan"
### Notes:
- Test uses environment variables for Google Workspace credentials
- Service Account JSON is provided as multi-line JSON string (not base64 encoded)
- Customer ID must start with 'C' followed by alphanumeric characters (e.g., C01234abc)
- Delegated user email must be a super admin in the Google Workspace domain
- Provider cleanup performed before each test to ensure clean state
- Requires valid Google Workspace account with Service Account having domain-wide delegation enabled
- Service Account must have appropriate Google Workspace API scopes for security scanning

View File

@@ -27,6 +27,9 @@ import {
AlibabaCloudProviderData,
AlibabaCloudProviderCredential,
ALIBABACLOUD_CREDENTIAL_OPTIONS,
GoogleWorkspaceProviderData,
GoogleWorkspaceProviderCredential,
GOOGLEWORKSPACE_CREDENTIAL_OPTIONS,
} from "./providers-page";
import { ScansPage } from "../scans/scans-page";
import fs from "fs";
@@ -1348,6 +1351,90 @@ test.describe("Add Provider", () => {
},
);
});
test.describe.serial("Add Google Workspace Provider", () => {
let providersPage: ProvidersPage;
let scansPage: ScansPage;
const customerId = process.env.E2E_GOOGLEWORKSPACE_CUSTOMER_ID ?? "";
const serviceAccountJson =
process.env.E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON ?? "";
const delegatedUser = process.env.E2E_GOOGLEWORKSPACE_DELEGATED_USER ?? "";
test.beforeEach(async ({ page }) => {
test.skip(
!customerId || !serviceAccountJson || !delegatedUser,
"Google Workspace E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, customerId!);
});
test.use({ storageState: "playwright/.auth/admin_user.json" });
test(
"should add a new Google Workspace provider with service account credentials",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@googleworkspace",
"@serial",
"@PROVIDER-E2E-017",
],
},
async ({ page }) => {
const googleWorkspaceProviderData: GoogleWorkspaceProviderData = {
customerId: customerId,
alias: "Test E2E Google Workspace - Service Account",
};
const googleWorkspaceCredentials: GoogleWorkspaceProviderCredential = {
type: GOOGLEWORKSPACE_CREDENTIAL_OPTIONS.GOOGLEWORKSPACE_SERVICE_ACCOUNT,
serviceAccountJson: serviceAccountJson,
delegatedUser: delegatedUser,
};
// Navigate to providers page
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Start adding new provider
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
// Select Google Workspace provider
await providersPage.selectGoogleWorkspaceProvider();
// Fill provider details (customer ID and alias)
await providersPage.fillGoogleWorkspaceProviderDetails(
googleWorkspaceProviderData,
);
await providersPage.clickNext();
// Verify credentials page is loaded
await providersPage.verifyGoogleWorkspaceCredentialsPageLoaded();
// Fill credentials
await providersPage.fillGoogleWorkspaceCredentials(
googleWorkspaceCredentials,
);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to scan page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(customerId);
},
);
});
});
test.describe("Update Provider Credentials", () => {

View File

@@ -364,6 +364,13 @@ export type OpenStackCredentials = {
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type GoogleWorkspaceCredentials = {
[ProviderCredentialFields.GOOGLEWORKSPACE_CUSTOMER_ID]: string;
[ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]: string;
[ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER]: string;
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type CredentialsFormSchema =
| AWSCredentials
| AWSCredentialsRole
@@ -379,7 +386,8 @@ export type CredentialsFormSchema =
| AlibabaCloudCredentials
| AlibabaCloudCredentialsRole
| CloudflareCredentials
| OpenStackCredentials;
| OpenStackCredentials
| GoogleWorkspaceCredentials;
export interface SearchParamsProps {
[key: string]: string | string[] | undefined;

View File

@@ -145,6 +145,17 @@ export const addProviderFormSchema = z
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z.string(),
}),
z.object({
providerType: z.literal("googleworkspace"),
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z
.string()
.trim()
.regex(
/^C[0-9a-zA-Z]+$/,
"Customer ID must start with 'C' followed by alphanumeric characters (e.g., C01234abc)",
),
}),
]),
);
@@ -316,21 +327,56 @@ export const addCredentialsFormSchema = (
})
.optional(),
}
: providerType === "openstack"
: providerType === "googleworkspace"
? {
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]:
[ProviderCredentialFields.GOOGLEWORKSPACE_CUSTOMER_ID]:
z
.string()
.min(
1,
"Clouds YAML content is required",
.trim()
.regex(
/^C[0-9a-zA-Z]+$/,
"Customer ID must start with 'C' followed by alphanumeric characters (e.g., C01234abc)",
),
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]:
z
.string()
.min(1, "Cloud name is required"),
[ProviderCredentialFields.GOOGLEWORKSPACE_CREDENTIALS_CONTENT]:
z.string().refine(
(val) => {
try {
const parsed = JSON.parse(val);
return (
typeof parsed === "object" &&
parsed !== null &&
!Array.isArray(parsed)
);
} catch {
return false;
}
},
{
message:
"Invalid JSON format. Please provide a valid Service Account JSON.",
},
),
[ProviderCredentialFields.GOOGLEWORKSPACE_DELEGATED_USER]:
z.email({
error:
"Please enter a valid email address",
}),
}
: {}),
: providerType === "openstack"
? {
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]:
z
.string()
.min(
1,
"Clouds YAML content is required",
),
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]:
z
.string()
.min(1, "Cloud name is required"),
}
: {}),
})
.superRefine((data: Record<string, string | undefined>, ctx) => {
if (providerType === "m365") {

View File

@@ -6,6 +6,7 @@ export const PROVIDER_TYPES = [
"m365",
"mongodbatlas",
"github",
"googleworkspace",
"iac",
"image",
"oraclecloud",
@@ -24,6 +25,7 @@ export const PROVIDER_DISPLAY_NAMES: Record<ProviderType, string> = {
m365: "Microsoft 365",
mongodbatlas: "MongoDB Atlas",
github: "GitHub",
googleworkspace: "Google Workspace",
iac: "Infrastructure as Code",
image: "Container Registry",
oraclecloud: "Oracle Cloud Infrastructure",