mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-08 20:33:41 +00:00
feat(ui): pre-fill Cloudflare and GitHub token creation URLs (#12349)
This commit is contained in:
1 parent
d8c8027215
commit
e2cae35d38
9 files changed
+519
-7
No files matched your search
@@ -213,6 +213,7 @@ export const BaseCredentialsForm = ({
|
||||
<GitHubCredentialsForm
|
||||
control={form.control}
|
||||
credentialsType={effectiveVia || undefined}
|
||||
providerUid={providerUid}
|
||||
/>
|
||||
)}
|
||||
{providerType === "iac" && (
|
||||
@@ -256,6 +257,7 @@ export const BaseCredentialsForm = ({
|
||||
control={
|
||||
form.control as unknown as Control<CloudflareTokenCredentials>
|
||||
}
|
||||
providerUid={providerUid}
|
||||
/>
|
||||
)}
|
||||
{providerType === "cloudflare" && effectiveVia === "api_key" && (
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { CloudflareTokenCredentials } from "@/types";
|
||||
|
||||
import { CloudflareApiTokenCredentialsForm } from "./cloudflare-api-token-credentials-form";
|
||||
|
||||
// Wraps the form in a react-hook-form context so the WizardInputField mounts
|
||||
// without exploding. We are testing the surrounding links, not the input.
|
||||
const Harness = ({ providerUid }: { providerUid?: string }) => {
|
||||
const form = useForm<CloudflareTokenCredentials>();
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<CloudflareApiTokenCredentialsForm
|
||||
control={form.control}
|
||||
providerUid={providerUid}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const USER_URL =
|
||||
"https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22%3A%22account_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22zone_settings%22%2C%22type%22%3A%22read%22%7D%2C%7B%22key%22%3A%22dns%22%2C%22type%22%3A%22read%22%7D%5D&accountId=%2A&zoneId=all&name=Prowler%20Security%20Scanner";
|
||||
|
||||
describe("CloudflareApiTokenCredentialsForm", () => {
|
||||
it("always renders the User API Token link with the correct href and safe target attributes", () => {
|
||||
// Given
|
||||
render(<Harness />);
|
||||
|
||||
// When
|
||||
const link = screen.getByRole("link", {
|
||||
name: /create a pre-configured user api token/i,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(link).toHaveAttribute("href", USER_URL);
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
it("does not render the Account-Owned link when providerUid is missing so the user is not offered an ambiguous duplicate", () => {
|
||||
// Given
|
||||
render(<Harness />);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
screen.queryByRole("link", {
|
||||
name: /create a pre-configured account-owned api token/i,
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the Account-Owned link routed through Cloudflare's dashboard `to=` param with the account id substituted when providerUid is provided", () => {
|
||||
// Given
|
||||
render(<Harness providerUid="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" />);
|
||||
|
||||
// When
|
||||
const link = screen.getByRole("link", {
|
||||
name: /create a pre-configured account-owned api token/i,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
const parsed = new URL(link.getAttribute("href") ?? "");
|
||||
expect(parsed.origin).toBe("https://dash.cloudflare.com");
|
||||
expect(parsed.pathname).toBe("/");
|
||||
expect(parsed.searchParams.get("to")).toBe(
|
||||
"/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4/api-tokens",
|
||||
);
|
||||
expect(parsed.searchParams.get("name")).toBe("Prowler Security Scanner");
|
||||
});
|
||||
|
||||
it("ignores whitespace around providerUid so a stray user-typed space does not hide the Account-Owned link", () => {
|
||||
// Given
|
||||
render(<Harness providerUid=" " />);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
screen.queryByRole("link", {
|
||||
name: /create a pre-configured account-owned api token/i,
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+42
-3
@@ -3,14 +3,31 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import {
|
||||
buildCloudflareAccountOwnedApiTokenUrl,
|
||||
PRECONFIGURED_CREDENTIAL_URLS,
|
||||
} from "@/lib/external-urls";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { CloudflareTokenCredentials } from "@/types";
|
||||
|
||||
interface CloudflareApiTokenCredentialsFormProps {
|
||||
control: Control<CloudflareTokenCredentials>;
|
||||
// Cloudflare Account ID captured in the previous wizard step. When present,
|
||||
// it flows into the account-owned token URL as the account path segment so
|
||||
// Cloudflare lands the user directly on the correct account's Create Custom
|
||||
// Token page. When absent, only the user-scoped link is shown to avoid
|
||||
// relying on Cloudflare's `:account` router placeholder, which can silently
|
||||
// fall through when the user is signed into more than one account.
|
||||
providerUid?: string;
|
||||
}
|
||||
|
||||
export const CloudflareApiTokenCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<CloudflareTokenCredentials>;
|
||||
}) => {
|
||||
providerUid,
|
||||
}: CloudflareApiTokenCredentialsFormProps) => {
|
||||
const trimmedProviderUid = providerUid?.trim();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
@@ -33,6 +50,28 @@ export const CloudflareApiTokenCredentialsForm = ({
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<Button variant="link" size="link-sm" asChild>
|
||||
<a
|
||||
href={PRECONFIGURED_CREDENTIAL_URLS.CLOUDFLARE_API_TOKEN_USER}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Create a pre-configured User API Token
|
||||
</a>
|
||||
</Button>
|
||||
{trimmedProviderUid && (
|
||||
<Button variant="link" size="link-sm" asChild>
|
||||
<a
|
||||
href={buildCloudflareAccountOwnedApiTokenUrl(trimmedProviderUid)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Create a pre-configured Account-Owned API Token
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-text-neutral-tertiary text-xs">
|
||||
Tokens never leave your browser unencrypted and are stored as secrets in
|
||||
the backend. You can revoke the token from the Cloudflare dashboard
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { GitHubPersonalAccessTokenForm } from "./github-personal-access-token-form";
|
||||
|
||||
// Wraps the form in a react-hook-form context so the WizardInputField mounts
|
||||
// without exploding. We are testing the surrounding links, not the input.
|
||||
const Harness = ({ providerUid }: { providerUid?: string }) => {
|
||||
const form = useForm();
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<GitHubPersonalAccessTokenForm
|
||||
control={form.control}
|
||||
providerUid={providerUid}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const USER_URL =
|
||||
"https://github.com/settings/personal-access-tokens/new?name=Prowler+Security+Scanner&description=Fine-grained+PAT+for+Prowler+security+scanning&expires_in=90&administration=read&contents=read&vulnerability_alerts=read&emails=read";
|
||||
|
||||
const expectSafeExternalLink = (href: string) => (name: RegExp) => {
|
||||
const link = screen.getByRole("link", { name });
|
||||
expect(link).toHaveAttribute("href", href);
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
};
|
||||
|
||||
describe("GitHubPersonalAccessTokenForm", () => {
|
||||
it("renders the personal-repositories link with the correct href and safe target attributes", () => {
|
||||
// Given
|
||||
render(<Harness />);
|
||||
|
||||
// Then
|
||||
expectSafeExternalLink(USER_URL)(
|
||||
/create a pre-configured token for personal repositories/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not render the organization link when providerUid is missing so the user is not offered an identical-looking duplicate", () => {
|
||||
// Given
|
||||
render(<Harness />);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
screen.queryByRole("link", {
|
||||
name: /create a pre-configured token for organization/i,
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the organization link with the identifier pinned as target_name when providerUid is provided", () => {
|
||||
// Given
|
||||
render(<Harness providerUid="prowler-cloud" />);
|
||||
|
||||
// When
|
||||
const orgLink = screen.getByRole("link", {
|
||||
name: /create a pre-configured token for organization prowler-cloud/i,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(orgLink).toHaveAttribute("target", "_blank");
|
||||
expect(orgLink).toHaveAttribute("rel", "noopener noreferrer");
|
||||
const orgUrl = new URL(orgLink.getAttribute("href") ?? "");
|
||||
expect(orgUrl.origin + orgUrl.pathname).toBe(
|
||||
"https://github.com/settings/personal-access-tokens/new",
|
||||
);
|
||||
expect(orgUrl.searchParams.get("target_name")).toBe("prowler-cloud");
|
||||
expect(orgUrl.searchParams.get("organization_administration")).toBe("read");
|
||||
expect(orgUrl.searchParams.get("members")).toBe("read");
|
||||
expect(orgUrl.searchParams.get("emails")).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores whitespace around providerUid so a stray user-typed space does not hide the organization link", () => {
|
||||
// Given
|
||||
render(<Harness providerUid=" " />);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
screen.queryByRole("link", {
|
||||
name: /create a pre-configured token for organization/i,
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+43
-3
@@ -3,13 +3,29 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import {
|
||||
buildGitHubPersonalAccessTokenOrgUrl,
|
||||
PRECONFIGURED_CREDENTIAL_URLS,
|
||||
} from "@/lib/external-urls";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
|
||||
interface GitHubPersonalAccessTokenFormProps {
|
||||
control: Control<any>;
|
||||
// GitHub identifier entered in the previous wizard step. When it names an
|
||||
// organization, it flows into the org-scoped token URL as `target_name` so
|
||||
// GitHub pre-selects the right Resource Owner and surfaces the org-only
|
||||
// permissions (`organization_administration`, `members`). When absent, only
|
||||
// the personal-repositories link is shown.
|
||||
providerUid?: string;
|
||||
}
|
||||
|
||||
export const GitHubPersonalAccessTokenForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<any>;
|
||||
}) => {
|
||||
providerUid,
|
||||
}: GitHubPersonalAccessTokenFormProps) => {
|
||||
const trimmedProviderUid = providerUid?.trim();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
@@ -30,6 +46,30 @@ export const GitHubPersonalAccessTokenForm = ({
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<Button variant="link" size="link-sm" asChild>
|
||||
<a
|
||||
href={
|
||||
PRECONFIGURED_CREDENTIAL_URLS.GITHUB_PERSONAL_ACCESS_TOKEN_USER
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Create a pre-configured token for personal repositories
|
||||
</a>
|
||||
</Button>
|
||||
{trimmedProviderUid && (
|
||||
<Button variant="link" size="link-sm" asChild>
|
||||
<a
|
||||
href={buildGitHubPersonalAccessTokenOrgUrl(trimmedProviderUid)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{`Create a pre-configured token for organization ${trimmedProviderUid}`}
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -11,15 +11,22 @@ import {
|
||||
interface GitHubCredentialsFormProps {
|
||||
control: Control<any>;
|
||||
credentialsType?: string;
|
||||
providerUid?: string;
|
||||
}
|
||||
|
||||
export const GitHubCredentialsForm = ({
|
||||
control,
|
||||
credentialsType,
|
||||
providerUid,
|
||||
}: GitHubCredentialsFormProps) => {
|
||||
switch (credentialsType) {
|
||||
case "personal_access_token":
|
||||
return <GitHubPersonalAccessTokenForm control={control} />;
|
||||
return (
|
||||
<GitHubPersonalAccessTokenForm
|
||||
control={control}
|
||||
providerUid={providerUid}
|
||||
/>
|
||||
);
|
||||
case "oauth_app":
|
||||
return <GitHubOAuthAppForm control={control} />;
|
||||
case "github_app":
|
||||
|
||||
Reference in new issue
Block a user