keycloak-uncached
Changes
export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java 11(+11 -0)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_ca.properties 0(+0 -0)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_es.properties 0(+0 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js 402(+381 -21)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html 12(+12 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-protocol-mapper-detail.html 13(+13 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-detail.html 55(+55 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-list.html 50(+50 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers.html 57(+57 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers-add.html 53(+53 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-protocol-mapper-detail.html 13(+13 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html 57(+22 -35)
forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html 16(+16 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplateResource.java 40(+40 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientTemplatesResource.java 34(+34 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java 3(+3 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java 14(+14 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java 149(+149 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheRealmProvider.java 48(+40 -8)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java 4(+2 -2)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanRealmCache.java 33(+33 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java 47(+47 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java 1(+1 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java 8(+8 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClientTemplate.java 66(+66 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java 12(+12 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmCache.java 12(+12 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientTemplateAdapter.java 229(+229 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java 13(+13 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java 2(+1 -1)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoClientTemplateEntity.java 21(+21 -0)
pom.xml 2(+1 -1)
services/src/main/java/org/keycloak/services/resources/admin/ClientTemplateResource.java 147(+147 -0)
Details
diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml
index e415488..36f33bf 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 @@
<class>org.keycloak.models.jpa.entities.GroupAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>
+ <class>org.keycloak.models.jpa.entities.ClientTemplateEntity</class>
<!-- JpaAuditProviders -->
<class>org.keycloak.events.jpa.EventEntity</class>
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 c4dce2b..db2a791
--- 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 @@
<constraints nullable="true"/>
</column>
</addColumn>
+ <createTable tableName="CLIENT_TEMPLATE">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="NAME" type="VARCHAR(255)"/>
+ <column name="REALM_ID" type="VARCHAR(36)"/>
+ <column name="DESCRIPTION" type="VARCHAR(255)"/>
+ <column name="PROTOCOL" type="VARCHAR(255)"/>
+ </createTable>
+
+
+
+ <dropNotNullConstraint tableName="PROTOCOL_MAPPER" columnName="CLIENT_ID" columnDataType="VARCHAR(36)"/>
+ <addColumn tableName="CLIENT">
+ <column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
+ </addColumn>
+ <addColumn tableName="PROTOCOL_MAPPER">
+ <column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
+ </addColumn>
+ <createTable tableName="REALM_CLIENT_TEMPLATE">
+ <column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="REALM_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="ID" constraintName="PK_CLI_TEMPLATE" tableName="CLIENT_TEMPLATE"/>
+ <addUniqueConstraint columnNames="REALM_ID,NAME" constraintName="UK_CLI_TEMPLATE" tableName="CLIENT_TEMPLATE"/>
+ <addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="CLIENT_TEMPLATE" constraintName="FK_REALM_CLI_TMPLT" referencedColumnNames="ID" referencedTableName="REALM"/>
+ <addForeignKeyConstraint baseColumnNames="CLIENT_TEMPLATE_ID" baseTableName="PROTOCOL_MAPPER" constraintName="FK_CLI_TMPLT_MAPPER" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
+ <addForeignKeyConstraint baseColumnNames="CLIENT_TEMPLATE_ID" baseTableName="CLIENT" constraintName="FK_CLI_TMPLT_CLIENT" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
+ <addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_RLM" referencedColumnNames="ID" referencedTableName="REALM"/>
+ <addForeignKeyConstraint baseColumnNames="CLIENT_TEMPLATE_ID" baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_CLI" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
+
</changeSet>
</databaseChangeLog>
\ No newline at end of file
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 a648f12..2384a2e 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<String, Integer> registeredNodes;
protected List<ProtocolMapperRepresentation> 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 0000000..f0bf09e
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<ProtocolMapperRepresentation> 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<ProtocolMapperRepresentation> getProtocolMappers() {
+ return protocolMappers;
+ }
+
+ public void setProtocolMappers(List<ProtocolMapperRepresentation> 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 bc9ebf8..cc57124 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<ScopeMappingRepresentation> scopeMappings;
protected Map<String, List<ScopeMappingRepresentation>> clientScopeMappings;
protected List<ClientRepresentation> clients;
+ protected List<ClientTemplateRepresentation> clientTemplates;
protected Map<String, String> browserSecurityHeaders;
protected Map<String, String> smtpServer;
protected List<UserFederationProviderRepresentation> userFederationProviders;
@@ -804,4 +805,12 @@ public class RealmRepresentation {
public void setGroups(List<GroupRepresentation> groups) {
this.groups = groups;
}
+
+ public List<ClientTemplateRepresentation> getClientTemplates() {
+ return clientTemplates;
+ }
+
+ public void setClientTemplates(List<ClientTemplateRepresentation> 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 12b358e..f5dfcc2 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<ClientTemplateModel> templates = realm.getClientTemplates();
+ List<ClientTemplateRepresentation> templateReps = new ArrayList<>();
+ for (ClientTemplateModel app : templates) {
+ ClientTemplateRepresentation clientRep = ModelToRepresentation.toRepresentation(app);
+ templateReps.add(clientRep);
+ }
+ rep.setClientTemplates(templateReps);
+
// Clients
List<ClientModel> clients = realm.getClients();
List<ClientRepresentation> 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 bfbcee9..826a3da 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 4e81a43..420517d 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 61b608f..94854f3 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 7b94680..7a47a83 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 5b81a01..6a2bd48 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 @@
</div>
<kc-tooltip>{{:: 'client-protocol.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="protocol">Client Template</label>
+ <div class="col-sm-6">
+ <div>
+ <select class="form-control" id="template"
+ ng-model="client.clientTemplate"
+ ng-options="template.name as template.name for template in templates">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>Client template this client inherits configuration from</kc-tooltip>
+ </div>
<div class="form-group" data-ng-show="protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="accessType">{{:: 'access-type' | translate}}</label>
<div class="col-sm-6">
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 0000000..b2af24a
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-protocol-mapper-detail.html
@@ -0,0 +1,13 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{model.realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{model.realm.realm}}/clients/{{model.client.id}}">{{model.client.clientId}}</a></li>
+ <li><a href="#/realms/{{model.realm.realm}}/clients/{{model.client.id}}/mappers">{{:: 'mappers' | translate}}</a></li>
+ <li class="active" data-ng-show="model.create">{{:: 'create-protocol-mappers' | translate}}</li>
+ <li class="active" data-ng-hide="model.create">{{model.mapper.name}}</li>
+ </ol>
+ <div ng-include="resourceUrl + '/partials/protocol-mapper-detail.html'"/>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..5b286a9
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-detail.html
@@ -0,0 +1,55 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/client-templates">Client Templates</a></li>
+ <li data-ng-show="create">Add Client Template</li>
+ <li data-ng-hide="create">{{template.name}}</li>
+ </ol>
+
+ <kc-tabs-client-template></kc-tabs-client-template>
+
+ <form class="form-horizontal" name="clientForm" novalidate kc-read-only="!access.manageClients">
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="template.name" autofocus>
+ </div>
+ <kc-tooltip>Name of the client template. Must be unique in the realm</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Description </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="template.description">
+ </div>
+ <kc-tooltip>Description of the client template</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="protocol">Protocol</label>
+ <div class="col-sm-6">
+ <div>
+ <select class="form-control" id="protocol"
+ ng-change="changeProtocol()"
+ ng-model="protocol"
+ ng-options="aProtocol for aProtocol in protocols">
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>Which SSO protocol configuration is being supplied by this client template</kc-tooltip>
+ </div>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageClients">
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
+ <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..81d6d6f
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-list.html
@@ -0,0 +1,50 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <h1>
+ <span>Client Templates</span>
+ <kc-tooltip>Client templates allow you to define common configuration that is shared between multiple clients</kc-tooltip>
+ </h1>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+ <div class="form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ </div>
+
+ <div class="pull-right" data-ng-show="access.manageClients">
+ <a id="createClient" class="btn btn-default" href="#/create/client-template/{{realm.realm}}">{{:: 'create' | translate}}</a>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="clients.length == 0">
+ <th>{{:: 'name' | translate}}</th>
+ <th colspan="2">{{:: 'actions' | translate}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="template in templates | filter:search | orderBy:'name'">
+ <td><a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}">{{template.name}}</a></td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/client-templates/{{template.id}}">{{:: 'edit' | translate}}</button>
+ </td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm" data-ng-click="removeClientTemplate(template)">{{:: 'delete' | translate}}</button>
+ </td>
+ </tr>
+ <tr data-ng-show="(clients | filter:search).length == 0">
+ <td class="text-muted" colspan="3" data-ng-show="search.name">{{:: 'no-results' | translate}}</td>
+ <td class="text-muted" colspan="3" data-ng-hide="search.name">{{:: 'no-clients-available' | translate}}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..7b767b2
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers.html
@@ -0,0 +1,57 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/client-templates">Client Templates</a></li>
+ <li>{{template.name}}</li>
+ </ol>
+
+ <kc-tabs-client-template></kc-tabs-client-template>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="6">
+ <div class="form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ </div>
+
+ <div class="pull-right" data-ng-show="access.manageClients">
+ <a class="btn btn-default" href="#/create/client-template/{{realm.realm}}/{{template.id}}/mappers">{{:: 'create' | translate}}</a>
+ <a class="btn btn-default" href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/add-mappers">{{:: 'add-builtin' | translate}}</a>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="mappers.length == 0">
+ <th>{{:: 'name' | translate}}</th>
+ <th>{{:: 'category' | translate}}</th>
+ <th>{{:: 'type' | translate}}</th>
+ <th colspan="2">{{:: 'actions' | translate}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="mapper in mappers | filter:search">
+ <td><a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers/{{mapper.id}}">{{mapper.name}}</a></td>
+ <td>{{mapperTypes[mapper.protocolMapper].category}}</td>
+ <td>{{mapperTypes[mapper.protocolMapper].name}}</td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm" kc-open="/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers/{{mapper.id}}">{{:: 'edit' | translate}}</button>
+ </td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm" data-ng-click="removeMapper(mapper)">{{:: 'delete' | translate}}</button>
+ </td>
+ </tr>
+ <tr data-ng-show="mappers.length == 0">
+ <td>{{:: 'no-mappers-available' | translate}}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..cb09fbd
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-mappers-add.html
@@ -0,0 +1,53 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/client-templates">Client Templates</a></li>
+ <li><a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}">{{template.name}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers">{{:: 'mappers' | translate}}</a></li>
+ <li class="active">{{:: 'add-builtin-protocol-mappers' | translate}}</li>
+ </ol>
+
+ <h1>{{:: 'add-builtin-protocol-mapper' | translate}}</h1>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="4">
+ <div class="form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="mappers.length == 0">
+ <th>{{:: 'name' | translate}}</th>
+ <th>{{:: 'category' | translate}}</th>
+ <th>{{:: 'type' | translate}}</th>
+ <th>{{:: 'add' | translate}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="mapper in mappers | filter:search">
+ <td>{{mapper.name}}</td>
+ <td>{{mapperTypes[mapper.protocolMapper].category}}</td>
+ <td>{{mapperTypes[mapper.protocolMapper].name}}</td>
+ <td><input type="checkbox" ng-model="mapper.isChecked" id="{{mapper.protocolMapper}}"></td>
+ </tr>
+ <tr data-ng-show="mappers.length == 0">
+ <td>{{:: 'no-mappers-available' | translate}}</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div data-ng-show="access.manageRealm">
+ <button class="btn btn-primary" data-ng-click="add()">{{:: 'add-selected' | translate}}</button>
+ </div>
+</div>
+
+<kc-menu></kc-menu>
\ 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 0000000..78ded5a
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-template-protocol-mapper-detail.html
@@ -0,0 +1,13 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{model.realm.realm}}/client-templates">Client Templates</a></li>
+ <li><a href="#/realms/{{model.realm.realm}}/client-templates/{{model.template.id}}">{{model.template.name}}</a></li>
+ <li><a href="#/realms/{{model.realm.realm}}/client-templates/{{model.template.id}}/mappers">{{:: 'mappers' | translate}}</a></li>
+ <li class="active" data-ng-show="model.create">{{:: 'create-protocol-mappers' | translate}}</li>
+ <li class="active" data-ng-hide="model.create">{{model.mapper.name}}</li>
+ </ol>
+ <div ng-include="resourceUrl + '/partials/protocol-mapper-detail.html'"/>
+</div>
+
+<kc-menu></kc-menu>
\ 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 afc9a5f..13855ad 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 @@
-<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
-
- <ol class="breadcrumb">
- <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
- <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
- <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/mappers">{{:: 'mappers' | translate}}</a></li>
- <li class="active" data-ng-show="create">{{:: 'create-protocol-mappers' | translate}}</li>
- <li class="active" data-ng-hide="create">{{mapper.name}}</li>
- </ol>
-
- <h1 data-ng-show="create">{{:: 'create-protocol-mapper' | translate}}</h1>
- <h1 data-ng-hide="create">{{mapper.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageRealm"
- data-ng-hide="changed" data-ng-click="remove()"></i></h1>
+ <h1 data-ng-show="model.create">{{:: 'create-protocol-mapper' | translate}}</h1>
+ <h1 data-ng-hide="model.create">{{model.mapper.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!model.create && access.manageRealm"
+ data-ng-hide="model.changed" data-ng-click="remove()"></i></h1>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
@@ -18,75 +8,72 @@
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="protocol">{{:: 'protocol' | translate}}</label>
<div class="col-md-6">
- <input class="form-control" id="protocol" type="text" ng-model="protocol" readonly>
+ <input class="form-control" id="protocol" type="text" ng-model="model.protocol" readonly>
</div>
<kc-tooltip>{{:: 'protocol.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group clearfix" data-ng-show="!create">
+ <div class="form-group clearfix" data-ng-show="!model.create">
<label class="col-md-2 control-label" for="mapperId">{{:: 'id' | translate}} </label>
<div class="col-md-6">
- <input class="form-control" id="mapperId" type="text" ng-model="mapper.id" readonly>
+ <input class="form-control" id="mapperId" type="text" ng-model="model.mapper.id" readonly>
</div>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="name">{{:: 'name' | translate}}</label>
<div class="col-md-6">
- <input class="form-control" id="name" type="text" ng-model="mapper.name" data-ng-readonly="!create" required>
+ <input class="form-control" id="name" type="text" ng-model="model.mapper.name" data-ng-readonly="!model.create" required>
</div>
<kc-tooltip>{{:: 'mapper.name.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label for="consentRequired" class="col-sm-2 control-label">{{:: 'consent-required' | translate}}</label>
<div class="col-md-6">
- <input ng-model="mapper.consentRequired" name="consentRequired" id="consentRequired" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ <input ng-model="model.mapper.consentRequired" name="consentRequired" id="consentRequired" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'mapper.consent-required.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group" data-ng-show="mapper.consentRequired">
+ <div class="form-group" data-ng-show="model.mapper.consentRequired">
<label class="col-md-2 control-label" for="consentText">{{:: 'consent-text' | translate}} </label>
<div class="col-md-6">
- <textarea class="form-control" rows="5" cols="50" id="consentText" name="consentText" data-ng-model="mapper.consentText"></textarea>
+ <textarea class="form-control" rows="5" cols="50" id="consentText" name="consentText" data-ng-model="model.mapper.consentText"></textarea>
</div>
<kc-tooltip>{{:: 'consent-text.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group" data-ng-show="create">
+ <div class="form-group" data-ng-show="model.create">
<label class="col-md-2 control-label" for="mapperTypeCreate">{{:: 'mapper-type' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="mapperTypeCreate"
- ng-model="mapperType"
- ng-options="mapperType.name for mapperType in mapperTypes"
+ ng-model="model.mapperType"
+ ng-options="mapperType.name for mapperType in model.mapperTypes"
required>
</select>
</div>
</div>
- <kc-tooltip>{{mapperType.helpText}}</kc-tooltip>
+ <kc-tooltip>{{model.mapperType.helpText}}</kc-tooltip>
</div>
- <div class="form-group clearfix" data-ng-hide="create">
+ <div class="form-group clearfix" data-ng-hide="model.create">
<label class="col-md-2 control-label" for="mapperType">{{:: 'mapper-type' | translate}}</label>
<div class="col-md-6">
- <input class="form-control" id="mapperType" type="text" ng-model="mapperType.name" data-ng-readonly="true">
+ <input class="form-control" id="mapperType" type="text" ng-model="model.mapperType.name" data-ng-readonly="true">
</div>
- <kc-tooltip>{{mapperType.helpText}}</kc-tooltip>
+ <kc-tooltip>{{model.mapperType.helpText}}</kc-tooltip>
</div>
- <kc-provider-config config="mapper.config" properties="mapperType.properties" realm="realm"></kc-provider-config>
+ <kc-provider-config config="model.mapper.config" properties="model.mapperType.properties" realm="model.realm"></kc-provider-config>
</fieldset>
<div class="form-group">
- <div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageRealm">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="model.create && access.manageRealm">
<button kc-save>{{:: 'save' | translate}}</button>
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
</div>
</div>
<div class="form-group">
- <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
- <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
- <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!model.create && access.manageRealm">
+ <button kc-save data-ng-disabled="!model.changed">{{:: 'save' | translate}}</button>
+ <button kc-reset data-ng-disabled="!model.changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
-</div>
-
-<kc-menu></kc-menu>
\ 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 8da0e56..5904fd7 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'">
<a href="#/realms/{{realm.realm}}"><span class="pficon pficon-settings"></span> Realm Settings</a>
</li>
- <li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'clients' || path[1] == 'client' || path[3] == 'clients') && 'active'"><a href="#/realms/{{realm.realm}}/clients"><i class="fa fa-cubes"></i> Clients</a></li>
+ <li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'clients' || path[1] == 'client' || path[3] == 'clients') && 'active'"><a href="#/realms/{{realm.realm}}/clients"><i class="fa fa-cube"></i> Clients</a></li>
+ <li data-ng-show="access.viewClients" data-ng-class="(path[2] == 'client-templates' || path[1] == 'client-template' || path[3] == 'client-templates') && 'active'"><a href="#/realms/{{realm.realm}}/client-templates"><i class="fa fa-cubes"></i> Client Templates</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'roles' || path[2] == 'default-roles' || (path[1] == 'role' && path[3] != 'clients')) && 'active'"><a href="#/realms/{{realm.realm}}/roles"><i class="fa fa-tasks"></i> Roles</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'identity-provider-settings' || path[2] == 'identity-provider-mappers') && 'active'"><a href="#/realms/{{realm.realm}}/identity-provider-settings"><i class="fa fa-exchange"></i> Identity Providers</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[1] == 'user-federation' || path[2] == 'user-federation') && 'active'"><a href="#/realms/{{realm.realm}}/user-federation"><i class="fa fa-database"></i> User Federation</a></li>
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 0000000..728e091
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-template.html
@@ -0,0 +1,16 @@
+<div data-ng-controller="ClientTemplateTabCtrl">
+
+ <h1 data-ng-show="create">Add Client Template</h1>
+ <h1 data-ng-hide="create">
+ {{template.name|capitalize}}
+ <i id="removeClientTemplate" class="pficon pficon-delete clickable" data-ng-show="access.manageClients" data-ng-click="removeClientTemplate()"></i>
+ </h1>
+
+ <ul class="nav nav-tabs nav-tabs-pf" data-ng-hide="create && !path[4]">
+ <li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}">{{:: 'settings' | translate}}</a></li>
+ <li ng-class="{active: path[4] == 'mappers'}">
+ <a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers">{{:: 'mappers' | translate}}</a>
+ <kc-tooltip>{{:: 'mappers.tooltip' | translate}}</kc-tooltip>
+ </li>
+ </ul>
+</div>
\ 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 bc75fa9..adca52b 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
@@ -61,6 +63,19 @@ public class Keycloak {
}
/**
+ * Create a secure proxy based on an absolute URI.
+ * All set up with appropriate token
+ *
+ * @param proxyClass
+ * @param absoluteURI
+ * @param <T>
+ * @return
+ */
+ public <T> T proxy(Class<T> proxyClass, URI absoluteURI) {
+ return client.target(absoluteURI).register(new BearerAuthFilter(tokenManager)).proxy(proxyClass);
+ }
+
+ /**
* Closes the underlying client. After calling this method, this <code>Keycloak</code> instance cannot be reused.
*/
public void close() {
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 0000000..0a5d8ff
--- /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 0000000..f0f2f66
--- /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<ClientTemplateRepresentation> 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 6dee1cb..7145f1e 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 492c2e1..f7e0305 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ProtocolMapperModel> getProtocolMappers();
- ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model);
- void removeProtocolMapper(ProtocolMapperModel mapping);
- void updateProtocolMapper(ProtocolMapperModel mapping);
- ProtocolMapperModel getProtocolMapperById(String id);
- ProtocolMapperModel getProtocolMapperByName(String protocol, String name);
-
- Map<String, Integer> getRegisteredNodes();
+ Map<String, Integer> 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 0000000..f7d67ac
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
index 46259c9..24e5fc6 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<String> scopeIds = new ArrayList<String>();
private List<ClientIdentityProviderMappingEntity> identityProviders = new ArrayList<ClientIdentityProviderMappingEntity>();
private List<ProtocolMapperEntity> protocolMappers = new ArrayList<ProtocolMapperEntity>();
+ private String clientTemplate;
public String getClientId() {
return clientId;
@@ -300,5 +301,13 @@ public class ClientEntity extends AbstractIdentifiableEntity {
public void setRegisteredNodes(Map<String, Integer> 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 0000000..849eaa1
--- /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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientTemplateEntity extends AbstractIdentifiableEntity {
+
+ private String name;
+ private String description;
+ private String realmId;
+ private String protocol;
+ private List<ProtocolMapperEntity> protocolMappers = new ArrayList<ProtocolMapperEntity>();
+
+ 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<ProtocolMapperEntity> getProtocolMappers() {
+ return protocolMappers;
+ }
+
+ public void setProtocolMappers(List<ProtocolMapperEntity> 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/ProtocolMapperContainerModel.java b/model/api/src/main/java/org/keycloak/models/ProtocolMapperContainerModel.java
new file mode 100755
index 0000000..11736b1
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ProtocolMapperContainerModel {
+ Set<ProtocolMapperModel> 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 6c2c260..bcc16f9 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<GroupModel> getTopLevelGroups();
boolean removeGroup(GroupModel group);
void moveGroup(GroupModel group, GroupModel toParent);
+
+ List<ClientTemplateModel> 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 f649f35..6d6511b 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 58344cc..e7a33ac 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 1c2b5c7..044a938 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<UserCredentialModel> input);
boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input);
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 1d34068..7b6a198 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<ProtocolMapperRepresentation> 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<String> 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 5328488..4c7dad1 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<String, ClientTemplateModel> createClientTemplates(KeycloakSession session, RealmRepresentation rep, RealmModel realm) {
+ Map<String, ClientTemplateModel> 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<ProtocolMapperModel> 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 2f7e61f..d96e6bc 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 0000000..2a1674d
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<ProtocolMapperModel> 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 3e45a69..478a087 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<String> realmInvalidations = new HashSet<String>();
- protected Set<String> appInvalidations = new HashSet<String>();
- protected Set<String> roleInvalidations = new HashSet<String>();
- protected Set<String> groupInvalidations = new HashSet<String>();
- protected Map<String, RealmModel> managedRealms = new HashMap<String, RealmModel>();
- protected Map<String, ClientModel> managedApplications = new HashMap<String, ClientModel>();
- protected Map<String, RoleModel> managedRoles = new HashMap<String, RoleModel>();
- protected Map<String, GroupModel> managedGroups = new HashMap<String, GroupModel>();
+ protected Set<String> realmInvalidations = new HashSet<>();
+ protected Set<String> appInvalidations = new HashSet<>();
+ protected Set<String> clientTemplateInvalidations = new HashSet<>();
+ protected Set<String> roleInvalidations = new HashSet<>();
+ protected Set<String> groupInvalidations = new HashSet<>();
+ protected Map<String, RealmModel> managedRealms = new HashMap<>();
+ protected Map<String, ClientModel> managedApplications = new HashMap<>();
+ protected Map<String, ClientTemplateModel> managedClientTemplates = new HashMap<>();
+ protected Map<String, RoleModel> managedRoles = new HashMap<>();
+ protected Map<String, GroupModel> 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 c7f5bf2..ba933b5 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 8155801..4cd4f79 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 c24f151..e7d15eb 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<ClientTemplateModel> getClientTemplates() {
+ if (updated != null) return updated.getClientTemplates();
+ List<ClientTemplateModel> apps = new LinkedList<ClientTemplateModel>();
+ 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 26ad12d..6386f9f 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/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
index a683956..dbf4754 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<String, String> roles = new HashMap<String, String>();
private int nodeReRegistrationTimeout;
private Map<String, Integer> 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<String, Integer>(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<String, Integer> 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 0000000..2df28c1
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<ProtocolMapperModel> protocolMappers = new HashSet<ProtocolMapperModel>();
+
+ 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<ProtocolMapperModel> 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 e423562..40e4851 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<String> groups = new HashSet<String>();
private Map<String, String> realmRoles = new HashMap<String, String>();
private Map<String, String> clients = new HashMap<String, String>();
+ private List<String> clientTemplates= new LinkedList<>();
private boolean internationalizationEnabled;
private Set<String> supportedLocales = new HashSet<String>();
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<String> getDefaultGroups() {
return defaultGroups;
}
+
+ public List<String> getClientTemplates() {
+ return clientTemplates;
+ }
}
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 007daac..e826ac1 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/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
index f677859..a8abd11 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 0000000..8e92e79
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<ProtocolMapperModel> getProtocolMappers() {
+ Set<ProtocolMapperModel> mappings = new HashSet<ProtocolMapperModel>();
+ 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<String, String> config = new HashMap<String, String>();
+ 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<String, String> config = new HashMap<String, String>();
+ 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/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
index 1569875..6767e7b 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
@@ -58,6 +58,10 @@ public class ClientEntity {
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<String, Integer> 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 0000000..ff4bd14
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<ProtocolMapperEntity> protocolMappers = new ArrayList<ProtocolMapperEntity>();
+ @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<ProtocolMapperEntity> getProtocolMappers() {
+ return protocolMappers;
+ }
+
+ public void setProtocolMappers(Collection<ProtocolMapperEntity> 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 6098df1..34ff5df 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 05af313..b8946f4 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<ClientEntity> 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<ClientTemplateEntity> clientTemplates = new ArrayList<>();
+
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<RoleEntity> roles = new ArrayList<RoleEntity>();
@@ -741,5 +745,12 @@ public class RealmEntity {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
+ public Collection<ClientTemplateEntity> getClientTemplates() {
+ return clientTemplates;
+ }
+
+ public void setClientTemplates(Collection<ClientTemplateEntity> clientTemplates) {
+ this.clientTemplates = clientTemplates;
+ }
}
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 09b4fa2..25df437 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 b536a9c..36fd1fd 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 cfd7347..4f96829 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<ClientTemplateModel> getClientTemplates() {
+ List<ClientTemplateModel> 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<ClientTemplateEntity> 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/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 eb6bcd9..af448c4 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<MongoClientEntity> 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<MongoClientEntity> 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 0000000..ee19dea
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ */
+public class ClientTemplateAdapter extends AbstractMongoAdapter<MongoClientTemplateEntity> 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<ProtocolMapperModel> getProtocolMappers() {
+ Set<ProtocolMapperModel> result = new HashSet<ProtocolMapperModel>();
+ 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<String, String> config = new HashMap<String, String>();
+ 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<String, String> config = new HashMap<String, String>();
+ 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 32be0be..16fd369 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
*/
public class GroupAdapter extends AbstractMongoAdapter<MongoGroupEntity> 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 18acfa9..1bba807 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 3b5a1ac..b535c85 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 4229cd9..c3b159a 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<MongoRealmEntity> impleme
mapper.setConfig(config);
return mapper;
}
+
+ @Override
+ public List<ClientTemplateModel> getClientTemplates() {
+ DBObject query = new QueryBuilder()
+ .and("realmId").is(getId())
+ .get();
+ List<MongoClientTemplateEntity> clientEntities = getMongoStore().loadEntities(MongoClientTemplateEntity.class, query, invocationContext);
+
+ List<ClientTemplateModel> 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 0000000..ab51105
--- /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 <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+@MongoCollection(collectionName = "clientTemplates")
+public class MongoClientTemplateEntity extends ClientTemplateEntity implements MongoIdentifiableEntity {
+
+ @Override
+ public void afterRemove(MongoStoreInvocationContext context) {
+
+ }
+}
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index c7b2c69..b074d3f 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1393,7 +1393,7 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkMode>once</forkMode>
- <argLine>-Xms512m -Xmx1024m -XX:MaxPermSize=512m</argLine>
+ <argLine>-Xms512m -Xmx2048m -XX:MaxPermSize=1024m</argLine>
</configuration>
</plugin>
<plugin>
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 e76734e..346832a 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<String> requestedRoles = new HashSet<String>();
// 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<String> requestedProtocolMappers = new HashSet<String>();
- 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 b0fd08e..5e24681 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<ProtocolMapperModel> getRequestedProtocolMappers() {
Set<ProtocolMapperModel> requestedProtocolMappers = new HashSet<ProtocolMapperModel>();
- if (clientSession.getProtocolMappers() != null) {
- for (String protocolMapperId : clientSession.getProtocolMappers()) {
- ProtocolMapperModel protocolMapper = clientSession.getClient().getProtocolMapperById(protocolMapperId);
+ Set<String> 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 0000000..509d485
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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 0000000..6a65875
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<ClientTemplateRepresentation> getClientTemplates() {
+ auth.requireAny();
+
+ List<ClientTemplateRepresentation> rep = new ArrayList<>();
+ List<ClientTemplateModel> 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 f53d256..a115fc5 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 a4d5f27..038f43b 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
@@ -145,6 +145,18 @@ public class RealmAdminResource {
}
/**
+ * 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
*
* @return
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 3f4c53b..4a16c8c 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<String, String> error = response.readEntity(new GenericType <HashMap<String, String>>() {});
+ HashMap<String, String> error = response.readEntity(new GenericType<HashMap<String, String>>() {
+ });
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<String, String> error = response.readEntity(new GenericType <HashMap<String, String>>() {});
+ HashMap<String, String> error = response.readEntity(new GenericType<HashMap<String, String>>() {
+ });
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<String> departments = (List<String>)idToken.getOtherClaims().get("department");
+ List<String> departments = (List<String>) 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<String>)idToken.getOtherClaims().get("department");
+ departments = (List<String>) 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<ClientRepresentation> 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();
}