diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index de4bf382e5f..cb6aa323ea2 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -264,13 +264,22 @@ public class RealmRepresentation { return scopeMappings; } - public ScopeMappingRepresentation scopeMapping(String username) { + public ScopeMappingRepresentation clientScopeMapping(String clientName) { ScopeMappingRepresentation mapping = new ScopeMappingRepresentation(); - mapping.setClient(username); + mapping.setClient(clientName); if (scopeMappings == null) scopeMappings = new ArrayList(); scopeMappings.add(mapping); return mapping; } + + public ScopeMappingRepresentation clientTemplateScopeMapping(String clientTemplateName) { + ScopeMappingRepresentation mapping = new ScopeMappingRepresentation(); + mapping.setClientTemplate(clientTemplateName); + if (scopeMappings == null) scopeMappings = new ArrayList(); + scopeMappings.add(mapping); + return mapping; + } + @Deprecated public Set getRequiredCredentials() { return requiredCredentials; diff --git a/core/src/main/java/org/keycloak/representations/idm/ScopeMappingRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ScopeMappingRepresentation.java index 0300bd40810..f8e2a7b361f 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ScopeMappingRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ScopeMappingRepresentation.java @@ -27,6 +27,7 @@ import java.util.Set; public class ScopeMappingRepresentation { protected String self; // link protected String client; + protected String clientTemplate; protected Set roles; public String getSelf() { @@ -45,6 +46,14 @@ public class ScopeMappingRepresentation { this.client = client; } + public String getClientTemplate() { + return clientTemplate; + } + + public void setClientTemplate(String clientTemplate) { + this.clientTemplate = clientTemplate; + } + public Set getRoles() { return roles; } diff --git a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 98ac227123d..e0dc211448d 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -632,5 +632,15 @@ public final class KeycloakModelUtils { return false; } + public static ClientTemplateModel getClientTemplateByName(RealmModel realm, String templateName) { + for (ClientTemplateModel clientTemplate : realm.getClientTemplates()) { + if (templateName.equals(clientTemplate.getName())) { + return clientTemplate; + } + } + + return null; + } + } diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index e6bb3c58c23..24e3ac8b6aa 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -42,6 +42,7 @@ import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.ScopeContainerModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; @@ -250,16 +251,14 @@ public class RepresentationToModel { if (rep.getScopeMappings() != null) { for (ScopeMappingRepresentation scope : rep.getScopeMappings()) { - ClientModel client = newRealm.getClientByClientId(scope.getClient()); - if (client == null) { - throw new RuntimeException("Unknown client specification in realm scope mappings"); - } + ScopeContainerModel scopeContainer = getScopeContainerHavingScope(newRealm, scope); + for (String roleString : scope.getRoles()) { RoleModel role = newRealm.getRole(roleString.trim()); if (role == null) { role = newRealm.addRole(roleString.trim()); } - client.addScopeMapping(role); + scopeContainer.addScopeMapping(role); } } @@ -1205,20 +1204,36 @@ public class RepresentationToModel { public static void createClientScopeMappings(RealmModel realm, ClientModel clientModel, List mappings) { for (ScopeMappingRepresentation mapping : mappings) { - ClientModel client = realm.getClientByClientId(mapping.getClient()); - if (client == null) { - throw new RuntimeException("Unknown client specified in client scope mappings"); - } + ScopeContainerModel scopeContainer = getScopeContainerHavingScope(realm, mapping); + for (String roleString : mapping.getRoles()) { RoleModel role = clientModel.getRole(roleString.trim()); if (role == null) { role = clientModel.addRole(roleString.trim()); } - client.addScopeMapping(role); + scopeContainer.addScopeMapping(role); } } } + private static ScopeContainerModel getScopeContainerHavingScope(RealmModel realm, ScopeMappingRepresentation scope) { + if (scope.getClient() != null) { + ClientModel client = realm.getClientByClientId(scope.getClient()); + if (client == null) { + throw new RuntimeException("Unknown client specification in scope mappings: " + scope.getClient()); + } + return client; + } else if (scope.getClientTemplate() != null) { + ClientTemplateModel clientTemplate = KeycloakModelUtils.getClientTemplateByName(realm, scope.getClientTemplate()); + if (clientTemplate == null) { + throw new RuntimeException("Unknown clientTemplate specification in scope mappings: " + scope.getClientTemplate()); + } + return clientTemplate; + } else { + throw new RuntimeException("Either client or clientTemplate needs to be specified in scope mappings"); + } + } + // Users public static UserModel createUser(KeycloakSession session, RealmModel newRealm, UserRepresentation userRep) { diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index 72cabb5259a..cf0c5ca2d9b 100755 --- a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -88,13 +88,14 @@ public class ExportUtils { List allClients = new ArrayList<>(clients); Map> clientScopeReps = new HashMap<>(); + // Scopes of clients for (ClientModel client : allClients) { Set clientScopes = client.getScopeMappings(); ScopeMappingRepresentation scopeMappingRep = null; for (RoleModel scope : clientScopes) { if (scope.getContainer() instanceof RealmModel) { if (scopeMappingRep == null) { - scopeMappingRep = rep.scopeMapping(client.getClientId()); + scopeMappingRep = rep.clientScopeMapping(client.getClientId()); } scopeMappingRep.role(scope.getName()); } else { @@ -108,7 +109,7 @@ public class ExportUtils { ScopeMappingRepresentation currentClientScope = null; for (ScopeMappingRepresentation scopeMapping : currentAppScopes) { - if (scopeMapping.getClient().equals(client.getClientId())) { + if (client.getClientId().equals(scopeMapping.getClient())) { currentClientScope = scopeMapping; break; } @@ -123,6 +124,42 @@ public class ExportUtils { } } + // Scopes of client templates + for (ClientTemplateModel clientTemplate : realm.getClientTemplates()) { + Set clientScopes = clientTemplate.getScopeMappings(); + ScopeMappingRepresentation scopeMappingRep = null; + for (RoleModel scope : clientScopes) { + if (scope.getContainer() instanceof RealmModel) { + if (scopeMappingRep == null) { + scopeMappingRep = rep.clientTemplateScopeMapping(clientTemplate.getName()); + } + scopeMappingRep.role(scope.getName()); + } else { + ClientModel app = (ClientModel)scope.getContainer(); + String appName = app.getClientId(); + List currentAppScopes = clientScopeReps.get(appName); + if (currentAppScopes == null) { + currentAppScopes = new ArrayList<>(); + clientScopeReps.put(appName, currentAppScopes); + } + + ScopeMappingRepresentation currentClientTemplateScope = null; + for (ScopeMappingRepresentation scopeMapping : currentAppScopes) { + if (clientTemplate.getName().equals(scopeMapping.getClientTemplate())) { + currentClientTemplateScope = scopeMapping; + break; + } + } + if (currentClientTemplateScope == null) { + currentClientTemplateScope = new ScopeMappingRepresentation(); + currentClientTemplateScope.setClientTemplate(clientTemplate.getName()); + currentAppScopes.add(currentClientTemplateScope); + } + currentClientTemplateScope.role(scope.getName()); + } + } + } + if (clientScopeReps.size() > 0) { rep.setClientScopeMappings(clientScopeReps); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java index 2d1f54fea56..49580b1fe77 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java @@ -136,7 +136,7 @@ public class ClientTemplateResource { @NoCache public Response deleteClientTemplate() { auth.requireManage(); - + try { realm.removeClientTemplate(template.getId()); adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CompositeRolesModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CompositeRolesModelTest.java index 6a6c52d132c..038c14802e6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CompositeRolesModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CompositeRolesModelTest.java @@ -58,7 +58,7 @@ public class CompositeRolesModelTest extends AbstractModelTest { RealmRepresentation rep = AbstractModelTest.loadJson("model/testrealm-noclient-id.json"); rep.setId("TestNoClientID"); expectedException.expect(RuntimeException.class); - expectedException.expectMessage("Unknown client specified in client scope mappings"); + expectedException.expectMessage("Unknown client specification in scope mappings: some-client"); manager.importRealm(rep); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index 0f026664372..136fc691750 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -26,6 +26,7 @@ import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper; import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.Constants; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; @@ -321,13 +322,32 @@ public class ImportTest extends AbstractModelTest { Assert.assertEquals(1, otherApp.getProtocolMappers().size()); Assert.assertNull(otherApp.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, "username")); ProtocolMapperModel gssCredentialMapper = otherApp.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME); - Assert.assertEquals(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, gssCredentialMapper.getName()); - Assert.assertEquals( OIDCLoginProtocol.LOGIN_PROTOCOL, gssCredentialMapper.getProtocol()); - Assert.assertEquals(UserSessionNoteMapper.PROVIDER_ID, gssCredentialMapper.getProtocolMapper()); - String includeInAccessToken = gssCredentialMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN); - String includeInIdToken = gssCredentialMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN); - Assert.assertTrue(includeInAccessToken.equalsIgnoreCase("true")); - Assert.assertTrue(includeInIdToken == null || Boolean.parseBoolean(includeInIdToken) == false); + assertGssProtocolMapper(gssCredentialMapper); + + // Test clientTemplates + List clientTemplates = realm.getClientTemplates(); + Assert.assertEquals(1, clientTemplates.size()); + ClientTemplateModel clientTemplate = clientTemplates.get(0); + Assert.assertEquals("foo-template", clientTemplate.getName()); + Assert.assertEquals("foo-template-desc", clientTemplate.getDescription()); + Assert.assertEquals(OIDCLoginProtocol.LOGIN_PROTOCOL, clientTemplate.getProtocol()); + Assert.assertEquals(1, clientTemplate.getProtocolMappers().size()); + ProtocolMapperModel templateGssCredentialMapper = clientTemplate.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME); + assertGssProtocolMapper(templateGssCredentialMapper); + + // Test client template scopes + Set allClientTemplateScopes = clientTemplate.getScopeMappings(); + Assert.assertEquals(3, allClientTemplateScopes.size()); + Assert.assertTrue(allClientTemplateScopes.contains(realm.getRole("admin"))); + Assert.assertTrue(allClientTemplateScopes.contains(application.getRole("app-user"))); + Assert.assertTrue(allClientTemplateScopes.contains(application.getRole("app-admin"))); + + Set clientTemplateRealmScopes = clientTemplate.getRealmScopeMappings(); + Assert.assertTrue(clientTemplateRealmScopes.contains(realm.getRole("admin"))); + + Set clientTemplateAppScopes = KeycloakModelUtils.getClientScopeMappings(application, clientTemplate);//application.getClientScopeMappings(oauthClient); + Assert.assertTrue(clientTemplateAppScopes.contains(application.getRole("app-user"))); + Assert.assertTrue(clientTemplateAppScopes.contains(application.getRole("app-admin"))); // Test user consents admin = session.users().getUserByUsername("admin", realm); @@ -380,4 +400,14 @@ public class ImportTest extends AbstractModelTest { Assert.assertEquals(expectedType, requiredCreds.get(0).getType()); } + private static void assertGssProtocolMapper(ProtocolMapperModel gssCredentialMapper) { + Assert.assertEquals(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, gssCredentialMapper.getName()); + Assert.assertEquals( OIDCLoginProtocol.LOGIN_PROTOCOL, gssCredentialMapper.getProtocol()); + Assert.assertEquals(UserSessionNoteMapper.PROVIDER_ID, gssCredentialMapper.getProtocolMapper()); + String includeInAccessToken = gssCredentialMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN); + String includeInIdToken = gssCredentialMapper.getConfig().get(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN); + Assert.assertTrue(includeInAccessToken.equalsIgnoreCase("true")); + Assert.assertTrue(includeInIdToken == null || Boolean.parseBoolean(includeInIdToken) == false); + } + } diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json index a08f486a7ad..e2c07c023c9 100755 --- a/testsuite/integration/src/test/resources/model/testrealm.json +++ b/testsuite/integration/src/test/resources/model/testrealm.json @@ -195,6 +195,28 @@ "secret": "clientpassword" } ], + "clientTemplates" : [ + { + "name" : "foo-template", + "description" : "foo-template-desc", + "protocol" : "openid-connect", + "protocolMappers" : [ + { + "name" : "gss delegation credential", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : true, + "consentText" : "gss delegation credential", + "config" : { + "user.session.note" : "gss_delegation_credential", + "access.token.claim" : "true", + "claim.name" : "gss_delegation_credential", + "Claim JSON Type" : "String" + } + } + ] + } + ], "roles" : { "realm" : [ { @@ -226,6 +248,10 @@ { "client": "oauthclient", "roles": ["admin"] + }, + { + "clientTemplate": "foo-template", + "roles": ["admin"] } ], "applicationScopeMappings": { @@ -233,6 +259,10 @@ { "client": "oauthclient", "roles": ["app-user"] + }, + { + "clientTemplate": "foo-template", + "roles": ["app-user", "app-admin" ] } ]