Merge branch 'velias-KEYCLOAK-1542'

This commit is contained in:
Stian Thorgersen 2015-07-24 14:48:10 +02:00
commit a1f0208767
68 changed files with 1464 additions and 427 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

@ -13,10 +13,12 @@ import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
@ -29,6 +31,8 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
private volatile EntityManagerFactory emf;
private Config.Scope config;
private Map<String,String> operationalInfo;
@Override
public JpaConnectionProvider create(KeycloakSession session) {
@ -120,56 +124,73 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
if (databaseSchema != null) {
logger.trace("Updating database");
connection = getConnection();
try{
prepareOperationalInfo(connection);
if (databaseSchema != null) {
logger.trace("Updating database");
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class);
if (updater == null) {
throw new RuntimeException("Can't update database: JPA updater provider not found");
}
if (databaseSchema.equals("update")) {
String currentVersion = null;
try {
ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema));
if (resultSet.next()) {
currentVersion = resultSet.getString(1);
}
} catch (SQLException e) {
}
if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) {
updater.update(session, connection, schema);
} else {
logger.debug("Database is up to date");
}
} else if (databaseSchema.equals("validate")) {
updater.validate(connection, schema);
} else {
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
}
logger.trace("Database update completed");
}
logger.trace("Creating EntityManagerFactory");
emf = Persistence.createEntityManagerFactory(unitName, properties);
logger.trace("EntityManagerFactory created");
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class);
if (updater == null) {
throw new RuntimeException("Can't update database: JPA updater provider not found");
}
connection = getConnection();
if (databaseSchema.equals("update")) {
String currentVersion = null;
try {
ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema));
if (resultSet.next()) {
currentVersion = resultSet.getString(1);
}
} catch (SQLException e) {
}
if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) {
updater.update(session, connection, schema);
} else {
logger.debug("Database is up to date");
}
} else if (databaseSchema.equals("validate")) {
updater.validate(connection, schema);
} else {
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
}
logger.trace("Database update completed");
}
logger.trace("Creating EntityManagerFactory");
emf = Persistence.createEntityManagerFactory(unitName, properties);
logger.trace("EntityManagerFactory created");
// Close after creating EntityManagerFactory to prevent in-mem databases from closing
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
logger.warn(e);
}
} finally {
// Close after creating EntityManagerFactory to prevent in-mem databases from closing
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
logger.warn(e);
}
}
}
}
}
}
}
protected void prepareOperationalInfo(Connection connection) {
try {
operationalInfo = new LinkedHashMap<>();
DatabaseMetaData md = connection.getMetaData();
operationalInfo.put("databaseUrl",md.getURL());
operationalInfo.put("databaseUser", md.getUserName());
operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion());
operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion());
} catch (SQLException e) {
logger.warn("Unable to prepare operational info due database exception: " + e.getMessage());
}
}
private Connection getConnection() {
try {
@ -185,5 +206,10 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
throw new RuntimeException("Failed to connect to database", e);
}
}
@Override
public Map<String,String> getOperationalInfo() {
return operationalInfo;
}
}

View file

@ -1,9 +1,10 @@
package org.keycloak.connections.jpa;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface JpaConnectionProviderFactory extends ProviderFactory<JpaConnectionProvider> {
public interface JpaConnectionProviderFactory extends ServerInfoAwareProviderFactory<JpaConnectionProvider> {
}

View file

@ -5,6 +5,7 @@ import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.mongo.api.MongoStore;
@ -18,6 +19,8 @@ import javax.net.ssl.SSLSocketFactory;
import java.lang.reflect.Method;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -57,6 +60,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
private MongoStore mongoStore;
private DB db;
protected Config.Scope config;
private Map<String,String> operationalInfo;
@Override
public MongoConnectionProvider create(KeycloakSession session) {
@ -159,7 +164,13 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
} else {
client = new MongoClient(new ServerAddress(host, port), clientOptions);
}
operationalInfo = new LinkedHashMap<>();
operationalInfo.put("mongoServerAddress", client.getAddress().toString());
operationalInfo.put("mongoDatabaseName", dbName);
operationalInfo.put("mongoUser", user);
operationalInfo.put("mongoDriverVersion", client.getVersion());
logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName);
return client;
}
@ -206,5 +217,10 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
}
}
}
@Override
public Map<String,String> getOperationalInfo() {
return operationalInfo;
}
}

View file

@ -1,9 +1,9 @@
package org.keycloak.connections.mongo;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface MongoConnectionProviderFactory extends ProviderFactory<MongoConnectionProvider> {
public interface MongoConnectionProviderFactory extends ServerInfoAwareProviderFactory<MongoConnectionProvider> {
}

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

@ -28,6 +28,7 @@
<!ENTITY Email SYSTEM "modules/email.xml">
<!ENTITY Roles SYSTEM "modules/roles.xml">
<!ENTITY DirectAccess SYSTEM "modules/direct-access.xml">
<!ENTITY ServiceAccounts SYSTEM "modules/service-accounts.xml">
<!ENTITY CORS SYSTEM "modules/cors.xml">
<!ENTITY Timeouts SYSTEM "modules/timeouts.xml">
<!ENTITY Events SYSTEM "modules/events.xml">
@ -122,6 +123,7 @@ This one is short
&AccessTypes;
&Roles;
&DirectAccess;
&ServiceAccounts;
&CORS;
&Timeouts;
&AdminApi;

View file

@ -0,0 +1,56 @@
<chapter id="service-accounts">
<title>Service Accounts</title>
<para>
Keycloak allows you to obtain an access token dedicated to some Client Application (not to any user).
See <ulink url="http://tools.ietf.org/html/rfc6749#section-4.4">Client Credentials Grant</ulink>
from OAuth 2.0 spec.
</para>
<para>
To use it you must have
registered a valid confidential Client and you need to check the switch <literal>Service Accounts Enabled</literal> in Keycloak
admin console for this client. In tab <literal>Service Account Roles</literal> you can configure the roles available to the service account retrieved on behalf of this client.
Don't forget that you need those roles to be available in Scopes of this client as well (unless you have <literal>Full Scope Allowed</literal> on).
As in normal login, roles from access token are intersection of scopes and the service account roles.
</para>
<para>
The REST URL to invoke on is <literal>/{keycloak-root}/realms/{realm-name}/protocol/openid-connect/token</literal>.
Invoking on this URL is a POST request and requires you to post the clientId and clientSecret of the client in <literal>Authorization: Basic</literal> header.
Later we want to add more mechanisms for authenticating clients. You also need to use parameter <literal>grant_type=client_credentials</literal> as per OAuth2 specification.
</para>
<para>
For example the POST invocation to retrieve service account can look like this:
<programlisting><![CDATA[
POST /auth/realms/demo/protocol/openid-connect/token
Authorization: Basic cHJvZHVjdC1zYS1jbGllbnQ6cGFzc3dvcmQ=
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials]]>
</programlisting>
The response would be this <ulink url="http://tools.ietf.org/html/rfc6749#section-4.4.3">standard JSON document</ulink> from the OAuth 2.0 specification.
<programlisting><![CDATA[
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"bearer",
"expires_in":60,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"refresh_expires_in":600,
"id_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"not-before-policy":0,
"session-state":"234234-234234-234234"
}]]>
</programlisting>
</para>
<para>
The retrieved access token can be refreshed or logged out by out-of-bound request.
</para>
<para>
See the example application <literal>service-account</literal>
from the main Keycloak <literal>demo</literal> example.
</para>
</chapter>

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

@ -82,8 +82,8 @@ module.config([ '$routeProvider', function($routeProvider) {
realm : function(RealmLoader) {
return RealmLoader();
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
serverInfo : function(ServerInfo) {
return ServerInfo.delay;
}
},
controller : 'RealmLoginSettingsCtrl'
@ -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',
@ -1124,7 +1133,22 @@ module.config([ '$routeProvider', function($routeProvider) {
controller : 'AuthenticationConfigCreateCtrl'
})
.when('/server-info', {
templateUrl : resourceUrl + '/partials/server-info.html'
templateUrl : resourceUrl + '/partials/server-info.html',
resolve : {
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
}
},
controller : 'ServerInfoCtrl'
})
.when('/server-info/providers', {
templateUrl : resourceUrl + '/partials/server-info-providers.html',
resolve : {
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
}
},
controller : 'ServerInfoCtrl'
})
.when('/logout', {
templateUrl : resourceUrl + '/partials/home.html',
@ -1858,4 +1882,4 @@ module.directive( 'kcOpen', function ( $location ) {
});
});
};
});
});

View file

@ -549,7 +549,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
"bearer-only"
];
$scope.protocols = serverInfo.protocols;
$scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers);
$scope.signatureAlgorithms = [
"RSA_SHA1",
@ -1323,25 +1323,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

@ -7,9 +7,6 @@ module.controller('GlobalCtrl', function($scope, $http, Auth, WhoAmI, Current, $
$scope.resourceUrl = resourceUrl;
$scope.auth = Auth;
$scope.serverInfo = ServerInfo.get();
$scope.serverInfoUpdate = function() {
$scope.serverInfo = ServerInfo.get();
};
function hasAnyAccess() {
var realmAccess = Auth.user && Auth.user['realm_access'];
@ -125,6 +122,25 @@ module.controller('RealmTabCtrl', function(Dialog, $scope, Current, Realm, Notif
};
});
module.controller('ServerInfoCtrl', function($scope, ServerInfo) {
ServerInfo.reload();
$scope.serverInfo = ServerInfo.get();
$scope.$watch($scope.serverInfo, function() {
$scope.providers = [];
for(var spi in $scope.serverInfo.providers) {
var p = angular.copy($scope.serverInfo.providers[spi]);
p.name = spi;
$scope.providers.push(p)
}
});
$scope.serverInfoReload = function() {
ServerInfo.reload();
}
});
module.controller('RealmListCtrl', function($scope, Realm, Current) {
$scope.realms = Realm.query();
Current.realms = $scope.realms;
@ -1217,7 +1233,7 @@ module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmE
}
});
$scope.eventListeners = serverInfo.eventListeners;
$scope.eventListeners = Object.keys(serverInfo.providers.eventsListener.providers);
$scope.eventSelectOptions = {
'multiple': true,

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

@ -35,8 +35,10 @@ module.factory('RealmListLoader', function(Loader, Realm, $q) {
return Loader.get(Realm);
});
module.factory('ServerInfoLoader', function(Loader, ServerInfo, $q) {
return Loader.get(ServerInfo);
module.factory('ServerInfoLoader', function(Loader, ServerInfo) {
return function() {
return ServerInfo.promise;
};
});
module.factory('RealmLoader', function(Loader, Realm, $route, $q) {
@ -282,6 +284,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

@ -215,10 +215,27 @@ module.factory('RealmLDAPConnectionTester', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection');
});
module.factory('ServerInfo', function($resource) {
return $resource(authUrl + '/admin/serverinfo');
});
module.service('ServerInfo', function($resource, $q, $http) {
var info = {};
var delay = $q.defer();
$http.get(authUrl + '/admin/serverinfo').success(function(data) {
info = data;
delay.resolve(info);
});
return {
get: function() {
return info;
},
reload: function() {
$http.get(authUrl + '/admin/serverinfo').success(function(data) {
angular.copy(data, info);
});
},
promise: delay.promise
}
});
module.factory('ClientProtocolMapper', function($resource) {
@ -897,6 +914,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

@ -0,0 +1,55 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>
Server Info
<i id="serverInfoReload" class="pficon pficon-restart clickable" data-ng-click="serverInfoReload()"></i>
</h1>
<ul class="nav nav-tabs">
<li><a href="#/server-info">Info</a></li>
<li class="active"><a href="#/server-info/providers">Providers</a></li>
</ul>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<div class="form-group">
<div class="input-group">
<input type="text" placeholder="Search..." data-ng-model="search" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
</div>
</div>
</div>
</th>
</tr>
<tr>
<th width="20%">SPI</th>
<th>Providers</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="spi in (providers | orderBy:'name' | filter:search)">
<td>{{spi.name}}</td>
<td>
<div data-ng-repeat="(providerName, provider) in spi.providers">
{{providerName}}
<span ng-show="provider.operationalInfo">
<button type="button" class="btn btn-default btn-xs" ng-click="collapseRep = !collapseRep">
<span class="glyphicon glyphicon-plus" data-ng-show="!collapseRep"></span>
<span class="glyphicon glyphicon-minus" data-ng-show="collapseRep"></span>
</button>
<table ng-show="collapseRep" class="table table-striped table-bordered" style="margin-top: 0px;">
<tr ng-repeat="(key, value) in provider.operationalInfo">
<td width="20%">{{key}}</td>
<td>{{value}}</td>
</tr>
</table>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<kc-menu></kc-menu>

View file

@ -1,66 +1,104 @@
<div class="col-md-12">
<h1>Server Info</h1>
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>
Server Info
<i id="serverInfoReload" class="pficon pficon-restart clickable" data-ng-click="serverInfoReload()"></i>
</h1>
<ul class="nav nav-tabs">
<li class="active"><a href="#/server-info">Info</a></li>
<li><a href="#/server-info/providers">Providers</a></li>
</ul>
<table class="table table-striped table-bordered">
<tr>
<td>Version</td>
<td>{{serverInfo.version}}</td>
<td width="20%">Keycloak Version</td>
<td>{{serverInfo.systemInfo.version}}</td>
</tr>
<tr>
<td>Server Time</td>
<td>{{serverInfo.serverTime}} (<a data-ng-click="serverInfoUpdate()">update</a>)</td>
<td>{{serverInfo.systemInfo.serverTime}}</td>
</tr>
<tr>
<td>Server Uptime</td>
<td>{{serverInfo.systemInfo.uptime}}</td>
</tr>
</table>
<fieldset>
<legend collapsed>Providers</legend>
<div class="form-group">
<h3>Public SPIs</h3>
<kc-tooltip>For public SPIs there are built-in providers, but it's also supported to write your own custom providers.</kc-tooltip>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>SPI</th>
<th>Providers</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="spi in (serverInfo.providers | filter:{internal:false} | orderBy:'name')">
<td>{{spi.name}}</td>
<td>
<div data-ng-repeat="provider in (spi.implementations | orderBy:'toString()')">
{{provider}}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="form-group">
<h3>Internal SPIs</h3>
<kc-tooltip>For internal SPIs there are only built-in providers. It's not recommended to write your own custom providers as internal SPIs may change or be removed without notice.</kc-tooltip>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>SPI</th>
<th>Providers</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="spi in (serverInfo.providers | filter:{internal:true} | orderBy:'name')">
<td>{{spi.name}}</td>
<td>
<div data-ng-repeat="provider in (spi.implementations | orderBy:'toString()')">
{{provider}}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<legend>Memory</legend>
<table class="table table-striped table-bordered" style="margin-top: 0;">
<tr>
<td width="20%">Total Memory</td>
<td>{{serverInfo.memoryInfo.totalFormated}}</td>
</tr>
<tr>
<td>Free Memory</td>
<td>{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)</td>
</tr>
<tr>
<td>Used Memory</td>
<td>{{serverInfo.memoryInfo.usedFormated}}</td>
</tr>
</table>
</fieldset>
</div>
<fieldset>
<legend>System</legend>
<table class="table table-striped table-bordered" style="margin-top: 0;">
<tr>
<td width="20%">Current Working Directory</td>
<td>{{serverInfo.systemInfo.userDir}}</td>
</tr>
<tr>
<td>Java Version</td>
<td>{{serverInfo.systemInfo.javaVersion}}</td>
</tr>
<tr>
<td>Java Vendor</td>
<td>{{serverInfo.systemInfo.javaVendor}}</td>
</tr>
<tr>
<td>Java Runtime</td>
<td>{{serverInfo.systemInfo.javaRuntime}}</td>
</tr>
<tr>
<td>Java VM</td>
<td>{{serverInfo.systemInfo.javaVm}}</td>
</tr>
<tr>
<td>Java VM Version</td>
<td>{{serverInfo.systemInfo.javaVmVersion}}</td>
</tr>
<tr>
<td>Java Home</td>
<td>{{serverInfo.systemInfo.javaHome}}</td>
</tr>
<tr>
<td>User Name</td>
<td>{{serverInfo.systemInfo.userName}}</td>
</tr>
<tr>
<td>User Timezone</td>
<td>{{serverInfo.systemInfo.userTimezone}}</td>
</tr>
<tr>
<td>User Locale</td>
<td>{{serverInfo.systemInfo.userLocale}}</td>
</tr>
<tr>
<td>System Encoding</td>
<td>{{serverInfo.systemInfo.fileEncoding}}</td>
</tr>
<tr>
<td>Operating System</td>
<td>{{serverInfo.systemInfo.osName}} {{serverInfo.systemInfo.osVersion}}</td>
</tr>
<tr>
<td>OS Architecture</td>
<td>{{serverInfo.systemInfo.osArchitecture}}</td>
</tr>
</table>
</fieldset>
</div>
<kc-menu></kc-menu>

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

@ -18,6 +18,8 @@ public interface KeycloakSessionFactory extends ProviderEventManager {
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String id);
List<ProviderFactory> getProviderFactories(Class<? extends Provider> clazz);
long getServerStartupTimestamp();
void close();
}

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

@ -0,0 +1,20 @@
package org.keycloak.provider;
import java.util.Map;
/**
* Marker interface for ProviderFactory of Provider which wants to show some info on "Server Info" page in Admin console.
*
* @author Vlastimil Elias (velias at redhat dot com)
*/
public interface ServerInfoAwareProviderFactory<T extends Provider> extends ProviderFactory<T> {
/**
* Get operational info about given provider. This info contains informations about providers configuration and operational conditions (eg. errors in connection to remote systems etc) which is
* shown on "Server Info" page.
*
* @return Map with keys describing value and relevant values itself
*/
public Map<String, String> getOperationalInfo();
}

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

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

@ -28,6 +28,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
private Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<Class<? extends Provider>, Map<String, ProviderFactory>>();
protected CopyOnWriteArrayList<ProviderEventListener> listeners = new CopyOnWriteArrayList<ProviderEventListener>();
protected long serverStartupTimestamp;
@Override
public void register(ProviderEventListener listener) {
listeners.add(listener);
@ -46,6 +48,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
}
public void init() {
serverStartupTimestamp = System.currentTimeMillis();
ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers"));
for (Spi spi : ServiceLoader.load(Spi.class, getClass().getClassLoader())) {
@ -148,4 +152,12 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
return factory.getClass().getPackage().getName().startsWith("org.keycloak");
}
/**
* @return timestamp of Keycloak server startup
*/
@Override
public long getServerStartupTimestamp() {
return serverStartupTimestamp;
}
}

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

@ -20,6 +20,7 @@ import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.admin.info.ServerInfoAdminResource;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

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

@ -0,0 +1,54 @@
package org.keycloak.services.resources.admin.info;
public class MemoryInfoRepresentation {
protected long total;
protected long used;
public static MemoryInfoRepresentation create() {
MemoryInfoRepresentation rep = new MemoryInfoRepresentation();
Runtime runtime = Runtime.getRuntime();
rep.total = runtime.maxMemory();
rep.used = runtime.totalMemory() - runtime.freeMemory();
return rep;
}
public long getTotal() {
return total;
}
public String getTotalFormated() {
return formatMemory(getTotal());
}
public long getFree() {
return getTotal() - getUsed();
}
public String getFreeFormated() {
return formatMemory(getFree());
}
public long getUsed() {
return used;
}
public String getUsedFormated() {
return formatMemory(getUsed());
}
public long getFreePercentage() {
return getFree() * 100 / getTotal();
}
private String formatMemory(long bytes) {
if (bytes > 1024L * 1024L) {
return bytes / (1024L * 1024L) + " MB";
} else if (bytes > 1024L) {
return bytes / (1024L) + " kB";
} else {
return bytes + " B";
}
}
}

View file

@ -0,0 +1,17 @@
package org.keycloak.services.resources.admin.info;
import java.util.Map;
public class ProviderRepresentation {
private Map<String, String> operationalInfo;
public Map<String, String> getOperationalInfo() {
return operationalInfo;
}
public void setOperationalInfo(Map<String, String> operationalInfo) {
this.operationalInfo = operationalInfo;
}
}

View file

@ -1,9 +1,7 @@
package org.keycloak.services.resources.admin;
package org.keycloak.services.resources.admin.info;
import org.keycloak.Version;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.exportimport.ClientImporter;
@ -18,6 +16,7 @@ import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import org.keycloak.provider.Spi;
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@ -26,14 +25,7 @@ import org.keycloak.social.SocialIdentityProvider;
import javax.ws.rs.GET;
import javax.ws.rs.core.Context;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -53,13 +45,12 @@ public class ServerInfoAdminResource {
@GET
public ServerInfoRepresentation getInfo() {
ServerInfoRepresentation info = new ServerInfoRepresentation();
info.version = Version.VERSION;
info.serverTime = new Date().toString();
info.setSystemInfo(SystemInfoRepresentation.create(session));
info.setMemoryInfo(MemoryInfoRepresentation.create());
setSocialProviders(info);
setIdentityProviders(info);
setThemes(info);
setEventListeners(info);
setProtocols(info);
setClientImporters(info);
setProviders(info);
setProtocolMapperTypes(info);
@ -69,42 +60,55 @@ public class ServerInfoAdminResource {
}
private void setProviders(ServerInfoRepresentation info) {
List<SpiInfoRepresentation> providers = new LinkedList<>();
Map<String, SpiInfoRepresentation> spis = new HashMap<>();
for (Spi spi : ServiceLoader.load(Spi.class)) {
SpiInfoRepresentation spiRep = new SpiInfoRepresentation();
spiRep.setName(spi.getName());
spiRep.setInternal(spi.isInternal());
spiRep.setImplementations(session.listProviderIds(spi.getProviderClass()));
providers.add(spiRep);
spiRep.setSystemInfo(ServerInfoAwareProviderFactory.class.isAssignableFrom(spi.getProviderFactoryClass()));
Set<String> providerIds = session.listProviderIds(spi.getProviderClass());
Map<String, ProviderRepresentation> providers = new HashMap<>();
if (providerIds != null) {
for (String name : providerIds) {
ProviderRepresentation provider = new ProviderRepresentation();
if (spiRep.isSystemInfo()) {
provider.setOperationalInfo(((ServerInfoAwareProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name)).getOperationalInfo());
}
providers.put(name, provider);
}
}
spiRep.setProviders(providers);
spis.put(spi.getName(), spiRep);
}
info.providers = providers;
info.setProviders(spis);
}
private void setThemes(ServerInfoRepresentation info) {
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
info.themes = new HashMap<String, List<String>>();
info.setThemes(new HashMap<String, List<String>>());
for (Theme.Type type : Theme.Type.values()) {
List<String> themes = new LinkedList<String>(themeProvider.nameSet(type));
Collections.sort(themes);
info.themes.put(type.toString().toLowerCase(), themes);
info.getThemes().put(type.toString().toLowerCase(), themes);
}
}
private void setSocialProviders(ServerInfoRepresentation info) {
info.socialProviders = new LinkedList<>();
info.setSocialProviders(new LinkedList<Map<String, String>>());
List<ProviderFactory> providerFactories = session.getKeycloakSessionFactory().getProviderFactories(SocialIdentityProvider.class);
setIdentityProviders(providerFactories, info.socialProviders, "Social");
setIdentityProviders(providerFactories, info.getSocialProviders(), "Social");
}
private void setIdentityProviders(ServerInfoRepresentation info) {
info.identityProviders = new LinkedList<>();
info.setIdentityProviders(new LinkedList<Map<String, String>>());
List<ProviderFactory> providerFactories = session.getKeycloakSessionFactory().getProviderFactories(IdentityProvider.class);
setIdentityProviders(providerFactories, info.identityProviders, "User-defined");
setIdentityProviders(providerFactories, info.getIdentityProviders(), "User-defined");
providerFactories = session.getKeycloakSessionFactory().getProviderFactories(SocialIdentityProvider.class);
setIdentityProviders(providerFactories, info.identityProviders, "Social");
setIdentityProviders(providerFactories, info.getIdentityProviders(), "Social");
}
public void setIdentityProviders(List<ProviderFactory> factories, List<Map<String, String>> providers, String groupName) {
@ -119,31 +123,14 @@ public class ServerInfoAdminResource {
}
}
private void setEventListeners(ServerInfoRepresentation info) {
info.eventListeners = new LinkedList<String>();
Set<String> providers = session.listProviderIds(EventListenerProvider.class);
if (providers != null) {
info.eventListeners.addAll(providers);
}
}
private void setProtocols(ServerInfoRepresentation info) {
info.protocols = new LinkedList<String>();
for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(LoginProtocol.class)) {
info.protocols.add(p.getId());
}
Collections.sort(info.protocols);
}
private void setProtocolMapperTypes(ServerInfoRepresentation info) {
info.protocolMapperTypes = new HashMap<String, List<ProtocolMapperTypeRepresentation>>();
info.setProtocolMapperTypes(new HashMap<String, List<ProtocolMapperTypeRepresentation>>());
for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(ProtocolMapper.class)) {
ProtocolMapper mapper = (ProtocolMapper)p;
List<ProtocolMapperTypeRepresentation> types = info.protocolMapperTypes.get(mapper.getProtocol());
List<ProtocolMapperTypeRepresentation> types = info.getProtocolMapperTypes().get(mapper.getProtocol());
if (types == null) {
types = new LinkedList<ProtocolMapperTypeRepresentation>();
info.protocolMapperTypes.put(mapper.getProtocol(), types);
info.getProtocolMapperTypes().put(mapper.getProtocol(), types);
}
ProtocolMapperTypeRepresentation rep = new ProtocolMapperTypeRepresentation();
rep.setId(mapper.getId());
@ -166,136 +153,25 @@ public class ServerInfoAdminResource {
}
private void setBuiltinProtocolMappers(ServerInfoRepresentation info) {
info.builtinProtocolMappers = new HashMap<>();
info.setBuiltinProtocolMappers(new HashMap<String, List<ProtocolMapperRepresentation>>());
for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(LoginProtocol.class)) {
LoginProtocolFactory factory = (LoginProtocolFactory)p;
List<ProtocolMapperRepresentation> mappers = new LinkedList<>();
for (ProtocolMapperModel mapper : factory.getBuiltinMappers()) {
mappers.add(ModelToRepresentation.toRepresentation(mapper));
}
info.builtinProtocolMappers.put(p.getId(), mappers);
info.getBuiltinProtocolMappers().put(p.getId(), mappers);
}
}
private void setClientImporters(ServerInfoRepresentation info) {
info.clientImporters = new LinkedList<Map<String, String>>();
info.setClientImporters(new LinkedList<Map<String, String>>());
for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(ClientImporter.class)) {
ClientImporterFactory factory = (ClientImporterFactory)p;
Map<String, String> data = new HashMap<String, String>();
data.put("id", factory.getId());
data.put("name", factory.getDisplayName());
info.clientImporters.add(data);
}
}
public static class ServerInfoRepresentation {
private String version;
private String serverTime;
private Map<String, List<String>> themes;
private List<Map<String, String>> socialProviders;
public List<Map<String, String>> identityProviders;
private List<String> protocols;
private List<Map<String, String>> clientImporters;
private List<SpiInfoRepresentation> providers;
private List<String> eventListeners;
private Map<String, List<ProtocolMapperTypeRepresentation>> protocolMapperTypes;
private Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers;
private Map<String, List<String>> enums;
public ServerInfoRepresentation() {
}
public String getServerTime() {
return serverTime;
}
public String getVersion() {
return version;
}
public Map<String, List<String>> getThemes() {
return themes;
}
public List<Map<String, String>> getSocialProviders() {
return socialProviders;
}
public List<Map<String, String>> getIdentityProviders() {
return this.identityProviders;
}
public List<String> getEventListeners() {
return eventListeners;
}
public List<String> getProtocols() {
return protocols;
}
public List<Map<String, String>> getClientImporters() {
return clientImporters;
}
public List<SpiInfoRepresentation> getProviders() {
return providers;
}
public Map<String, List<ProtocolMapperTypeRepresentation>> getProtocolMapperTypes() {
return protocolMapperTypes;
}
public Map<String, List<ProtocolMapperRepresentation>> getBuiltinProtocolMappers() {
return builtinProtocolMappers;
}
public void setBuiltinProtocolMappers(Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers) {
this.builtinProtocolMappers = builtinProtocolMappers;
}
public Map<String, List<String>> getEnums() {
return enums;
}
public void setEnums(Map<String, List<String>> enums) {
this.enums = enums;
}
}
public static class SpiInfoRepresentation {
private String name;
private boolean internal;
private Set<String> implementations;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isInternal() {
return internal;
}
public void setInternal(boolean internal) {
this.internal = internal;
}
public Set<String> getImplementations() {
return implementations;
}
public void setImplementations(Set<String> implementations) {
this.implementations = implementations;
info.getClientImporters().add(data);
}
}

View file

@ -0,0 +1,108 @@
package org.keycloak.services.resources.admin.info;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ServerInfoRepresentation {
private SystemInfoRepresentation systemInfo;
private MemoryInfoRepresentation memoryInfo;
private Map<String, List<String>> themes;
private List<Map<String, String>> socialProviders;
private List<Map<String, String>> identityProviders;
private List<Map<String, String>> clientImporters;
private Map<String, SpiInfoRepresentation> providers;
private Map<String, List<ProtocolMapperTypeRepresentation>> protocolMapperTypes;
private Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers;
private Map<String, List<String>> enums;
public SystemInfoRepresentation getSystemInfo() {
return systemInfo;
}
public void setSystemInfo(SystemInfoRepresentation systemInfo) {
this.systemInfo = systemInfo;
}
public MemoryInfoRepresentation getMemoryInfo() {
return memoryInfo;
}
public void setMemoryInfo(MemoryInfoRepresentation memoryInfo) {
this.memoryInfo = memoryInfo;
}
public Map<String, List<String>> getThemes() {
return themes;
}
public void setThemes(Map<String, List<String>> themes) {
this.themes = themes;
}
public List<Map<String, String>> getSocialProviders() {
return socialProviders;
}
public void setSocialProviders(List<Map<String, String>> socialProviders) {
this.socialProviders = socialProviders;
}
public List<Map<String, String>> getIdentityProviders() {
return identityProviders;
}
public void setIdentityProviders(List<Map<String, String>> identityProviders) {
this.identityProviders = identityProviders;
}
public List<Map<String, String>> getClientImporters() {
return clientImporters;
}
public void setClientImporters(List<Map<String, String>> clientImporters) {
this.clientImporters = clientImporters;
}
public Map<String, SpiInfoRepresentation> getProviders() {
return providers;
}
public void setProviders(Map<String, SpiInfoRepresentation> providers) {
this.providers = providers;
}
public Map<String, List<ProtocolMapperTypeRepresentation>> getProtocolMapperTypes() {
return protocolMapperTypes;
}
public void setProtocolMapperTypes(Map<String, List<ProtocolMapperTypeRepresentation>> protocolMapperTypes) {
this.protocolMapperTypes = protocolMapperTypes;
}
public Map<String, List<ProtocolMapperRepresentation>> getBuiltinProtocolMappers() {
return builtinProtocolMappers;
}
public void setBuiltinProtocolMappers(Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers) {
this.builtinProtocolMappers = builtinProtocolMappers;
}
public Map<String, List<String>> getEnums() {
return enums;
}
public void setEnums(Map<String, List<String>> enums) {
this.enums = enums;
}
}

View file

@ -0,0 +1,39 @@
package org.keycloak.services.resources.admin.info;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class SpiInfoRepresentation {
private boolean internal;
private boolean systemInfo;
private Map<String, ProviderRepresentation> providers;
public boolean isInternal() {
return internal;
}
public void setInternal(boolean internal) {
this.internal = internal;
}
public boolean isSystemInfo() {
return systemInfo;
}
public void setSystemInfo(boolean systemInfo) {
this.systemInfo = systemInfo;
}
public Map<String, ProviderRepresentation> getProviders() {
return providers;
}
public void setProviders(Map<String, ProviderRepresentation> providers) {
this.providers = providers;
}
}

View file

@ -0,0 +1,217 @@
package org.keycloak.services.resources.admin.info;
import org.keycloak.Version;
import org.keycloak.models.KeycloakSession;
import java.util.Date;
import java.util.Locale;
public class SystemInfoRepresentation {
private String version;
private String serverTime;
private String uptime;
private long uptimeMillis;
private String javaVersion;
private String javaVendor;
private String javaVm;
private String javaVmVersion;
private String javaRuntime;
private String javaHome;
private String osName;
private String osArchitecture;
private String osVersion;
private String fileEncoding;
private String userName;
private String userDir;
private String userTimezone;
private String userLocale;
public static SystemInfoRepresentation create(KeycloakSession session) {
SystemInfoRepresentation rep = new SystemInfoRepresentation();
rep.version = Version.VERSION;
rep.serverTime = new Date().toString();
rep.uptimeMillis = System.currentTimeMillis() - session.getKeycloakSessionFactory().getServerStartupTimestamp();
rep.uptime = formatUptime(rep.uptimeMillis);
rep.javaVersion = System.getProperty("java.version");
rep.javaVendor = System.getProperty("java.vendor");
rep.javaVm = System.getProperty("java.vm.name");
rep.javaVmVersion = System.getProperty("java.vm.version");
rep.javaRuntime = System.getProperty("java.runtime.name");
rep.javaHome = System.getProperty("java.home");
rep.osName = System.getProperty("os.name");
rep.osArchitecture = System.getProperty("os.arch");
rep.osVersion = System.getProperty("os.version");
rep.fileEncoding = System.getProperty("file.encoding");
rep.userName = System.getProperty("user.name");
rep.userDir = System.getProperty("user.dir");
rep.userTimezone = System.getProperty("user.timezone");
rep.userLocale = (new Locale(System.getProperty("user.country"), System.getProperty("user.language")).toString());
return rep;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getServerTime() {
return serverTime;
}
public void setServerTime(String serverTime) {
this.serverTime = serverTime;
}
public String getUptime() {
return uptime;
}
public void setUptime(String uptime) {
this.uptime = uptime;
}
public long getUptimeMillis() {
return uptimeMillis;
}
public void setUptimeMillis(long uptimeMillis) {
this.uptimeMillis = uptimeMillis;
}
public String getJavaVersion() {
return javaVersion;
}
public void setJavaVersion(String javaVersion) {
this.javaVersion = javaVersion;
}
public String getJavaVendor() {
return javaVendor;
}
public void setJavaVendor(String javaVendor) {
this.javaVendor = javaVendor;
}
public String getJavaVm() {
return javaVm;
}
public void setJavaVm(String javaVm) {
this.javaVm = javaVm;
}
public String getJavaVmVersion() {
return javaVmVersion;
}
public void setJavaVmVersion(String javaVmVersion) {
this.javaVmVersion = javaVmVersion;
}
public String getJavaRuntime() {
return javaRuntime;
}
public void setJavaRuntime(String javaRuntime) {
this.javaRuntime = javaRuntime;
}
public String getJavaHome() {
return javaHome;
}
public void setJavaHome(String javaHome) {
this.javaHome = javaHome;
}
public String getOsName() {
return osName;
}
public void setOsName(String osName) {
this.osName = osName;
}
public String getOsArchitecture() {
return osArchitecture;
}
public void setOsArchitecture(String osArchitecture) {
this.osArchitecture = osArchitecture;
}
public String getOsVersion() {
return osVersion;
}
public void setOsVersion(String osVersion) {
this.osVersion = osVersion;
}
public String getFileEncoding() {
return fileEncoding;
}
public void setFileEncoding(String fileEncoding) {
this.fileEncoding = fileEncoding;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserDir() {
return userDir;
}
public void setUserDir(String userDir) {
this.userDir = userDir;
}
public String getUserTimezone() {
return userTimezone;
}
public void setUserTimezone(String userTimezone) {
this.userTimezone = userTimezone;
}
public String getUserLocale() {
return userLocale;
}
public void setUserLocale(String userLocale) {
this.userLocale = userLocale;
}
private static String formatUptime(long uptime) {
long diffInSeconds = uptime / 1000;
long diff[] = new long[]{0, 0, 0, 0}; // sec
diff[3] = (diffInSeconds >= 60 ? diffInSeconds % 60 : diffInSeconds); // min
diff[2] = (diffInSeconds = (diffInSeconds / 60)) >= 60 ? diffInSeconds % 60 : diffInSeconds; // hours
diff[1] = (diffInSeconds = (diffInSeconds / 60)) >= 24 ? diffInSeconds % 24 : diffInSeconds; // days
diff[0] = (diffInSeconds = (diffInSeconds / 24));
return String.format(
"%d day%s, %d hour%s, %d minute%s, %d second%s",
diff[0],
diff[0] != 1 ? "s" : "",
diff[1],
diff[1] != 1 ? "s" : "",
diff[2],
diff[2] != 1 ? "s" : "",
diff[3],
diff[3] != 1 ? "s" : "");
}
}

View file

@ -25,6 +25,7 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.Config;
import org.keycloak.Version;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
@ -55,6 +56,7 @@ import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@ -295,4 +297,62 @@ public class AdminAPITest {
testCreateRealm("/admin-test/testrealm.json");
}
@Test
public void testServerInfo() {
String token = createToken();
final String authHeader = "Bearer " + token;
ClientRequestFilter authFilter = new ClientRequestFilter() {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
}
};
Client client = ClientBuilder.newBuilder().register(authFilter).build();
UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
WebTarget target = client.target(AdminRoot.adminBaseUrl(authBase).path("serverinfo"));
Map<?, ?> response = target.request().accept("application/json").get(Map.class);
Assert.assertNotNull(response);
Assert.assertEquals(Version.VERSION, response.get("version"));
Assert.assertNotNull(response.get("serverTime"));
Assert.assertNotNull(response.get("providers"));
Assert.assertNotNull(response.get("themes"));
Assert.assertNotNull(response.get("enums"));
// System.out.println(response);
}
@Test
public void testServerInfoPage() {
String token = createToken();
final String authHeader = "Bearer " + token;
ClientRequestFilter authFilter = new ClientRequestFilter() {
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
}
};
Client client = ClientBuilder.newBuilder().register(authFilter).build();
UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
WebTarget target = client.target(AdminRoot.adminBaseUrl(authBase).path("serverinfopage"));
Map<?, ?> response = target.request().accept("application/json").get(Map.class);
Assert.assertNotNull(response);
Assert.assertEquals(Version.VERSION, response.get("version"));
Assert.assertNotNull(response.get("serverTime"));
Assert.assertNotNull(response.get("providers"));
Assert.assertNotNull(response.get("serverStartupTime"));
Assert.assertNotNull(response.get("memoryInfo"));
Assert.assertNotNull(response.get("systemInfo"));
// System.out.println(response);
}
}

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