keycloak-memoizeit

Merge pull request #1484 from mposolda/service-acc KEYCLOAK-401

7/23/2015 7:09:33 AM

Changes

forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html 28(+0 -28)

Details

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 103c7ce..5fc0f23 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 @@
         <dropColumn tableName="CLIENT_SESSION" columnName="ACTION"/>
         <addColumn tableName="USER_ENTITY">
             <column name="CREATED_TIMESTAMP" type="BIGINT"/>
+            <column name="SERVICE_ACCOUNT_CLIENT_LINK" type="VARCHAR(36)"/>
         </addColumn>
     </changeSet>
 </databaseChangeLog>
diff --git a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java
index 928f62d..561a5d0 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 1d2bee3..ea20afc 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<String, List<String>> but for backwards compatibility, we also need to support Map<String, String>
     protected Map<String, Object> 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 f9dc9f1..d03654d 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 a26a058..d669e6b 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 a3396e3..601ddaf 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<UserModel> allUsers = session.users().getUsers(realm);
+            List<UserModel> allUsers = session.users().getUsers(realm, true);
             List<UserRepresentation> users = new ArrayList<UserRepresentation>();
             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 c72708b..0ecc10a 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 5ddf659..8fb7c36 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 1363919..3fd0fb0 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 f3fe77f..3508bf2 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 773f6f0..37a9566 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 1d6a6be..3763ba9 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 49a1997..727c66d 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 @@
                 </div>
                 <kc-tooltip>'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.</kc-tooltip>
             </div>
+            <div class="form-group" data-ng-show="protocol == 'openid-connect' && !client.publicClient && !client.bearerOnly">
+                <label class="col-md-2 control-label" for="serviceAccountsEnabled">Service Accounts Enabled</label>
+                <kc-tooltip>Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client.</kc-tooltip>
+                <div class="col-md-6">
+                    <input ng-model="client.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch />
+                </div>
+            </div>
             <div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
                 <label class="col-md-2 control-label" for="samlServerSignature">Include AuthnStatement</label>
                 <div class="col-sm-6">
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 0000000..03a38d9
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html
@@ -0,0 +1,113 @@
+<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</a></li>
+        <li>{{client.clientId}}</li>
+    </ol>
+
+    <h1>{{client.clientId|capitalize}}</h1>
+
+    <kc-tabs-client></kc-tabs-client>
+
+    <h2><span>{{client.clientId}}</span> Service Accounts </h2>
+    <p class="subtitle"></p>
+
+    <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-show="client.serviceAccountsEnabled">
+        <div class="form-group">
+            <label class="col-md-2 control-label" class="control-label">Realm Roles</label>
+            <div class="col-md-10">
+                <div class="row">
+                    <div class="col-md-3">
+                        <label class="control-label" for="available">Available Roles</label>
+                        <kc-tooltip>Realm level roles that can be assigned to service account.</kc-tooltip>
+
+                        <select id="available" class="form-control" multiple size="5"
+                                ng-multiple="true"
+                                ng-model="selectedRealmRoles"
+                                ng-options="r.name for r in realmRoles">
+                        </select>
+                        <button ng-disabled="selectedRealmRoles.length == 0" class="btn btn-default" type="submit" ng-click="addRealmRole()">
+                            Add selected <i class="fa fa-angle-double-right"></i>
+                        </button>
+                    </div>
+                    <div class="col-md-3">
+                        <label class="control-label" for="assigned">Assigned Roles</label>
+                        <kc-tooltip>Realm level roles assigned to service account.</kc-tooltip>
+                        <select id="assigned" class="form-control" multiple size=5
+                                ng-multiple="true"
+                                ng-model="selectedRealmMappings"
+                                ng-options="r.name for r in realmMappings">
+                        </select>
+                        <button ng-disabled="selectedRealmMappings.length == 0" class="btn btn-default" type="submit" ng-click="deleteRealmRole()">
+                            <i class="fa fa-angle-double-left"></i> Remove selected
+                        </button>
+                    </div>
+                    <div class="col-md-3">
+                        <label class="control-label" for="realm-composite">Effective Roles  </label>
+                        <kc-tooltip>Assigned realm level roles that may have been inherited from a composite role.</kc-tooltip>
+                        <select id="realm-composite" class="form-control" multiple size=5
+                                disabled="true"
+                                ng-model="dummymodel"
+                                ng-options="r.name for r in realmComposite">
+                        </select>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <label class="col-md-2 control-label" class="control-label">
+                <span>Client Roles</span>
+                <select class="form-control" id="clients" name="clients" ng-change="changeClient()" ng-model="targetClient" ng-options="a.clientId for a in clients" ng-disabled="false"></select>
+            </label>
+
+            <div class="col-md-10">
+                <div class="row" data-ng-hide="targetClient">
+                    <div class="col-md-4"><span class="text-muted">Select client to view roles for client</span></div>
+                </div>
+                <div class="row" data-ng-show="targetClient">
+                    <div class="col-md-3">
+                        <label class="control-label" for="client-available">Available Roles</label>
+                        <kc-tooltip>Client roles available to be assigned.</kc-tooltip>
+                        <select id="client-available" class="form-control" multiple size="5"
+                                ng-multiple="true"
+                                ng-model="selectedClientRoles"
+                                ng-options="r.name for r in clientRoles">
+                        </select>
+                        <button ng-disabled="selectedClientRoles.length == 0" class="btn btn-default" type="submit" ng-click="addClientRole()">
+                            Add selected <i class="fa fa-angle-double-right"></i>
+                        </button>
+                    </div>
+                    <div class="col-md-3">
+                        <label class="control-label" for="client-assigned">Assigned Roles</label>
+                        <kc-tooltip>Assigned client roles.</kc-tooltip>
+                        <select id="client-assigned" class="form-control" multiple size=5
+                                ng-multiple="true"
+                                ng-model="selectedClientMappings"
+                                ng-options="r.name for r in clientMappings">
+                        </select>
+                        <button ng-disabled="selectedClientMappings.length == 0" class="btn btn-default" type="submit" ng-click="deleteClientRole()">
+                            <i class="fa fa-angle-double-left"></i> Remove selected
+                        </button>
+                    </div>
+                    <div class="col-md-3">
+                        <label class="control-label" for="client-composite">Effective Roles</label>
+                        <kc-tooltip>Assigned client roles that may have been inherited from a composite role.</kc-tooltip>
+                        <select id="client-composite" class="form-control" multiple size=5
+                                disabled="true"
+                                ng-model="dummymodel"
+                                ng-options="r.name for r in clientComposite">
+                        </select>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </form>
+
+    <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-show="!client.serviceAccountsEnabled">
+        <legend><span class="text">Service account is not enabled for {{client.clientId}}.</span></legend>
+    </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/role-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
index 8223936..3228be5 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 @@
         <div class="form-group">
                 <label class="col-md-2 control-label" class="control-label">
                     <span>Client Roles</span>
-                    <select class="form-control" id="clients" name="clients" ng-change="changeClient()" ng-model="client" ng-options="a.clientId for a in clients" ng-disabled="false"></select>
+                    <select class="form-control" id="clients" name="clients" ng-change="changeClient()" ng-model="targetClient" ng-options="a.clientId for a in clients" ng-disabled="false"></select>
                 </label>
                 <div class="col-md-10" kc-read-only="!access.manageUsers">
-                    <div class="row" data-ng-hide="client">
+                    <div class="row" data-ng-hide="targetClient">
                         <div class="col-md-4"><span class="text-muted">Select client to view roles for client</span></div>
                     </div>
-                    <div class="row" data-ng-show="client">
+                    <div class="row" data-ng-show="targetClient">
                         <div class="col-md-3">
                             <label class="control-label" for="available-client">Available Roles</label>
                             <kc-tooltip>Assignable roles from this client.</kc-tooltip>
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 c82d99f..2aad2d5 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 @@
             <kc-tooltip>Helper utility for generating various client adapter configuration formats which you can download or cut and paste to configure your clients.</kc-tooltip>
         </li>
 
-        <li ng-class="{active: path[4] == 'service-accounts'}" data-ng-show="!client.publicClient && !client.bearerOnly">
-            <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/service-accounts">Service Accounts</a>
-            <kc-tooltip>Allows you to authenticate this client to Keycloak and retrieve access tokens dedicated to this client.</kc-tooltip>
+        <li ng-class="{active: path[4] == 'service-account-roles'}" data-ng-show="client.serviceAccountsEnabled">
+            <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/service-account-roles">Service Account Roles</a>
+            <kc-tooltip>Allows you to authenticate role mappings for the service account dedicated to this client.</kc-tooltip>
         </li>
     </ul>
 </div>
\ 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 e2ef2f6..5a8a6e0 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<UserModel> users = session.userStorage().getUsers(realm);
+        List<UserModel> 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/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
index eeae34f..8c82a8e 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<CredentialEntity> credentials = new ArrayList<CredentialEntity>();
     private List<FederatedIdentityEntity> 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/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index 23aaf1b..ee18d79 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<UserModel> 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<UserModel> 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<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
+    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) {
         return query(new PaginatedQuery() {
             @Override
             public List<UserModel> 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 19fdad2..94c2ffc 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<UserConsentModel> 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 f48062f..1690b7a 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<UserModel> getUsers(RealmModel realm);
+    UserModel getUserByServiceAccountClient(ClientModel client);
+    List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts);
+
+    // Service account is included for counts
     int getUsersCount(RealmModel realm);
-    List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults);
+    List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts);
     List<UserModel> searchForUser(String search, RealmModel realm);
     List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults);
     List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm);
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 f5261a0..cfe0852 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 a38b305..83c8273 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 9599ab9..3c1edb3 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
@@ -208,6 +208,16 @@ public class UserModelDelegate implements UserModel {
     }
 
     @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/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index 5db8f93..98e4254 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
@@ -478,6 +478,16 @@ public class UserAdapter implements UserModel, Comparable {
     }
 
     @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/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
index ff152f8..0bcc37a 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<UserModel> 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<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+        return getUsers(realm, -1, -1, includeServiceAccounts);
     }
 
     @Override
@@ -117,12 +127,27 @@ public class FileUserProvider implements UserProvider {
     }
 
     @Override
-    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
-        List users = new ArrayList(inMemoryModel.getUsers(realm.getId()));
+    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
+        List<UserModel> users = new ArrayList<>(inMemoryModel.getUsers(realm.getId()));
+
+        if (!includeServiceAccounts) {
+            users = filterServiceAccountUsers(users);
+        }
+
         List<UserModel> sortedList = sortedSubList(users, firstResult, maxResults);
         return sortedList;
     }
 
+    private List<UserModel> filterServiceAccountUsers(List<UserModel> users) {
+        List<UserModel> result = new ArrayList<>();
+        for (UserModel user : users) {
+            if (user.getServiceAccountClientLink() == null) {
+                result.add(user);
+            }
+        }
+        return result;
+    }
+
     protected List<UserModel> 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/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 4e99e44..aed1394 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<UserModel> getUsers(RealmModel realm) {
-        return getDelegate().getUsers(realm);
+    public UserModel getUserByServiceAccountClient(ClientModel client) {
+        return getDelegate().getUserByServiceAccountClient(client);
+    }
+
+    @Override
+    public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+        return getDelegate().getUsers(realm, includeServiceAccounts);
     }
 
     @Override
@@ -217,8 +222,8 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
     }
 
     @Override
-    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
-        return getDelegate().getUsers(realm, firstResult, maxResults);
+    public List<UserModel> 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/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
index 3d1395b..853677b 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<String, String> attributes = new MultivaluedHashMap<>();
     private Set<String> requiredActions = new HashSet<>();
     private Set<String> roleMappings = new HashSet<String>();
@@ -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/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 3abe72f..8ed8b6a 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<UserModel> getUsers(RealmModel realm) {
-        return getDelegate().getUsers(realm);
+    public UserModel getUserByServiceAccountClient(ClientModel client) {
+        return getDelegate().getUserByServiceAccountClient(client);
+    }
+
+    @Override
+    public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+        return getDelegate().getUsers(realm, includeServiceAccounts);
     }
 
     @Override
@@ -84,8 +89,8 @@ public class NoCacheUserProvider implements CacheUserProvider {
     }
 
     @Override
-    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
-        return getDelegate().getUsers(realm, firstResult, maxResults);
+    public List<UserModel> 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 b075ea1..f2b5e33 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
@@ -244,6 +244,18 @@ public class UserAdapter implements UserModel {
    }
 
     @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<RoleModel> getRealmRoleMappings() {
         if (updated != null) return updated.getRealmRoleMappings();
         Set<RoleModel> roleMappings = getRoleMappings();
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 5e0769a..2da1641 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/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index ae04a5f..4f02f6c 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<UserModel> getUsers(RealmModel realm) {
-        return getUsers(realm, -1, -1);
+    public UserModel getUserByServiceAccountClient(ClientModel client) {
+        TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserByServiceAccount", UserEntity.class);
+        query.setParameter("realmId", client.getRealm().getId());
+        query.setParameter("clientInternalId", client.getId());
+        List<UserEntity> 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<UserModel> 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<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
-        TypedQuery<UserEntity> query = em.createNamedQuery("getAllUsersByRealm", UserEntity.class);
+    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
+        String queryName = includeServiceAccounts ? "getAllUsersByRealm" : "getAllUsersByRealmExcludeServiceAccount" ;
+
+        TypedQuery<UserEntity> 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 ca0c284..e603777 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
@@ -544,6 +544,16 @@ public class UserAdapter implements UserModel {
     }
 
     @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/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 cc720c5..a433fea 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<UserModel> convertUserEntities(RealmModel realm, List<MongoUserEntity> userEntities) {
         List<UserModel> userModels = new ArrayList<UserModel>();
         for (MongoUserEntity user : userEntities) {
@@ -115,8 +125,8 @@ public class MongoUserProvider implements UserProvider {
 
 
     @Override
-    public List<UserModel> getUsers(RealmModel realm) {
-        return getUsers(realm, -1, -1);
+    public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+        return getUsers(realm, -1, -1, includeServiceAccounts);
     }
 
     @Override
@@ -128,10 +138,15 @@ public class MongoUserProvider implements UserProvider {
     }
 
     @Override
-    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
-        DBObject query = new QueryBuilder()
-                .and("realmId").is(realm.getId())
-                .get();
+    public List<UserModel> 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<MongoUserEntity> 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 a840813..6dae14b 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<MongoUserEntity> implement
 
     @Override
     public Map<String, List<String>> getAttributes() {
-        return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes());
+        return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map) user.getAttributes());
     }
 
     public MongoUserEntity getUser() {
@@ -461,6 +461,17 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
     }
 
     @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();
         if (getConsentEntityByClientId(clientId) != null) {
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 3c8b8ad..1a8ad0b 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<String, String> search = new HashMap<>();
-        search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
-        List<UserModel> 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 a7f9079..1b5a4e8 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<String, String> search = new HashMap<>();
-        search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
-        List<UserModel> 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 67cfb65..b85aa39 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;
@@ -293,6 +294,31 @@ public class ClientResource {
     }
 
     /**
+     * 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 83e6c2f..eeffe5d 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 8348037..1a1aa29 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 b883226..8ce5c7c 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<UserModel> users = this.session.users().getUsers(realm);
+        List<UserModel> users = this.session.users().getUsers(realm, true);
 
         for (UserModel user : users) {
             Set<FederatedIdentityModel> 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 ee6c5c6..8eb05b6 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<UserModel> users = session.userStorage().getUsers(appRealm);
+            List<UserModel> 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 e50caf8..9b03d5b 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 238184e..48ed318 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 31a9574..332c94b 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 d0c9d00..9455271 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<UserModel> 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 340d9d3..9df4385 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",