From 8ea09d38168c22937363cf77a07f9de5dc7b48b0 Mon Sep 17 00:00:00 2001 From: Daniel Gozalo <48915630+dgozalo@users.noreply.github.com> Date: Wed, 12 Jan 2022 14:27:24 +0100 Subject: [PATCH] [fixes #9222] - Let users configure Dynamic Client Scopes (#9327) --- .../java/org/keycloak/common/Profile.java | 3 +- .../java/org/keycloak/common/ProfileTest.java | 4 +- ...HelpCommandTest.testBuildHelp.approved.txt | 2 + ...pCommandTest.testBuildHelpAll.approved.txt | 2 + ...mmandTest.testStartDevHelpAll.approved.txt | 2 + .../delegate/ClientModelLazyDelegate.java | 15 +++ .../org/keycloak/models/ClientScopeModel.java | 15 +++ .../resources/admin/ClientScopeResource.java | 48 ++++++++- .../resources/admin/ClientScopesResource.java | 2 +- .../admin/client/ClientScopeTest.java | 100 ++++++++++++++++++ .../messages/admin-messages_en.properties | 4 + .../admin/resources/js/controllers/clients.js | 22 ++++ .../partials/client-scope-detail.html | 18 ++++ 13 files changed, 232 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index e778083fb3..faad755025 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -64,7 +64,8 @@ public class Profile { CIBA(Type.DEFAULT), MAP_STORAGE(Type.EXPERIMENTAL), PAR(Type.DEFAULT), - DECLARATIVE_USER_PROFILE(Type.PREVIEW); + DECLARATIVE_USER_PROFILE(Type.PREVIEW), + DYNAMIC_SCOPES(Type.EXPERIMENTAL); private final Type typeProject; private final Type typeProduct; diff --git a/common/src/test/java/org/keycloak/common/ProfileTest.java b/common/src/test/java/org/keycloak/common/ProfileTest.java index 696c10a268..8fc41c6a99 100644 --- a/common/src/test/java/org/keycloak/common/ProfileTest.java +++ b/common/src/test/java/org/keycloak/common/ProfileTest.java @@ -21,7 +21,7 @@ public class ProfileTest { @Test public void checkDefaultsKeycloak() { Assert.assertEquals("community", Profile.getName()); - assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE); + assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE); assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE); assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS); @@ -37,7 +37,7 @@ public class ProfileTest { Profile.init(); Assert.assertEquals("product", Profile.getName()); - assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE); + assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE); assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN, Profile.Feature.DECLARATIVE_USER_PROFILE); assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS); diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.approved.txt index 1ff9d090aa..9f2a0a9dfa 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.approved.txt @@ -60,6 +60,8 @@ Feature: Enables the DECLARATIVE_USER_PROFILE feature. --features-docker Enables the DOCKER feature. +--features-dynamic_scopes + Enables the DYNAMIC_SCOPES feature. --features-impersonation Enables the IMPERSONATION feature. --features-map_storage diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelpAll.approved.txt index 28ee3462bf..f4e04a7595 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelpAll.approved.txt @@ -83,6 +83,8 @@ Feature: Enables the DECLARATIVE_USER_PROFILE feature. --features-docker Enables the DOCKER feature. +--features-dynamic_scopes + Enables the DYNAMIC_SCOPES feature. --features-impersonation Enables the IMPERSONATION feature. --features-map_storage diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.approved.txt index 82b0b47805..796ed1d9ec 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.approved.txt @@ -72,6 +72,8 @@ Feature: Enables the DECLARATIVE_USER_PROFILE feature. --features-docker Enables the DOCKER feature. +--features-dynamic_scopes + Enables the DYNAMIC_SCOPES feature. --features-impersonation Enables the IMPERSONATION feature. --features-map_storage diff --git a/server-spi-private/src/main/java/org/keycloak/models/delegate/ClientModelLazyDelegate.java b/server-spi-private/src/main/java/org/keycloak/models/delegate/ClientModelLazyDelegate.java index 4e03535554..2edf4737e9 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/delegate/ClientModelLazyDelegate.java +++ b/server-spi-private/src/main/java/org/keycloak/models/delegate/ClientModelLazyDelegate.java @@ -508,6 +508,21 @@ public class ClientModelLazyDelegate implements ClientModel { getDelegate().setIncludeInTokenScope(includeInTokenScope); } + @Override + public boolean isDynamicScope() { + return getDelegate().isDynamicScope(); + } + + @Override + public void setIsDynamicScope(boolean isDynamicScope) { + getDelegate().setIsDynamicScope(isDynamicScope); + } + + @Override + public String getDynamicScopeRegexp() { + return getDelegate().getDynamicScopeRegexp(); + } + @Override public Set getScopeMappings() { return getDelegate().getScopeMappings(); diff --git a/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java b/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java index d2f9c56fe4..22e542bad6 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java @@ -18,6 +18,7 @@ package org.keycloak.models; import java.util.Map; +import java.util.Optional; import org.keycloak.common.util.ObjectUtil; import org.keycloak.provider.ProviderEvent; @@ -67,6 +68,8 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon String CONSENT_SCREEN_TEXT = "consent.screen.text"; String GUI_ORDER = "gui.order"; String INCLUDE_IN_TOKEN_SCOPE = "include.in.token.scope"; + String IS_DYNAMIC_SCOPE = "is.dynamic.scope"; + String DYNAMIC_SCOPE_REGEXP = "dynamic.scope.regexp"; default boolean isDisplayOnConsentScreen() { String displayVal = getAttribute(DISPLAY_ON_CONSENT_SCREEN); @@ -107,4 +110,16 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon default void setIncludeInTokenScope(boolean includeInTokenScope) { setAttribute(INCLUDE_IN_TOKEN_SCOPE, String.valueOf(includeInTokenScope)); } + + default boolean isDynamicScope() { + return Optional.ofNullable(getAttribute(IS_DYNAMIC_SCOPE)).isPresent(); + } + + default void setIsDynamicScope(boolean isDynamicScope) { + setAttribute(IS_DYNAMIC_SCOPE, String.valueOf(isDynamicScope)); + } + + default String getDynamicScopeRegexp() { + return getAttribute(DYNAMIC_SCOPE_REGEXP); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java index 696820c0d5..abf27dc9a2 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java @@ -19,6 +19,8 @@ package org.keycloak.services.resources.admin; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.common.Profile; +import org.keycloak.events.Errors; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.ClientScopeModel; @@ -29,7 +31,9 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.ClientScopeRepresentation; +import org.keycloak.saml.common.util.StringUtil; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.ErrorResponseException; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import javax.ws.rs.Consumes; @@ -41,6 +45,10 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + /** * Base resource class for managing one particular client of a realm. @@ -56,6 +64,7 @@ public class ClientScopeResource { private AdminEventBuilder adminEvent; protected ClientScopeModel clientScope; protected KeycloakSession session; + protected static Pattern dynamicScreenPattern = Pattern.compile("[^\\s\\*]*\\*{1}[^\\s\\*]*"); public ClientScopeResource(RealmModel realm, AdminPermissionEvaluator auth, ClientScopeModel clientScope, KeycloakSession session, AdminEventBuilder adminEvent) { this.realm = realm; @@ -96,7 +105,7 @@ public class ClientScopeResource { @Consumes(MediaType.APPLICATION_JSON) public Response update(final ClientScopeRepresentation rep) { auth.clients().requireManageClientScopes(); - + validateDynamicClientScope(rep); try { RepresentationToModel.updateClientScope(rep, clientScope); adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success(); @@ -143,4 +152,41 @@ public class ClientScopeResource { return ErrorResponse.error(me.getMessage(), Response.Status.BAD_REQUEST); } } + + /** + * Performs some validation based on attributes combinations and format. + * Validations differ based on whether the DYNAMIC_SCOPES feature is enabled or not + * @param clientScope + * @throws ErrorResponseException + */ + public static void validateDynamicClientScope(ClientScopeRepresentation clientScope) throws ErrorResponseException { + if(clientScope.getAttributes() == null) { + return; + } + boolean isDynamic = Boolean.parseBoolean(clientScope.getAttributes().get(ClientScopeModel.IS_DYNAMIC_SCOPE)); + String regexp = clientScope.getAttributes().get(ClientScopeModel.DYNAMIC_SCOPE_REGEXP); + if(Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES)) { + // if the scope is dynamic but the regexp is empty, it's not considered valid + if(isDynamic && StringUtil.isNullOrEmpty(regexp)) { + throw new ErrorResponseException(ErrorResponse.error("Dynamic scope regexp must not be null or empty", Response.Status.BAD_REQUEST)); + } + // Always validate the dynamic scope regexp to avoid inserting a wrong value even when the feature is disabled + if(!StringUtil.isNullOrEmpty(regexp) && !dynamicScreenPattern.matcher(regexp).matches()) { + throw new ErrorResponseException(ErrorResponse.error(String.format("Invalid format for the Dynamic Scope regexp %1s", regexp), Response.Status.BAD_REQUEST)); + } + } else { + // if the value is not null or empty we won't accept the request as the feature is disabled + Optional.ofNullable(regexp).ifPresent(s -> { + if(!s.isEmpty()) { + throw new ErrorResponseException(ErrorResponse.error(String.format("Unexpected value \"%1s\" for attribute %2s in ClientScope", + regexp, ClientScopeModel.DYNAMIC_SCOPE_REGEXP), Response.Status.BAD_REQUEST)); + } + }); + // If isDynamic is true, we won't accept the request as the feature is disabled + if(isDynamic) { + throw new ErrorResponseException(ErrorResponse.error(String.format("Unexpected value \"%1s\" for attribute %2s in ClientScope", + isDynamic, ClientScopeModel.IS_DYNAMIC_SCOPE), Response.Status.BAD_REQUEST)); + } + } + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java index c23d1d9c96..6045cdfea3 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java @@ -94,7 +94,7 @@ public class ClientScopesResource { @NoCache public Response createClientScope(ClientScopeRepresentation rep) { auth.clients().requireManageClientScopes(); - + ClientScopeResource.validateDynamicClientScope(rep); try { ClientScopeModel clientModel = RepresentationToModel.createClientScope(session, realm, rep); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java index 5f9ed8fb59..8cbd43db1a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java @@ -24,10 +24,12 @@ import org.keycloak.admin.client.resource.ClientScopesResource; import org.keycloak.admin.client.resource.ProtocolMappersResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RoleMappingResource; +import org.keycloak.common.Profile; import org.keycloak.common.util.ObjectUtil; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.AccountRoles; +import org.keycloak.models.ClientScopeModel; import org.keycloak.models.Constants; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.saml.SamlProtocol; @@ -38,15 +40,19 @@ import org.keycloak.representations.idm.MappingsRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.DisableFeature; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.Matchers; import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.util.JsonSerialization; import javax.ws.rs.ClientErrorException; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -58,6 +64,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.keycloak.testsuite.Assert.assertNames; /** @@ -653,6 +660,99 @@ public class ClientScopeTest extends AbstractClientTest { testRealmResource().clients().get(clientUuid).update(clientRep); } + @Test + @EnableFeature(value = Profile.Feature.DYNAMIC_SCOPES, skipRestart = true) + public void testCreateValidDynamicScope() { + ClientScopeRepresentation scopeRep = new ClientScopeRepresentation(); + scopeRep.setName("dynamic-scope-def"); + scopeRep.setProtocol("openid-connect"); + scopeRep.setAttributes(new HashMap(){{ + put(ClientScopeModel.IS_DYNAMIC_SCOPE, "true"); + put(ClientScopeModel.DYNAMIC_SCOPE_REGEXP, "dynamic-scope-def:*"); + }}); + String scopeDefId = createClientScope(scopeRep); + getCleanup().addClientScopeId(scopeDefId); + + // Assert updated attributes + scopeRep = clientScopes().get(scopeDefId).toRepresentation(); + assertEquals("dynamic-scope-def", scopeRep.getName()); + assertEquals("true", scopeRep.getAttributes().get(ClientScopeModel.IS_DYNAMIC_SCOPE)); + assertEquals("dynamic-scope-def:*", scopeRep.getAttributes().get(ClientScopeModel.DYNAMIC_SCOPE_REGEXP)); + } + + @Test + @EnableFeature(value = Profile.Feature.DYNAMIC_SCOPES, skipRestart = true) + public void testCreateNonDynamicScopeWithFeatureEnabled() { + ClientScopeRepresentation scopeRep = new ClientScopeRepresentation(); + scopeRep.setName("non-dynamic-scope-def"); + scopeRep.setProtocol("openid-connect"); + scopeRep.setAttributes(new HashMap(){{ + put(ClientScopeModel.IS_DYNAMIC_SCOPE, "false"); + put(ClientScopeModel.DYNAMIC_SCOPE_REGEXP, ""); + }}); + String scopeDefId = createClientScope(scopeRep); + getCleanup().addClientScopeId(scopeDefId); + + // Assert updated attributes + scopeRep = clientScopes().get(scopeDefId).toRepresentation(); + assertEquals("non-dynamic-scope-def", scopeRep.getName()); + assertEquals("false", scopeRep.getAttributes().get(ClientScopeModel.IS_DYNAMIC_SCOPE)); + assertEquals("", scopeRep.getAttributes().get(ClientScopeModel.DYNAMIC_SCOPE_REGEXP)); + } + + @Test + @DisableFeature(value = Profile.Feature.DYNAMIC_SCOPES, skipRestart = true) + public void testCreateDynamicScopeWithFeatureDisabledAndIsDynamicScopeTrue() { + ClientScopeRepresentation scopeRep = new ClientScopeRepresentation(); + scopeRep.setName("non-dynamic-scope-def2"); + scopeRep.setProtocol("openid-connect"); + scopeRep.setAttributes(new HashMap(){{ + put(ClientScopeModel.IS_DYNAMIC_SCOPE, "true"); + put(ClientScopeModel.DYNAMIC_SCOPE_REGEXP, ""); + }}); + handleExpectedCreateFailure(scopeRep, 400, "Unexpected value \"true\" for attribute is.dynamic.scope in ClientScope"); + } + + @Test + @DisableFeature(value = Profile.Feature.DYNAMIC_SCOPES, skipRestart = true) + public void testCreateDynamicScopeWithFeatureDisabledAndNonEmptyDynamicScopeRegexp() { + ClientScopeRepresentation scopeRep = new ClientScopeRepresentation(); + scopeRep.setName("non-dynamic-scope-def3"); + scopeRep.setProtocol("openid-connect"); + scopeRep.setAttributes(new HashMap(){{ + put(ClientScopeModel.IS_DYNAMIC_SCOPE, "false"); + put(ClientScopeModel.DYNAMIC_SCOPE_REGEXP, "not-empty"); + }}); + handleExpectedCreateFailure(scopeRep, 400, "Unexpected value \"not-empty\" for attribute dynamic.scope.regexp in ClientScope"); + } + + @Test + @EnableFeature(value = Profile.Feature.DYNAMIC_SCOPES, skipRestart = true) + public void testCreateInvalidRegexpDynamicScope() { + ClientScopeRepresentation scopeRep = new ClientScopeRepresentation(); + scopeRep.setName("dynamic-scope-def4"); + scopeRep.setProtocol("openid-connect"); + scopeRep.setAttributes(new HashMap(){{ + put(ClientScopeModel.IS_DYNAMIC_SCOPE, "true"); + put(ClientScopeModel.DYNAMIC_SCOPE_REGEXP, "dynamic-scope-def:*:*"); + }}); + handleExpectedCreateFailure(scopeRep, 400, "Invalid format for the Dynamic Scope regexp dynamic-scope-def:*:*"); + } + + private void handleExpectedCreateFailure(ClientScopeRepresentation scopeRep, int expectedErrorCode, String expectedErrorMessage) { + try(Response resp = clientScopes().create(scopeRep)) { + Assert.assertEquals(expectedErrorCode, resp.getStatus()); + String respBody = resp.readEntity(String.class); + Map responseJson = null; + try { + responseJson = JsonSerialization.readValue(respBody, Map.class); + Assert.assertEquals(expectedErrorMessage, responseJson.get("errorMessage")); + } catch (IOException e) { + fail("Failed to extract the errorMessage from a CreateScope Response"); + } + } + } + private ClientScopesResource clientScopes() { return testRealmResource().clientScopes(); } diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 1e315e5024..daa196b814 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -1163,6 +1163,10 @@ client-scope.gui-order=GUI order client-scope.gui-order.tooltip=Specify order of the provider in GUI (such as in Consent page) as integer client-scope.include-in-token-scope=Include In Token Scope client-scope.include-in-token-scope.tooltip=If on, the name of this client scope will be added to the access token property 'scope' as well as to the Token Introspection Endpoint response. If off, this client scope will be omitted from the token and from the Token Introspection Endpoint response. +client-scope.is-dynamic-scope=Dynamic Scope +client-scope.is-dynamic-scope.tooltip=If on, this scope will be considered a Dynamic Scope, which will be comprised of a static and a variable portion. +client-scope.dynamic-scope-regexp=Dynamic Scope Format +client-scope.dynamic-scope-regexp.tooltip=This is the regular expression that the system will use to extract the scope name and variable. add-user-federation-provider=Add user federation provider add-user-storage-provider=Add user storage provider diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 3aba1c39ef..d8e8c3df0a 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -3262,6 +3262,20 @@ module.controller('ClientScopeDetailCtrl', function($scope, realm, clientScope, $scope.displayOnConsentScreen = true; } + if(serverInfo.featureEnabled("DYNAMIC_SCOPES")) { + if ($scope.clientScope.attributes["is.dynamic.scope"]) { + if ($scope.clientScope.attributes["is.dynamic.scope"] === "true") { + $scope.isDynamicScope = true; + } else { + $scope.isDynamicScope = false; + } + } else { + $scope.isDynamicScope = false; + } + + $scope.clientScope.attributes["dynamic.scope.regexp"] = $scope.clientScope.name + ":*"; + } + if ($scope.clientScope.attributes["include.in.token.scope"]) { if ($scope.clientScope.attributes["include.in.token.scope"] == "true") { $scope.includeInTokenScope = true; @@ -3320,6 +3334,14 @@ module.controller('ClientScopeDetailCtrl', function($scope, realm, clientScope, $scope.clientScope.attributes["display.on.consent.screen"] = "false"; } + if(serverInfo.featureEnabled("DYNAMIC_SCOPES")) { + if ($scope.isDynamicScope === true) { + $scope.clientScope.attributes["is.dynamic.scope"] = "true"; + } else { + $scope.clientScope.attributes["is.dynamic.scope"] = "false"; + } + } + if ($scope.includeInTokenScope == true) { $scope.clientScope.attributes["include.in.token.scope"] = "true"; } else { diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html index 88c3216d82..9f1ba99aee 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html @@ -17,6 +17,24 @@ {{:: 'client-scope.name.tooltip' | translate}} +
+ +
+ +
+ {{:: 'client-scope.is-dynamic-scope.tooltip' | translate}} +
+
+ +
+ +
+ {{:: 'client-scope.dynamic-scope-regexp.tooltip' | translate}} +