diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml index 103c7cea69..5fc0f23b5a 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml @@ -142,6 +142,7 @@ + diff --git a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java index 928f62d915..561a5d078b 100644 --- a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java +++ b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java @@ -8,7 +8,6 @@ public interface ServiceAccountConstants { String CLIENT_AUTH = "client_auth"; String SERVICE_ACCOUNT_USER_PREFIX = "service-account-"; - String SERVICE_ACCOUNT_CLIENT_ATTRIBUTE = "serviceAccountClient"; String CLIENT_ID_PROTOCOL_MAPPER = "Client ID"; String CLIENT_HOST_PROTOCOL_MAPPER = "Client Host"; diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 1d2bee3c63..ea20afc708 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -26,6 +26,7 @@ public class UserRepresentation { protected String lastName; protected String email; protected String federationLink; + protected String serviceAccountClientId; // For rep, it points to clientId (not DB ID) // Currently there is Map> but for backwards compatibility, we also need to support Map protected Map attributes; @@ -218,4 +219,12 @@ public class UserRepresentation { public void setFederationLink(String federationLink) { this.federationLink = federationLink; } + + public String getServiceAccountClientId() { + return serviceAccountClientId; + } + + public void setServiceAccountClientId(String serviceAccountClientId) { + this.serviceAccountClientId = serviceAccountClientId; + } } diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java index f9dc9f165e..d03654d542 100644 --- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java +++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java @@ -140,15 +140,7 @@ public class ProductServiceAccountServlet extends HttpServlet { int status = response.getStatusLine().getStatusCode(); if (status != 200) { String json = getContent(entity); - String error = "Failed retrieve products."; - - if (status == 401) { - error = error + " You need to login first with the service account."; - } else if (status == 403) { - error = error + " Maybe service account user doesn't have needed role? Assign role 'user' in Keycloak admin console to user '" + - ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + getKeycloakDeployment().getResourceName() + "' and then logout and login again."; - } - error = error + " Status: " + status + ", Response: " + json; + String error = "Failed retrieve products. Status: " + status + ", Response: " + json; req.setAttribute(ERROR, error); } else if (entity == null) { req.setAttribute(ERROR, "No entity"); diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json index a26a058209..d669e6bccd 100755 --- a/examples/demo-template/testrealm.json +++ b/examples/demo-template/testrealm.json @@ -71,6 +71,13 @@ "clientRoles": { "realm-management": [ "realm-admin" ] } + }, + { + "username" : "service-account-product-sa-client", + "enabled": true, + "email" : "service-account-product-sa-client@placeholder.org", + "serviceAccountClientId": "product-sa-client", + "realmRoles": [ "user" ] } ], "roles" : { 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 a3396e3f1a..601ddaf9fa 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 @@ -123,7 +123,7 @@ public class ExportUtils { // Finally users if needed if (includeUsers) { - List allUsers = session.users().getUsers(realm); + List allUsers = session.users().getUsers(realm, true); List users = new ArrayList(); for (UserModel user : allUsers) { UserRepresentation userRep = exportUser(session, realm, user); @@ -286,6 +286,15 @@ public class ExportUtils { userRep.setClientConsents(consentReps); } + // Service account + if (user.getServiceAccountClientLink() != null) { + String clientInternalId = user.getServiceAccountClientLink(); + ClientModel client = realm.getClientById(clientInternalId); + if (client != null) { + userRep.setServiceAccountClientId(client.getClientId()); + } + } + return userRep; } diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java index c72708b362..0ecc10a1ce 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java @@ -92,7 +92,7 @@ public abstract class MultipleStepsExportProvider implements ExportProvider { @Override protected void runExportImportTask(KeycloakSession session) throws IOException { RealmModel realm = session.realms().getRealmByName(realmName); - usersHolder.users = session.users().getUsers(realm, usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart); + usersHolder.users = session.users().getUsers(realm, usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart, true); writeUsers(realmName + "-users-" + (usersHolder.currentPageStart / countPerPage) + ".json", session, realm, usersHolder.users); 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 5ddf659deb..8fb7c36fed 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 @@ -368,6 +368,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, clients : function(ClientListLoader) { return ClientListLoader(); + }, + client : function() { + return {}; } }, controller : 'UserRoleMappingCtrl' @@ -762,17 +765,23 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ClientInstallationCtrl' }) - .when('/realms/:realm/clients/:client/service-accounts', { - templateUrl : resourceUrl + '/partials/client-service-accounts.html', + .when('/realms/:realm/clients/:client/service-account-roles', { + templateUrl : resourceUrl + '/partials/client-service-account-roles.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); }, + user : function(ClientServiceAccountUserLoader) { + return ClientServiceAccountUserLoader(); + }, + clients : function(ClientListLoader) { + return ClientListLoader(); + }, client : function(ClientLoader) { return ClientLoader(); } }, - controller : 'ClientServiceAccountsCtrl' + controller : 'UserRoleMappingCtrl' }) .when('/create/client/:realm', { templateUrl : resourceUrl + '/partials/client-detail.html', 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 13639198b2..3fd0fb0ef4 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 @@ -1298,25 +1298,5 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv }); -module.controller('ClientServiceAccountsCtrl', function($scope, $http, realm, client, Notifications, Client) { - $scope.realm = realm; - $scope.client = angular.copy(client); - - $scope.serviceAccountsEnabledChanged = function() { - if (client.serviceAccountsEnabled != $scope.client.serviceAccountsEnabled) { - Client.update({ - realm : realm.realm, - client : client.id - }, $scope.client, function() { - $scope.changed = false; - client = angular.copy($scope.client); - Notifications.success("Service Account settings updated."); - }); - } - } - -}); - - diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index f3fe77f4ca..3508bf2c3a 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -1,4 +1,4 @@ -module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, clients, Notifications, RealmRoleMapping, +module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, clients, client, Notifications, RealmRoleMapping, ClientRoleMapping, AvailableRealmRoleMapping, AvailableClientRoleMapping, CompositeRealmRoleMapping, CompositeClientRoleMapping) { $scope.realm = realm; @@ -7,6 +7,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.selectedRealmMappings = []; $scope.realmMappings = []; $scope.clients = clients; + $scope.client = client; $scope.clientRoles = []; $scope.clientComposite = []; $scope.selectedClientRoles = []; @@ -28,11 +29,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.selectedRealmMappings = []; $scope.selectRealmRoles = []; - if ($scope.client) { + if ($scope.targetClient) { console.log('load available'); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; } @@ -49,11 +50,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.selectedRealmMappings = []; $scope.selectRealmRoles = []; - if ($scope.client) { + if ($scope.targetClient) { console.log('load available'); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; } @@ -62,11 +63,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl }; $scope.addClientRole = function() { - $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.client.id, + $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id, $scope.selectedClientRoles).success(function() { - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); @@ -76,11 +77,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl }; $scope.deleteClientRole = function() { - $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.client.id, + $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id, {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() { - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); @@ -92,11 +93,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.changeClient = function() { console.log('changeClient'); - if ($scope.client) { + if ($scope.targetClient) { console.log('load available'); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); } else { $scope.clientRoles = null; $scope.clientMappings = null; 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 773f6f0271..37a9566397 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 @@ -282,6 +282,15 @@ module.factory('ClientListLoader', function(Loader, Client, $route, $q) { }); }); +module.factory('ClientServiceAccountUserLoader', function(Loader, ClientServiceAccountUser, $route, $q) { + return Loader.get(ClientServiceAccountUser, function() { + return { + realm : $route.current.params.realm, + client : $route.current.params.client + } + }); +}); + module.factory('RoleMappingLoader', function(Loader, RoleMapping, $route, $q) { var realm = $route.current.params.realm || $route.current.params.client; 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 1d6a6be925..3763ba9a9f 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 @@ -897,6 +897,13 @@ module.factory('ClientOrigins', function($resource) { }); }); +module.factory('ClientServiceAccountUser', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/service-account-user', { + realm : '@realm', + client : '@client' + }); +}); + module.factory('Current', function(Realm, $route, $rootScope) { var current = { realms: {}, 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 49a1997b19..727c66d5fb 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,13 @@ 'Confidential' clients require a secret to initiate login protocol. 'Public' clients do not require a secret. 'Bearer-only' clients are web services that never initiate a login. +
+ + Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. +
+ +
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html new file mode 100644 index 0000000000..03a38d9ac2 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html @@ -0,0 +1,113 @@ +
+ + + +

{{client.clientId|capitalize}}

+ + + +

{{client.clientId}} Service Accounts

+

+ +
+
+ +
+
+
+ + Realm level roles that can be assigned to service account. + + + +
+
+ + Realm level roles assigned to service account. + + +
+
+ + Assigned realm level roles that may have been inherited from a composite role. + +
+
+
+
+ +
+ + +
+
+
Select client to view roles for client
+
+
+
+ + Client roles available to be assigned. + + +
+
+ + Assigned client roles. + + +
+
+ + Assigned client roles that may have been inherited from a composite role. + +
+
+
+
+
+ +
+ Service account is not enabled for {{client.clientId}}. +
+ +
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html deleted file mode 100644 index 1e5f0e5790..0000000000 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html +++ /dev/null @@ -1,28 +0,0 @@ -
- - - -

{{client.clientId|capitalize}}

- - - -

{{client.clientId}} Service Accounts

-

-
-
-
- - Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. -
- -
-
-
-
- -
- - \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html index 8223936aff..3228be5c74 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html @@ -52,13 +52,13 @@
-
+
Select client to view roles for client
-
+
Assignable roles from this client. diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html index c82d99f99a..2aad2d5f12 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html @@ -33,9 +33,9 @@ Helper utility for generating various client adapter configuration formats which you can download or cut and paste to configure your clients. -
  • - Service Accounts - Allows you to authenticate this client to Keycloak and retrieve access tokens dedicated to this client. +
  • + Service Account Roles + Allows you to authenticate role mappings for the service account dedicated to this client.
  • \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java index e2ef2f66fc..5a8a6e0d65 100755 --- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java +++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java @@ -32,7 +32,7 @@ public class MigrateTo1_4_0 { } public void migrateUsers(KeycloakSession session, RealmModel realm) { - List users = session.userStorage().getUsers(realm); + List users = session.userStorage().getUsers(realm, false); for (UserModel user : users) { String email = user.getEmail(); email = KeycloakModelUtils.toLowerCaseSafe(email); 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 23aaf1bd8d..ee18d798fa 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -204,8 +204,17 @@ public class UserFederationManager implements UserProvider { } @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, 0, Integer.MAX_VALUE - 1); + public UserModel getUserByServiceAccountClient(ClientModel client) { + UserModel user = session.userStorage().getUserByServiceAccountClient(client); + if (user != null) { + user = validateAndProxyUser(client.getRealm(), user); + } + return user; + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, 0, Integer.MAX_VALUE - 1, includeServiceAccounts); } @@ -242,11 +251,11 @@ public class UserFederationManager implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { + public List getUsers(RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) { return query(new PaginatedQuery() { @Override public List query(RealmModel realm, int first, int max) { - return session.userStorage().getUsers(realm, first, max); + return session.userStorage().getUsers(realm, first, max, includeServiceAccounts); } }, realm, firstResult, maxResults); } diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java index 19fdad2591..94c2ffcdfd 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -104,6 +104,9 @@ public interface UserModel { String getFederationLink(); void setFederationLink(String link); + String getServiceAccountClientLink(); + void setServiceAccountClientLink(String clientInternalId); + void addConsent(UserConsentModel consent); UserConsentModel getConsentByClient(String clientInternalId); List getConsents(); 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 f48062f361..1690b7a70c 100755 --- a/model/api/src/main/java/org/keycloak/models/UserProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java @@ -25,9 +25,12 @@ public interface UserProvider extends Provider { UserModel getUserByUsername(String username, RealmModel realm); UserModel getUserByEmail(String email, RealmModel realm); UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm); - List getUsers(RealmModel realm); + UserModel getUserByServiceAccountClient(ClientModel client); + List getUsers(RealmModel realm, boolean includeServiceAccounts); + + // Service account is included for counts int getUsersCount(RealmModel realm); - List getUsers(RealmModel realm, int firstResult, int maxResults); + List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts); List searchForUser(String search, RealmModel realm); List searchForUser(String search, RealmModel realm, int firstResult, int maxResults); List searchForUserByAttributes(Map attributes, RealmModel realm); diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java index eeae34ffe4..8c82a8e13b 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java @@ -27,6 +27,7 @@ public class UserEntity extends AbstractIdentifiableEntity { private List credentials = new ArrayList(); private List federatedIdentities; private String federationLink; + private String serviceAccountClientLink; public String getUsername() { return username; @@ -148,5 +149,13 @@ public class UserEntity extends AbstractIdentifiableEntity { public void setFederationLink(String federationLink) { this.federationLink = federationLink; } + + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } + + public void setServiceAccountClientLink(String serviceAccountClientLink) { + this.serviceAccountClientLink = serviceAccountClientLink; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index f5261a095a..cfe08521b0 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -2,6 +2,7 @@ package org.keycloak.models.utils; import org.bouncycastle.openssl.PEMWriter; import org.keycloak.constants.KerberosConstants; +import org.keycloak.constants.ServiceAccountConstants; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -350,6 +351,8 @@ public final class KeycloakModelUtils { return mapperModel; } + // END USER FEDERATION RELATED STUFF + public static String toLowerCaseSafe(String str) { return str==null ? null : str.toLowerCase(); } 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 a38b305861..83c8273acf 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 @@ -902,6 +902,14 @@ public class RepresentationToModel { user.addConsent(consentModel); } } + if (userRep.getServiceAccountClientId() != null) { + String clientId = userRep.getServiceAccountClientId(); + ClientModel client = clientMap.get(clientId); + if (client == null) { + throw new RuntimeException("Unable to find client specified for service account link. Client: " + clientId); + } + user.setServiceAccountClientLink(client.getId());; + } return user; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index 9599ab93b9..3c1edb3797 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -207,6 +207,16 @@ public class UserModelDelegate implements UserModel { delegate.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + return delegate.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + delegate.setServiceAccountClientLink(clientInternalId); + } + @Override public void addConsent(UserConsentModel consent) { delegate.addConsent(consent); diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java index ff152f8f7c..0bcc37a8b2 100755 --- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java @@ -107,8 +107,18 @@ public class FileUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, -1, -1); + public UserModel getUserByServiceAccountClient(ClientModel client) { + for (UserModel user : inMemoryModel.getUsers(client.getRealm().getId())) { + if (client.getId().equals(user.getServiceAccountClientLink())) { + return user; + } + } + return null; + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, -1, -1, includeServiceAccounts); } @Override @@ -117,12 +127,27 @@ public class FileUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - List users = new ArrayList(inMemoryModel.getUsers(realm.getId())); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + List users = new ArrayList<>(inMemoryModel.getUsers(realm.getId())); + + if (!includeServiceAccounts) { + users = filterServiceAccountUsers(users); + } + List sortedList = sortedSubList(users, firstResult, maxResults); return sortedList; } + private List filterServiceAccountUsers(List users) { + List result = new ArrayList<>(); + for (UserModel user : users) { + if (user.getServiceAccountClientLink() == null) { + result.add(user); + } + } + return result; + } + protected List sortedSubList(List list, int firstResult, int maxResults) { if (list.isEmpty()) return list; @@ -183,6 +208,9 @@ public class FileUserProvider implements UserProvider { } } + // Remove users with service account link + found = filterServiceAccountUsers(found); + return sortedSubList(found, firstResult, maxResults); } diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index 5db8f93506..98e4254ef2 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -477,6 +477,16 @@ public class UserAdapter implements UserModel, Comparable { user.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + return user.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + user.setServiceAccountClientLink(clientInternalId); + } + @Override public void addConsent(UserConsentModel consent) { // TODO diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java index 4e99e44eb7..aed1394415 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java @@ -207,8 +207,13 @@ public class DefaultCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm) { - return getDelegate().getUsers(realm); + public UserModel getUserByServiceAccountClient(ClientModel client) { + return getDelegate().getUserByServiceAccountClient(client); + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, includeServiceAccounts); } @Override @@ -217,8 +222,8 @@ public class DefaultCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - return getDelegate().getUsers(realm, firstResult, maxResults); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, firstResult, maxResults, includeServiceAccounts); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java index 3abe72f7c8..8ed8b6a12b 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java @@ -74,8 +74,13 @@ public class NoCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm) { - return getDelegate().getUsers(realm); + public UserModel getUserByServiceAccountClient(ClientModel client) { + return getDelegate().getUserByServiceAccountClient(client); + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, includeServiceAccounts); } @Override @@ -84,8 +89,8 @@ public class NoCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - return getDelegate().getUsers(realm, firstResult, maxResults); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, firstResult, maxResults, includeServiceAccounts); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java index b075ea118c..f2b5e33da7 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java @@ -243,6 +243,18 @@ public class UserAdapter implements UserModel { updated.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + if (updated != null) return updated.getServiceAccountClientLink(); + return cached.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + getDelegateForUpdate(); + updated.setServiceAccountClientLink(clientInternalId); + } + @Override public Set getRealmRoleMappings() { if (updated != null) return updated.getRealmRoleMappings(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java index 3d1395b583..853677b6d6 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java @@ -31,6 +31,7 @@ public class CachedUser implements Serializable { private boolean enabled; private boolean totp; private String federationLink; + private String serviceAccountClientLink; private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); private Set requiredActions = new HashSet<>(); private Set roleMappings = new HashSet(); @@ -49,6 +50,7 @@ public class CachedUser implements Serializable { this.enabled = user.isEnabled(); this.totp = user.isTotp(); this.federationLink = user.getFederationLink(); + this.serviceAccountClientLink = user.getServiceAccountClientLink(); this.requiredActions.addAll(user.getRequiredActions()); for (RoleModel role : user.getRoleMappings()) { roleMappings.add(role.getId()); @@ -114,4 +116,8 @@ public class CachedUser implements Serializable { public String getFederationLink() { return federationLink; } + + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } } 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 ae04a5f6b5..4f02f6c090 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 @@ -272,13 +272,29 @@ public class JpaUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, -1, -1); + public UserModel getUserByServiceAccountClient(ClientModel client) { + TypedQuery query = em.createNamedQuery("getRealmUserByServiceAccount", UserEntity.class); + query.setParameter("realmId", client.getRealm().getId()); + query.setParameter("clientInternalId", client.getId()); + List results = query.getResultList(); + if (results.isEmpty()) { + return null; + } else if (results.size() > 1) { + throw new IllegalStateException("More service account linked users found for client=" + client.getClientId() + + ", results=" + results); + } else { + UserEntity user = results.get(0); + return new UserAdapter(client.getRealm(), em, user); + } + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, -1, -1, includeServiceAccounts); } @Override public int getUsersCount(RealmModel realm) { - // TODO: named query? Object count = em.createNamedQuery("getRealmUserCount") .setParameter("realmId", realm.getId()) .getSingleResult(); @@ -286,8 +302,10 @@ public class JpaUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - TypedQuery query = em.createNamedQuery("getAllUsersByRealm", UserEntity.class); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + String queryName = includeServiceAccounts ? "getAllUsersByRealm" : "getAllUsersByRealmExcludeServiceAccount" ; + + TypedQuery query = em.createNamedQuery(queryName, UserEntity.class); query.setParameter("realmId", realm.getId()); if (firstResult != -1) { query.setFirstResult(firstResult); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index ca0c284b45..e60377746c 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -543,6 +543,16 @@ public class UserAdapter implements UserModel { user.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + return user.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + user.setServiceAccountClientLink(clientInternalId); + } + @Override public void addConsent(UserConsentModel consent) { String clientId = consent.getClient().getId(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java index 5e0769a6b2..2da1641586 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java @@ -21,12 +21,15 @@ import java.util.Collection; */ @NamedQueries({ @NamedQuery(name="getAllUsersByRealm", query="select u from UserEntity u where u.realmId = :realmId order by u.username"), - @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and ( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"), + @NamedQuery(name="getAllUsersByRealmExcludeServiceAccount", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) order by u.username"), + @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) and " + + "( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"), @NamedQuery(name="getRealmUserById", query="select u from UserEntity u where u.id = :id and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByUsername", query="select u from UserEntity u where u.username = :username and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByEmail", query="select u from UserEntity u where u.email = :email and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByLastName", query="select u from UserEntity u where u.lastName = :lastName and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByFirstLastName", query="select u from UserEntity u where u.firstName = :first and u.lastName = :last and u.realmId = :realmId"), + @NamedQuery(name="getRealmUserByServiceAccount", query="select u from UserEntity u where u.serviceAccountClientLink = :clientInternalId and u.realmId = :realmId"), @NamedQuery(name="getRealmUserCount", query="select count(u) from UserEntity u where u.realmId = :realmId"), @NamedQuery(name="deleteUsersByRealm", query="delete from UserEntity u where u.realmId = :realmId"), @NamedQuery(name="deleteUsersByRealmAndLink", query="delete from UserEntity u where u.realmId = :realmId and u.federationLink=:link") @@ -77,6 +80,9 @@ public class UserEntity { @Column(name="federation_link") protected String federationLink; + @Column(name="SERVICE_ACCOUNT_CLIENT_LINK") + protected String serviceAccountClientLink; + public String getId() { return id; } @@ -198,6 +204,14 @@ public class UserEntity { this.federationLink = federationLink; } + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } + + public void setServiceAccountClientLink(String serviceAccountClientLink) { + this.serviceAccountClientLink = serviceAccountClientLink; + } + @Override public boolean equals(Object o) { if (this == o) return true; 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 cc720c5f94..a433fead56 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 @@ -105,6 +105,16 @@ public class MongoUserProvider implements UserProvider { return userEntity == null ? null : new UserAdapter(session, realm, userEntity, invocationContext); } + @Override + public UserModel getUserByServiceAccountClient(ClientModel client) { + DBObject query = new QueryBuilder() + .and("serviceAccountClientLink").is(client.getId()) + .and("realmId").is(client.getRealm().getId()) + .get(); + MongoUserEntity userEntity = getMongoStore().loadSingleEntity(MongoUserEntity.class, query, invocationContext); + return userEntity == null ? null : new UserAdapter(session, client.getRealm(), userEntity, invocationContext); + } + protected List convertUserEntities(RealmModel realm, List userEntities) { List userModels = new ArrayList(); for (MongoUserEntity user : userEntities) { @@ -115,8 +125,8 @@ public class MongoUserProvider implements UserProvider { @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, -1, -1); + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, -1, -1, includeServiceAccounts); } @Override @@ -128,10 +138,15 @@ public class MongoUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - DBObject query = new QueryBuilder() - .and("realmId").is(realm.getId()) - .get(); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + QueryBuilder queryBuilder = new QueryBuilder() + .and("realmId").is(realm.getId()); + + if (!includeServiceAccounts) { + queryBuilder = queryBuilder.and("serviceAccountClientLink").is(null); + } + + DBObject query = queryBuilder.get(); DBObject sort = new BasicDBObject("username", 1); List users = getMongoStore().loadEntities(MongoUserEntity.class, query, sort, firstResult, maxResults, invocationContext); return convertUserEntities(realm, users); @@ -170,6 +185,7 @@ public class MongoUserProvider implements UserProvider { QueryBuilder builder = new QueryBuilder().and( new QueryBuilder().and("realmId").is(realm.getId()).get(), + new QueryBuilder().and("serviceAccountClientLink").is(null).get(), new QueryBuilder().or( new QueryBuilder().put("username").regex(caseInsensitivePattern).get(), new QueryBuilder().put("email").regex(caseInsensitivePattern).get(), diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index a8408134d5..6dae14b371 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -187,7 +187,7 @@ public class UserAdapter extends AbstractMongoAdapter implement @Override public Map> getAttributes() { - return user.getAttributes()==null ? Collections.>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes()); + return user.getAttributes()==null ? Collections.>emptyMap() : Collections.unmodifiableMap((Map) user.getAttributes()); } public MongoUserEntity getUser() { @@ -460,6 +460,17 @@ public class UserAdapter extends AbstractMongoAdapter implement updateUser(); } + @Override + public String getServiceAccountClientLink() { + return user.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + user.setServiceAccountClientLink(clientInternalId); + updateUser(); + } + @Override public void addConsent(UserConsentModel consent) { String clientId = consent.getClient().getId(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java index 3c8b8ad852..1a8ad0bbc2 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java @@ -106,20 +106,13 @@ public class ServiceAccountManager { protected Response finishClientAuthorization() { event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH); - Map search = new HashMap<>(); - search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId()); - List users = session.users().searchForUserByUserAttributes(search, realm); + clientUser = session.users().getUserByServiceAccountClient(client); - if (users.size() == 0) { + if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) { // May need to handle bootstrap here as well - logger.warnf("Service account user for client '%s' not found. Creating now", client.getClientId()); + logger.infof("Service account user for client '%s' not found or default protocol mapper for service account not found. Creating now", client.getClientId()); new ClientManager(new RealmManager(session)).enableServiceAccount(client); - users = session.users().searchForUserByUserAttributes(search, realm); - clientUser = users.get(0); - } else if (users.size() == 1) { - clientUser = users.get(0); - } else { - throw new ModelDuplicateException("Multiple service account users found for client '" + client.getClientId() + "' . Check your DB"); + clientUser = session.users().getUserByServiceAccountClient(client); } String clientUsername = clientUser.getUsername(); diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index a7f9079509..1b5a4e88b0 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -51,6 +51,12 @@ public class ClientManager { if (sessions != null) { sessions.onClientRemoved(realm, client); } + + UserModel serviceAccountUser = realmManager.getSession().users().getUserByServiceAccountClient(client); + if (serviceAccountUser != null) { + realmManager.getSession().users().removeUser(realm, serviceAccountUser); + } + return true; } else { return false; @@ -93,18 +99,15 @@ public class ClientManager { client.setServiceAccountsEnabled(true); // Add dedicated user for this service account - RealmModel realm = client.getRealm(); - Map search = new HashMap<>(); - search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId()); - List serviceAccountUsers = realmManager.getSession().users().searchForUserByUserAttributes(search, realm); - if (serviceAccountUsers.size() == 0) { + if (realmManager.getSession().users().getUserByServiceAccountClient(client) == null) { String username = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + client.getClientId(); logger.infof("Creating service account user '%s'", username); - UserModel user = realmManager.getSession().users().addUser(realm, username); + // Don't use federation for service account user + UserModel user = realmManager.getSession().userStorage().addUser(client.getRealm(), username); user.setEnabled(true); user.setEmail(username + "@placeholder.org"); - user.setSingleAttribute(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId()); + user.setServiceAccountClientLink(client.getId()); } // Add protocol mappers to retrieve clientId in access token diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 67cfb65da8..b85aa39252 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -19,6 +19,7 @@ import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; @@ -292,6 +293,31 @@ public class ClientResource { adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); } + /** + * Returns user dedicated to this service account + * + * @return + */ + @Path("service-account-user") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public UserRepresentation getServiceAccountUser() { + auth.requireView(); + + UserModel user = session.users().getUserByServiceAccountClient(client); + if (user == null) { + if (client.isServiceAccountsEnabled()) { + new ClientManager(new RealmManager(session)).enableServiceAccount(client); + user = session.users().getUserByServiceAccountClient(client); + } else { + throw new BadRequestException("Service account not enabled for the client '" + client.getClientId() + "'"); + } + } + + return ModelToRepresentation.toRepresentation(user); + } + /** * If the client has an admin URL, push the client's revocation policy to it. * diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index 83e6c2fa40..eeffe5dc22 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -109,7 +109,7 @@ public class IdentityProviderResource { // Admin changed the ID (alias) of identity provider. We must update all clients and users logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId); - updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm), oldProviderId, newProviderId); + updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm, false), oldProviderId, newProviderId); } adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(providerRep).success(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 8348037217..1a1aa29a5c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -554,7 +554,7 @@ public class UsersResource { } userModels = session.users().searchForUserByAttributes(attributes, realm, firstResult, maxResults); } else { - userModels = session.users().getUsers(realm, firstResult, maxResults); + userModels = session.users().getUsers(realm, firstResult, maxResults, false); } for (UserModel user : userModels) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index b883226ed8..8ce5c7ce6b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -814,7 +814,7 @@ public abstract class AbstractIdentityProviderTest { private void removeTestUsers() { RealmModel realm = getRealm(); - List users = this.session.users().getUsers(realm); + List users = this.session.users().getUsers(realm, true); for (UserModel user : users) { Set identities = this.session.users().getFederatedIdentities(user, realm); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java index ee6c5c69f1..8eb05b64e1 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java @@ -288,14 +288,14 @@ public abstract class AbstractKerberosTest { RealmManager manager = new RealmManager(session); RealmModel appRealm = manager.getRealm("test"); - List users = session.userStorage().getUsers(appRealm); + List users = session.userStorage().getUsers(appRealm, true); for (UserModel user : users) { if (!user.getUsername().equals(AssertEvents.DEFAULT_USERNAME)) { session.userStorage().removeUser(appRealm, user); } } - Assert.assertEquals(1, session.userStorage().getUsers(appRealm).size()); + Assert.assertEquals(1, session.userStorage().getUsers(appRealm, true).size()); } finally { keycloakRule.stopSession(session, true); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java index e50caf845c..9b03d5ba06 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java @@ -227,7 +227,7 @@ public class SyncProvidersTest { RealmModel testRealm = session.realms().getRealm("test"); // Remove all users from model - for (UserModel user : session.userStorage().getUsers(testRealm)) { + for (UserModel user : session.userStorage().getUsers(testRealm, true)) { session.userStorage().removeUser(testRealm, user); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java index 238184e0f4..48ed318e37 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java @@ -433,7 +433,7 @@ public class AdapterTest extends AbstractModelTest { RealmModel otherRealm = adapter.createRealm("other"); realmManager.getSession().users().addUser(otherRealm, "bburke"); - Assert.assertEquals(1, realmManager.getSession().users().getUsers(otherRealm).size()); + Assert.assertEquals(1, realmManager.getSession().users().getUsers(otherRealm, false).size()); Assert.assertEquals(1, realmManager.getSession().users().searchForUser("bu", otherRealm).size()); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index 31a9574092..332c94b56e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -304,6 +304,14 @@ public class ImportTest extends AbstractModelTest { Assert.assertTrue(otherAppAdminConsent.isRoleGranted(realm.getRole("admin"))); Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin"))); Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper)); + + // Test service accounts + Assert.assertFalse(application.isServiceAccountsEnabled()); + Assert.assertTrue(otherApp.isServiceAccountsEnabled()); + Assert.assertNull(session.users().getUserByServiceAccountClient(application)); + UserModel linked = session.users().getUserByServiceAccountClient(otherApp); + Assert.assertNotNull(linked); + Assert.assertEquals("my-service-user", linked.getUsername()); } @Test diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java index d0c9d001d1..9455271a09 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java @@ -7,6 +7,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.services.managers.ClientManager; import static org.junit.Assert.assertNotNull; @@ -226,6 +227,61 @@ public class UserModelTest extends AbstractModelTest { Assert.assertEquals(0, users.size()); } + @Test + public void testServiceAccountLink() throws Exception { + RealmModel realm = realmManager.createRealm("original"); + ClientModel client = realm.addClient("foo"); + + UserModel user1 = session.users().addUser(realm, "user1"); + user1.setFirstName("John"); + user1.setLastName("Doe"); + + UserModel user2 = session.users().addUser(realm, "user2"); + user2.setFirstName("John"); + user2.setLastName("Doe"); + + // Search + Assert.assertNull(session.users().getUserByServiceAccountClient(client)); + List users = session.users().searchForUser("John Doe", realm); + Assert.assertEquals(2, users.size()); + Assert.assertTrue(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + // Link service account + user1.setServiceAccountClientLink(client.getId()); + + commit(); + + // Search and assert service account user not found + realm = realmManager.getRealmByName("original"); + UserModel searched = session.users().getUserByServiceAccountClient(client); + Assert.assertEquals(searched, user1); + users = session.users().searchForUser("John Doe", realm); + Assert.assertEquals(1, users.size()); + Assert.assertFalse(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + users = session.users().getUsers(realm, false); + Assert.assertEquals(1, users.size()); + Assert.assertFalse(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + users = session.users().getUsers(realm, true); + Assert.assertEquals(2, users.size()); + Assert.assertTrue(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + Assert.assertEquals(2, session.users().getUsersCount(realm)); + + // Remove client + new ClientManager(realmManager).removeClient(realm, client); + commit(); + + // Assert service account removed as well + realm = realmManager.getRealmByName("original"); + Assert.assertNull(session.users().getUserByUsername("user1", realm)); + } + public static void assertEquals(UserModel expected, UserModel actual) { Assert.assertEquals(expected.getUsername(), actual.getUsername()); Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json index 340d9d3c23..9df4385fa2 100755 --- a/testsuite/integration/src/test/resources/model/testrealm.json +++ b/testsuite/integration/src/test/resources/model/testrealm.json @@ -141,6 +141,11 @@ "userName": "mySocialUser@gmail.com" } ] + }, + { + "username": "my-service-user", + "enabled": true, + "serviceAccountClientId": "OtherApp" } ], "clients": [ @@ -158,6 +163,7 @@ "clientId": "OtherApp", "name": "Other Application", "enabled": true, + "serviceAccountsEnabled": true, "protocolMappers" : [ { "name" : "gss delegation credential",