diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml old mode 100644 new mode 100755 index c4dce2b8f5..db2a791fcc --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.8.0.xml @@ -7,6 +7,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml index e415488be3..36f33bfe76 100755 --- a/connections/jpa/src/main/resources/META-INF/persistence.xml +++ b/connections/jpa/src/main/resources/META-INF/persistence.xml @@ -35,6 +35,7 @@ org.keycloak.models.jpa.entities.GroupAttributeEntity org.keycloak.models.jpa.entities.GroupRoleMappingEntity org.keycloak.models.jpa.entities.UserGroupMembershipEntity + org.keycloak.models.jpa.entities.ClientTemplateEntity org.keycloak.events.jpa.EventEntity diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java index a648f12a22..2384a2ed3b 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java @@ -40,6 +40,7 @@ public class ClientRepresentation { protected Integer nodeReRegistrationTimeout; protected Map registeredNodes; protected List protocolMappers; + protected String clientTemplate; public String getId() { return id; @@ -290,4 +291,11 @@ public class ClientRepresentation { this.protocolMappers = protocolMappers; } + public String getClientTemplate() { + return clientTemplate; + } + + public void setClientTemplate(String clientTemplate) { + this.clientTemplate = clientTemplate; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java new file mode 100755 index 0000000000..f0bf09e392 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/ClientTemplateRepresentation.java @@ -0,0 +1,61 @@ +package org.keycloak.representations.idm; + +import java.util.List; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientTemplateRepresentation { + /** + * Use this value in ClientRepresentation.setClientTemplate when you want to clear this value + */ + public static final String NONE = "NONE"; + protected String id; + protected String name; + protected String description; + protected String protocol; + protected List protocolMappers; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + + public List getProtocolMappers() { + return protocolMappers; + } + + public void setProtocolMappers(List protocolMappers) { + this.protocolMappers = protocolMappers; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } +} 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 bc9ebf81a0..cc57124991 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -65,6 +65,7 @@ public class RealmRepresentation { protected List scopeMappings; protected Map> clientScopeMappings; protected List clients; + protected List clientTemplates; protected Map browserSecurityHeaders; protected Map smtpServer; protected List userFederationProviders; @@ -804,4 +805,12 @@ public class RealmRepresentation { public void setGroups(List groups) { this.groups = groups; } + + public List getClientTemplates() { + return clientTemplates; + } + + public void setClientTemplates(List clientTemplates) { + this.clientTemplates = clientTemplates; + } } diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index 12b358e2db..f5dfcc2bae 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -7,6 +7,7 @@ import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.SerializationConfig; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -19,6 +20,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; @@ -47,6 +49,15 @@ public class ExportUtils { public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, boolean includeUsers) { RealmRepresentation rep = ModelToRepresentation.toRepresentation(realm, true); + // Client Templates + List templates = realm.getClientTemplates(); + List templateReps = new ArrayList<>(); + for (ClientTemplateModel app : templates) { + ClientTemplateRepresentation clientRep = ModelToRepresentation.toRepresentation(app); + templateReps.add(clientRep); + } + rep.setClientTemplates(templateReps); + // Clients List clients = realm.getClients(); List clientReps = new ArrayList<>(); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_ca.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_ca.properties old mode 100644 new mode 100755 diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_es.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_es.properties old mode 100644 new mode 100755 diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index bfbcee9c5b..826a3daa30 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -784,7 +784,7 @@ module.config([ '$routeProvider', function($routeProvider) { controller : 'AddBuiltinProtocolMapperCtrl' }) .when('/realms/:realm/clients/:client/mappers/:id', { - templateUrl : resourceUrl + '/partials/protocol-mapper-detail.html', + templateUrl : resourceUrl + '/partials/client-protocol-mapper-detail.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); @@ -803,7 +803,7 @@ module.config([ '$routeProvider', function($routeProvider) { controller : 'ClientProtocolMapperCtrl' }) .when('/create/client/:realm/:client/mappers', { - templateUrl : resourceUrl + '/partials/protocol-mapper-detail.html', + templateUrl : resourceUrl + '/partials/client-protocol-mapper-detail.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); @@ -817,6 +817,70 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ClientProtocolMapperCreateCtrl' }) + .when('/realms/:realm/client-templates/:template/mappers', { + templateUrl : resourceUrl + '/partials/client-template-mappers.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + template : function(ClientTemplateLoader) { + return ClientTemplateLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); + } + }, + controller : 'ClientTemplateProtocolMapperListCtrl' + }) + .when('/realms/:realm/client-templates/:template/add-mappers', { + templateUrl : resourceUrl + '/partials/client-template-mappers-add.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + template : function(ClientTemplateLoader) { + return ClientTemplateLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); + } + }, + controller : 'ClientTemplateAddBuiltinProtocolMapperCtrl' + }) + .when('/realms/:realm/client-templates/:template/mappers/:id', { + templateUrl : resourceUrl + '/partials/client-template-protocol-mapper-detail.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + template : function(ClientTemplateLoader) { + return ClientTemplateLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); + }, + mapper : function(ClientTemplateProtocolMapperLoader) { + return ClientTemplateProtocolMapperLoader(); + } + + }, + controller : 'ClientTemplateProtocolMapperCtrl' + }) + .when('/create/client-template/:realm/:template/mappers', { + templateUrl : resourceUrl + '/partials/client-template-protocol-mapper-detail.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); + }, + template : function(ClientTemplateLoader) { + return ClientTemplateLoader(); + } + }, + controller : 'ClientTemplateProtocolMapperCreateCtrl' + }) .when('/realms/:realm/clients/:client/sessions', { templateUrl : resourceUrl + '/partials/client-sessions.html', resolve : { @@ -1063,6 +1127,9 @@ module.config([ '$routeProvider', function($routeProvider) { realm : function(RealmLoader) { return RealmLoader(); }, + templates : function(ClientTemplateListLoader) { + return ClientTemplateListLoader(); + }, clients : function(ClientListLoader) { return ClientListLoader(); }, @@ -1081,6 +1148,9 @@ module.config([ '$routeProvider', function($routeProvider) { realm : function(RealmLoader) { return RealmLoader(); }, + templates : function(ClientTemplateListLoader) { + return ClientTemplateListLoader(); + }, clients : function(ClientListLoader) { return ClientListLoader(); }, @@ -1093,6 +1163,42 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ClientDetailCtrl' }) + .when('/create/client-template/:realm', { + templateUrl : resourceUrl + '/partials/client-template-detail.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + templates : function(ClientTemplateListLoader) { + return ClientTemplateListLoader(); + }, + template : function() { + return {}; + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); + } + }, + controller : 'ClientTemplateDetailCtrl' + }) + .when('/realms/:realm/client-templates/:template', { + templateUrl : resourceUrl + '/partials/client-template-detail.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + templates : function(ClientTemplateListLoader) { + return ClientTemplateListLoader(); + }, + template : function(ClientTemplateLoader) { + return ClientTemplateLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); + } + }, + controller : 'ClientTemplateDetailCtrl' + }) .when('/realms/:realm/clients', { templateUrl : resourceUrl + '/partials/client-list.html', resolve : { @@ -1109,6 +1215,22 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ClientListCtrl' }) + .when('/realms/:realm/client-templates', { + templateUrl : resourceUrl + '/partials/client-template-list.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + templates : function(ClientTemplateListLoader) { + return ClientTemplateListLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); + } + + }, + controller : 'ClientTemplateListCtrl' + }) .when('/import/client/:realm', { templateUrl : resourceUrl + '/partials/client-import.html', resolve : { @@ -2025,6 +2147,15 @@ module.directive('kcTabsClient', function () { } }); +module.directive('kcTabsClientTemplate', function () { + return { + scope: true, + restrict: 'E', + replace: true, + templateUrl: resourceUrl + '/templates/kc-tabs-client-template.html' + } +}); + module.directive('kcNavigationUser', function () { return { scope: true, diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 4e81a43035..420517d0f8 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -726,7 +726,10 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, Clie } }); -module.controller('ClientDetailCtrl', function($scope, realm, client, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) { +module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) { + + + $scope.accessTypes = [ "confidential", "public", @@ -735,6 +738,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort(); + $scope.templates = [ {name:'NONE'}]; + for (var i = 0; i < templates.length; i++) { + var template = templates[i]; + $scope.templates.push(template); + } + $scope.signatureAlgorithms = [ "RSA_SHA1", "RSA_SHA256", @@ -1444,22 +1453,32 @@ module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client }); module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo, client, mapper, ClientProtocolMapper, Notifications, Dialog, $location) { + /* $scope.realm = realm; $scope.client = client; $scope.create = false; - if (client.protocol == null) { - client.protocol = 'openid-connect'; - } $scope.protocol = client.protocol; $scope.mapper = angular.copy(mapper); $scope.changed = false; - $scope.boolval = true; - $scope.boolvalId = 'boolval'; + */ + + if (client.protocol == null) { + client.protocol = 'openid-connect'; + } + + $scope.model = { + realm: realm, + client: client, + create: false, + protocol: client.protocol, + mapper: angular.copy(mapper), + changed: false + } var protocolMappers = serverInfo.protocolMapperTypes[client.protocol]; for (var i = 0; i < protocolMappers.length; i++) { if (protocolMappers[i].id == mapper.protocolMapper) { - $scope.mapperType = protocolMappers[i]; + $scope.model.mapperType = protocolMappers[i]; } } $scope.$watch(function() { @@ -1468,9 +1487,9 @@ module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo $scope.path = $location.path().substring(1).split("/"); }); - $scope.$watch('mapper', function() { - if (!angular.equals($scope.mapper, mapper)) { - $scope.changed = true; + $scope.$watch('model.mapper', function() { + if (!angular.equals($scope.model.mapper, mapper)) { + $scope.model.changed = true; } }, true); @@ -1479,17 +1498,17 @@ module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo realm : realm.realm, client: client.id, id : mapper.id - }, $scope.mapper, function() { - $scope.changed = false; + }, $scope.model.mapper, function() { + $scope.model.changed = false; mapper = angular.copy($scope.mapper); - $location.url("/realms/" + realm.realm + '/clients/' + client.id + "/mappers/" + mapper.id); + $location.url("/realms/" + realm.realm + '/clients/' + client.id + "/mappers/" + $scope.model.mapper.id); Notifications.success("Your changes have been saved."); }); }; $scope.reset = function() { - $scope.mapper = angular.copy(mapper); - $scope.changed = false; + $scope.model.mapper = angular.copy(mapper); + $scope.model.changed = false; }; $scope.cancel = function() { @@ -1499,7 +1518,7 @@ module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo $scope.remove = function() { Dialog.confirmDelete($scope.mapper.name, 'mapper', function() { - ClientProtocolMapper.remove({ realm: realm.realm, client: client.id, id : $scope.mapper.id }, function() { + ClientProtocolMapper.remove({ realm: realm.realm, client: client.id, id : $scope.model.mapper.id }, function() { Notifications.success("The mapper has been deleted."); $location.url("/realms/" + realm.realm + '/clients/' + client.id + "/mappers"); }); @@ -1509,16 +1528,27 @@ module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo }); module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serverInfo, client, ClientProtocolMapper, Notifications, Dialog, $location) { - $scope.realm = realm; - $scope.client = client; - $scope.create = true; if (client.protocol == null) { client.protocol = 'openid-connect'; } var protocol = client.protocol; + /* + $scope.realm = realm; + $scope.client = client; + $scope.create = true; $scope.protocol = protocol; $scope.mapper = { protocol : client.protocol, config: {}}; $scope.mapperTypes = serverInfo.protocolMapperTypes[protocol]; + */ + $scope.model = { + realm: realm, + client: client, + create: true, + protocol: client.protocol, + mapper: { protocol : client.protocol, config: {}}, + changed: false, + mapperTypes: serverInfo.protocolMapperTypes[protocol] + } $scope.$watch(function() { return $location.path(); @@ -1527,10 +1557,10 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv }); $scope.save = function() { - $scope.mapper.protocolMapper = $scope.mapperType.id; + $scope.model.mapper.protocolMapper = $scope.model.mapperType.id; ClientProtocolMapper.save({ realm : realm.realm, client: client.id - }, $scope.mapper, function(data, headers) { + }, $scope.model.mapper, function(data, headers) { var l = headers().location; var id = l.substring(l.lastIndexOf("/") + 1); $location.url("/realms/" + realm.realm + '/clients/' + client.id + "/mappers/" + id); @@ -1546,5 +1576,335 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv }); +module.controller('ClientTemplateTabCtrl', function(Dialog, $scope, Current, Notifications, $location) { + $scope.removeClientTemplate = function() { + Dialog.confirmDelete($scope.template.name, 'client template', function() { + $scope.template.$remove({ + realm : Current.realm.realm, + template : $scope.template.id + }, function() { + $location.url("/realms/" + Current.realm.realm + "/client-templates"); + Notifications.success("The client template has been deleted."); + }); + }); + }; +}); + + + +module.controller('ClientTemplateListCtrl', function($scope, realm, templates, ClientTemplate, serverInfo, $route, Dialog, Notifications) { + $scope.realm = realm; + $scope.templates = templates; + + $scope.removeClientTemplate = function(template) { + Dialog.confirmDelete(template.name, 'client template', function() { + ClientTemplate.remove({ + realm : realm.realm, + template : template.id + }, function() { + $route.reload(); + Notifications.success("The client template been deleted."); + }); + }); + }; +}); + +module.controller('ClientTemplateDetailCtrl', function($scope, realm, template, $route, serverInfo, ClientTemplate, $location, $modal, Dialog, Notifications) { + $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort(); + + $scope.realm = realm; + $scope.create = !template.name; + + function updateProperties() { + if ($scope.template.protocol) { + $scope.protocol = $scope.protocols[$scope.protocols.indexOf($scope.template.protocol)]; + } else { + $scope.protocol = $scope.protocols[0]; + } + } + + if (!$scope.create) { + $scope.template = angular.copy(template); + updateProperties(); + } else { + $scope.template = { + }; + $scope.protocol = $scope.protocols[0]; + } + + + $scope.switchChange = function() { + $scope.changed = true; + } + + $scope.changeProtocol = function() { + if ($scope.protocol == "openid-connect") { + $scope.template.protocol = "openid-connect"; + } else if ($scope.protocol == "saml") { + $scope.template.protocol = "saml"; + } + }; + + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); + + function isChanged() { + if (!angular.equals($scope.template, template)) { + return true; + } + return false; + } + + $scope.$watch('template', function() { + $scope.changed = isChanged(); + }, true); + + $scope.save = function() { + $scope.template.protocol = $scope.protocol; + + if ($scope.create) { + ClientTemplate.save({ + realm: realm.realm, + template: '' + }, $scope.template, function (data, headers) { + $scope.changed = false; + var l = headers().location; + var id = l.substring(l.lastIndexOf("/") + 1); + $location.url("/realms/" + realm.realm + "/client-templates/" + id); + Notifications.success("The client template has been created."); + }); + } else { + ClientTemplate.update({ + realm : realm.realm, + template : template.id + }, $scope.template, function() { + $scope.changed = false; + template = angular.copy($scope.template); + $location.url("/realms/" + realm.realm + "/client-templates/" + template.id); + Notifications.success("Your changes have been saved to the client template."); + }); + } + }; + + $scope.reset = function() { + $route.reload(); + }; + + $scope.cancel = function() { + $location.url("/realms/" + realm.realm + "/client-templates"); + }; +}); + +module.controller('ClientTemplateProtocolMapperListCtrl', function($scope, realm, template, serverInfo, + ClientTemplateProtocolMappersByProtocol, ClientTemplateProtocolMapper, + $route, Dialog, Notifications) { + $scope.realm = realm; + $scope.template = template; + if (template.protocol == null) { + template.protocol = 'openid-connect'; + } + + var protocolMappers = serverInfo.protocolMapperTypes[template.protocol]; + var mapperTypes = {}; + for (var i = 0; i < protocolMappers.length; i++) { + mapperTypes[protocolMappers[i].id] = protocolMappers[i]; + } + $scope.mapperTypes = mapperTypes; + + $scope.removeMapper = function(mapper) { + console.debug(mapper); + Dialog.confirmDelete(mapper.name, 'mapper', function() { + ClientTemplateProtocolMapper.remove({ realm: realm.realm, template: template.id, id : mapper.id }, function() { + Notifications.success("The mapper has been deleted."); + $route.reload(); + }); + }); + }; + + var updateMappers = function() { + $scope.mappers = ClientTemplateProtocolMappersByProtocol.query({realm : realm.realm, template : template.id, protocol : template.protocol}); + }; + + updateMappers(); +}); + +module.controller('ClientTemplateProtocolMapperCtrl', function($scope, realm, serverInfo, template, mapper, ClientTemplateProtocolMapper, Notifications, Dialog, $location) { + + if (template.protocol == null) { + template.protocol = 'openid-connect'; + } + + $scope.model = { + realm: realm, + template: template, + create: false, + protocol: template.protocol, + mapper: angular.copy(mapper), + changed: false + } + + var protocolMappers = serverInfo.protocolMapperTypes[template.protocol]; + for (var i = 0; i < protocolMappers.length; i++) { + if (protocolMappers[i].id == mapper.protocolMapper) { + $scope.model.mapperType = protocolMappers[i]; + } + } + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); + + $scope.$watch('model.mapper', function() { + if (!angular.equals($scope.model.mapper, mapper)) { + $scope.model.changed = true; + } + }, true); + + $scope.save = function() { + ClientTemplateProtocolMapper.update({ + realm : realm.realm, + template: template.id, + id : mapper.id + }, $scope.model.mapper, function() { + $scope.model.changed = false; + mapper = angular.copy($scope.mapper); + $location.url("/realms/" + realm.realm + '/client-templates/' + template.id + "/mappers/" + $scope.model.mapper.id); + Notifications.success("Your changes have been saved."); + }); + }; + + $scope.reset = function() { + $scope.model.mapper = angular.copy(mapper); + $scope.model.changed = false; + }; + + $scope.cancel = function() { + //$location.url("/realms"); + window.history.back(); + }; + + $scope.remove = function() { + Dialog.confirmDelete($scope.mapper.name, 'mapper', function() { + ClientTemplateProtocolMapper.remove({ realm: realm.realm, template: template.id, id : $scope.model.mapper.id }, function() { + Notifications.success("The mapper has been deleted."); + $location.url("/realms/" + realm.realm + '/client-templates/' + template.id + "/mappers"); + }); + }); + }; + +}); + +module.controller('ClientTemplateProtocolMapperCreateCtrl', function($scope, realm, serverInfo, template, ClientTemplateProtocolMapper, Notifications, Dialog, $location) { + if (template.protocol == null) { + template.protocol = 'openid-connect'; + } + var protocol = template.protocol; + $scope.model = { + realm: realm, + template: template, + create: true, + protocol: template.protocol, + mapper: { protocol : template.protocol, config: {}}, + changed: false, + mapperTypes: serverInfo.protocolMapperTypes[protocol] + } + + $scope.$watch(function() { + return $location.path(); + }, function() { + $scope.path = $location.path().substring(1).split("/"); + }); + + $scope.save = function() { + $scope.model.mapper.protocolMapper = $scope.model.mapperType.id; + ClientTemplateProtocolMapper.save({ + realm : realm.realm, template: template.id + }, $scope.model.mapper, function(data, headers) { + var l = headers().location; + var id = l.substring(l.lastIndexOf("/") + 1); + $location.url("/realms/" + realm.realm + '/client-templates/' + template.id + "/mappers/" + id); + Notifications.success("Mapper has been created."); + }); + }; + + $scope.cancel = function() { + //$location.url("/realms"); + window.history.back(); + }; + + +}); + +module.controller('ClientTemplateAddBuiltinProtocolMapperCtrl', function($scope, realm, template, serverInfo, + ClientTemplateProtocolMappersByProtocol, + $http, $location, Dialog, Notifications) { + $scope.realm = realm; + $scope.template = template; + if (template.protocol == null) { + template.protocol = 'openid-connect'; + } + + var protocolMappers = serverInfo.protocolMapperTypes[template.protocol]; + var mapperTypes = {}; + for (var i = 0; i < protocolMappers.length; i++) { + mapperTypes[protocolMappers[i].id] = protocolMappers[i]; + } + $scope.mapperTypes = mapperTypes; + + + + + var updateMappers = function() { + var clientMappers = ClientTemplateProtocolMappersByProtocol.query({realm : realm.realm, template : template.id, protocol : template.protocol}, function() { + var builtinMappers = serverInfo.builtinProtocolMappers[template.protocol]; + for (var i = 0; i < clientMappers.length; i++) { + for (var j = 0; j < builtinMappers.length; j++) { + if (builtinMappers[j].name == clientMappers[i].name + && builtinMappers[j].protocolMapper == clientMappers[i].protocolMapper) { + builtinMappers.splice(j, 1); + break; + } + } + } + $scope.mappers = builtinMappers; + for (var i = 0; i < $scope.mappers.length; i++) { + $scope.mappers[i].isChecked = false; + } + + + }); + }; + + updateMappers(); + + $scope.add = function() { + var toAdd = []; + for (var i = 0; i < $scope.mappers.length; i++) { + if ($scope.mappers[i].isChecked) { + delete $scope.mappers[i].isChecked; + toAdd.push($scope.mappers[i]); + } + } + $http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/protocol-mappers/add-models', + toAdd).success(function() { + Notifications.success("Mappers added"); + $location.url('/realms/' + realm.realm + '/client-templates/' + template.id + '/mappers'); + }).error(function() { + Notifications.error("Error adding mappers"); + $location.url('/realms/' + realm.realm + '/client-templates/' + template.id + '/mappers'); + }); + }; + +}); + + + + + + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 61b608fe8e..94854f30e6 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -107,6 +107,16 @@ module.factory('ClientProtocolMapperLoader', function(Loader, ClientProtocolMapp }); }); +module.factory('ClientTemplateProtocolMapperLoader', function(Loader, ClientTemplateProtocolMapper, $route, $q) { + return Loader.get(ClientTemplateProtocolMapper, function() { + return { + realm : $route.current.params.realm, + template : $route.current.params.template, + id: $route.current.params.id + } + }); +}); + module.factory('UserLoader', function(Loader, User, $route, $q) { return Loader.get(User, function() { return { @@ -311,6 +321,23 @@ module.factory('ClientListLoader', function(Loader, Client, $route, $q) { }); }); +module.factory('ClientTemplateLoader', function(Loader, ClientTemplate, $route, $q) { + return Loader.get(ClientTemplate, function() { + return { + realm : $route.current.params.realm, + template : $route.current.params.template + } + }); +}); + +module.factory('ClientTemplateListLoader', function(Loader, ClientTemplate, $route, $q) { + return Loader.query(ClientTemplate, function() { + return { + realm : $route.current.params.realm + } + }); +}); + module.factory('ClientServiceAccountUserLoader', function(Loader, ClientServiceAccountUser, $route, $q) { return Loader.get(ClientServiceAccountUser, function() { return { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 7b94680e79..7a47a83a0c 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -309,6 +309,18 @@ module.factory('ClientProtocolMapper', function($resource) { }); }); +module.factory('ClientTemplateProtocolMapper', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/protocol-mappers/models/:id', { + realm : '@realm', + template: '@template', + id : "@id" + }, { + update : { + method : 'PUT' + } + }); +}); + module.factory('User', function($resource) { return $resource(authUrl + '/admin/realms/:realm/users/:userId', { realm : '@realm', @@ -828,6 +840,14 @@ module.factory('ClientProtocolMappersByProtocol', function($resource) { }); }); +module.factory('ClientTemplateProtocolMappersByProtocol', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/protocol-mappers/protocol/:protocol', { + realm : '@realm', + template : "@template", + protocol : "@protocol" + }); +}); + module.factory('ClientSessionStats', function($resource) { return $resource(authUrl + '/admin/realms/:realm/clients/:client/session-stats', { realm : '@realm', @@ -956,6 +976,18 @@ module.factory('Client', function($resource) { }); }); +module.factory('ClientTemplate', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-templates/:template', { + realm : '@realm', + template : '@template' + }, { + update : { + method : 'PUT' + } + }); +}); + + module.factory('ClientDescriptionConverter', function($resource) { return $resource(authUrl + '/admin/realms/:realm/client-description-converter', { realm : '@realm' diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index 5b81a0167b..6a2bd481df 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -72,6 +72,18 @@ {{:: 'client-protocol.tooltip' | translate}} +
+ +
+
+ +
+
+ Client template this client inherits configuration from +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-protocol-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-protocol-mapper-detail.html new file mode 100755 index 0000000000..b2af24a932 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-protocol-mapper-detail.html @@ -0,0 +1,13 @@ +
+ + +
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-detail.html new file mode 100755 index 0000000000..5b286a99a5 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-detail.html @@ -0,0 +1,55 @@ +
+ + + + + +
+
+
+ +
+ +
+ Name of the client template. Must be unique in the realm +
+
+ +
+ +
+ Description of the client template +
+
+ +
+
+ +
+
+ Which SSO protocol configuration is being supplied by this client template +
+
+ +
+
+ + +
+
+ + +
+
+
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-list.html new file mode 100755 index 0000000000..81d6d6f7af --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-list.html @@ -0,0 +1,50 @@ +
+

+ Client Templates + Client templates allow you to define common configuration that is shared between multiple clients +

+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ +
+
+
+ + +
+
{{:: 'name' | translate}}{{:: 'actions' | translate}}
{{template.name}} + + + +
{{:: 'no-results' | translate}}{{:: 'no-clients-available' | translate}}
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers-add.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers-add.html new file mode 100755 index 0000000000..cb09fbdfe8 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers-add.html @@ -0,0 +1,53 @@ +
+ + + +

{{:: 'add-builtin-protocol-mapper' | translate}}

+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ +
+
+
+
+
{{:: 'name' | translate}}{{:: 'category' | translate}}{{:: 'type' | translate}}{{:: 'add' | translate}}
{{mapper.name}}{{mapperTypes[mapper.protocolMapper].category}}{{mapperTypes[mapper.protocolMapper].name}}
{{:: 'no-mappers-available' | translate}}
+ +
+ +
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers.html new file mode 100755 index 0000000000..7b767b28ea --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers.html @@ -0,0 +1,57 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
{{:: 'name' | translate}}{{:: 'category' | translate}}{{:: 'type' | translate}}{{:: 'actions' | translate}}
{{mapper.name}}{{mapperTypes[mapper.protocolMapper].category}}{{mapperTypes[mapper.protocolMapper].name}} + + + +
{{:: 'no-mappers-available' | translate}}
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-protocol-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-protocol-mapper-detail.html new file mode 100755 index 0000000000..78ded5a6e0 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-protocol-mapper-detail.html @@ -0,0 +1,13 @@ +
+ + +
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html index afc9a5f5be..13855adfd7 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html @@ -1,16 +1,6 @@ -
- - - -

{{:: 'create-protocol-mapper' | translate}}

-

{{mapper.name|capitalize}}

+

{{:: 'create-protocol-mapper' | translate}}

+

{{model.mapper.name|capitalize}}

@@ -18,75 +8,72 @@
- +
{{:: 'protocol.tooltip' | translate}}
-
+
- +
- +
{{:: 'mapper.name.tooltip' | translate}}
- +
{{:: 'mapper.consent-required.tooltip' | translate}}
-
+
- +
{{:: 'consent-text.tooltip' | translate}}
-
+
- {{mapperType.helpText}} + {{model.mapperType.helpText}}
-
+
- +
- {{mapperType.helpText}} + {{model.mapperType.helpText}}
- +
-
+
-
- - +
+ +
-
- - \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html index 8da0e56148..5904fd75ed 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html @@ -29,7 +29,8 @@ || path[2] == 'keys-settings' || path[2] == 'smtp-settings' || path[2] == 'ldap-settings' || path[2] == 'auth-settings') && path[3] != 'clients') && 'active'"> Realm Settings -
  • Clients
  • +
  • Clients
  • +
  • Client Templates
  • Roles
  • Identity Providers
  • User Federation
  • diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html new file mode 100755 index 0000000000..728e0917d7 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html @@ -0,0 +1,16 @@ +
    + +

    Add Client Template

    +

    + {{template.name|capitalize}} + +

    + + +
    \ No newline at end of file diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java index bc75fa9559..adca52b4f1 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java @@ -9,6 +9,8 @@ import org.keycloak.admin.client.resource.RealmsResource; import org.keycloak.admin.client.resource.ServerInfoResource; import org.keycloak.admin.client.token.TokenManager; +import java.net.URI; + /** * Provides a Keycloak client. By default, this implementation uses a {@link ResteasyClient RESTEasy client} with the * default {@link ResteasyClientBuilder} settings. To customize the underling client, use a {@link KeycloakBuilder} to @@ -60,6 +62,19 @@ public class Keycloak { return tokenManager; } + /** + * Create a secure proxy based on an absolute URI. + * All set up with appropriate token + * + * @param proxyClass + * @param absoluteURI + * @param + * @return + */ + public T proxy(Class proxyClass, URI absoluteURI) { + return client.target(absoluteURI).register(new BearerAuthFilter(tokenManager)).proxy(proxyClass); + } + /** * Closes the underlying client. After calling this method, this Keycloak instance cannot be reused. */ diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java new file mode 100755 index 0000000000..0a5d8ff417 --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java @@ -0,0 +1,40 @@ +package org.keycloak.admin.client.resource; + +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.UserSessionRepresentation; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author rodrigo.sasaki@icarros.com.br + */ +public interface ClientTemplateResource { + + @Path("protocol-mappers") + public ProtocolMappersResource getProtocolMappers(); + + @GET + @Produces(MediaType.APPLICATION_JSON) + public ClientTemplateRepresentation toRepresentation(); + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + public void update(ClientTemplateRepresentation rep); + + @DELETE + public void remove(); +} \ No newline at end of file diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplatesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplatesResource.java new file mode 100755 index 0000000000..f0f2f669b0 --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplatesResource.java @@ -0,0 +1,34 @@ +package org.keycloak.admin.client.resource; + +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * @author rodrigo.sasaki@icarros.com.br + */ +public interface ClientTemplatesResource { + + @Path("{id}") + public ClientTemplatesResource get(@PathParam("id") String id); + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response create(ClientTemplateRepresentation clientRepresentation); + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List findAll(); + + + +} diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index 6dee1cbf78..7145f1e979 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -26,6 +26,9 @@ public interface RealmResource { @Path("clients") ClientsResource clients(); + @Path("client-templates") + ClientTemplatesResource clientTemplates(); + @Path("client-description-converter") @POST @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN }) diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java index 492c2e1ef4..f7e0305a42 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java @@ -8,7 +8,7 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public interface ClientModel extends RoleContainerModel { +public interface ClientModel extends RoleContainerModel, ProtocolMapperContainerModel { // COMMON ATTRIBUTES @@ -134,6 +134,9 @@ public interface ClientModel extends RoleContainerModel { RealmModel getRealm(); + ClientTemplateModel getClientTemplate(); + void setClientTemplate(ClientTemplateModel template); + /** * Time in seconds since epoc * @@ -143,14 +146,7 @@ public interface ClientModel extends RoleContainerModel { void setNotBefore(int notBefore); - Set getProtocolMappers(); - ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model); - void removeProtocolMapper(ProtocolMapperModel mapping); - void updateProtocolMapper(ProtocolMapperModel mapping); - ProtocolMapperModel getProtocolMapperById(String id); - ProtocolMapperModel getProtocolMapperByName(String protocol, String name); - - Map getRegisteredNodes(); + Map getRegisteredNodes(); /** * Register node or just update the 'lastReRegistration' time if this node is already registered diff --git a/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java b/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java new file mode 100755 index 0000000000..f7d67ac18e --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/ClientTemplateModel.java @@ -0,0 +1,27 @@ +package org.keycloak.models; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface ClientTemplateModel extends ProtocolMapperContainerModel { + String getId(); + + String getName(); + + RealmModel getRealm(); + void setName(String name); + + String getDescription(); + + void setDescription(String description); + + String getProtocol(); + void setProtocol(String protocol); + + +} diff --git a/model/api/src/main/java/org/keycloak/models/ProtocolMapperContainerModel.java b/model/api/src/main/java/org/keycloak/models/ProtocolMapperContainerModel.java new file mode 100755 index 0000000000..11736b1a1d --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/ProtocolMapperContainerModel.java @@ -0,0 +1,21 @@ +package org.keycloak.models; + +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface ProtocolMapperContainerModel { + Set getProtocolMappers(); + + ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model); + + void removeProtocolMapper(ProtocolMapperModel mapping); + + void updateProtocolMapper(ProtocolMapperModel mapping); + + ProtocolMapperModel getProtocolMapperById(String id); + + ProtocolMapperModel getProtocolMapperByName(String protocol, String name); +} diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 6c2c260421..bcc16f96aa 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -353,4 +353,15 @@ public interface RealmModel extends RoleContainerModel { List getTopLevelGroups(); boolean removeGroup(GroupModel group); void moveGroup(GroupModel group, GroupModel toParent); + + List getClientTemplates(); + + ClientTemplateModel addClientTemplate(String name); + + ClientTemplateModel addClientTemplate(String id, String name); + + boolean removeClientTemplate(String id); + + ClientTemplateModel getClientTemplateById(String id); + } diff --git a/model/api/src/main/java/org/keycloak/models/RealmProvider.java b/model/api/src/main/java/org/keycloak/models/RealmProvider.java index f649f3509c..6d6511b8d5 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmProvider.java +++ b/model/api/src/main/java/org/keycloak/models/RealmProvider.java @@ -20,6 +20,7 @@ public interface RealmProvider extends Provider { RoleModel getRoleById(String id, RealmModel realm); ClientModel getClientById(String id, RealmModel realm); + ClientTemplateModel getClientTemplateById(String id, RealmModel realm); GroupModel getGroupById(String id, RealmModel realm); diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index 58344ccb7f..e7a33ac6c6 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -388,8 +388,8 @@ public class UserFederationManager implements UserProvider { } @Override - public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) { - session.userStorage().preRemove(client, protocolMapper); + public void preRemove(ProtocolMapperModel protocolMapper) { + session.userStorage().preRemove(protocolMapper); } public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) { diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java index 1c2b5c7158..044a9385f0 100755 --- a/model/api/src/main/java/org/keycloak/models/UserProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java @@ -57,7 +57,7 @@ public interface UserProvider extends Provider { void preRemove(RealmModel realm, GroupModel group); void preRemove(RealmModel realm, ClientModel client); - void preRemove(ClientModel realm, ProtocolMapperModel protocolMapper); + void preRemove(ProtocolMapperModel protocolMapper); boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input); boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input); diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java index 46259c9198..24e5fc655a 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java @@ -48,6 +48,7 @@ public class ClientEntity extends AbstractIdentifiableEntity { private List scopeIds = new ArrayList(); private List identityProviders = new ArrayList(); private List protocolMappers = new ArrayList(); + private String clientTemplate; public String getClientId() { return clientId; @@ -300,5 +301,13 @@ public class ClientEntity extends AbstractIdentifiableEntity { public void setRegisteredNodes(Map registeredNodes) { this.registeredNodes = registeredNodes; } + + public String getClientTemplate() { + return clientTemplate; + } + + public void setClientTemplate(String clientTemplate) { + this.clientTemplate = clientTemplate; + } } diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java new file mode 100755 index 0000000000..849eaa12e9 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/entities/ClientTemplateEntity.java @@ -0,0 +1,59 @@ +package org.keycloak.models.entities; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Marek Posolda + */ +public class ClientTemplateEntity extends AbstractIdentifiableEntity { + + private String name; + private String description; + private String realmId; + private String protocol; + private List protocolMappers = new ArrayList(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public List getProtocolMappers() { + return protocolMappers; + } + + public void setProtocolMappers(List protocolMappers) { + this.protocolMappers = protocolMappers; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } +} + diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 1d34068971..7b6a198d76 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -5,6 +5,7 @@ import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; @@ -27,6 +28,7 @@ import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.GroupRepresentation; @@ -404,6 +406,24 @@ public class ModelToRepresentation { return rep; } + public static ClientTemplateRepresentation toRepresentation(ClientTemplateModel clientModel) { + ClientTemplateRepresentation rep = new ClientTemplateRepresentation(); + rep.setId(clientModel.getId()); + rep.setName(clientModel.getName()); + rep.setDescription(clientModel.getDescription()); + rep.setProtocol(clientModel.getProtocol()); + if (!clientModel.getProtocolMappers().isEmpty()) { + List mappings = new LinkedList<>(); + for (ProtocolMapperModel model : clientModel.getProtocolMappers()) { + mappings.add(toRepresentation(model)); + } + rep.setProtocolMappers(mappings); + } + + return rep; + } + + public static ClientRepresentation toRepresentation(ClientModel clientModel) { ClientRepresentation rep = new ClientRepresentation(); rep.setId(clientModel.getId()); @@ -429,6 +449,7 @@ public class ModelToRepresentation { rep.setNotBefore(clientModel.getNotBefore()); rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout()); rep.setClientAuthenticatorType(clientModel.getClientAuthenticatorType()); + if (clientModel.getClientTemplate() != null) rep.setClientTemplate(clientModel.getClientTemplate().getName()); Set redirectUris = clientModel.getRedirectUris(); if (redirectUris != null) { diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 53284884b8..4c7dad1189 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -1,5 +1,6 @@ package org.keycloak.models.utils; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.Constants; import org.keycloak.common.util.Base64; import org.jboss.logging.Logger; @@ -35,6 +36,7 @@ import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.ClaimRepresentation; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.GroupRepresentation; @@ -182,6 +184,10 @@ public class RepresentationToModel { importIdentityProviders(rep, newRealm); importIdentityProviderMappers(rep, newRealm); + if (rep.getClientTemplates() != null) { + createClientTemplates(session, rep, newRealm); + } + if (rep.getClients() != null) { createClients(session, rep, newRealm); } @@ -878,6 +884,15 @@ public class RepresentationToModel { } } + if (resourceRep.getClientTemplate() != null) { + for (ClientTemplateModel template : realm.getClientTemplates()) { + if (template.getName().equals(resourceRep.getClientTemplate())) { + client.setClientTemplate(template); + break; + } + } + } + return client; } @@ -934,6 +949,61 @@ public class RepresentationToModel { } } + if (rep.getClientTemplate() != null) { + if (rep.getClientTemplate().equals(ClientTemplateRepresentation.NONE)) { + resource.setClientTemplate(null); + } else { + RealmModel realm = resource.getRealm(); + for (ClientTemplateModel template : realm.getClientTemplates()) { + if (template.getName().equals(rep.getClientTemplate())) { + resource.setClientTemplate(template); + break; + } + } + } + } + + + } + + // CLIENT TEMPLATES + + private static Map createClientTemplates(KeycloakSession session, RealmRepresentation rep, RealmModel realm) { + Map appMap = new HashMap<>(); + for (ClientTemplateRepresentation resourceRep : rep.getClientTemplates()) { + ClientTemplateModel app = createClientTemplate(session, realm, resourceRep); + appMap.put(app.getName(), app); + } + return appMap; + } + + public static ClientTemplateModel createClientTemplate(KeycloakSession session, RealmModel realm, ClientTemplateRepresentation resourceRep) { + logger.debug("Create client template: {0}" + resourceRep.getName()); + + ClientTemplateModel client = resourceRep.getId()!=null ? realm.addClientTemplate(resourceRep.getId(), resourceRep.getName()) : realm.addClientTemplate(resourceRep.getName()); + if (resourceRep.getName() != null) client.setName(resourceRep.getName()); + if(resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription()); + if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol()); + + if (resourceRep.getProtocolMappers() != null) { + // first, remove all default/built in mappers + Set mappers = client.getProtocolMappers(); + for (ProtocolMapperModel mapper : mappers) client.removeProtocolMapper(mapper); + + for (ProtocolMapperRepresentation mapper : resourceRep.getProtocolMappers()) { + client.addProtocolMapper(toModel(mapper)); + } + } + + return client; + } + + public static void updateClientTemplate(ClientTemplateRepresentation rep, ClientTemplateModel resource) { + if (rep.getName() != null) resource.setName(rep.getName()); + if (rep.getDescription() != null) resource.setDescription(rep.getDescription()); + + + if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol()); } public static long getClaimsMask(ClaimRepresentation rep) { diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index 2f7e61f0f3..d96e6bcace 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -55,6 +55,20 @@ public class ClientAdapter implements ClientModel { updated.setWebOrigins(webOrigins); } + @Override + public ClientTemplateModel getClientTemplate() { + if (updated != null) return updated.getClientTemplate(); + if (cached.getClientTemplate() == null) return null; + return cacheSession.getClientTemplateById(cached.getClientTemplate(), cachedRealm); + } + + @Override + public void setClientTemplate(ClientTemplateModel template) { + getDelegateForUpdate(); + updated.setClientTemplate(template); + + } + public void addWebOrigin(String webOrigin) { getDelegateForUpdate(); updated.addWebOrigin(webOrigin); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java new file mode 100755 index 0000000000..2a1674d9ad --- /dev/null +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java @@ -0,0 +1,149 @@ +package org.keycloak.models.cache.infinispan; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.cache.CacheRealmProvider; +import org.keycloak.models.cache.RealmCache; +import org.keycloak.models.cache.entities.CachedClient; +import org.keycloak.models.cache.entities.CachedClientTemplate; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientTemplateAdapter implements ClientTemplateModel { + protected CacheRealmProvider cacheSession; + protected RealmModel cachedRealm; + protected RealmCache cache; + + protected ClientTemplateModel updated; + protected CachedClientTemplate cached; + + public ClientTemplateAdapter(RealmModel cachedRealm, CachedClientTemplate cached, CacheRealmProvider cacheSession, RealmCache cache) { + this.cachedRealm = cachedRealm; + this.cache = cache; + this.cacheSession = cacheSession; + this.cached = cached; + } + + private void getDelegateForUpdate() { + if (updated == null) { + cacheSession.registerApplicationInvalidation(getId()); + updated = cacheSession.getDelegate().getClientTemplateById(getId(), cachedRealm); + if (updated == null) throw new IllegalStateException("Not found in database"); + } + } + + @Override + public String getId() { + if (updated != null) return updated.getId(); + return cached.getId(); + } + + public RealmModel getRealm() { + return cachedRealm; + } + + @Override + public Set getProtocolMappers() { + if (updated != null) return updated.getProtocolMappers(); + return cached.getProtocolMappers(); + } + + @Override + public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) { + getDelegateForUpdate(); + return updated.addProtocolMapper(model); + } + + @Override + public void removeProtocolMapper(ProtocolMapperModel mapping) { + getDelegateForUpdate(); + updated.removeProtocolMapper(mapping); + + } + + @Override + public void updateProtocolMapper(ProtocolMapperModel mapping) { + getDelegateForUpdate(); + updated.updateProtocolMapper(mapping); + + } + + @Override + public ProtocolMapperModel getProtocolMapperById(String id) { + for (ProtocolMapperModel mapping : cached.getProtocolMappers()) { + if (mapping.getId().equals(id)) return mapping; + } + return null; + } + + @Override + public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) { + for (ProtocolMapperModel mapping : cached.getProtocolMappers()) { + if (mapping.getProtocol().equals(protocol) && mapping.getName().equals(name)) return mapping; + } + return null; + } + + @Override + public String getName() { + if (updated != null) return updated.getName(); + return cached.getName(); + } + + @Override + public void setName(String name) { + getDelegateForUpdate(); + updated.setName(name); + } + + @Override + public String getDescription() { + if (updated != null) return updated.getDescription(); + return cached.getDescription(); + } + + @Override + public void setDescription(String description) { + getDelegateForUpdate(); + updated.setDescription(description); + } + + @Override + public String getProtocol() { + if (updated != null) return updated.getProtocol(); + return cached.getProtocol(); + } + + @Override + public void setProtocol(String protocol) { + getDelegateForUpdate(); + updated.setProtocol(protocol); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof ClientModel)) return false; + + ClientTemplateModel that = (ClientTemplateModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + +} diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java index 3e45a69f75..478a087271 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java @@ -18,14 +18,16 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider { protected boolean transactionActive; protected boolean setRollbackOnly; - protected Set realmInvalidations = new HashSet(); - protected Set appInvalidations = new HashSet(); - protected Set roleInvalidations = new HashSet(); - protected Set groupInvalidations = new HashSet(); - protected Map managedRealms = new HashMap(); - protected Map managedApplications = new HashMap(); - protected Map managedRoles = new HashMap(); - protected Map managedGroups = new HashMap(); + protected Set realmInvalidations = new HashSet<>(); + protected Set appInvalidations = new HashSet<>(); + protected Set clientTemplateInvalidations = new HashSet<>(); + protected Set roleInvalidations = new HashSet<>(); + protected Set groupInvalidations = new HashSet<>(); + protected Map managedRealms = new HashMap<>(); + protected Map managedApplications = new HashMap<>(); + protected Map managedClientTemplates = new HashMap<>(); + protected Map managedRoles = new HashMap<>(); + protected Map managedGroups = new HashMap<>(); protected boolean clearAll; @@ -69,6 +71,10 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider { public void registerApplicationInvalidation(String id) { appInvalidations.add(id); } + @Override + public void registerClientTemplateInvalidation(String id) { + clientTemplateInvalidations.add(id); + } @Override public void registerRoleInvalidation(String id) { @@ -94,6 +100,9 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider { for (String id : appInvalidations) { cache.invalidateCachedApplicationById(id); } + for (String id : clientTemplateInvalidations) { + cache.invalidateCachedClientTemplateById(id); + } } private KeycloakTransaction getTransaction() { @@ -313,5 +322,28 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider { managedApplications.put(id, adapter); return adapter; } + @Override + public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) { + if (!cache.isEnabled()) return getDelegate().getClientTemplateById(id, realm); + CachedClientTemplate cached = cache.getClientTemplate(id); + if (cached != null && !cached.getRealm().equals(realm.getId())) { + cached = null; + } + + if (cached == null) { + ClientTemplateModel model = getDelegate().getClientTemplateById(id, realm); + if (model == null) return null; + if (clientTemplateInvalidations.contains(id)) return model; + cached = new CachedClientTemplate(cache, getDelegate(), realm, model); + cache.addCachedClientTemplate(cached); + } else if (clientTemplateInvalidations.contains(id)) { + return getDelegate().getClientTemplateById(id, realm); + } else if (managedClientTemplates.containsKey(id)) { + return managedClientTemplates.get(id); + } + ClientTemplateModel adapter = new ClientTemplateAdapter(realm, cached, this, cache); + managedClientTemplates.put(id, adapter); + return adapter; + } } diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java index c7f5bf2325..ba933b5198 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java @@ -348,7 +348,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider { } @Override - public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) { - getDelegate().preRemove(client, protocolMapper); + public void preRemove(ProtocolMapperModel protocolMapper) { + getDelegate().preRemove(protocolMapper); } } diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java index 81558012b4..4cd4f79641 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java @@ -4,6 +4,7 @@ import org.infinispan.Cache; import org.jboss.logging.Logger; import org.keycloak.models.cache.RealmCache; import org.keycloak.models.cache.entities.CachedClient; +import org.keycloak.models.cache.entities.CachedClientTemplate; import org.keycloak.models.cache.entities.CachedGroup; import org.keycloak.models.cache.entities.CachedRealm; import org.keycloak.models.cache.entities.CachedRole; @@ -177,4 +178,36 @@ public class InfinispanRealmCache implements RealmCache { return o != null && type.isInstance(o) ? type.cast(o) : null; } + @Override + public CachedClientTemplate getClientTemplate(String id) { + if (!enabled) return null; + return get(id, CachedClientTemplate.class); + } + + @Override + public void invalidateClientTemplate(CachedClientTemplate app) { + logger.tracev("Removing client template {0}", app.getId()); + cache.remove(app.getId()); + } + + @Override + public void addCachedClientTemplate(CachedClientTemplate app) { + if (!enabled) return; + logger.tracev("Adding client template {0}", app.getId()); + cache.putForExternalRead(app.getId(), app); + } + + @Override + public void invalidateCachedClientTemplateById(String id) { + logger.tracev("Removing client template {0}", id); + cache.remove(id); + } + + @Override + public void evictCachedClientTemplateById(String id) { + logger.tracev("Evicting client template {0}", id); + cache.evict(id); + } + + } diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index c24f151ecd..e7d15eb862 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -1361,4 +1361,51 @@ public class RealmAdapter implements RealmModel { getDelegateForUpdate(); updated.moveGroup(group, toParent); } + + @Override + public List getClientTemplates() { + if (updated != null) return updated.getClientTemplates(); + List apps = new LinkedList(); + for (String id : cached.getClientTemplates()) { + ClientTemplateModel model = cacheSession.getClientTemplateById(id, this); + if (model == null) { + throw new IllegalStateException("Cached clientemplate not found: " + id); + } + apps.add(model); + } + return apps; + + } + + @Override + public ClientTemplateModel addClientTemplate(String name) { + getDelegateForUpdate(); + ClientTemplateModel app = updated.addClientTemplate(name); + cacheSession.registerClientTemplateInvalidation(app.getId()); + return app; + } + + @Override + public ClientTemplateModel addClientTemplate(String id, String name) { + getDelegateForUpdate(); + ClientTemplateModel app = updated.addClientTemplate(id, name); + cacheSession.registerClientTemplateInvalidation(app.getId()); + return app; + } + + @Override + public boolean removeClientTemplate(String id) { + cacheSession.registerClientTemplateInvalidation(id); + getDelegateForUpdate(); + return updated.removeClientTemplate(id); + } + + @Override + public ClientTemplateModel getClientTemplateById(String id) { + if (updated != null) return updated.getClientTemplateById(id); + return cacheSession.getClientTemplateById(id, this); + } + + + } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java index 26ad12de2d..6386f9f482 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java @@ -15,6 +15,7 @@ public interface CacheRealmProvider extends RealmProvider { void registerRealmInvalidation(String id); void registerApplicationInvalidation(String id); + void registerClientTemplateInvalidation(String id); void registerRoleInvalidation(String id); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java index 007daac779..e826ac1715 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java @@ -1,6 +1,7 @@ package org.keycloak.models.cache; import org.keycloak.models.cache.entities.CachedClient; +import org.keycloak.models.cache.entities.CachedClientTemplate; import org.keycloak.models.cache.entities.CachedGroup; import org.keycloak.models.cache.entities.CachedRealm; import org.keycloak.models.cache.entities.CachedRole; @@ -55,4 +56,15 @@ public interface RealmCache { boolean isEnabled(); void setEnabled(boolean enabled); + + CachedClientTemplate getClientTemplate(String id); + + void invalidateClientTemplate(CachedClientTemplate app); + + void evictCachedClientTemplateById(String id); + + void addCachedClientTemplate(CachedClientTemplate app); + + void invalidateCachedClientTemplateById(String id); + } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java index a68395603c..dbf47544ab 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java @@ -55,6 +55,7 @@ public class CachedClient implements Serializable { private Map roles = new HashMap(); private int nodeReRegistrationTimeout; private Map registeredNodes; + private String clientTemplate; public CachedClient(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientModel model) { id = model.getId(); @@ -98,6 +99,9 @@ public class CachedClient implements Serializable { nodeReRegistrationTimeout = model.getNodeReRegistrationTimeout(); registeredNodes = new TreeMap(model.getRegisteredNodes()); + if (model.getClientTemplate() != null) { + clientTemplate = model.getClientTemplate().getId(); + } } public String getId() { return id; @@ -230,4 +234,8 @@ public class CachedClient implements Serializable { public Map getRegisteredNodes() { return registeredNodes; } + + public String getClientTemplate() { + return clientTemplate; + } } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java new file mode 100755 index 0000000000..2df28c161a --- /dev/null +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java @@ -0,0 +1,66 @@ +package org.keycloak.models.cache.entities; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.models.RoleModel; +import org.keycloak.models.cache.RealmCache; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class CachedClientTemplate implements Serializable { + + private String id; + private String name; + private String description; + private String realm; + private String protocol; + private Set protocolMappers = new HashSet(); + + public CachedClientTemplate(RealmCache cache, RealmProvider delegate, RealmModel realm, ClientTemplateModel model) { + id = model.getId(); + name = model.getName(); + description = model.getDescription(); + this.realm = realm.getId(); + protocol = model.getProtocol(); + for (ProtocolMapperModel mapper : model.getProtocolMappers()) { + this.protocolMappers.add(mapper); + } + } + public String getId() { + return id; + } + + + public String getName() { + return name; + } + + public String getDescription() { return description; } + + public void setDescription(String description) { this.description = description; } + + public String getRealm() { + return realm; + } + public Set getProtocolMappers() { + return protocolMappers; + } + + public String getProtocol() { + return protocol; + } +} diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index e423562f45..40e4851f94 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -5,6 +5,7 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; @@ -112,6 +113,7 @@ public class CachedRealm implements Serializable { private Set groups = new HashSet(); private Map realmRoles = new HashMap(); private Map clients = new HashMap(); + private List clientTemplates= new LinkedList<>(); private boolean internationalizationEnabled; private Set supportedLocales = new HashSet(); private String defaultLocale; @@ -210,6 +212,12 @@ public class CachedRealm implements Serializable { cache.addCachedClient(cachedClient); } + for (ClientTemplateModel template : model.getClientTemplates()) { + clientTemplates.add(template.getId()); + CachedClientTemplate cachedClient = new CachedClientTemplate(cache, delegate, model, template); + cache.addCachedClientTemplate(cachedClient); + } + internationalizationEnabled = model.isInternationalizationEnabled(); supportedLocales.addAll(model.getSupportedLocales()); defaultLocale = model.getDefaultLocale(); @@ -531,4 +539,8 @@ public class CachedRealm implements Serializable { public List getDefaultGroups() { return defaultGroups; } + + public List getClientTemplates() { + return clientTemplates; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index f6778594c9..a8abd1137b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -2,6 +2,7 @@ package org.keycloak.models.jpa; import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ProtocolMapperModel; @@ -9,6 +10,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.jpa.entities.ClientEntity; +import org.keycloak.models.jpa.entities.ClientTemplateEntity; import org.keycloak.models.jpa.entities.ProtocolMapperEntity; import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.jpa.entities.ScopeMappingEntity; @@ -298,6 +300,25 @@ public class ClientAdapter implements ClientModel { return copy; } + @Override + public ClientTemplateModel getClientTemplate() { + ClientTemplateEntity templateEntity = entity.getClientTemplate(); + if (templateEntity == null) return null; + return session.realms().getClientTemplateById(templateEntity.getId(), realm); + } + + @Override + public void setClientTemplate(ClientTemplateModel template) { + if (template == null) { + entity.setClientTemplate(null); + + } else { + ClientTemplateEntity templateEntity = em.getReference(ClientTemplateEntity.class, template.getId()); + entity.setClientTemplate(templateEntity); + } + + } + public static boolean contains(String str, String[] array) { for (String s : array) { if (str.equals(s)) return true; @@ -371,7 +392,7 @@ public class ClientAdapter implements ClientModel { public void removeProtocolMapper(ProtocolMapperModel mapping) { ProtocolMapperEntity toDelete = getProtocolMapperEntity(mapping.getId()); if (toDelete != null) { - session.users().preRemove(this, mapping); + session.users().preRemove(mapping); this.entity.getProtocolMappers().remove(toDelete); em.remove(toDelete); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java new file mode 100755 index 0000000000..8e92e79dc8 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientTemplateAdapter.java @@ -0,0 +1,222 @@ +package org.keycloak.models.jpa; + +import org.keycloak.connections.jpa.util.JpaUtils; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.jpa.entities.ClientEntity; +import org.keycloak.models.jpa.entities.ClientTemplateEntity; +import org.keycloak.models.jpa.entities.ProtocolMapperEntity; +import org.keycloak.models.jpa.entities.RoleEntity; +import org.keycloak.models.jpa.entities.ScopeMappingEntity; +import org.keycloak.models.utils.KeycloakModelUtils; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientTemplateAdapter implements ClientTemplateModel { + + protected KeycloakSession session; + protected RealmModel realm; + protected EntityManager em; + protected ClientTemplateEntity entity; + + public ClientTemplateAdapter(RealmModel realm, EntityManager em, KeycloakSession session, ClientTemplateEntity entity) { + this.session = session; + this.realm = realm; + this.em = em; + this.entity = entity; + } + + public ClientTemplateEntity getEntity() { + return entity; + } + + @Override + public String getId() { + return entity.getId(); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public String getName() { + return entity.getName(); + } + + @Override + public void setName(String name) { + entity.setName(name); + } + + @Override + public String getDescription() { return entity.getDescription(); } + + @Override + public void setDescription(String description) { entity.setDescription(description); } + + @Override + public String getProtocol() { + return entity.getProtocol(); + } + + @Override + public void setProtocol(String protocol) { + entity.setProtocol(protocol); + + } + + @Override + public Set getProtocolMappers() { + Set mappings = new HashSet(); + for (ProtocolMapperEntity entity : this.entity.getProtocolMappers()) { + ProtocolMapperModel mapping = new ProtocolMapperModel(); + mapping.setId(entity.getId()); + mapping.setName(entity.getName()); + mapping.setProtocol(entity.getProtocol()); + mapping.setProtocolMapper(entity.getProtocolMapper()); + mapping.setConsentRequired(entity.isConsentRequired()); + mapping.setConsentText(entity.getConsentText()); + Map config = new HashMap(); + if (entity.getConfig() != null) { + config.putAll(entity.getConfig()); + } + mapping.setConfig(config); + mappings.add(mapping); + } + return mappings; + } + + @Override + public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) { + if (getProtocolMapperByName(model.getProtocol(), model.getName()) != null) { + throw new ModelDuplicateException("Protocol mapper name must be unique per protocol"); + } + String id = model.getId() != null ? model.getId() : KeycloakModelUtils.generateId(); + ProtocolMapperEntity entity = new ProtocolMapperEntity(); + entity.setId(id); + entity.setName(model.getName()); + entity.setProtocol(model.getProtocol()); + entity.setProtocolMapper(model.getProtocolMapper()); + entity.setClientTemplate(this.entity); + entity.setConfig(model.getConfig()); + entity.setConsentRequired(model.isConsentRequired()); + entity.setConsentText(model.getConsentText()); + + em.persist(entity); + this.entity.getProtocolMappers().add(entity); + return entityToModel(entity); + } + + protected ProtocolMapperEntity getProtocolMapperEntity(String id) { + for (ProtocolMapperEntity entity : this.entity.getProtocolMappers()) { + if (entity.getId().equals(id)) { + return entity; + } + } + return null; + + } + + protected ProtocolMapperEntity getProtocolMapperEntityByName(String protocol, String name) { + for (ProtocolMapperEntity entity : this.entity.getProtocolMappers()) { + if (entity.getProtocol().equals(protocol) && entity.getName().equals(name)) { + return entity; + } + } + return null; + + } + + @Override + public void removeProtocolMapper(ProtocolMapperModel mapping) { + ProtocolMapperEntity toDelete = getProtocolMapperEntity(mapping.getId()); + if (toDelete != null) { + session.users().preRemove(mapping); + + this.entity.getProtocolMappers().remove(toDelete); + em.remove(toDelete); + } + + } + + @Override + public void updateProtocolMapper(ProtocolMapperModel mapping) { + ProtocolMapperEntity entity = getProtocolMapperEntity(mapping.getId()); + entity.setProtocolMapper(mapping.getProtocolMapper()); + entity.setConsentRequired(mapping.isConsentRequired()); + entity.setConsentText(mapping.getConsentText()); + if (entity.getConfig() == null) { + entity.setConfig(mapping.getConfig()); + } else { + entity.getConfig().clear(); + entity.getConfig().putAll(mapping.getConfig()); + } + em.flush(); + + } + + @Override + public ProtocolMapperModel getProtocolMapperById(String id) { + ProtocolMapperEntity entity = getProtocolMapperEntity(id); + if (entity == null) return null; + return entityToModel(entity); + } + + @Override + public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) { + ProtocolMapperEntity entity = getProtocolMapperEntityByName(protocol, name); + if (entity == null) return null; + return entityToModel(entity); + } + + protected ProtocolMapperModel entityToModel(ProtocolMapperEntity entity) { + ProtocolMapperModel mapping = new ProtocolMapperModel(); + mapping.setId(entity.getId()); + mapping.setName(entity.getName()); + mapping.setProtocol(entity.getProtocol()); + mapping.setProtocolMapper(entity.getProtocolMapper()); + mapping.setConsentRequired(entity.isConsentRequired()); + mapping.setConsentText(entity.getConsentText()); + Map config = new HashMap(); + if (entity.getConfig() != null) config.putAll(entity.getConfig()); + mapping.setConfig(config); + return mapping; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof ClientTemplateModel)) return false; + + ClientTemplateModel that = (ClientTemplateModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index 09b4fa2ad1..25df437f5b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -2,12 +2,14 @@ package org.keycloak.models.jpa; import org.keycloak.migration.MigrationModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; import org.keycloak.models.jpa.entities.ClientEntity; +import org.keycloak.models.jpa.entities.ClientTemplateEntity; import org.keycloak.models.jpa.entities.GroupEntity; import org.keycloak.models.jpa.entities.RealmEntity; import org.keycloak.models.jpa.entities.RoleEntity; @@ -145,4 +147,12 @@ public class JpaRealmProvider implements RealmProvider { return new ClientAdapter(realm, em, session, app); } + @Override + public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) { + ClientTemplateEntity app = em.find(ClientTemplateEntity.class, id); + + // Check if application belongs to this realm + if (app == null || !realm.getId().equals(app.getRealm().getId())) return null; + return new ClientTemplateAdapter(realm, em, session, app); + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index b536a9c459..36fd1fd634 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -227,7 +227,7 @@ public class JpaUserProvider implements UserProvider { } @Override - public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) { + public void preRemove(ProtocolMapperModel protocolMapper) { em.createNamedQuery("deleteUserConsentProtMappersByProtocolMapper") .setParameter("protocolMapperId", protocolMapper.getId()) .executeUpdate(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index cfd7347ae9..4f96829e61 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -7,6 +7,7 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; @@ -26,6 +27,7 @@ import org.keycloak.models.jpa.entities.AuthenticationExecutionEntity; import org.keycloak.models.jpa.entities.AuthenticationFlowEntity; import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity; import org.keycloak.models.jpa.entities.ClientEntity; +import org.keycloak.models.jpa.entities.ClientTemplateEntity; import org.keycloak.models.jpa.entities.GroupEntity; import org.keycloak.models.jpa.entities.IdentityProviderEntity; import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity; @@ -2095,4 +2097,64 @@ public class RealmAdapter implements RealmModel { public void addTopLevelGroup(GroupModel subGroup) { subGroup.setParent(null); } + + @Override + public List getClientTemplates() { + List list = new LinkedList<>(); + if (realm.getClientTemplates() == null) return list; + for (ClientTemplateEntity entity : realm.getClientTemplates()) { + list.add(new ClientTemplateAdapter(this, em, session, entity)); + } + return list; + } + + @Override + public ClientTemplateModel addClientTemplate(String name) { + return this.addClientTemplate(KeycloakModelUtils.generateId(), name); + } + + @Override + public ClientTemplateModel addClientTemplate(String id, String name) { + ClientTemplateEntity entity = new ClientTemplateEntity(); + entity.setId(id); + entity.setName(name); + entity.setRealm(realm); + realm.getClientTemplates().add(entity); + em.persist(entity); + em.flush(); + final ClientTemplateModel resource = new ClientTemplateAdapter(this, em, session, entity); + em.flush(); + return resource; + } + + @Override + public boolean removeClientTemplate(String id) { + if (id == null) return false; + ClientTemplateModel client = getClientTemplateById(id); + if (client == null) return false; + + ClientTemplateEntity clientEntity = null; + Iterator it = realm.getClientTemplates().iterator(); + while (it.hasNext()) { + ClientTemplateEntity ae = it.next(); + if (ae.getId().equals(id)) { + clientEntity = ae; + it.remove(); + break; + } + } + if (client == null) { + return false; + } + em.remove(clientEntity); + em.flush(); + + return true; + } + + @Override + public ClientTemplateModel getClientTemplateById(String id) { + return session.realms().getClientTemplateById(id, this); + } + } \ No newline at end of file diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index 1569875955..6767e7b20c 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -57,6 +57,10 @@ public class ClientEntity { @Column(name="FULL_SCOPE_ALLOWED") private boolean fullScopeAllowed; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "CLIENT_TEMPLATE_ID") + protected ClientTemplateEntity clientTemplate; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "REALM_ID") protected RealmEntity realm; @@ -392,4 +396,12 @@ public class ClientEntity { public void setRegisteredNodes(Map registeredNodes) { this.registeredNodes = registeredNodes; } + + public ClientTemplateEntity getClientTemplate() { + return clientTemplate; + } + + public void setClientTemplate(ClientTemplateEntity clientTemplate) { + this.clientTemplate = clientTemplate; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java new file mode 100755 index 0000000000..ff4bd14c0e --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java @@ -0,0 +1,96 @@ +package org.keycloak.models.jpa.entities; + +import javax.persistence.CascadeType; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToOne; +import javax.persistence.MapKeyColumn; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Entity +@Table(name="CLIENT_TEMPLATE", uniqueConstraints = {@UniqueConstraint(columnNames = {"REALM_ID", "NAME"})}) +public class ClientTemplateEntity { + + @Id + @Column(name="ID", length = 36) + private String id; + @Column(name = "NAME") + private String name; + @Column(name = "DESCRIPTION") + private String description; + @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "clientTemplate") + Collection protocolMappers = new ArrayList(); + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "REALM_ID") + protected RealmEntity realm; + + @Column(name="PROTOCOL") + private String protocol; + + + public RealmEntity getRealm() { + return realm; + } + + public void setRealm(RealmEntity realm) { + this.realm = realm; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Collection getProtocolMappers() { + return protocolMappers; + } + + public void setProtocolMappers(Collection protocolMappers) { + this.protocolMappers = protocolMappers; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ProtocolMapperEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ProtocolMapperEntity.java index 6098df1ac9..34ff5dff1e 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ProtocolMapperEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ProtocolMapperEntity.java @@ -46,6 +46,10 @@ public class ProtocolMapperEntity { @JoinColumn(name = "CLIENT_ID") private ClientEntity client; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "CLIENT_TEMPLATE_ID") + private ClientTemplateEntity clientTemplate; + public String getId() { return id; } @@ -94,6 +98,14 @@ public class ProtocolMapperEntity { this.client = client; } + public ClientTemplateEntity getClientTemplate() { + return clientTemplate; + } + + public void setClientTemplate(ClientTemplateEntity clientTemplate) { + this.clientTemplate = clientTemplate; + } + public boolean isConsentRequired() { return consentRequired; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 05af313de4..b8946f4891 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -132,6 +132,10 @@ public class RealmEntity { @JoinTable(name="REALM_CLIENT", joinColumns={ @JoinColumn(name="REALM_ID") }, inverseJoinColumns={ @JoinColumn(name="CLIENT_ID") }) Collection clients = new ArrayList<>(); + @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true) + @JoinTable(name="REALM_CLIENT_TEMPLATE", joinColumns={ @JoinColumn(name="REALM_ID") }, inverseJoinColumns={ @JoinColumn(name="CLIENT_TEMPLATE_ID") }) + Collection clientTemplates = new ArrayList<>(); + @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") Collection roles = new ArrayList(); @@ -741,5 +745,12 @@ public class RealmEntity { this.clientAuthenticationFlow = clientAuthenticationFlow; } + public Collection getClientTemplates() { + return clientTemplates; + } + + public void setClientTemplates(Collection clientTemplates) { + this.clientTemplates = clientTemplates; + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java index eb6bcd9544..af448c4018 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java @@ -4,6 +4,7 @@ import com.mongodb.DBObject; import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ProtocolMapperModel; @@ -363,7 +364,7 @@ public class ClientAdapter extends AbstractMongoAdapter imple public void removeProtocolMapper(ProtocolMapperModel mapping) { for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) { if (entity.getId().equals(mapping.getId())) { - session.users().preRemove(this, mapping); + session.users().preRemove(mapping); getMongoEntity().getProtocolMappers().remove(entity); updateMongoEntity(); @@ -713,5 +714,16 @@ public class ClientAdapter extends AbstractMongoAdapter imple return getId().hashCode(); } + @Override + public ClientTemplateModel getClientTemplate() { + if (getMongoEntity().getClientTemplate() == null) return null; + return session.realms().getClientTemplateById(getMongoEntity().getClientTemplate(), realm); + } + @Override + public void setClientTemplate(ClientTemplateModel template) { + getMongoEntity().setClientTemplate(template.getId()); + updateMongoEntity(); + + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java new file mode 100755 index 0000000000..ee19deae45 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java @@ -0,0 +1,229 @@ +package org.keycloak.models.mongo.keycloak.adapters; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.entities.ClientTemplateEntity; +import org.keycloak.models.entities.ProtocolMapperEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoClientTemplateEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; +import org.keycloak.models.mongo.utils.MongoModelUtils; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + */ +public class ClientTemplateAdapter extends AbstractMongoAdapter implements ClientTemplateModel { + + protected final MongoClientTemplateEntity clientTemplateEntity; + private final RealmModel realm; + protected KeycloakSession session; + + public ClientTemplateAdapter(KeycloakSession session, RealmModel realm, MongoClientTemplateEntity clientEntity, MongoStoreInvocationContext invContext) { + super(invContext); + this.session = session; + this.realm = realm; + this.clientTemplateEntity = clientEntity; + } + + @Override + public MongoClientTemplateEntity getMongoEntity() { + return clientTemplateEntity; + } + + @Override + public RealmModel getRealm() { + return realm; + } + + + @Override + public String getId() { + return getMongoEntity().getId(); + } + + @Override + public String getName() { + return getMongoEntity().getName(); + } + + @Override + public void setName(String name) { + getMongoEntity().setName(name); + updateMongoEntity(); + } + + @Override + public String getDescription() { return getMongoEntity().getDescription(); } + + @Override + public String getProtocol() { + return getMongoEntity().getProtocol(); + } + + @Override + public void setProtocol(String protocol) { + getMongoEntity().setProtocol(protocol); + updateMongoEntity(); + + } + + + @Override + public void setDescription(String description) { + getMongoEntity().setDescription(description); + updateMongoEntity(); + } + + @Override + public Set getProtocolMappers() { + Set result = new HashSet(); + for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) { + ProtocolMapperModel mapping = new ProtocolMapperModel(); + mapping.setId(entity.getId()); + mapping.setName(entity.getName()); + mapping.setProtocol(entity.getProtocol()); + mapping.setProtocolMapper(entity.getProtocolMapper()); + mapping.setConsentRequired(entity.isConsentRequired()); + mapping.setConsentText(entity.getConsentText()); + Map config = new HashMap(); + if (entity.getConfig() != null) { + config.putAll(entity.getConfig()); + } + mapping.setConfig(config); + result.add(mapping); + } + return result; + } + + @Override + public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) { + if (getProtocolMapperByName(model.getProtocol(), model.getName()) != null) { + throw new ModelDuplicateException("Protocol mapper name must be unique per protocol"); + } + ProtocolMapperEntity entity = new ProtocolMapperEntity(); + String id = model.getId() != null ? model.getId() : KeycloakModelUtils.generateId(); + entity.setId(id); + entity.setProtocol(model.getProtocol()); + entity.setName(model.getName()); + entity.setProtocolMapper(model.getProtocolMapper()); + entity.setConfig(model.getConfig()); + entity.setConsentRequired(model.isConsentRequired()); + entity.setConsentText(model.getConsentText()); + getMongoEntity().getProtocolMappers().add(entity); + updateMongoEntity(); + return entityToModel(entity); + } + + @Override + public void removeProtocolMapper(ProtocolMapperModel mapping) { + for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) { + if (entity.getId().equals(mapping.getId())) { + session.users().preRemove(mapping); + + getMongoEntity().getProtocolMappers().remove(entity); + updateMongoEntity(); + break; + } + } + + } + + protected ProtocolMapperEntity getProtocolMapperyEntityById(String id) { + for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) { + if (entity.getId().equals(id)) { + return entity; + } + } + return null; + + } + protected ProtocolMapperEntity getProtocolMapperEntityByName(String protocol, String name) { + for (ProtocolMapperEntity entity : getMongoEntity().getProtocolMappers()) { + if (entity.getProtocol().equals(protocol) && entity.getName().equals(name)) { + return entity; + } + } + return null; + + } + + + @Override + public void updateProtocolMapper(ProtocolMapperModel mapping) { + ProtocolMapperEntity entity = getProtocolMapperyEntityById(mapping.getId()); + entity.setProtocolMapper(mapping.getProtocolMapper()); + entity.setConsentRequired(mapping.isConsentRequired()); + entity.setConsentText(mapping.getConsentText()); + if (entity.getConfig() != null) { + entity.getConfig().clear(); + entity.getConfig().putAll(mapping.getConfig()); + } else { + entity.setConfig(mapping.getConfig()); + } + updateMongoEntity(); + + } + + @Override + public ProtocolMapperModel getProtocolMapperById(String id) { + ProtocolMapperEntity entity = getProtocolMapperyEntityById(id); + if (entity == null) return null; + return entityToModel(entity); + } + + @Override + public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) { + ProtocolMapperEntity entity = getProtocolMapperEntityByName(protocol, name); + if (entity == null) return null; + return entityToModel(entity); + } + + protected ProtocolMapperModel entityToModel(ProtocolMapperEntity entity) { + ProtocolMapperModel mapping = new ProtocolMapperModel(); + mapping.setId(entity.getId()); + mapping.setName(entity.getName()); + mapping.setProtocol(entity.getProtocol()); + mapping.setProtocolMapper(entity.getProtocolMapper()); + mapping.setConsentRequired(entity.isConsentRequired()); + mapping.setConsentText(entity.getConsentText()); + Map config = new HashMap(); + if (entity.getConfig() != null) config.putAll(entity.getConfig()); + mapping.setConfig(config); + return mapping; + } + + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof ClientTemplateModel)) return false; + + ClientTemplateModel that = (ClientTemplateModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java index 32be0be2e6..16fd3695b8 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java @@ -28,7 +28,7 @@ import java.util.Set; /** * - * @author Marek Posolda + * @author Bill Burke */ public class GroupAdapter extends AbstractMongoAdapter implements GroupModel { diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java index 18acfa9cc6..1bba807316 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java @@ -7,12 +7,14 @@ import org.keycloak.connections.mongo.api.MongoStore; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.migration.MigrationModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoClientTemplateEntity; import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity; import org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity; @@ -143,4 +145,15 @@ public class MongoRealmProvider implements RealmProvider { return new ClientAdapter(session, realm, appData, invocationContext); } + @Override + public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) { + MongoClientTemplateEntity appData = getMongoStore().loadEntity(MongoClientTemplateEntity.class, id, invocationContext); + + // Check if application belongs to this realm + if (appData == null || !realm.getId().equals(appData.getRealmId())) { + return null; + } + + return new ClientTemplateAdapter(session, realm, appData, invocationContext); + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index 3b5a1acf8d..b535c85725 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -435,7 +435,7 @@ public class MongoUserProvider implements UserProvider { } @Override - public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) { + public void preRemove(ProtocolMapperModel protocolMapper) { // Remove this protocol mapper from all consents, which has it DBObject query = new QueryBuilder() .and("grantedProtocolMappers").is(protocolMapper.getId()) diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 4229cd97a2..c3b159a327 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -9,6 +9,7 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; @@ -35,6 +36,7 @@ import org.keycloak.models.entities.RequiredCredentialEntity; import org.keycloak.models.entities.UserFederationMapperEntity; import org.keycloak.models.entities.UserFederationProviderEntity; import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity; +import org.keycloak.models.mongo.keycloak.entities.MongoClientTemplateEntity; import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; @@ -2013,4 +2015,52 @@ public class RealmAdapter extends AbstractMongoAdapter impleme mapper.setConfig(config); return mapper; } + + @Override + public List getClientTemplates() { + DBObject query = new QueryBuilder() + .and("realmId").is(getId()) + .get(); + List clientEntities = getMongoStore().loadEntities(MongoClientTemplateEntity.class, query, invocationContext); + + List result = new LinkedList<>(); + for (MongoClientTemplateEntity clientEntity : clientEntities) { + result.add(new ClientTemplateAdapter(session, this, clientEntity, invocationContext)); + } + return result; + } + + @Override + public ClientTemplateModel addClientTemplate(String name) { + return this.addClientTemplate(null, name); + } + + @Override + public ClientTemplateModel addClientTemplate(String id, String name) { + MongoClientTemplateEntity clientEntity = new MongoClientTemplateEntity(); + clientEntity.setId(id); + clientEntity.setName(name); + clientEntity.setRealmId(getId()); + getMongoStore().insertEntity(clientEntity, invocationContext); + + final ClientTemplateModel model = new ClientTemplateAdapter(session, this, clientEntity, invocationContext); + return model; + } + + @Override + public boolean removeClientTemplate(String id) { + if (id == null) return false; + ClientTemplateModel client = getClientTemplateById(id); + if (client == null) return false; + + + return getMongoStore().removeEntity(MongoClientTemplateEntity.class, id, invocationContext); + } + + @Override + public ClientTemplateModel getClientTemplateById(String id) { + return model.getClientTemplateById(id, this); + } + + } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientTemplateEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientTemplateEntity.java new file mode 100755 index 0000000000..ab511051a7 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientTemplateEntity.java @@ -0,0 +1,21 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.connections.mongo.api.MongoCollection; +import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.entities.ClientEntity; +import org.keycloak.models.entities.ClientTemplateEntity; + +/** + * @author Bill Burke + */ +@MongoCollection(collectionName = "clientTemplates") +public class MongoClientTemplateEntity extends ClientTemplateEntity implements MongoIdentifiableEntity { + + @Override + public void afterRemove(MongoStoreInvocationContext context) { + + } +} diff --git a/pom.xml b/pom.xml index c7b2c6942a..b074d3f498 100755 --- a/pom.xml +++ b/pom.xml @@ -1393,7 +1393,7 @@ maven-surefire-plugin once - -Xms512m -Xmx1024m -XX:MaxPermSize=512m + -Xms512m -Xmx2048m -XX:MaxPermSize=1024m diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index e76734eb43..346832a38b 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -13,6 +13,7 @@ import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -258,13 +259,23 @@ public class TokenManager { Set requestedRoles = new HashSet(); // todo scope param protocol independent String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE); - for (RoleModel r : TokenManager.getAccess(scopeParam, true, clientSession.getClient(), user)) { + ClientModel client = clientSession.getClient(); + for (RoleModel r : TokenManager.getAccess(scopeParam, true, client, user)) { requestedRoles.add(r.getId()); } clientSession.setRoles(requestedRoles); Set requestedProtocolMappers = new HashSet(); - for (ProtocolMapperModel protocolMapper : clientSession.getClient().getProtocolMappers()) { + ClientTemplateModel clientTemplate = client.getClientTemplate(); + if (clientTemplate != null) { + for (ProtocolMapperModel protocolMapper : clientTemplate.getProtocolMappers()) { + if (protocolMapper.getProtocol().equals(clientSession.getAuthMethod())) { + requestedProtocolMappers.add(protocolMapper.getId()); + } + } + + } + for (ProtocolMapperModel protocolMapper : client.getProtocolMappers()) { if (protocolMapper.getProtocol().equals(clientSession.getAuthMethod())) { requestedProtocolMappers.add(protocolMapper.getId()); } diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java index b0fd08e491..5e2468155c 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java @@ -1,6 +1,8 @@ package org.keycloak.services.managers; +import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; @@ -185,9 +187,15 @@ public class ClientSessionCode { public Set getRequestedProtocolMappers() { Set requestedProtocolMappers = new HashSet(); - if (clientSession.getProtocolMappers() != null) { - for (String protocolMapperId : clientSession.getProtocolMappers()) { - ProtocolMapperModel protocolMapper = clientSession.getClient().getProtocolMapperById(protocolMapperId); + Set protocolMappers = clientSession.getProtocolMappers(); + ClientModel client = clientSession.getClient(); + ClientTemplateModel template = client.getClientTemplate(); + if (protocolMappers != null) { + for (String protocolMapperId : protocolMappers) { + ProtocolMapperModel protocolMapper = client.getProtocolMapperById(protocolMapperId); + if (protocolMapper == null && template != null) { + protocolMapper = template.getProtocolMapperById(protocolMapperId); + } if (protocolMapper != null) { requestedProtocolMappers.add(protocolMapper); } 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 new file mode 100755 index 0000000000..509d485e31 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java @@ -0,0 +1,147 @@ +package org.keycloak.services.resources.admin; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.BadRequestException; +import org.jboss.resteasy.spi.NotFoundException; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.common.util.Time; +import org.keycloak.events.admin.OperationType; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.representations.adapters.action.GlobalRequestResult; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.UserSessionRepresentation; +import org.keycloak.services.ErrorResponse; +import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils; +import org.keycloak.services.managers.ClientManager; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.managers.ResourceAdminManager; +import org.keycloak.services.resources.KeycloakApplication; +import org.keycloak.util.JsonSerialization; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.lang.Boolean.TRUE; + + +/** + * Base resource class for managing one particular client of a realm. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientTemplateResource { + protected static final Logger logger = Logger.getLogger(ClientTemplateResource.class); + protected RealmModel realm; + private RealmAuth auth; + private AdminEventBuilder adminEvent; + protected ClientTemplateModel client; + protected KeycloakSession session; + + @Context + protected UriInfo uriInfo; + + @Context + protected KeycloakApplication keycloak; + + protected KeycloakApplication getKeycloakApplication() { + return keycloak; + } + + public ClientTemplateResource(RealmModel realm, RealmAuth auth, ClientTemplateModel clientModel, KeycloakSession session, AdminEventBuilder adminEvent) { + this.realm = realm; + this.auth = auth; + this.client = clientModel; + this.session = session; + this.adminEvent = adminEvent; + + auth.init(RealmAuth.Resource.CLIENT); + } + + @Path("protocol-mappers") + public ProtocolMappersResource getProtocolMappers() { + ProtocolMappersResource mappers = new ProtocolMappersResource(client, auth, adminEvent); + ResteasyProviderFactory.getInstance().injectProperties(mappers); + return mappers; + } + + /** + * Update the client template + * @param rep + * @return + */ + @PUT + @Consumes(MediaType.APPLICATION_JSON) + public Response update(final ClientTemplateRepresentation rep) { + auth.requireManage(); + + try { + RepresentationToModel.updateClientTemplate(rep, client); + adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success(); + return Response.noContent().build(); + } catch (ModelDuplicateException e) { + return ErrorResponse.exists("Client Template " + rep.getName() + " already exists"); + } + } + + + /** + * Get representation of the client template + * + * @return + */ + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public ClientTemplateRepresentation getClient() { + auth.requireView(); + return ModelToRepresentation.toRepresentation(client); + } + + /** + * Delete the client template + * + */ + @DELETE + @NoCache + public void deleteClientTemplate() { + auth.requireManage(); + realm.removeClientTemplate(client.getId()); + adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); + } + + + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java new file mode 100755 index 0000000000..6a65875ae8 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java @@ -0,0 +1,127 @@ +package org.keycloak.services.resources.admin; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.NotFoundException; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.events.admin.OperationType; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientTemplateModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; +import org.keycloak.services.ErrorResponse; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.ArrayList; +import java.util.List; + +/** + * Base resource class for managing a realm's client templates. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ClientTemplatesResource { + protected static final Logger logger = Logger.getLogger(RealmAdminResource.class); + protected RealmModel realm; + private RealmAuth auth; + private AdminEventBuilder adminEvent; + + @Context + protected KeycloakSession session; + + public ClientTemplatesResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) { + this.realm = realm; + this.auth = auth; + this.adminEvent = adminEvent; + + auth.init(RealmAuth.Resource.CLIENT); + } + + /** + * Get client templates belonging to the realm + * + * Returns a list of client templates belonging to the realm + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public List getClientTemplates() { + auth.requireAny(); + + List rep = new ArrayList<>(); + List clientModels = realm.getClientTemplates(); + + boolean view = auth.hasView(); + for (ClientTemplateModel clientModel : clientModels) { + if (view) { + rep.add(ModelToRepresentation.toRepresentation(clientModel)); + } else { + ClientTemplateRepresentation client = new ClientTemplateRepresentation(); + client.setId(clientModel.getId()); + client.setName(clientModel.getName()); + client.setDescription(clientModel.getDescription()); + rep.add(client); + } + } + return rep; + } + + /** + * Create a new client template + * + * Client Template's name must be unique! + * + * @param uriInfo + * @param rep + * @return + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response createClientTemplate(final @Context UriInfo uriInfo, final ClientTemplateRepresentation rep) { + auth.requireManage(); + + try { + ClientTemplateModel clientModel = RepresentationToModel.createClientTemplate(session, realm, rep); + + adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientModel.getId()).representation(rep).success(); + + return Response.created(uriInfo.getAbsolutePathBuilder().path(clientModel.getId()).build()).build(); + } catch (ModelDuplicateException e) { + return ErrorResponse.exists("Client Template " + rep.getName() + " already exists"); + } + } + + /** + * Base path for managing a specific client template. + * + * @param id id of client template (not name) + * @return + */ + @Path("{id}") + public ClientTemplateResource getClient(final @PathParam("id") String id) { + ClientTemplateModel clientModel = realm.getClientTemplateById(id); + if (clientModel == null) { + throw new NotFoundException("Could not find client template"); + } + + ClientTemplateResource clientResource = new ClientTemplateResource(realm, auth, clientModel, session, adminEvent); + ResteasyProviderFactory.getInstance().injectProperties(clientResource); + return clientResource; + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java index f53d2564b8..a115fc5349 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java @@ -7,6 +7,7 @@ import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.ProtocolMapperContainerModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; @@ -38,7 +39,7 @@ import java.util.List; public class ProtocolMappersResource { protected static final Logger logger = Logger.getLogger(ProtocolMappersResource.class); - protected ClientModel client; + protected ProtocolMapperContainerModel client; protected RealmAuth auth; @@ -50,7 +51,7 @@ public class ProtocolMappersResource { @Context protected KeycloakSession session; - public ProtocolMappersResource(ClientModel client, RealmAuth auth, AdminEventBuilder adminEvent) { + public ProtocolMappersResource(ProtocolMapperContainerModel client, RealmAuth auth, AdminEventBuilder adminEvent) { this.auth = auth; this.client = client; this.adminEvent = adminEvent; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index a4d5f27f77..038f43b568 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -144,6 +144,18 @@ public class RealmAdminResource { return clientsResource; } + /** + * Base path for managing client templates under this realm. + * + * @return + */ + @Path("client-templates") + public ClientTemplatesResource getClientTemplates() { + ClientTemplatesResource clientsResource = new ClientTemplatesResource(realm, auth, adminEvent); + ResteasyProviderFactory.getInstance().injectProperties(clientsResource); + return clientsResource; + } + /** * Base path for managing client initial access tokens * diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index 3f4c53b105..4a16c8ced7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -32,19 +32,24 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; -import org.keycloak.common.VerificationException; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.ClientTemplateResource; +import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.enums.SslRequired; +import org.keycloak.common.util.Time; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.Event; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.mappers.AddressMapper; import org.keycloak.protocol.oidc.mappers.HardcodedClaim; @@ -53,6 +58,9 @@ import org.keycloak.protocol.oidc.mappers.RoleNameMapper; import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientTemplateRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; @@ -63,7 +71,6 @@ import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.keycloak.util.BasicAuthHelper; -import org.keycloak.common.util.Time; import org.openqa.selenium.WebDriver; import javax.ws.rs.client.Client; @@ -75,7 +82,6 @@ import javax.ws.rs.core.GenericType; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; -import java.io.IOException; import java.net.URI; import java.util.Arrays; import java.util.HashMap; @@ -91,12 +97,15 @@ import static org.junit.Assert.*; */ public class AccessTokenTest { + protected static Keycloak keycloak; + @ClassRule public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true); + keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CLI_CLIENT_ID); } }); @@ -393,7 +402,8 @@ public class AccessTokenTest { { Response response = validateTarget.queryParam("access_token", "bad token").request().get(); Assert.assertEquals(400, response.getStatus()); - HashMap error = response.readEntity(new GenericType >() {}); + HashMap error = response.readEntity(new GenericType>() { + }); Assert.assertNotNull(error.get("error")); } @@ -428,7 +438,8 @@ public class AccessTokenTest { { Response response = validateTarget.queryParam("access_token", tokenResponse.getToken()).request().get(); Assert.assertEquals(400, response.getStatus()); - HashMap error = response.readEntity(new GenericType >() {}); + HashMap error = response.readEntity(new GenericType>() { + }); Assert.assertNotNull(error.get("error")); } @@ -657,6 +668,7 @@ public class AccessTokenTest { } + @Test public void testTokenMapping() throws Exception { Client client = ClientBuilder.newClient(); @@ -678,7 +690,8 @@ public class AccessTokenTest { ClientModel app = realm.getClientByClientId("test-app"); ProtocolMapperModel mapper = AddressMapper.createAddressMapper(true, true); app.addProtocolMapper(mapper); - app.addProtocolMapper(HardcodedClaim.create("hard", "hard", "coded", "String", false, null, true, true)); + ProtocolMapperModel hard = HardcodedClaim.create("hard", "hard", "coded", "String", false, null, true, true); + app.addProtocolMapper(hard); app.addProtocolMapper(HardcodedClaim.create("hard-nested", "nested.hard", "coded-nested", "String", false, null, true, true)); app.addProtocolMapper(UserAttributeMapper.createClaimMapper("custom phone", "phone", "home_phone", "String", true, "", true, true, false)); app.addProtocolMapper(UserAttributeMapper.createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true, false)); @@ -705,11 +718,11 @@ public class AccessTokenTest { Assert.assertNotNull(idToken.getOtherClaims().get("home_phone")); Assert.assertEquals("617-777-6666", idToken.getOtherClaims().get("home_phone")); Assert.assertEquals("coded", idToken.getOtherClaims().get("hard")); - Map nested = (Map)idToken.getOtherClaims().get("nested"); + Map nested = (Map) idToken.getOtherClaims().get("nested"); Assert.assertEquals("coded-nested", nested.get("hard")); - nested = (Map)idToken.getOtherClaims().get("home"); + nested = (Map) idToken.getOtherClaims().get("home"); Assert.assertEquals("617-777-6666", nested.get("phone")); - List departments = (List)idToken.getOtherClaims().get("department"); + List departments = (List) idToken.getOtherClaims().get("department"); Assert.assertEquals(2, departments.size()); Assert.assertTrue(departments.contains("finance") && departments.contains("development")); @@ -724,11 +737,11 @@ public class AccessTokenTest { Assert.assertNotNull(accessToken.getOtherClaims().get("home_phone")); Assert.assertEquals("617-777-6666", accessToken.getOtherClaims().get("home_phone")); Assert.assertEquals("coded", accessToken.getOtherClaims().get("hard")); - nested = (Map)accessToken.getOtherClaims().get("nested"); + nested = (Map) accessToken.getOtherClaims().get("nested"); Assert.assertEquals("coded-nested", nested.get("hard")); - nested = (Map)accessToken.getOtherClaims().get("home"); + nested = (Map) accessToken.getOtherClaims().get("home"); Assert.assertEquals("617-777-6666", nested.get("phone")); - departments = (List)idToken.getOtherClaims().get("department"); + departments = (List) idToken.getOtherClaims().get("department"); Assert.assertEquals(2, departments.size()); Assert.assertTrue(departments.contains("finance") && departments.contains("development")); Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("hardcoded")); @@ -739,7 +752,6 @@ public class AccessTokenTest { response.close(); } - client.close(); // undo mappers { @@ -751,11 +763,12 @@ public class AccessTokenTest { || model.getName().equals("hard") || model.getName().equals("hard-nested") || model.getName().equals("custom phone") + || model.getName().equals("departments") || model.getName().equals("nested phone") || model.getName().equals("rename-app-role") || model.getName().equals("hard-realm") || model.getName().equals("hard-app") - ) { + ) { app.removeProtocolMapper(model); } } @@ -763,7 +776,101 @@ public class AccessTokenTest { session.close(); } + events.clear(); + + { + Response response = executeGrantAccessTokenRequest(grantTarget); + Assert.assertEquals(200, response.getStatus()); + org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class); + IDToken idToken = getIdToken(tokenResponse); + Assert.assertNull(idToken.getAddress()); + Assert.assertNull(idToken.getOtherClaims().get("home_phone")); + Assert.assertNull(idToken.getOtherClaims().get("hard")); + Assert.assertNull(idToken.getOtherClaims().get("nested")); + Assert.assertNull(idToken.getOtherClaims().get("department")); + + response.close(); + } + + + events.clear(); + client.close(); + + + } + + @Test + public void testClientTemplate() throws Exception { + RealmResource realm = keycloak.realms().realm("test"); + ClientTemplateRepresentation rep = new ClientTemplateRepresentation(); + rep.setName("template"); + rep.setProtocol("oidc"); + Response response = realm.clientTemplates().create(rep); + Assert.assertEquals(201, response.getStatus()); + URI templateUri = response.getLocation(); + response.close(); + ClientTemplateResource templateResource = keycloak.proxy(ClientTemplateResource.class, templateUri); + ProtocolMapperModel hard = HardcodedClaim.create("hard", "hard", "coded", "String", false, null, true, true); + ProtocolMapperRepresentation mapper = ModelToRepresentation.toRepresentation(hard); + response = templateResource.getProtocolMappers().createMapper(mapper); + Assert.assertEquals(201, response.getStatus()); + response.close(); + List clients = realm.clients().findAll(); + ClientRepresentation clientRep = null; + for (ClientRepresentation c : clients) { + if (c.getClientId().equals("test-app")) { + clientRep = c; + break; + } + + } + Assert.assertNotNull(clientRep); + clientRep.setClientTemplate("template"); + realm.clients().get(clientRep.getId()).update(clientRep); + + { + Client client = ClientBuilder.newClient(); + UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT); + URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test"); + WebTarget grantTarget = client.target(grantUri); + + response = executeGrantAccessTokenRequest(grantTarget); + Assert.assertEquals(200, response.getStatus()); + org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class); + IDToken idToken = getIdToken(tokenResponse); + Assert.assertEquals("coded", idToken.getOtherClaims().get("hard")); + + AccessToken accessToken = getAccessToken(tokenResponse); + Assert.assertEquals("coded", accessToken.getOtherClaims().get("hard")); + + + response.close(); + client.close(); + } + // undo mappers + clientRep.setClientTemplate(ClientTemplateRepresentation.NONE); + realm.clients().get(clientRep.getId()).update(clientRep); + + { + Client client = ClientBuilder.newClient(); + UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT); + URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test"); + WebTarget grantTarget = client.target(grantUri); + + response = executeGrantAccessTokenRequest(grantTarget); + Assert.assertEquals(200, response.getStatus()); + org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class); + IDToken idToken = getIdToken(tokenResponse); + Assert.assertNull(idToken.getOtherClaims().get("hard")); + + AccessToken accessToken = getAccessToken(tokenResponse); + Assert.assertNull(accessToken.getOtherClaims().get("hard")); + + + response.close(); + client.close(); + } events.clear(); }