From 22cccdd1dbf3907d97939236bf28c5db8a24e4ee Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Thu, 11 Sep 2025 11:44:44 +0200 Subject: [PATCH] Update Admin UI for SPIFFE Identity Provider (#42507) Closes #42464 Signed-off-by: stianst --- .github/workflows/js-ci.yml | 2 +- .../admin/messages/messages_en.properties | 3 + .../add/AddSpiffeConnect.tsx | 99 +++++++++++++++++++ .../identity-providers/add/DetailSettings.tsx | 19 ++++ .../identity-providers/add/SpiffeSettings.tsx | 35 +++++++ .../admin-ui/src/identity-providers/routes.ts | 2 + .../routes/IdentityProviderSpiffe.tsx | 23 +++++ .../admin-ui/test/identity-providers/main.ts | 12 +++ .../test/identity-providers/spiffe.spec.ts | 34 +++++++ .../keycloak-server/scripts/start-server.js | 2 +- 10 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 js/apps/admin-ui/src/identity-providers/add/AddSpiffeConnect.tsx create mode 100644 js/apps/admin-ui/src/identity-providers/add/SpiffeSettings.tsx create mode 100644 js/apps/admin-ui/src/identity-providers/routes/IdentityProviderSpiffe.tsx create mode 100644 js/apps/admin-ui/test/identity-providers/spiffe.spec.ts diff --git a/.github/workflows/js-ci.yml b/.github/workflows/js-ci.yml index 68f842e75f6..cc8e67aac42 100644 --- a/.github/workflows/js-ci.yml +++ b/.github/workflows/js-ci.yml @@ -230,7 +230,7 @@ jobs: - name: Start Keycloak server run: | tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz - keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz:v2,transient-users &> ~/server.log & + keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz:v2,transient-users,spiffe &> ~/server.log & env: KC_BOOTSTRAP_ADMIN_USERNAME: admin KC_BOOTSTRAP_ADMIN_PASSWORD: admin diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 499b6dd6516..628f5a78a70 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -934,6 +934,9 @@ disableConfirmClient=Are you sure you want to disable this client? custom=Custom Attribute... keyTab=Key tab addSamlProvider=Add SAML provider +addSpiffeProvider=Add SPIFFE provider +spiffeTrustDomain=SPIFFE Trust Domain name +spiffeBundleEndpoint=SPIFFE Bundle Endpoint permission=Permission saveEventListeners=Save Event Listeners capabilityConfig=Capability config diff --git a/js/apps/admin-ui/src/identity-providers/add/AddSpiffeConnect.tsx b/js/apps/admin-ui/src/identity-providers/add/AddSpiffeConnect.tsx new file mode 100644 index 00000000000..3aaa7b20d07 --- /dev/null +++ b/js/apps/admin-ui/src/identity-providers/add/AddSpiffeConnect.tsx @@ -0,0 +1,99 @@ +import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; +import { + ActionGroup, + AlertVariant, + Button, + PageSection, +} from "@patternfly/react-core"; +import { FormProvider, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { Link, useNavigate } from "react-router-dom"; +import { useAdminClient } from "../../admin-client"; +import { useAlerts } from "@keycloak/keycloak-ui-shared"; +import { FormAccess } from "../../components/form/FormAccess"; +import { ViewHeader } from "../../components/view-header/ViewHeader"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import { toIdentityProvider } from "../routes/IdentityProvider"; +import { toIdentityProviders } from "../routes/IdentityProviders"; +import { SpiffeSettings } from "./SpiffeSettings"; + +type DiscoveryIdentityProvider = IdentityProviderRepresentation & { + discoveryEndpoint?: string; +}; + +export default function AddSamlConnect() { + const { adminClient } = useAdminClient(); + + const { t } = useTranslation(); + const navigate = useNavigate(); + const id = "spiffe"; + + const form = useForm({ + defaultValues: { alias: id, config: { allowCreate: "true" } }, + mode: "onChange", + }); + const { + handleSubmit, + formState: { isDirty }, + } = form; + + const { addAlert, addError } = useAlerts(); + const { realm } = useRealm(); + + const onSubmit = async (provider: DiscoveryIdentityProvider) => { + delete provider.discoveryEndpoint; + try { + await adminClient.identityProviders.create({ + ...provider, + providerId: id, + }); + addAlert(t("createIdentityProviderSuccess"), AlertVariant.success); + navigate( + toIdentityProvider({ + realm, + providerId: id, + alias: provider.alias!, + tab: "settings", + }), + ); + } catch (error: any) { + addError("createIdentityProviderError", error); + } + }; + + return ( + <> + + + + + + + + + + + + + + ); +} diff --git a/js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx b/js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx index 413f898f130..e5f43071297 100644 --- a/js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx +++ b/js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx @@ -66,6 +66,7 @@ import { OIDCAuthentication } from "./OIDCAuthentication"; import { OIDCGeneralSettings } from "./OIDCGeneralSettings"; import { ReqAuthnConstraints } from "./ReqAuthnConstraintsSettings"; import { SamlGeneralSettings } from "./SamlGeneralSettings"; +import { SpiffeSettings } from "./SpiffeSettings"; import { AdminEvents } from "../../events/AdminEvents"; import { UserProfileClaimsSettings } from "./OAuth2UserProfileClaimsSettings"; @@ -412,6 +413,7 @@ export default function DetailSettings() { const isOIDC = provider.providerId!.includes("oidc"); const isSAML = provider.providerId!.includes("saml"); const isOAuth2 = provider.providerId!.includes("oauth2"); + const isSPIFFE = provider.providerId!.includes("spiffe"); const isSocial = !isOIDC && !isSAML && !isOAuth2; const loader = async () => { @@ -442,6 +444,7 @@ export default function DetailSettings() { const sections = [ { title: t("generalSettings"), + isHidden: isSPIFFE, panel: ( ), }, + { + title: t("generalSettings"), + isHidden: !isSPIFFE, + panel: ( +
+ + + + ), + }, { title: t("samlSettings"), isHidden: !isSAML, @@ -506,6 +523,7 @@ export default function DetailSettings() { }, { title: t("advancedSettings"), + isHidden: isSPIFFE, panel: ( {t("mappers")}} {...mappersTab} diff --git a/js/apps/admin-ui/src/identity-providers/add/SpiffeSettings.tsx b/js/apps/admin-ui/src/identity-providers/add/SpiffeSettings.tsx new file mode 100644 index 00000000000..f572b8e9b5f --- /dev/null +++ b/js/apps/admin-ui/src/identity-providers/add/SpiffeSettings.tsx @@ -0,0 +1,35 @@ +import { TextControl } from "@keycloak/keycloak-ui-shared"; +import { useTranslation } from "react-i18next"; + +export const SpiffeSettings = () => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + ); +}; diff --git a/js/apps/admin-ui/src/identity-providers/routes.ts b/js/apps/admin-ui/src/identity-providers/routes.ts index 04fc1440eaf..6179afc39ab 100644 --- a/js/apps/admin-ui/src/identity-providers/routes.ts +++ b/js/apps/admin-ui/src/identity-providers/routes.ts @@ -3,6 +3,7 @@ import { IdentityProviderRoute } from "./routes/IdentityProvider"; import { IdentityProviderKeycloakOidcRoute } from "./routes/IdentityProviderKeycloakOidc"; import { IdentityProviderOidcRoute } from "./routes/IdentityProviderOidc"; import { IdentityProviderSamlRoute } from "./routes/IdentityProviderSaml"; +import { IdentityProviderSpiffeRoute } from "./routes/IdentityProviderSpiffe"; import { IdentityProvidersRoute } from "./routes/IdentityProviders"; import { IdentityProviderAddMapperRoute } from "./routes/AddMapper"; import { IdentityProviderEditMapperRoute } from "./routes/EditMapper"; @@ -15,6 +16,7 @@ const routes: AppRouteObject[] = [ IdentityProvidersRoute, IdentityProviderOidcRoute, IdentityProviderSamlRoute, + IdentityProviderSpiffeRoute, IdentityProviderKeycloakOidcRoute, IdentityProviderCreateRoute, IdentityProviderRoute, diff --git a/js/apps/admin-ui/src/identity-providers/routes/IdentityProviderSpiffe.tsx b/js/apps/admin-ui/src/identity-providers/routes/IdentityProviderSpiffe.tsx new file mode 100644 index 00000000000..0e07b32701a --- /dev/null +++ b/js/apps/admin-ui/src/identity-providers/routes/IdentityProviderSpiffe.tsx @@ -0,0 +1,23 @@ +import { lazy } from "react"; +import type { Path } from "react-router-dom"; +import { generateEncodedPath } from "../../utils/generateEncodedPath"; +import type { AppRouteObject } from "../../routes"; + +export type IdentityProviderSpiffeParams = { realm: string }; + +const AddSpiffeConnect = lazy(() => import("../add/AddSpiffeConnect")); + +export const IdentityProviderSpiffeRoute: AppRouteObject = { + path: "/:realm/identity-providers/spiffe/add", + element: , + breadcrumb: (t) => t("addSpiffeProvider"), + handle: { + access: "manage-identity-providers", + }, +}; + +export const toIdentityProviderSpiffe = ( + params: IdentityProviderSpiffeParams, +): Partial => ({ + pathname: generateEncodedPath(IdentityProviderSpiffeRoute.path, params), +}); diff --git a/js/apps/admin-ui/test/identity-providers/main.ts b/js/apps/admin-ui/test/identity-providers/main.ts index 53b47c088e1..adb6928dd7e 100644 --- a/js/apps/admin-ui/test/identity-providers/main.ts +++ b/js/apps/admin-ui/test/identity-providers/main.ts @@ -43,6 +43,18 @@ export async function createSAMLProvider( await clickAddButton(page); } +export async function createSPIFFEProvider( + page: Page, + providerName: string, + trustDomain: string, + bundleEndpoint: string, +) { + await clickProviderCard(page, providerName); + await page.getByTestId("config.trustDomain").fill(trustDomain); + await page.getByTestId("config.bundleEndpoint").fill(bundleEndpoint); + await clickAddButton(page); +} + export async function assertAuthorizationUrl(page: Page) { await expect(page.getByTestId("config.authorizationUrl")).toHaveValue( authorizationUrl, diff --git a/js/apps/admin-ui/test/identity-providers/spiffe.spec.ts b/js/apps/admin-ui/test/identity-providers/spiffe.spec.ts new file mode 100644 index 00000000000..36f357600a0 --- /dev/null +++ b/js/apps/admin-ui/test/identity-providers/spiffe.spec.ts @@ -0,0 +1,34 @@ +import { test } from "@playwright/test"; +import adminClient from "../utils/AdminClient.ts"; +import { login } from "../utils/login.ts"; +import { assertNotificationMessage } from "../utils/masthead.ts"; +import { goToIdentityProviders } from "../utils/sidebar.ts"; +import { clickTableRowItem } from "../utils/table.ts"; +import { clickSaveButton, createSPIFFEProvider } from "./main.ts"; + +test.beforeEach(async ({ page }) => { + await login(page); + await goToIdentityProviders(page); +}); + +test.afterAll(() => adminClient.deleteIdentityProvider("spiffe")); + +test.describe.serial("SPIFFE identity provider test", () => { + test("should create a SPIFFE provider", async ({ page }) => { + await createSPIFFEProvider(page, "spiffe", "mytrust", "https://mytrust"); + await assertNotificationMessage( + page, + "Identity provider successfully created", + ); + + await goToIdentityProviders(page); + await clickTableRowItem(page, "Spiffe"); + + await page.getByTestId("config.trustDomain").fill("mytrust2"); + await page.getByTestId("config.bundleEndpoint").fill("https://mytrust2"); + + await clickSaveButton(page); + + await assertNotificationMessage(page, "Provider successfully updated"); + }); +}); diff --git a/js/apps/keycloak-server/scripts/start-server.js b/js/apps/keycloak-server/scripts/start-server.js index d7b6e6a40d1..85ef7cdde29 100755 --- a/js/apps/keycloak-server/scripts/start-server.js +++ b/js/apps/keycloak-server/scripts/start-server.js @@ -60,7 +60,7 @@ async function startServer() { path.join(SERVER_DIR, `bin/kc${SCRIPT_EXTENSION}`), [ "start-dev", - `--features="login:v2,account:v3,admin-fine-grained-authz:v2,transient-users,oid4vc-vci,organization,declarative-ui,quick-theme"`, + `--features="login:v2,account:v3,admin-fine-grained-authz:v2,transient-users,oid4vc-vci,organization,declarative-ui,quick-theme,spiffe"`, ...keycloakArgs, ], {