diff --git a/src/App.tsx b/src/App.tsx index 7bc6be69f5..b42fa6bdc9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,14 @@ import React from "react"; -import { Page, PageSection } from "@patternfly/react-core"; +import { Page } from "@patternfly/react-core"; +import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; + import { Header } from "./PageHeader"; import { PageNav } from "./PageNav"; - import { Help } from "./components/help-enabler/HelpHeader"; -import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import { NewRealmForm } from "./realm/add/NewRealmForm"; import { NewClientForm } from "./clients/add/NewClientForm"; +import { NewClientScopeForm } from "./client-scopes/add/NewClientScopeForm"; + import { ImportForm } from "./clients/import/ImportForm"; import { ClientsSection } from "./clients/ClientsSection"; import { ClientScopesSection } from "./client-scopes/ClientScopesSection"; @@ -47,6 +49,11 @@ export const App = () => { path="/client-scopes" component={ClientScopesSection} > + { + const { t } = useTranslation("client-scopes"); + + const columns: (keyof ClientScopeRepresentation)[] = [ + "name", + "description", + "protocol", + ]; + + const data = clientScopes.map((c) => { + return { cells: columns.map((col) => c[col]) }; + }); + + return ( + <> + {}, + }, + { + title: t("common:delete"), + onClick: () => {}, + }, + ]} + aria-label={t("clientScopeList")} + > + + +
+ + ); +}; diff --git a/src/client-scopes/ClientScopesSection.tsx b/src/client-scopes/ClientScopesSection.tsx index e8210c6c64..da12b8cbdb 100644 --- a/src/client-scopes/ClientScopesSection.tsx +++ b/src/client-scopes/ClientScopesSection.tsx @@ -1,10 +1,53 @@ -import { PageSection } from "@patternfly/react-core"; -import React from "react"; +import React, { useContext, useState } from "react"; +import { Button, PageSection } from "@patternfly/react-core"; +import { useTranslation } from "react-i18next"; +import { useHistory } from "react-router-dom"; + +import { RealmContext } from "../components/realm-context/RealmContext"; +import { HttpClientContext } from "../http-service/HttpClientContext"; +import { ClientRepresentation } from "../realm/models/Realm"; +import { DataLoader } from "../components/data-loader/DataLoader"; +import { TableToolbar } from "../components/table-toolbar/TableToolbar"; +import { ClientScopeList } from "./ClientScopesList"; export const ClientScopesSection = () => { + const { t } = useTranslation("client-scopes"); + const history = useHistory(); + + const [max, setMax] = useState(10); + const [first, setFirst] = useState(0); + const httpClient = useContext(HttpClientContext)!; + const { realm } = useContext(RealmContext); + + const loader = async () => { + return await httpClient + .doGet(`/admin/realms/${realm}/client-scopes`, { params: { first, max } }) + .then((r) => r.data as ClientRepresentation[]); + }; return ( - <> - The Client Scopes Page - + + + {(scopes) => ( + { + setFirst(first); + setMax(max); + }} + toolbarItem={ + + } + > + + + )} + + ); }; diff --git a/src/client-scopes/__tests__/mock-client-scopes.json b/src/client-scopes/__tests__/mock-client-scopes.json new file mode 100644 index 0000000000..75457dedf9 --- /dev/null +++ b/src/client-scopes/__tests__/mock-client-scopes.json @@ -0,0 +1,485 @@ +[ + { + "id": "3507ed12-d8b0-455c-b91a-62a6765ecf0f", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "b0582082-abab-4c63-b3b7-a92afe6b3436", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "eb8c7985-5459-45a9-ace5-2959ce0fd1c9", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "348dfe5c-26e6-43e8-bc80-b7db9f842f24", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "bfe77908-4ca3-40ea-b5be-75bea87f5bb1", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "e604d76d-20ec-4d80-acee-1885af201568", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "63a71cf3-df7c-4a81-a23f-d3ba62801c72", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "5eb3444b-8e96-4267-9afc-20abd56613aa", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "2cdac876-a8ce-4cde-8bcb-00e28804ec91", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "3db88729-214e-4c71-8fac-ee744279538b", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "00ca4abc-fc26-4273-9d77-d7a793f38976", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "98885779-b84e-4565-bc1b-a0c703f03be0", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "82aca51c-22b4-4156-93a9-3ed33ec2adcc", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d974a079-8416-4dea-9e49-76dab694e836", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "2b0e5ec3-cc38-44c4-8851-98c0e3e3f60d", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "feef3c77-5a8e-4f22-94c8-fc606eb8dad0", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "e0340530-efde-4bdf-8399-c98b994e3c4f", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "bef63a97-20a4-4595-9e31-881273af8b47", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "3a4e571b-9ee4-4553-8a54-dcf0ab757b39", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "d0c55da1-f814-4bfe-a311-b34ddd7ee2fb", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "392fa527-96e9-41a5-8fa4-6deb1f3916a5", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "042e6e1e-f041-432f-88bc-79421366fb99", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "e269f729-2eca-4ff0-9caf-3baa4f6188c5", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "b3929aa6-6acf-4b13-9d23-ee459926feef", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "877d4b97-2520-40f7-9e58-cd99560a4637", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "f6aab00d-4b15-4ef3-a037-50d8a6c047ff", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "52483504-1da0-4645-8df0-d7ec36bf835a", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "dc401683-7876-4a01-a670-73deae0a10c2", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "4903f029-ca74-4447-b9ac-cf7799f2391c", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "715eec20-9d2b-45cf-b2c3-fd11aae96b63", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d1464021-822d-41d8-8195-d8962fe70f61", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "b85d197d-f195-4dcd-a873-77ee4ec9fcea", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "ef5b5c95-5236-41f1-ab9b-3e4213abbe76", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "30b4d89f-bfd9-45d4-b71f-01dd0f64da57", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "76f254c5-dc78-4048-abc9-c9de9d55f5a4", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + } +] diff --git a/src/client-scopes/add/NewClientScopeForm.tsx b/src/client-scopes/add/NewClientScopeForm.tsx new file mode 100644 index 0000000000..ad3e2d4812 --- /dev/null +++ b/src/client-scopes/add/NewClientScopeForm.tsx @@ -0,0 +1,181 @@ +import React, { useContext, useState } from "react"; +import { + ActionGroup, + AlertVariant, + Button, + Form, + FormGroup, + PageSection, + Select, + SelectVariant, + Switch, + TextInput, +} from "@patternfly/react-core"; +import { useTranslation } from "react-i18next"; +import { Controller, useForm } from "react-hook-form"; + +import { ClientScopeRepresentation } from "../models/client-scope"; +import { HelpItem } from "../../components/help-enabler/HelpItem"; +import { HttpClientContext } from "../../http-service/HttpClientContext"; +import { RealmContext } from "../../components/realm-context/RealmContext"; +import { useAlerts } from "../../components/alert/Alerts"; + +export const NewClientScopeForm = () => { + const { t } = useTranslation("client-scopes"); + const { register, control, handleSubmit } = useForm< + ClientScopeRepresentation + >(); + + const httpClient = useContext(HttpClientContext)!; + const { realm } = useContext(RealmContext); + + const [open, isOpen] = useState(false); + const [add, Alerts] = useAlerts(); + + const save = async (clientScopes: ClientScopeRepresentation) => { + try { + const keyValues = Object.keys(clientScopes.attributes!).map((key) => { + const newKey = key.replace(/_/g, "."); + return { [newKey]: clientScopes.attributes![key] }; + }); + clientScopes.attributes = Object.assign({}, ...keyValues); + + await httpClient.doPost( + `/admin/realms/${realm}/client-scopes`, + clientScopes + ); + add(t("createClientScopeSuccess"), AlertVariant.success); + } catch (error) { + add(`${t("createClientScopeError")} '${error}'`, AlertVariant.danger); + } + }; + + return ( + + +
+ + {t("name")} + + } + fieldId="kc-name" + isRequired + > + + + + {t("description")} + + } + fieldId="kc-description" + > + + + + {t("protocol")} + + } + fieldId="kc-protocol" + > + ( + + )} + /> + + + {t("displayOnConsentScreen")}{" "} + + + } + fieldId="kc-display.on.consent.screen" + > + ( + + )} + /> + + + {t("consentScreenText")}{" "} + + + } + fieldId="kc-consent-screen-text" + > + + + + {t("guiOrder")} + + } + fieldId="kc-gui-order" + > + + + + + + +
+
+ ); +}; diff --git a/src/client-scopes/messages.json b/src/client-scopes/messages.json new file mode 100644 index 0000000000..225c7bf4a2 --- /dev/null +++ b/src/client-scopes/messages.json @@ -0,0 +1,14 @@ +{ + "client-scopes": { + "createClientScope": "Create client scope", + "clientScopeList": "List of client scopes", + "name": "Name", + "description": "Description", + "protocol": "Protocol", + "createClientScopeSuccess": "Client scope created", + "createClientScopeError": "Could not create client scope:", + "displayOnConsentScreen": "Display on consent screen", + "consentScreenText": "Consent screen text", + "guiOrder": "GUI Order" + } +} diff --git a/src/client-scopes/models/client-scope.ts b/src/client-scopes/models/client-scope.ts new file mode 100644 index 0000000000..d44a532c3e --- /dev/null +++ b/src/client-scopes/models/client-scope.ts @@ -0,0 +1,24 @@ +/** + * https://www.keycloak.org/docs-api/4.1/rest-api/#_protocolmapperrepresentation + */ + +export interface ProtocolMapperRepresentation { + config?: Record; + id?: string; + name?: string; + protocol?: string; + protocolMapper?: string; +} + +/** + * https://www.keycloak.org/docs-api/6.0/rest-api/index.html#_clientscoperepresentation + */ + +export interface ClientScopeRepresentation { + attributes?: Record; + description?: string; + id?: string; + name?: string; + protocol?: string; + protocolMappers?: ProtocolMapperRepresentation[]; +} diff --git a/src/components/help-enabler/HelpItem.tsx b/src/components/help-enabler/HelpItem.tsx index f7ca0e3205..12be131d53 100644 --- a/src/components/help-enabler/HelpItem.tsx +++ b/src/components/help-enabler/HelpItem.tsx @@ -1,7 +1,8 @@ -import React from "react"; +import React, { useContext } from "react"; import { Tooltip } from "@patternfly/react-core"; import { HelpIcon } from "@patternfly/react-icons"; import { useTranslation } from "react-i18next"; +import { HelpContext } from "./HelpHeader"; type HelpItemProps = { item: string; @@ -9,11 +10,16 @@ type HelpItemProps = { export const HelpItem = ({ item }: HelpItemProps) => { const { t } = useTranslation(); + const { enabled } = useContext(HelpContext); return ( - - - - - + <> + {enabled && ( + + + + + + )} + ); }; diff --git a/src/help.json b/src/help.json index 0f72e894f3..9456b881fb 100644 --- a/src/help.json +++ b/src/help.json @@ -1,5 +1,13 @@ { "help": { - "storybook": "Sometimes you need some help and it's nice when the app does that" + "storybook": "Sometimes you need some help and it's nice when the app does that", + "clientScope": { + "name": "Name of the client scope. Must be unique in the realm. Name should not contain space characters as it is used as value of scope parameter", + "description": "Description of the client scope", + "protocol": "Which SSO protocol configuration is being supplied by this client scope", + "displayOnConsentScreen": "If on, and this client scope is added to some client with consent required, the text specified by 'Consent Screen Text' will be displayed on consent screen. If off, this client scope will not be displayed on the consent screen", + "consentScreenText": "Text that will be shown on the consent screen when this client scope is added to some client with consent required. Defaults to name of client scope if it is not filled", + "guiOrder": "Specify order of the provider in GUI (such as in Consent page) as integer" + } } } diff --git a/src/i18n.ts b/src/i18n.ts index 1453e643af..bea311de56 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -4,15 +4,15 @@ import { initReactI18next } from "react-i18next"; import common from "./common-messages.json"; import clients from "./clients/messages.json"; +import clientScopes from "./client-scopes/messages.json"; import realm from "./realm/messages.json"; import roles from "./realm-roles/messages.json"; import help from "./help.json"; const initOptions = { - ns: ["common", "help", "clients", "realm", "roles"], defaultNS: "common", resources: { - en: { ...common, ...help, ...clients, ...realm, ...roles }, + en: { ...common, ...help, ...clients, ...clientScopes, ...realm, ...roles }, }, lng: "en", fallbackLng: "en", diff --git a/src/realm/models/Realm.ts b/src/realm/models/Realm.ts index ff3fdb1935..7515973f01 100644 --- a/src/realm/models/Realm.ts +++ b/src/realm/models/Realm.ts @@ -1,3 +1,5 @@ +import { ClientScopeRepresentation } from "../../client-scopes/models/client-scope"; + export interface RealmRepresentation { id: string; realm: string; @@ -231,15 +233,6 @@ export interface ClientRepresentation { origin: string; } -export interface ClientScopeRepresentation { - id: string; - name: string; - description: string; - protocol: string; - attributes: { [index: string]: string }; - protocolMappers: ProtocolMapperRepresentation[]; -} - export interface UserFederationProviderRepresentation { id: string; displayName: string;