Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ All notable changes to the **Prowler UI** are documented in this file.

---

## [1.16.2] (Prowler v5.16.2) (UNRELEASED)

### 🐞 Fixed

- OCI update credentials form failing silently due to missing provider UID [(#9746)](https://github.com/prowler-cloud/prowler/pull/9746)

---

## [1.16.1] (Prowler v5.16.1)

### 🔄 Changed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { redirect } from "next/navigation";
import React from "react";

import { getProvider } from "@/actions/providers/providers";
import { CredentialsUpdateInfo } from "@/components/providers";
import {
UpdateViaCredentialsForm,
Expand All @@ -20,24 +22,52 @@ interface Props {

export default async function UpdateCredentialsPage({ searchParams }: Props) {
const resolvedSearchParams = await searchParams;
const { type: providerType, via } = resolvedSearchParams;
const { type: providerType, via, id: providerId } = resolvedSearchParams;

if (!providerId) {
redirect("/providers");
}

const formType = getProviderFormType(providerType, via);

const formData = new FormData();
formData.append("id", providerId);
const providerResponse = await getProvider(formData);

if (providerResponse?.errors) {
redirect("/providers");
}

const providerUid = providerResponse?.data?.attributes?.uid;

switch (formType) {
case "selector":
return (
<CredentialsUpdateInfo providerType={providerType} initialVia={via} />
);

case "credentials":
return <UpdateViaCredentialsForm searchParams={resolvedSearchParams} />;
return (
<UpdateViaCredentialsForm
searchParams={resolvedSearchParams}
providerUid={providerUid}
/>
);

case "role":
return <UpdateViaRoleForm searchParams={resolvedSearchParams} />;
return (
<UpdateViaRoleForm
searchParams={resolvedSearchParams}
providerUid={providerUid}
/>
);

case "service-account":
return (
<UpdateViaServiceAccountForm searchParams={resolvedSearchParams} />
<UpdateViaServiceAccountForm
searchParams={resolvedSearchParams}
providerUid={providerUid}
/>
);

default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { BaseCredentialsForm } from "./base-credentials-form";

export const UpdateViaCredentialsForm = ({
searchParams,
providerUid,
}: {
searchParams: { type: string; id: string; secretId?: string };
providerUid?: string;
}) => {
const providerType = searchParams.type as ProviderType;
const providerId = searchParams.id;
Expand All @@ -24,6 +26,7 @@ export const UpdateViaCredentialsForm = ({
<BaseCredentialsForm
providerType={providerType}
providerId={providerId}
providerUid={providerUid}
onSubmit={handleUpdateCredentials}
successNavigationUrl={successNavigationUrl}
submitButtonText="Next"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { BaseCredentialsForm } from "./base-credentials-form";

export const UpdateViaRoleForm = ({
searchParams,
providerUid,
}: {
searchParams: { type: string; id: string; secretId?: string };
providerUid?: string;
}) => {
const providerType = searchParams.type as ProviderType;
const providerId = searchParams.id;
Expand All @@ -24,6 +26,7 @@ export const UpdateViaRoleForm = ({
<BaseCredentialsForm
providerType={providerType}
providerId={providerId}
providerUid={providerUid}
onSubmit={handleUpdateCredentials}
successNavigationUrl={successNavigationUrl}
submitButtonText="Next"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { BaseCredentialsForm } from "./base-credentials-form";

export const UpdateViaServiceAccountForm = ({
searchParams,
providerUid,
}: {
searchParams: { type: string; id: string; secretId?: string };
providerUid?: string;
}) => {
const providerType = searchParams.type as ProviderType;
const providerId = searchParams.id;
Expand All @@ -24,6 +26,7 @@ export const UpdateViaServiceAccountForm = ({
<BaseCredentialsForm
providerType={providerType}
providerId={providerId}
providerUid={providerUid}
onSubmit={handleUpdateCredentials}
successNavigationUrl={successNavigationUrl}
submitButtonText="Next"
Expand Down
67 changes: 60 additions & 7 deletions ui/tests/providers/providers-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,18 +607,22 @@ export class ProvidersPage extends BasePage {
}

// Fallback logic: try finding any common primary action buttons in expected order
const candidates: Array<{ name: string | RegExp }> = [
{ name: "Next" }, // Try the "Next" button
{ name: "Save" }, // Try the "Save" button
const candidates: Array<{ name: string | RegExp; exact?: boolean }> = [
{ name: "Next", exact: true }, // Try the "Next" button (exact match to avoid Next.js dev tools)
{ name: "Save", exact: true }, // Try the "Save" button
{ name: "Launch scan" }, // Try the "Launch scan" button
{ name: /Continue|Proceed/i }, // Try "Continue" or "Proceed" (case-insensitive)
];

// Try each candidate name and click it if found
for (const candidate of candidates) {
const btn = this.page.getByRole("button", {
name: candidate.name,
});
// Exclude Next.js dev tools button by filtering out buttons with aria-haspopup attribute
const btn = this.page
.getByRole("button", {
name: candidate.name,
exact: candidate.exact,
})
.and(this.page.locator(":not([aria-haspopup])"));

if (await btn.count()) {
await btn.click();
Expand Down Expand Up @@ -847,7 +851,7 @@ export class ProvidersPage extends BasePage {
}

async verifyOCICredentialsPageLoaded(): Promise<void> {
// Verify the OCI credentials page is loaded
// Verify the OCI credentials page is loaded (add flow - all fields visible)

await this.verifyPageHasProwlerTitle();
await expect(this.ociTenancyIdInput).toBeVisible();
Expand All @@ -857,6 +861,17 @@ export class ProvidersPage extends BasePage {
await expect(this.ociRegionInput).toBeVisible();
}

async verifyOCIUpdateCredentialsPageLoaded(): Promise<void> {
// Verify the OCI update credentials page is loaded
// Note: Tenancy OCID is hidden in update flow (auto-populated from provider UID)

await this.verifyPageHasProwlerTitle();
await expect(this.ociUserIdInput).toBeVisible();
await expect(this.ociFingerprintInput).toBeVisible();
await expect(this.ociKeyContentInput).toBeVisible();
await expect(this.ociRegionInput).toBeVisible();
}

async verifyPageLoaded(): Promise<void> {
// Verify the providers page is loaded

Expand Down Expand Up @@ -995,4 +1010,42 @@ export class ProvidersPage extends BasePage {
throw new Error(`Invalid authentication method: ${method}`);
}
}

async clickProviderRowActions(providerUid: string): Promise<void> {
// Click the actions dropdown for a specific provider row
const row = this.providersTable.locator("tbody tr", {
hasText: providerUid,
});
await expect(row).toBeVisible();

// Click the dropdown trigger - it's the last button in the row (after the copy button)
const actionsButton = row.locator("button").last();
await actionsButton.click();
}

async clickUpdateCredentials(providerUid: string): Promise<void> {
// Click update credentials for a specific provider
await this.clickProviderRowActions(providerUid);

// Wait for dropdown menu to stabilize and click Update Credentials
const updateCredentialsOption = this.page.getByRole("menuitem", {
name: /Update Credentials/i,
});
await expect(updateCredentialsOption).toBeVisible();
// Wait a bit for the menu to stabilize before clicking
await this.page.waitForTimeout(100);
await updateCredentialsOption.click({ force: true });
}

async verifyUpdateCredentialsPageLoaded(): Promise<void> {
// Verify the update credentials page is loaded
await this.verifyPageHasProwlerTitle();
await expect(this.page).toHaveURL(/\/providers\/update-credentials/);
}

async verifyTestConnectionPageLoaded(): Promise<void> {
// Verify the test connection page is loaded
await this.verifyPageHasProwlerTitle();
await expect(this.page).toHaveURL(/\/providers\/test-connection/);
}
}
58 changes: 58 additions & 0 deletions ui/tests/providers/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -708,3 +708,61 @@
- Provider cleanup performed before each test to ensure clean state
- Requires valid OCI account with API Key set up
- API Key credential type is automatically used for OCI providers

---

## Test Case: `PROVIDER-E2E-013` - Update OCI Provider Credentials

**Priority:** `normal`

**Tags:**

- type → @e2e, @serial
- feature → @providers
- provider → @oci

**Description/Objective:** Validates the complete flow of updating credentials for an existing OCI provider. This test verifies that the provider UID is correctly passed to the update credentials form, which is required for OCI credential validation.

**Preconditions:**

- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION
- An OCI provider with the specified Tenancy ID must already exist (run PROVIDER-E2E-012 first)
- This test must be run serially and never in parallel with other tests

### Flow Steps:

1. Navigate to providers page
2. Verify OCI provider exists in the table
3. Click row actions menu for the OCI provider
4. Click "Update Credentials" option
5. Verify update credentials page is loaded
6. Verify OCI credentials form fields are visible (confirms providerUid is loaded)
7. Fill OCI credentials (user ID, fingerprint, key content, region)
8. Click Next to submit
9. Verify successful navigation to test connection page

### Expected Result:

- Update credentials page loads successfully
- OCI credentials form is displayed with all required fields
- Provider UID is correctly passed to the form (hidden field populated)
- Credentials can be updated and submitted
- User is redirected to test connection page after successful update

### Key verification points:

- Provider page loads correctly
- OCI provider row is visible in providers table
- Row actions dropdown opens and displays "Update Credentials" option
- Update credentials page URL contains correct parameters
- OCI credentials form displays all fields (tenancy ID, user ID, fingerprint, key content, region)
- Form submission succeeds (no silent failures due to missing provider UID)
- Successful redirect to test connection page

### Notes:

- Test uses same environment variables as PROVIDER-E2E-012 (add OCI provider)
- Requires PROVIDER-E2E-012 to be run first to create the OCI provider
- This test validates the fix for OCI update credentials form failing silently due to missing provider UID
- The provider UID is required for OCI credential validation (tenancy field auto-populated from UID)
84 changes: 84 additions & 0 deletions ui/tests/providers/providers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1139,3 +1139,87 @@ test.describe("Add Provider", () => {
);
});
});

test.describe("Update Provider Credentials", () => {
test.describe.serial("Update OCI Provider Credentials", () => {
let providersPage: ProvidersPage;

// Test data from environment variables (same as add OCI provider test)
const tenancyId = process.env.E2E_OCI_TENANCY_ID;
const userId = process.env.E2E_OCI_USER_ID;
const fingerprint = process.env.E2E_OCI_FINGERPRINT;
const keyContent = process.env.E2E_OCI_KEY_CONTENT;
const region = process.env.E2E_OCI_REGION;

// Validate required environment variables
if (!tenancyId || !userId || !fingerprint || !keyContent || !region) {
throw new Error(
"E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, and E2E_OCI_REGION environment variables are not set",
);
}

// Setup before each test
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
});

// Use admin authentication for provider management
test.use({ storageState: "playwright/.auth/admin_user.json" });

test(
"should update OCI provider credentials successfully",
{
tag: [
"@e2e",
"@providers",
"@oci",
"@serial",
"@PROVIDER-E2E-013",
],
},
async () => {
// Prepare updated credentials
const ociCredentials: OCIProviderCredential = {
type: OCI_CREDENTIAL_OPTIONS.OCI_API_KEY,
tenancyId: tenancyId,
userId: userId,
fingerprint: fingerprint,
keyContent: keyContent,
region: region,
};

// Navigate to providers page
await providersPage.goto();
await providersPage.verifyPageLoaded();

// Verify OCI provider exists in the table
const providerExists =
await providersPage.verifySingleRowForProviderUID(tenancyId);
if (!providerExists) {
throw new Error(
`OCI provider with tenancy ID ${tenancyId} not found. Run the add OCI provider test first.`,
);
}

// Click update credentials for the OCI provider
await providersPage.clickUpdateCredentials(tenancyId);

// Verify update credentials page is loaded
await providersPage.verifyUpdateCredentialsPageLoaded();

// Verify OCI credentials form fields are visible (confirms providerUid is loaded)
// Note: Tenancy OCID is hidden in update flow (auto-populated from provider UID)
await providersPage.verifyOCIUpdateCredentialsPageLoaded();

// Fill updated credentials
await providersPage.fillOCICredentials(ociCredentials);

// Click Next to submit
await providersPage.clickNext();

// Verify successful navigation to test connection page
await providersPage.verifyTestConnectionPageLoaded();
},
);
});
});
Loading