mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-25 16:42:34 +00:00
Update Admin UI for SPIFFE Identity Provider (#42507)
Closes #42464 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
2
.github/workflows/js-ci.yml
vendored
2
.github/workflows/js-ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<DiscoveryIdentityProvider>({
|
||||
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 (
|
||||
<>
|
||||
<ViewHeader titleKey={t("addSpiffeProvider")} />
|
||||
<PageSection variant="light">
|
||||
<FormProvider {...form}>
|
||||
<FormAccess
|
||||
role="manage-identity-providers"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<SpiffeSettings />
|
||||
<ActionGroup>
|
||||
<Button
|
||||
isDisabled={!isDirty}
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="createProvider"
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="cancel"
|
||||
component={(props) => (
|
||||
<Link {...props} to={toIdentityProviders({ realm })} />
|
||||
)}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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: (
|
||||
<FormAccess
|
||||
role="manage-identity-providers"
|
||||
@@ -486,6 +489,20 @@ export default function DetailSettings() {
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("generalSettings"),
|
||||
isHidden: !isSPIFFE,
|
||||
panel: (
|
||||
<Form
|
||||
isHorizontal
|
||||
className="pf-v5-u-py-lg"
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<SpiffeSettings />
|
||||
<FixedButtonsGroup name="idp-details" isSubmit reset={reset} />
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("samlSettings"),
|
||||
isHidden: !isSAML,
|
||||
@@ -506,6 +523,7 @@ export default function DetailSettings() {
|
||||
},
|
||||
{
|
||||
title: t("advancedSettings"),
|
||||
isHidden: isSPIFFE,
|
||||
panel: (
|
||||
<FormAccess
|
||||
role="manage-identity-providers"
|
||||
@@ -557,6 +575,7 @@ export default function DetailSettings() {
|
||||
</Tab>
|
||||
<Tab
|
||||
id="mappers"
|
||||
isHidden={isSPIFFE}
|
||||
data-testid="mappers-tab"
|
||||
title={<TabTitleText>{t("mappers")}</TabTitleText>}
|
||||
{...mappersTab}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { TextControl } from "@keycloak/keycloak-ui-shared";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const SpiffeSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextControl
|
||||
name="alias"
|
||||
label={t("alias")}
|
||||
labelIcon={t("aliasHelp")}
|
||||
rules={{
|
||||
required: t("required"),
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
name="config.trustDomain"
|
||||
label={t("spiffeTrustDomain")}
|
||||
rules={{
|
||||
required: t("required"),
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
name="config.bundleEndpoint"
|
||||
label={t("spiffeBundleEndpoint")}
|
||||
rules={{
|
||||
required: t("required"),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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: <AddSpiffeConnect />,
|
||||
breadcrumb: (t) => t("addSpiffeProvider"),
|
||||
handle: {
|
||||
access: "manage-identity-providers",
|
||||
},
|
||||
};
|
||||
|
||||
export const toIdentityProviderSpiffe = (
|
||||
params: IdentityProviderSpiffeParams,
|
||||
): Partial<Path> => ({
|
||||
pathname: generateEncodedPath(IdentityProviderSpiffeRoute.path, params),
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
34
js/apps/admin-ui/test/identity-providers/spiffe.spec.ts
Normal file
34
js/apps/admin-ui/test/identity-providers/spiffe.spec.ts
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
],
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user