mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
feat(ui): add Google Workspace provider integration (#10333)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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} />,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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":
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user