KEYCLOAK-401 Service account refactoring and fixes

This commit is contained in:
mposolda 2015-07-22 19:48:28 +02:00
parent 866f45bd95
commit c99785f266
48 changed files with 534 additions and 147 deletions

View file

@ -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>

View file

@ -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";

View file

@ -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;
}
}

View file

@ -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");

View file

@ -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" : {

View file

@ -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;
}

View file

@ -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);

View file

@ -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',

View file

@ -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.");
});
}
}
});

View file

@ -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;

View file

@ -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;

View file

@ -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: {},

View file

@ -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">

View file

@ -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>

View file

@ -1,28 +0,0 @@
<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="serviceAccountsEnabledForm" novalidate kc-read-only="!access.manageClients">
<fieldset class="border-top">
<div class="form-group">
<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" ng-click="serviceAccountsEnabledChanged()" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch />
</div>
</div>
</fieldset>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -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>

View file

@ -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>

View file

@ -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);

View file

@ -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);
}

View file

@ -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();

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<RoleModel> getRealmRoleMappings() {
if (updated != null) return updated.getRealmRoleMappings();

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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();

View file

@ -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;

View file

@ -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(),

View file

@ -460,6 +460,17 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> 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();

View file

@ -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();

View file

@ -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

View file

@ -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.
*

View file

@ -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();

View file

@ -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) {

View file

@ -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);

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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());
}

View file

@ -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

View file

@ -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());

View file

@ -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",