parent
93419a1797
commit
8ea09d3816
13 changed files with 232 additions and 5 deletions
|
@ -64,7 +64,8 @@ public class Profile {
|
||||||
CIBA(Type.DEFAULT),
|
CIBA(Type.DEFAULT),
|
||||||
MAP_STORAGE(Type.EXPERIMENTAL),
|
MAP_STORAGE(Type.EXPERIMENTAL),
|
||||||
PAR(Type.DEFAULT),
|
PAR(Type.DEFAULT),
|
||||||
DECLARATIVE_USER_PROFILE(Type.PREVIEW);
|
DECLARATIVE_USER_PROFILE(Type.PREVIEW),
|
||||||
|
DYNAMIC_SCOPES(Type.EXPERIMENTAL);
|
||||||
|
|
||||||
private final Type typeProject;
|
private final Type typeProject;
|
||||||
private final Type typeProduct;
|
private final Type typeProduct;
|
||||||
|
|
|
@ -21,7 +21,7 @@ public class ProfileTest {
|
||||||
@Test
|
@Test
|
||||||
public void checkDefaultsKeycloak() {
|
public void checkDefaultsKeycloak() {
|
||||||
Assert.assertEquals("community", Profile.getName());
|
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.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);
|
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ public class ProfileTest {
|
||||||
Profile.init();
|
Profile.init();
|
||||||
|
|
||||||
Assert.assertEquals("product", Profile.getName());
|
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.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);
|
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,8 @@ Feature:
|
||||||
Enables the DECLARATIVE_USER_PROFILE feature.
|
Enables the DECLARATIVE_USER_PROFILE feature.
|
||||||
--features-docker <enabled|disabled>
|
--features-docker <enabled|disabled>
|
||||||
Enables the DOCKER feature.
|
Enables the DOCKER feature.
|
||||||
|
--features-dynamic_scopes <enabled|disabled>
|
||||||
|
Enables the DYNAMIC_SCOPES feature.
|
||||||
--features-impersonation <enabled|disabled>
|
--features-impersonation <enabled|disabled>
|
||||||
Enables the IMPERSONATION feature.
|
Enables the IMPERSONATION feature.
|
||||||
--features-map_storage <enabled|disabled>
|
--features-map_storage <enabled|disabled>
|
||||||
|
|
|
@ -83,6 +83,8 @@ Feature:
|
||||||
Enables the DECLARATIVE_USER_PROFILE feature.
|
Enables the DECLARATIVE_USER_PROFILE feature.
|
||||||
--features-docker <enabled|disabled>
|
--features-docker <enabled|disabled>
|
||||||
Enables the DOCKER feature.
|
Enables the DOCKER feature.
|
||||||
|
--features-dynamic_scopes <enabled|disabled>
|
||||||
|
Enables the DYNAMIC_SCOPES feature.
|
||||||
--features-impersonation <enabled|disabled>
|
--features-impersonation <enabled|disabled>
|
||||||
Enables the IMPERSONATION feature.
|
Enables the IMPERSONATION feature.
|
||||||
--features-map_storage <enabled|disabled>
|
--features-map_storage <enabled|disabled>
|
||||||
|
|
|
@ -72,6 +72,8 @@ Feature:
|
||||||
Enables the DECLARATIVE_USER_PROFILE feature.
|
Enables the DECLARATIVE_USER_PROFILE feature.
|
||||||
--features-docker <enabled|disabled>
|
--features-docker <enabled|disabled>
|
||||||
Enables the DOCKER feature.
|
Enables the DOCKER feature.
|
||||||
|
--features-dynamic_scopes <enabled|disabled>
|
||||||
|
Enables the DYNAMIC_SCOPES feature.
|
||||||
--features-impersonation <enabled|disabled>
|
--features-impersonation <enabled|disabled>
|
||||||
Enables the IMPERSONATION feature.
|
Enables the IMPERSONATION feature.
|
||||||
--features-map_storage <enabled|disabled>
|
--features-map_storage <enabled|disabled>
|
||||||
|
|
|
@ -508,6 +508,21 @@ public class ClientModelLazyDelegate implements ClientModel {
|
||||||
getDelegate().setIncludeInTokenScope(includeInTokenScope);
|
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
|
@Override
|
||||||
public Set<RoleModel> getScopeMappings() {
|
public Set<RoleModel> getScopeMappings() {
|
||||||
return getDelegate().getScopeMappings();
|
return getDelegate().getScopeMappings();
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.models;
|
package org.keycloak.models;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.provider.ProviderEvent;
|
import org.keycloak.provider.ProviderEvent;
|
||||||
|
@ -67,6 +68,8 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon
|
||||||
String CONSENT_SCREEN_TEXT = "consent.screen.text";
|
String CONSENT_SCREEN_TEXT = "consent.screen.text";
|
||||||
String GUI_ORDER = "gui.order";
|
String GUI_ORDER = "gui.order";
|
||||||
String INCLUDE_IN_TOKEN_SCOPE = "include.in.token.scope";
|
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() {
|
default boolean isDisplayOnConsentScreen() {
|
||||||
String displayVal = getAttribute(DISPLAY_ON_CONSENT_SCREEN);
|
String displayVal = getAttribute(DISPLAY_ON_CONSENT_SCREEN);
|
||||||
|
@ -107,4 +110,16 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon
|
||||||
default void setIncludeInTokenScope(boolean includeInTokenScope) {
|
default void setIncludeInTokenScope(boolean includeInTokenScope) {
|
||||||
setAttribute(INCLUDE_IN_TOKEN_SCOPE, String.valueOf(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ package org.keycloak.services.resources.admin;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
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.OperationType;
|
||||||
import org.keycloak.events.admin.ResourceType;
|
import org.keycloak.events.admin.ResourceType;
|
||||||
import org.keycloak.models.ClientScopeModel;
|
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.ModelToRepresentation;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||||
|
import org.keycloak.saml.common.util.StringUtil;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
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.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
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.
|
* Base resource class for managing one particular client of a realm.
|
||||||
|
@ -56,6 +64,7 @@ public class ClientScopeResource {
|
||||||
private AdminEventBuilder adminEvent;
|
private AdminEventBuilder adminEvent;
|
||||||
protected ClientScopeModel clientScope;
|
protected ClientScopeModel clientScope;
|
||||||
protected KeycloakSession session;
|
protected KeycloakSession session;
|
||||||
|
protected static Pattern dynamicScreenPattern = Pattern.compile("[^\\s\\*]*\\*{1}[^\\s\\*]*");
|
||||||
|
|
||||||
public ClientScopeResource(RealmModel realm, AdminPermissionEvaluator auth, ClientScopeModel clientScope, KeycloakSession session, AdminEventBuilder adminEvent) {
|
public ClientScopeResource(RealmModel realm, AdminPermissionEvaluator auth, ClientScopeModel clientScope, KeycloakSession session, AdminEventBuilder adminEvent) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
|
@ -96,7 +105,7 @@ public class ClientScopeResource {
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response update(final ClientScopeRepresentation rep) {
|
public Response update(final ClientScopeRepresentation rep) {
|
||||||
auth.clients().requireManageClientScopes();
|
auth.clients().requireManageClientScopes();
|
||||||
|
validateDynamicClientScope(rep);
|
||||||
try {
|
try {
|
||||||
RepresentationToModel.updateClientScope(rep, clientScope);
|
RepresentationToModel.updateClientScope(rep, clientScope);
|
||||||
adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success();
|
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);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ public class ClientScopesResource {
|
||||||
@NoCache
|
@NoCache
|
||||||
public Response createClientScope(ClientScopeRepresentation rep) {
|
public Response createClientScope(ClientScopeRepresentation rep) {
|
||||||
auth.clients().requireManageClientScopes();
|
auth.clients().requireManageClientScopes();
|
||||||
|
ClientScopeResource.validateDynamicClientScope(rep);
|
||||||
try {
|
try {
|
||||||
ClientScopeModel clientModel = RepresentationToModel.createClientScope(session, realm, rep);
|
ClientScopeModel clientModel = RepresentationToModel.createClientScope(session, realm, rep);
|
||||||
|
|
||||||
|
|
|
@ -24,10 +24,12 @@ import org.keycloak.admin.client.resource.ClientScopesResource;
|
||||||
import org.keycloak.admin.client.resource.ProtocolMappersResource;
|
import org.keycloak.admin.client.resource.ProtocolMappersResource;
|
||||||
import org.keycloak.admin.client.resource.RealmResource;
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.admin.client.resource.RoleMappingResource;
|
import org.keycloak.admin.client.resource.RoleMappingResource;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.events.admin.OperationType;
|
import org.keycloak.events.admin.OperationType;
|
||||||
import org.keycloak.events.admin.ResourceType;
|
import org.keycloak.events.admin.ResourceType;
|
||||||
import org.keycloak.models.AccountRoles;
|
import org.keycloak.models.AccountRoles;
|
||||||
|
import org.keycloak.models.ClientScopeModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.saml.SamlProtocol;
|
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.ProtocolMapperRepresentation;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
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.AdminEventPaths;
|
||||||
import org.keycloak.testsuite.util.ClientBuilder;
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
import org.keycloak.testsuite.util.Matchers;
|
import org.keycloak.testsuite.util.Matchers;
|
||||||
import org.keycloak.testsuite.util.RoleBuilder;
|
import org.keycloak.testsuite.util.RoleBuilder;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import javax.ws.rs.ClientErrorException;
|
import javax.ws.rs.ClientErrorException;
|
||||||
import javax.ws.rs.NotFoundException;
|
import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.Response.Status;
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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.assertFalse;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.testsuite.Assert.assertNames;
|
import static org.keycloak.testsuite.Assert.assertNames;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -653,6 +660,99 @@ public class ClientScopeTest extends AbstractClientTest {
|
||||||
testRealmResource().clients().get(clientUuid).update(clientRep);
|
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<String, String>(){{
|
||||||
|
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<String, String>(){{
|
||||||
|
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<String, String>(){{
|
||||||
|
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<String, String>(){{
|
||||||
|
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<String, String>(){{
|
||||||
|
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<String, String> 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() {
|
private ClientScopesResource clientScopes() {
|
||||||
return testRealmResource().clientScopes();
|
return testRealmResource().clientScopes();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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=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.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-federation-provider=Add user federation provider
|
||||||
add-user-storage-provider=Add user storage provider
|
add-user-storage-provider=Add user storage provider
|
||||||
|
|
|
@ -3262,6 +3262,20 @@ module.controller('ClientScopeDetailCtrl', function($scope, realm, clientScope,
|
||||||
$scope.displayOnConsentScreen = true;
|
$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"]) {
|
||||||
if ($scope.clientScope.attributes["include.in.token.scope"] == "true") {
|
if ($scope.clientScope.attributes["include.in.token.scope"] == "true") {
|
||||||
$scope.includeInTokenScope = true;
|
$scope.includeInTokenScope = true;
|
||||||
|
@ -3320,6 +3334,14 @@ module.controller('ClientScopeDetailCtrl', function($scope, realm, clientScope,
|
||||||
$scope.clientScope.attributes["display.on.consent.screen"] = "false";
|
$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) {
|
if ($scope.includeInTokenScope == true) {
|
||||||
$scope.clientScope.attributes["include.in.token.scope"] = "true";
|
$scope.clientScope.attributes["include.in.token.scope"] = "true";
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,6 +17,24 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'client-scope.name.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'client-scope.name.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group clearfix block" data-ng-show="serverInfo.featureEnabled('DYNAMIC_SCOPES') && protocol === 'openid-connect'">
|
||||||
|
<label class="col-md-2 control-label" for="isDynamicScope">{{:: 'client-scope.is-dynamic-scope' | translate}}</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input ng-model="isDynamicScope" ng-click="switchChange()" name="isDynamicScope" id="isDynamicScope" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'client-scope.is-dynamic-scope.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" data-ng-show="protocol === 'openid-connect' && isDynamicScope && serverInfo.featureEnabled('DYNAMIC_SCOPES')">
|
||||||
|
<label class="col-md-2 control-label" for="dynamicScopeRegExp">{{:: 'client-scope.dynamic-scope-regexp' | translate}} <span class="required">*</span></label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input class="form-control" ng-readonly="true" type="text"
|
||||||
|
ng-value="(isDynamicScope && serverInfo.featureEnabled('DYNAMIC_SCOPES')) ? clientScope.attributes['dynamic.scope.regexp'] = clientScope.name+':*' : ''"
|
||||||
|
id="dynamicScopeRegExp" name="dynamicScopeRegExp"
|
||||||
|
data-ng-model="clientScope.attributes['dynamic.scope.regexp']" autofocus ng-disabled="!isDynamicScope"
|
||||||
|
ng-required="isDynamicScope">
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'client-scope.dynamic-scope-regexp.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-2 control-label" for="description">{{:: 'description' | translate}} </label>
|
<label class="col-md-2 control-label" for="description">{{:: 'description' | translate}} </label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
|
Loading…
Reference in a new issue