diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml index 103c7cea697..5fc0f23b5ae 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml @@ -142,6 +142,7 @@ + diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 61bbafa6bf9..a79c82b2286 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -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 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 getOperationalInfo() { + return operationalInfo; + } } diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java index 1cf4a5f2026..288e403335a 100644 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java @@ -1,9 +1,10 @@ package org.keycloak.connections.jpa; -import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.ServerInfoAwareProviderFactory; /** * @author Stian Thorgersen */ -public interface JpaConnectionProviderFactory extends ProviderFactory { +public interface JpaConnectionProviderFactory extends ServerInfoAwareProviderFactory { + } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 36be680a668..8da15b69c12 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -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 Stian Thorgersen @@ -57,6 +60,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro private MongoStore mongoStore; private DB db; protected Config.Scope config; + + private Map 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 getOperationalInfo() { + return operationalInfo; + } } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java index e787ce63825..bce5fe4b9cf 100644 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java @@ -1,9 +1,9 @@ package org.keycloak.connections.mongo; -import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.ServerInfoAwareProviderFactory; /** * @author Stian Thorgersen */ -public interface MongoConnectionProviderFactory extends ProviderFactory { +public interface MongoConnectionProviderFactory extends ServerInfoAwareProviderFactory { } diff --git a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java index 928f62d915b..561a5d078b1 100644 --- a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java +++ b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java @@ -8,7 +8,6 @@ public interface ServiceAccountConstants { String CLIENT_AUTH = "client_auth"; String SERVICE_ACCOUNT_USER_PREFIX = "service-account-"; - String SERVICE_ACCOUNT_CLIENT_ATTRIBUTE = "serviceAccountClient"; String CLIENT_ID_PROTOCOL_MAPPER = "Client ID"; String CLIENT_HOST_PROTOCOL_MAPPER = "Client Host"; diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 1d2bee3c639..ea20afc7083 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -26,6 +26,7 @@ public class UserRepresentation { protected String lastName; protected String email; protected String federationLink; + protected String serviceAccountClientId; // For rep, it points to clientId (not DB ID) // Currently there is Map> but for backwards compatibility, we also need to support Map protected Map attributes; @@ -218,4 +219,12 @@ public class UserRepresentation { public void setFederationLink(String federationLink) { this.federationLink = federationLink; } + + public String getServiceAccountClientId() { + return serviceAccountClientId; + } + + public void setServiceAccountClientId(String serviceAccountClientId) { + this.serviceAccountClientId = serviceAccountClientId; + } } diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml index fe6ee0c9cb3..29a49a7bb6b 100755 --- a/docbook/reference/en/en-US/master.xml +++ b/docbook/reference/en/en-US/master.xml @@ -28,6 +28,7 @@ + @@ -122,6 +123,7 @@ This one is short &AccessTypes; &Roles; &DirectAccess; + &ServiceAccounts; &CORS; &Timeouts; &AdminApi; diff --git a/docbook/reference/en/en-US/modules/service-accounts.xml b/docbook/reference/en/en-US/modules/service-accounts.xml new file mode 100644 index 00000000000..e641988a1cc --- /dev/null +++ b/docbook/reference/en/en-US/modules/service-accounts.xml @@ -0,0 +1,56 @@ + + Service Accounts + + Keycloak allows you to obtain an access token dedicated to some Client Application (not to any user). + See Client Credentials Grant + from OAuth 2.0 spec. + + + To use it you must have + registered a valid confidential Client and you need to check the switch Service Accounts Enabled in Keycloak + admin console for this client. In tab Service Account Roles 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 Full Scope Allowed on). + As in normal login, roles from access token are intersection of scopes and the service account roles. + + + + The REST URL to invoke on is /{keycloak-root}/realms/{realm-name}/protocol/openid-connect/token. + Invoking on this URL is a POST request and requires you to post the clientId and clientSecret of the client in Authorization: Basic header. + Later we want to add more mechanisms for authenticating clients. You also need to use parameter grant_type=client_credentials as per OAuth2 specification. + + + For example the POST invocation to retrieve service account can look like this: + + + The response would be this standard JSON document from the OAuth 2.0 specification. + + + + + The retrieved access token can be refreshed or logged out by out-of-bound request. + + + See the example application service-account + from the main Keycloak demo example. + + \ No newline at end of file diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java index f9dc9f165e6..d03654d542b 100644 --- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java +++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java @@ -140,15 +140,7 @@ public class ProductServiceAccountServlet extends HttpServlet { int status = response.getStatusLine().getStatusCode(); if (status != 200) { String json = getContent(entity); - String error = "Failed retrieve products."; - - if (status == 401) { - error = error + " You need to login first with the service account."; - } else if (status == 403) { - error = error + " Maybe service account user doesn't have needed role? Assign role 'user' in Keycloak admin console to user '" + - ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + getKeycloakDeployment().getResourceName() + "' and then logout and login again."; - } - error = error + " Status: " + status + ", Response: " + json; + String error = "Failed retrieve products. Status: " + status + ", Response: " + json; req.setAttribute(ERROR, error); } else if (entity == null) { req.setAttribute(ERROR, "No entity"); diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json index a26a0582097..d669e6bccd5 100755 --- a/examples/demo-template/testrealm.json +++ b/examples/demo-template/testrealm.json @@ -71,6 +71,13 @@ "clientRoles": { "realm-management": [ "realm-admin" ] } + }, + { + "username" : "service-account-product-sa-client", + "enabled": true, + "email" : "service-account-product-sa-client@placeholder.org", + "serviceAccountClientId": "product-sa-client", + "realmRoles": [ "user" ] } ], "roles" : { diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index a3396e3f1a5..601ddaf9fa9 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -123,7 +123,7 @@ public class ExportUtils { // Finally users if needed if (includeUsers) { - List allUsers = session.users().getUsers(realm); + List allUsers = session.users().getUsers(realm, true); List users = new ArrayList(); for (UserModel user : allUsers) { UserRepresentation userRep = exportUser(session, realm, user); @@ -286,6 +286,15 @@ public class ExportUtils { userRep.setClientConsents(consentReps); } + // Service account + if (user.getServiceAccountClientLink() != null) { + String clientInternalId = user.getServiceAccountClientLink(); + ClientModel client = realm.getClientById(clientInternalId); + if (client != null) { + userRep.setServiceAccountClientId(client.getClientId()); + } + } + return userRep; } diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java index c72708b362b..0ecc10a1ce0 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java @@ -92,7 +92,7 @@ public abstract class MultipleStepsExportProvider implements ExportProvider { @Override protected void runExportImportTask(KeycloakSession session) throws IOException { RealmModel realm = session.realms().getRealmByName(realmName); - usersHolder.users = session.users().getUsers(realm, usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart); + usersHolder.users = session.users().getUsers(realm, usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart, true); writeUsers(realmName + "-users-" + (usersHolder.currentPageStart / countPerPage) + ".json", session, realm, usersHolder.users); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 5ddf659deb2..4f02ce6b486 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -368,6 +368,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, clients : function(ClientListLoader) { return ClientListLoader(); + }, + client : function() { + return {}; } }, controller : 'UserRoleMappingCtrl' @@ -762,17 +765,23 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ClientInstallationCtrl' }) - .when('/realms/:realm/clients/:client/service-accounts', { - templateUrl : resourceUrl + '/partials/client-service-accounts.html', + .when('/realms/:realm/clients/:client/service-account-roles', { + templateUrl : resourceUrl + '/partials/client-service-account-roles.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); }, + user : function(ClientServiceAccountUserLoader) { + return ClientServiceAccountUserLoader(); + }, + clients : function(ClientListLoader) { + return ClientListLoader(); + }, client : function(ClientLoader) { return ClientLoader(); } }, - controller : 'ClientServiceAccountsCtrl' + controller : 'UserRoleMappingCtrl' }) .when('/create/client/:realm', { templateUrl : resourceUrl + '/partials/client-detail.html', @@ -1124,7 +1133,13 @@ module.config([ '$routeProvider', function($routeProvider) { controller : 'AuthenticationConfigCreateCtrl' }) .when('/server-info', { - templateUrl : resourceUrl + '/partials/server-info.html' + templateUrl : resourceUrl + '/partials/server-info.html', + resolve : { + serverInfoPage : function(ServerInfoPageLoader) { + return ServerInfoPageLoader(); + } + }, + controller : 'ServerInfoPageCtrl' }) .when('/logout', { templateUrl : resourceUrl + '/partials/home.html', @@ -1858,4 +1873,4 @@ module.directive( 'kcOpen', function ( $location ) { }); }); }; -}); \ No newline at end of file +}); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index db21798a4e8..6331c9d39a5 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -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."); - }); - } - } - -}); - - diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 85ad18b5c25..0b5745a0810 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -125,6 +125,20 @@ module.controller('RealmTabCtrl', function(Dialog, $scope, Current, Realm, Notif }; }); +module.controller('ServerInfoPageCtrl', function($scope, ServerInfoPage) { + $scope.serverInfoPage = ServerInfoPage.get(); + $scope.serverInfoPageUpdate = function() { + $scope.serverInfoPage = ServerInfoPage.get(); + }; +}); + +module.controller('ServerInfoPageCtrl', function($scope, ServerInfoPage) { + $scope.serverInfoPage = ServerInfoPage.get(); + $scope.serverInfoPageUpdate = function() { + $scope.serverInfoPage = ServerInfoPage.get(); + }; +}); + module.controller('RealmListCtrl', function($scope, Realm, Current) { $scope.realms = Realm.query(); Current.realms = $scope.realms; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index 3bdd55166cd..175a3b39352 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -1,4 +1,4 @@ -module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, clients, Notifications, RealmRoleMapping, +module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, clients, client, Notifications, RealmRoleMapping, ClientRoleMapping, AvailableRealmRoleMapping, AvailableClientRoleMapping, CompositeRealmRoleMapping, CompositeClientRoleMapping) { $scope.realm = realm; @@ -7,6 +7,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.selectedRealmMappings = []; $scope.realmMappings = []; $scope.clients = clients; + $scope.client = client; $scope.clientRoles = []; $scope.clientComposite = []; $scope.selectedClientRoles = []; @@ -28,11 +29,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.selectedRealmMappings = []; $scope.selectRealmRoles = []; - if ($scope.client) { + if ($scope.targetClient) { console.log('load available'); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; } @@ -49,11 +50,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.selectedRealmMappings = []; $scope.selectRealmRoles = []; - if ($scope.client) { + if ($scope.targetClient) { console.log('load available'); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; } @@ -62,11 +63,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl }; $scope.addClientRole = function() { - $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.client.id, + $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id, $scope.selectedClientRoles).success(function() { - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); @@ -76,11 +77,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl }; $scope.deleteClientRole = function() { - $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.client.id, + $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id, {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() { - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); @@ -92,11 +93,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.changeClient = function() { console.log('changeClient'); - if ($scope.client) { + if ($scope.targetClient) { console.log('load available'); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); } else { $scope.clientRoles = null; $scope.clientMappings = null; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 773f6f02718..2e65b3c7124 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -39,6 +39,10 @@ module.factory('ServerInfoLoader', function(Loader, ServerInfo, $q) { return Loader.get(ServerInfo); }); +module.factory('ServerInfoPageLoader', function(Loader, ServerInfoPage, $q) { + return Loader.get(ServerInfoPage); +}); + module.factory('RealmLoader', function(Loader, Realm, $route, $q) { return Loader.get(Realm, function() { return { @@ -282,6 +286,15 @@ module.factory('ClientListLoader', function(Loader, Client, $route, $q) { }); }); +module.factory('ClientServiceAccountUserLoader', function(Loader, ClientServiceAccountUser, $route, $q) { + return Loader.get(ClientServiceAccountUser, function() { + return { + realm : $route.current.params.realm, + client : $route.current.params.client + } + }); +}); + module.factory('RoleMappingLoader', function(Loader, RoleMapping, $route, $q) { var realm = $route.current.params.realm || $route.current.params.client; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 1d6a6be9256..b5d8c330f3d 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -219,6 +219,9 @@ module.factory('ServerInfo', function($resource) { return $resource(authUrl + '/admin/serverinfo'); }); +module.factory('ServerInfoPage', function($resource) { + return $resource(authUrl + '/admin/serverinfopage'); +}); module.factory('ClientProtocolMapper', function($resource) { @@ -897,6 +900,13 @@ module.factory('ClientOrigins', function($resource) { }); }); +module.factory('ClientServiceAccountUser', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/service-account-user', { + realm : '@realm', + client : '@client' + }); +}); + module.factory('Current', function(Realm, $route, $rootScope) { var current = { realms: {}, diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index 49a1997b19e..727c66d5fbf 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -72,6 +72,13 @@ 'Confidential' clients require a secret to initiate login protocol. 'Public' clients do not require a secret. 'Bearer-only' clients are web services that never initiate a login. +
+ + Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. +
+ +
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html new file mode 100644 index 00000000000..03a38d9ac2e --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html @@ -0,0 +1,113 @@ +
+ + + +

{{client.clientId|capitalize}}

+ + + +

{{client.clientId}} Service Accounts

+

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

{{client.clientId|capitalize}}

- - - -

{{client.clientId}} Service Accounts

-

-
-
-
- - Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. -
- -
-
-
-
- -
- - \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html index 8223936affc..3228be5c746 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html @@ -52,13 +52,13 @@
-
+
Select client to view roles for client
-
+
Assignable roles from this client. diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html index 34c9a845d3a..991f2084010 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html @@ -1,16 +1,100 @@
-

Server Info

+

Server Info

- +
+ + + + + + + + + + + + +
Keycloak Version{{serverInfoPage.version}}
Server Time{{serverInfoPage.serverTime}} (update)
Server Uptime{{serverInfoPage.serverUptime}}
+ +
+ Java VM Memory Statistics +
+ + + + + + + + + + + + + +
Total Memory{{serverInfoPage.memoryInfo.totalFormated}}
Free Memory{{serverInfoPage.memoryInfo.freeFormated}} ({{serverInfoPage.memoryInfo.freePercentage}}%)
Used Memory{{serverInfoPage.memoryInfo.usedFormated}}
+
+
+ +
+ System Info +
+ - - + + - - + + -
Version{{serverInfo.version}}Current Working Directory{{serverInfoPage.systemInfo.userDir}}
Server Time{{serverInfo.serverTime}} (update)Java Version{{serverInfoPage.systemInfo.javaVersion}}
+ + Java Vendor + {{serverInfoPage.systemInfo.javaVendor}} + + + Java Runtime + {{serverInfoPage.systemInfo.javaRuntime}} + + + Java VM + {{serverInfoPage.systemInfo.javaVm}} + + + Java VM Version + {{serverInfoPage.systemInfo.javaVmVersion}} + + + Java Home + {{serverInfoPage.systemInfo.javaHome}} + + + User Name + {{serverInfoPage.systemInfo.userName}} + + + User Timezone + {{serverInfoPage.systemInfo.userTimezone}} + + + User Locale + {{serverInfoPage.systemInfo.userLocale}} + + + System Encoding + {{serverInfoPage.systemInfo.fileEncoding}} + + + Operating System + {{serverInfoPage.systemInfo.osName}} {{serverInfoPage.systemInfo.osVersion}} + + + OS Architecture + {{serverInfoPage.systemInfo.osArchitecture}} + + +
+
Providers @@ -22,16 +106,28 @@ - + - + @@ -46,16 +142,28 @@
SPISPI Providers
{{spi.name}} -
- {{provider}} +
+ {{provider.name}} + + + + + + + +
{{key}}{{value}}
+
- + - + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html index c82d99f99aa..2aad2d5f12a 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html @@ -33,9 +33,9 @@ Helper utility for generating various client adapter configuration formats which you can download or cut and paste to configure your clients. -
  • - Service Accounts - Allows you to authenticate this client to Keycloak and retrieve access tokens dedicated to this client. +
  • + Service Account Roles + Allows you to authenticate role mappings for the service account dedicated to this client.
  • \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java index e2ef2f66fcf..5a8a6e0d65c 100755 --- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java +++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java @@ -32,7 +32,7 @@ public class MigrateTo1_4_0 { } public void migrateUsers(KeycloakSession session, RealmModel realm) { - List users = session.userStorage().getUsers(realm); + List users = session.userStorage().getUsers(realm, false); for (UserModel user : users) { String email = user.getEmail(); email = KeycloakModelUtils.toLowerCaseSafe(email); diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java b/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java index cbb3da22d5f..82665508c25 100755 --- a/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java +++ b/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java @@ -18,6 +18,8 @@ public interface KeycloakSessionFactory extends ProviderEventManager { ProviderFactory getProviderFactory(Class clazz, String id); List getProviderFactories(Class clazz); + + long getServerStartupTimestamp(); void close(); } diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index 23aaf1bd8db..ee18d798fa8 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -204,8 +204,17 @@ public class UserFederationManager implements UserProvider { } @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, 0, Integer.MAX_VALUE - 1); + public UserModel getUserByServiceAccountClient(ClientModel client) { + UserModel user = session.userStorage().getUserByServiceAccountClient(client); + if (user != null) { + user = validateAndProxyUser(client.getRealm(), user); + } + return user; + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, 0, Integer.MAX_VALUE - 1, includeServiceAccounts); } @@ -242,11 +251,11 @@ public class UserFederationManager implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { + public List getUsers(RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) { return query(new PaginatedQuery() { @Override public List query(RealmModel realm, int first, int max) { - return session.userStorage().getUsers(realm, first, max); + return session.userStorage().getUsers(realm, first, max, includeServiceAccounts); } }, realm, firstResult, maxResults); } diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java index 19fdad25919..94c2ffcdfdc 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -104,6 +104,9 @@ public interface UserModel { String getFederationLink(); void setFederationLink(String link); + String getServiceAccountClientLink(); + void setServiceAccountClientLink(String clientInternalId); + void addConsent(UserConsentModel consent); UserConsentModel getConsentByClient(String clientInternalId); List getConsents(); diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java index f48062f361e..1690b7a70c7 100755 --- a/model/api/src/main/java/org/keycloak/models/UserProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java @@ -25,9 +25,12 @@ public interface UserProvider extends Provider { UserModel getUserByUsername(String username, RealmModel realm); UserModel getUserByEmail(String email, RealmModel realm); UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm); - List getUsers(RealmModel realm); + UserModel getUserByServiceAccountClient(ClientModel client); + List getUsers(RealmModel realm, boolean includeServiceAccounts); + + // Service account is included for counts int getUsersCount(RealmModel realm); - List getUsers(RealmModel realm, int firstResult, int maxResults); + List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts); List searchForUser(String search, RealmModel realm); List searchForUser(String search, RealmModel realm, int firstResult, int maxResults); List searchForUserByAttributes(Map attributes, RealmModel realm); diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java index eeae34ffe45..8c82a8e13b8 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java @@ -27,6 +27,7 @@ public class UserEntity extends AbstractIdentifiableEntity { private List credentials = new ArrayList(); private List federatedIdentities; private String federationLink; + private String serviceAccountClientLink; public String getUsername() { return username; @@ -148,5 +149,13 @@ public class UserEntity extends AbstractIdentifiableEntity { public void setFederationLink(String federationLink) { this.federationLink = federationLink; } + + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } + + public void setServiceAccountClientLink(String serviceAccountClientLink) { + this.serviceAccountClientLink = serviceAccountClientLink; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index f5261a095ab..cfe08521b04 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -2,6 +2,7 @@ package org.keycloak.models.utils; import org.bouncycastle.openssl.PEMWriter; import org.keycloak.constants.KerberosConstants; +import org.keycloak.constants.ServiceAccountConstants; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -350,6 +351,8 @@ public final class KeycloakModelUtils { return mapperModel; } + // END USER FEDERATION RELATED STUFF + public static String toLowerCaseSafe(String str) { return str==null ? null : str.toLowerCase(); } diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index a38b305861a..83c8273acf4 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -902,6 +902,14 @@ public class RepresentationToModel { user.addConsent(consentModel); } } + if (userRep.getServiceAccountClientId() != null) { + String clientId = userRep.getServiceAccountClientId(); + ClientModel client = clientMap.get(clientId); + if (client == null) { + throw new RuntimeException("Unable to find client specified for service account link. Client: " + clientId); + } + user.setServiceAccountClientLink(client.getId());; + } return user; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index 9599ab93b95..3c1edb37974 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -207,6 +207,16 @@ public class UserModelDelegate implements UserModel { delegate.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + return delegate.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + delegate.setServiceAccountClientLink(clientInternalId); + } + @Override public void addConsent(UserConsentModel consent) { delegate.addConsent(consent); diff --git a/model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java b/model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java new file mode 100644 index 00000000000..97d17b1dcc0 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java @@ -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 extends ProviderFactory { + + /** + * 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 getOperationalInfo(); + +} diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java index ff152f8f7c6..0bcc37a8b2d 100755 --- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java @@ -107,8 +107,18 @@ public class FileUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, -1, -1); + public UserModel getUserByServiceAccountClient(ClientModel client) { + for (UserModel user : inMemoryModel.getUsers(client.getRealm().getId())) { + if (client.getId().equals(user.getServiceAccountClientLink())) { + return user; + } + } + return null; + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, -1, -1, includeServiceAccounts); } @Override @@ -117,12 +127,27 @@ public class FileUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - List users = new ArrayList(inMemoryModel.getUsers(realm.getId())); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + List users = new ArrayList<>(inMemoryModel.getUsers(realm.getId())); + + if (!includeServiceAccounts) { + users = filterServiceAccountUsers(users); + } + List sortedList = sortedSubList(users, firstResult, maxResults); return sortedList; } + private List filterServiceAccountUsers(List users) { + List result = new ArrayList<>(); + for (UserModel user : users) { + if (user.getServiceAccountClientLink() == null) { + result.add(user); + } + } + return result; + } + protected List sortedSubList(List list, int firstResult, int maxResults) { if (list.isEmpty()) return list; @@ -183,6 +208,9 @@ public class FileUserProvider implements UserProvider { } } + // Remove users with service account link + found = filterServiceAccountUsers(found); + return sortedSubList(found, firstResult, maxResults); } diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index 5db8f935063..98e4254ef24 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -477,6 +477,16 @@ public class UserAdapter implements UserModel, Comparable { user.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + return user.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + user.setServiceAccountClientLink(clientInternalId); + } + @Override public void addConsent(UserConsentModel consent) { // TODO diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java index 4e99e44eb78..aed1394415e 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java @@ -207,8 +207,13 @@ public class DefaultCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm) { - return getDelegate().getUsers(realm); + public UserModel getUserByServiceAccountClient(ClientModel client) { + return getDelegate().getUserByServiceAccountClient(client); + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, includeServiceAccounts); } @Override @@ -217,8 +222,8 @@ public class DefaultCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - return getDelegate().getUsers(realm, firstResult, maxResults); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, firstResult, maxResults, includeServiceAccounts); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java index 3abe72f7c87..8ed8b6a12b2 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java @@ -74,8 +74,13 @@ public class NoCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm) { - return getDelegate().getUsers(realm); + public UserModel getUserByServiceAccountClient(ClientModel client) { + return getDelegate().getUserByServiceAccountClient(client); + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, includeServiceAccounts); } @Override @@ -84,8 +89,8 @@ public class NoCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - return getDelegate().getUsers(realm, firstResult, maxResults); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, firstResult, maxResults, includeServiceAccounts); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java index b075ea118c2..f2b5e33da72 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java @@ -243,6 +243,18 @@ public class UserAdapter implements UserModel { updated.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + if (updated != null) return updated.getServiceAccountClientLink(); + return cached.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + getDelegateForUpdate(); + updated.setServiceAccountClientLink(clientInternalId); + } + @Override public Set getRealmRoleMappings() { if (updated != null) return updated.getRealmRoleMappings(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java index 3d1395b5833..853677b6d66 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java @@ -31,6 +31,7 @@ public class CachedUser implements Serializable { private boolean enabled; private boolean totp; private String federationLink; + private String serviceAccountClientLink; private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); private Set requiredActions = new HashSet<>(); private Set roleMappings = new HashSet(); @@ -49,6 +50,7 @@ public class CachedUser implements Serializable { this.enabled = user.isEnabled(); this.totp = user.isTotp(); this.federationLink = user.getFederationLink(); + this.serviceAccountClientLink = user.getServiceAccountClientLink(); this.requiredActions.addAll(user.getRequiredActions()); for (RoleModel role : user.getRoleMappings()) { roleMappings.add(role.getId()); @@ -114,4 +116,8 @@ public class CachedUser implements Serializable { public String getFederationLink() { return federationLink; } + + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index ae04a5f6b58..4f02f6c090b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -272,13 +272,29 @@ public class JpaUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, -1, -1); + public UserModel getUserByServiceAccountClient(ClientModel client) { + TypedQuery query = em.createNamedQuery("getRealmUserByServiceAccount", UserEntity.class); + query.setParameter("realmId", client.getRealm().getId()); + query.setParameter("clientInternalId", client.getId()); + List results = query.getResultList(); + if (results.isEmpty()) { + return null; + } else if (results.size() > 1) { + throw new IllegalStateException("More service account linked users found for client=" + client.getClientId() + + ", results=" + results); + } else { + UserEntity user = results.get(0); + return new UserAdapter(client.getRealm(), em, user); + } + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, -1, -1, includeServiceAccounts); } @Override public int getUsersCount(RealmModel realm) { - // TODO: named query? Object count = em.createNamedQuery("getRealmUserCount") .setParameter("realmId", realm.getId()) .getSingleResult(); @@ -286,8 +302,10 @@ public class JpaUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - TypedQuery query = em.createNamedQuery("getAllUsersByRealm", UserEntity.class); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + String queryName = includeServiceAccounts ? "getAllUsersByRealm" : "getAllUsersByRealmExcludeServiceAccount" ; + + TypedQuery query = em.createNamedQuery(queryName, UserEntity.class); query.setParameter("realmId", realm.getId()); if (firstResult != -1) { query.setFirstResult(firstResult); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index ca0c284b452..e60377746c9 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -543,6 +543,16 @@ public class UserAdapter implements UserModel { user.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + return user.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + user.setServiceAccountClientLink(clientInternalId); + } + @Override public void addConsent(UserConsentModel consent) { String clientId = consent.getClient().getId(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java index 5e0769a6b2b..2da16415862 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java @@ -21,12 +21,15 @@ import java.util.Collection; */ @NamedQueries({ @NamedQuery(name="getAllUsersByRealm", query="select u from UserEntity u where u.realmId = :realmId order by u.username"), - @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and ( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"), + @NamedQuery(name="getAllUsersByRealmExcludeServiceAccount", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) order by u.username"), + @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) and " + + "( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"), @NamedQuery(name="getRealmUserById", query="select u from UserEntity u where u.id = :id and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByUsername", query="select u from UserEntity u where u.username = :username and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByEmail", query="select u from UserEntity u where u.email = :email and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByLastName", query="select u from UserEntity u where u.lastName = :lastName and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByFirstLastName", query="select u from UserEntity u where u.firstName = :first and u.lastName = :last and u.realmId = :realmId"), + @NamedQuery(name="getRealmUserByServiceAccount", query="select u from UserEntity u where u.serviceAccountClientLink = :clientInternalId and u.realmId = :realmId"), @NamedQuery(name="getRealmUserCount", query="select count(u) from UserEntity u where u.realmId = :realmId"), @NamedQuery(name="deleteUsersByRealm", query="delete from UserEntity u where u.realmId = :realmId"), @NamedQuery(name="deleteUsersByRealmAndLink", query="delete from UserEntity u where u.realmId = :realmId and u.federationLink=:link") @@ -77,6 +80,9 @@ public class UserEntity { @Column(name="federation_link") protected String federationLink; + @Column(name="SERVICE_ACCOUNT_CLIENT_LINK") + protected String serviceAccountClientLink; + public String getId() { return id; } @@ -198,6 +204,14 @@ public class UserEntity { this.federationLink = federationLink; } + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } + + public void setServiceAccountClientLink(String serviceAccountClientLink) { + this.serviceAccountClientLink = serviceAccountClientLink; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index cc720c5f94c..a433fead56c 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -105,6 +105,16 @@ public class MongoUserProvider implements UserProvider { return userEntity == null ? null : new UserAdapter(session, realm, userEntity, invocationContext); } + @Override + public UserModel getUserByServiceAccountClient(ClientModel client) { + DBObject query = new QueryBuilder() + .and("serviceAccountClientLink").is(client.getId()) + .and("realmId").is(client.getRealm().getId()) + .get(); + MongoUserEntity userEntity = getMongoStore().loadSingleEntity(MongoUserEntity.class, query, invocationContext); + return userEntity == null ? null : new UserAdapter(session, client.getRealm(), userEntity, invocationContext); + } + protected List convertUserEntities(RealmModel realm, List userEntities) { List userModels = new ArrayList(); for (MongoUserEntity user : userEntities) { @@ -115,8 +125,8 @@ public class MongoUserProvider implements UserProvider { @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, -1, -1); + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, -1, -1, includeServiceAccounts); } @Override @@ -128,10 +138,15 @@ public class MongoUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - DBObject query = new QueryBuilder() - .and("realmId").is(realm.getId()) - .get(); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + QueryBuilder queryBuilder = new QueryBuilder() + .and("realmId").is(realm.getId()); + + if (!includeServiceAccounts) { + queryBuilder = queryBuilder.and("serviceAccountClientLink").is(null); + } + + DBObject query = queryBuilder.get(); DBObject sort = new BasicDBObject("username", 1); List users = getMongoStore().loadEntities(MongoUserEntity.class, query, sort, firstResult, maxResults, invocationContext); return convertUserEntities(realm, users); @@ -170,6 +185,7 @@ public class MongoUserProvider implements UserProvider { QueryBuilder builder = new QueryBuilder().and( new QueryBuilder().and("realmId").is(realm.getId()).get(), + new QueryBuilder().and("serviceAccountClientLink").is(null).get(), new QueryBuilder().or( new QueryBuilder().put("username").regex(caseInsensitivePattern).get(), new QueryBuilder().put("email").regex(caseInsensitivePattern).get(), diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index a8408134d5f..6dae14b371f 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -187,7 +187,7 @@ public class UserAdapter extends AbstractMongoAdapter implement @Override public Map> getAttributes() { - return user.getAttributes()==null ? Collections.>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes()); + return user.getAttributes()==null ? Collections.>emptyMap() : Collections.unmodifiableMap((Map) user.getAttributes()); } public MongoUserEntity getUser() { @@ -460,6 +460,17 @@ public class UserAdapter extends AbstractMongoAdapter implement updateUser(); } + @Override + public String getServiceAccountClientLink() { + return user.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + user.setServiceAccountClientLink(clientInternalId); + updateUser(); + } + @Override public void addConsent(UserConsentModel consent) { String clientId = consent.getClient().getId(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java index 3c8b8ad8526..1a8ad0bbc2d 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java @@ -106,20 +106,13 @@ public class ServiceAccountManager { protected Response finishClientAuthorization() { event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH); - Map search = new HashMap<>(); - search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId()); - List users = session.users().searchForUserByUserAttributes(search, realm); + clientUser = session.users().getUserByServiceAccountClient(client); - if (users.size() == 0) { + if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) { // May need to handle bootstrap here as well - logger.warnf("Service account user for client '%s' not found. Creating now", client.getClientId()); + logger.infof("Service account user for client '%s' not found or default protocol mapper for service account not found. Creating now", client.getClientId()); new ClientManager(new RealmManager(session)).enableServiceAccount(client); - users = session.users().searchForUserByUserAttributes(search, realm); - clientUser = users.get(0); - } else if (users.size() == 1) { - clientUser = users.get(0); - } else { - throw new ModelDuplicateException("Multiple service account users found for client '" + client.getClientId() + "' . Check your DB"); + clientUser = session.users().getUserByServiceAccountClient(client); } String clientUsername = clientUser.getUsername(); diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index e312aa01225..c5be21be10e 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -28,6 +28,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory { private Map, Map> factoriesMap = new HashMap, Map>(); protected CopyOnWriteArrayList listeners = new CopyOnWriteArrayList(); + 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; + } + } diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index a7f90795099..1b5a4e88b00 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -51,6 +51,12 @@ public class ClientManager { if (sessions != null) { sessions.onClientRemoved(realm, client); } + + UserModel serviceAccountUser = realmManager.getSession().users().getUserByServiceAccountClient(client); + if (serviceAccountUser != null) { + realmManager.getSession().users().removeUser(realm, serviceAccountUser); + } + return true; } else { return false; @@ -93,18 +99,15 @@ public class ClientManager { client.setServiceAccountsEnabled(true); // Add dedicated user for this service account - RealmModel realm = client.getRealm(); - Map search = new HashMap<>(); - search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId()); - List serviceAccountUsers = realmManager.getSession().users().searchForUserByUserAttributes(search, realm); - if (serviceAccountUsers.size() == 0) { + if (realmManager.getSession().users().getUserByServiceAccountClient(client) == null) { String username = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + client.getClientId(); logger.infof("Creating service account user '%s'", username); - UserModel user = realmManager.getSession().users().addUser(realm, username); + // Don't use federation for service account user + UserModel user = realmManager.getSession().userStorage().addUser(client.getRealm(), username); user.setEnabled(true); user.setEmail(username + "@placeholder.org"); - user.setSingleAttribute(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId()); + user.setServiceAccountClientLink(client.getId()); } // Add protocol mappers to retrieve clientId in access token diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index e1d1fa3c48a..6b3ca3eafa7 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -218,6 +218,32 @@ public class AdminRoot { return adminResource; } + /** + * Operational information about the server for "Server Info" page + * + * @param headers + * @return + */ + @Path("serverinfopage") + public ServerInfoPageAdminResource getServerInfoPage(@Context final HttpHeaders headers) { + handlePreflightRequest(); + + AdminAuth auth = authenticateRealmAdminRequest(headers); + if (!isAdmin(auth)) { + throw new ForbiddenException(); + } + + if (auth != null) { + logger.debug("authenticated admin access for: " + auth.getUser().getUsername()); + } + + Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response); + + ServerInfoPageAdminResource adminResource = new ServerInfoPageAdminResource(); + ResteasyProviderFactory.getInstance().injectProperties(adminResource); + return adminResource; + } + protected boolean isAdmin(AdminAuth auth) { RealmManager realmManager = new RealmManager(session); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 67cfb65da84..b85aa39252b 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -19,6 +19,7 @@ import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; @@ -292,6 +293,31 @@ public class ClientResource { adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); } + /** + * Returns user dedicated to this service account + * + * @return + */ + @Path("service-account-user") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public UserRepresentation getServiceAccountUser() { + auth.requireView(); + + UserModel user = session.users().getUserByServiceAccountClient(client); + if (user == null) { + if (client.isServiceAccountsEnabled()) { + new ClientManager(new RealmManager(session)).enableServiceAccount(client); + user = session.users().getUserByServiceAccountClient(client); + } else { + throw new BadRequestException("Service account not enabled for the client '" + client.getClientId() + "'"); + } + } + + return ModelToRepresentation.toRepresentation(user); + } + /** * If the client has an admin URL, push the client's revocation policy to it. * diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index 83e6c2fa40e..eeffe5dc22a 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -109,7 +109,7 @@ public class IdentityProviderResource { // Admin changed the ID (alias) of identity provider. We must update all clients and users logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId); - updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm), oldProviderId, newProviderId); + updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm, false), oldProviderId, newProviderId); } adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(providerRep).success(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java new file mode 100644 index 00000000000..c6a720816cf --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java @@ -0,0 +1,375 @@ +package org.keycloak.services.resources.admin; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +import javax.ws.rs.GET; +import javax.ws.rs.core.Context; + +import org.jboss.logging.Logger; +import org.keycloak.Version; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ServerInfoAwareProviderFactory; +import org.keycloak.provider.Spi; + +/** + * REST endpoint which return info for "Server Info" page. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class ServerInfoPageAdminResource { + + private static final Logger logger = Logger.getLogger(ServerInfoPageAdminResource.class); + + @Context + private KeycloakSession session; + + /** + * Returns a list of providers and other operational info about the page. + * + * @return + */ + @GET + public ServerInfoRepresentation getInfo() { + ServerInfoRepresentation info = new ServerInfoRepresentation(); + info.version = Version.VERSION; + info.serverTime = new Date().toString(); + info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); + info.memoryInfo = (new MemoryInfo()).init(Runtime.getRuntime()); + info.systemInfo = (new SystemInfo()).init(); + setProviders(info); + return info; + } + + private void setProviders(ServerInfoRepresentation info) { + List providers = new LinkedList<>(); + for (Spi spi : ServiceLoader.load(Spi.class)) { + SpiInfoRepresentation spiRep = new SpiInfoRepresentation(); + spiRep.setName(spi.getName()); + spiRep.setInternal(spi.isInternal()); + spiRep.setSystemInfo(ServerInfoAwareProviderFactory.class.isAssignableFrom(spi.getProviderFactoryClass())); + Set s = session.listProviderIds(spi.getProviderClass()); + Set srs = new HashSet<>(); + + if(s!=null){ + for(String name: s){ + SpiImplementationRepresentation sr = new SpiImplementationRepresentation(name); + if(spiRep.isSystemInfo()){ + sr.setOperationalInfo(((ServerInfoAwareProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name)).getOperationalInfo()); + } + srs.add(sr); + } + } + spiRep.setImplementations(srs); + providers.add(spiRep); + } + info.providers = providers; + } + + + public static class MemoryInfo implements Serializable { + + protected long total; + protected long used; + + public MemoryInfo(){ + } + + /** + * Fill object fwith info. + * @param runtime used to get memory info from. + * @return itself for chaining + */ + public MemoryInfo init(Runtime runtime){ + total = runtime.maxMemory(); + used = runtime.totalMemory() - runtime.freeMemory(); + return this; + } + + 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"; + } + } + + } + + public static class SystemInfo implements Serializable { + + protected String javaVersion; + protected String javaVendor; + protected String javaVm; + protected String javaVmVersion; + protected String javaRuntime; + protected String javaHome; + protected String osName; + protected String osArchitecture; + protected String osVersion; + protected String fileEncoding; + protected String userName; + protected String userDir; + protected String userTimezone; + protected String userLocale; + + public SystemInfo() { + } + + /** + * Fill object with info about current system loaded from {@link System} properties. + * @return object itself for chaining + */ + protected SystemInfo init(){ + javaVersion = System.getProperty("java.version"); + javaVendor = System.getProperty("java.vendor"); + javaVm = System.getProperty("java.vm.name"); + javaVmVersion = System.getProperty("java.vm.version"); + javaRuntime = System.getProperty("java.runtime.name"); + javaHome = System.getProperty("java.home"); + osName = System.getProperty("os.name"); + osArchitecture = System.getProperty("os.arch"); + osVersion = System.getProperty("os.version"); + fileEncoding = System.getProperty("file.encoding"); + userName = System.getProperty("user.name"); + userDir = System.getProperty("user.dir"); + userTimezone = System.getProperty("user.timezone"); + userLocale = (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); + return this; + } + + public String getJavaVersion(){ + return javaVersion; + } + + public String getJavaVendor(){ + return javaVendor; + } + + public String getJavaVm(){ + return javaVm; + } + + public String getJavaVmVersion(){ + return javaVmVersion; + } + + public String getJavaRuntime(){ + return javaRuntime; + } + + public String getJavaHome(){ + return javaHome; + } + + public String getOsName(){ + return osName; + } + + public String getOsArchitecture(){ + return osArchitecture; + } + + public String getOsVersion(){ + return osVersion; + } + + public String getFileEncoding(){ + return fileEncoding; + } + + public String getUserName(){ + return userName; + } + + public String getUserDir(){ + return userDir; + } + + public String getUserTimezone(){ + return userTimezone; + } + + public String getUserLocale(){ + return userLocale; + } + } + + public static class ServerInfoRepresentation implements Serializable { + + private String version; + private String serverTime; + private long serverStartupTime; + + + private List providers; + + private MemoryInfo memoryInfo; + private SystemInfo systemInfo; + + public ServerInfoRepresentation() { + } + + public SystemInfo getSystemInfo(){ + return systemInfo; + } + + public MemoryInfo getMemoryInfo(){ + return memoryInfo; + } + + public String getServerTime() { + return serverTime; + } + + public long getServerStartupTime() { + return serverStartupTime; + } + + /** + * @return server startup time formatted + */ + public String getServerStartupTimeFormatted() { + return (new Date(serverStartupTime)).toString(); + } + + /** + * @return server uptime in millis + */ + public long getServerUptimeMillis(){ + return System.currentTimeMillis() - serverStartupTime; + } + + /** + * @return server uptime formatted like "0 days, 10 hours, 24 minutes, 55 seconds" + */ + public String getServerUptime(){ + long diffInSeconds = getServerUptimeMillis()/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" : ""); + } + + public String getVersion() { + return version; + } + + + public List getProviders() { + return providers; + } + } + + public static class SpiInfoRepresentation implements Serializable { + private String name; + private boolean internal; + private boolean systemInfo; + private Set 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 getImplementations() { + return implementations; + } + + public boolean isSystemInfo() { + return systemInfo; + } + + public void setSystemInfo(boolean systemInfo) { + this.systemInfo = systemInfo; + } + + public void setImplementations(Set implementations) { + this.implementations = implementations; + } + } + + public static class SpiImplementationRepresentation implements Serializable { + + private String name; + private Map operationalInfo; + + public SpiImplementationRepresentation(String name) { + super(); + this.name = name; + } + + public Map getOperationalInfo() { + return operationalInfo; + } + + public void setOperationalInfo(Map operationalInfo) { + this.operationalInfo = operationalInfo; + } + + public String getName() { + return name; + } + + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 83480372171..1a1aa29a5cb 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -554,7 +554,7 @@ public class UsersResource { } userModels = session.users().searchForUserByAttributes(attributes, realm, firstResult, maxResults); } else { - userModels = session.users().getUsers(realm, firstResult, maxResults); + userModels = session.users().getUsers(realm, firstResult, maxResults, false); } for (UserModel user : userModels) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java index 3bde1cb7406..33ead317e41 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java @@ -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); + + } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index b883226ed81..8ce5c7ce6b6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -814,7 +814,7 @@ public abstract class AbstractIdentityProviderTest { private void removeTestUsers() { RealmModel realm = getRealm(); - List users = this.session.users().getUsers(realm); + List users = this.session.users().getUsers(realm, true); for (UserModel user : users) { Set identities = this.session.users().getFederatedIdentities(user, realm); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java index ee6c5c69f1a..8eb05b64e1f 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java @@ -288,14 +288,14 @@ public abstract class AbstractKerberosTest { RealmManager manager = new RealmManager(session); RealmModel appRealm = manager.getRealm("test"); - List users = session.userStorage().getUsers(appRealm); + List users = session.userStorage().getUsers(appRealm, true); for (UserModel user : users) { if (!user.getUsername().equals(AssertEvents.DEFAULT_USERNAME)) { session.userStorage().removeUser(appRealm, user); } } - Assert.assertEquals(1, session.userStorage().getUsers(appRealm).size()); + Assert.assertEquals(1, session.userStorage().getUsers(appRealm, true).size()); } finally { keycloakRule.stopSession(session, true); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java index e50caf845ca..9b03d5ba06d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java @@ -227,7 +227,7 @@ public class SyncProvidersTest { RealmModel testRealm = session.realms().getRealm("test"); // Remove all users from model - for (UserModel user : session.userStorage().getUsers(testRealm)) { + for (UserModel user : session.userStorage().getUsers(testRealm, true)) { session.userStorage().removeUser(testRealm, user); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java index 238184e0f4c..48ed318e374 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java @@ -433,7 +433,7 @@ public class AdapterTest extends AbstractModelTest { RealmModel otherRealm = adapter.createRealm("other"); realmManager.getSession().users().addUser(otherRealm, "bburke"); - Assert.assertEquals(1, realmManager.getSession().users().getUsers(otherRealm).size()); + Assert.assertEquals(1, realmManager.getSession().users().getUsers(otherRealm, false).size()); Assert.assertEquals(1, realmManager.getSession().users().searchForUser("bu", otherRealm).size()); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index 31a9574092d..332c94b56e6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -304,6 +304,14 @@ public class ImportTest extends AbstractModelTest { Assert.assertTrue(otherAppAdminConsent.isRoleGranted(realm.getRole("admin"))); Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin"))); Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper)); + + // Test service accounts + Assert.assertFalse(application.isServiceAccountsEnabled()); + Assert.assertTrue(otherApp.isServiceAccountsEnabled()); + Assert.assertNull(session.users().getUserByServiceAccountClient(application)); + UserModel linked = session.users().getUserByServiceAccountClient(otherApp); + Assert.assertNotNull(linked); + Assert.assertEquals("my-service-user", linked.getUsername()); } @Test diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java index d0c9d001d13..9455271a098 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java @@ -7,6 +7,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.services.managers.ClientManager; import static org.junit.Assert.assertNotNull; @@ -226,6 +227,61 @@ public class UserModelTest extends AbstractModelTest { Assert.assertEquals(0, users.size()); } + @Test + public void testServiceAccountLink() throws Exception { + RealmModel realm = realmManager.createRealm("original"); + ClientModel client = realm.addClient("foo"); + + UserModel user1 = session.users().addUser(realm, "user1"); + user1.setFirstName("John"); + user1.setLastName("Doe"); + + UserModel user2 = session.users().addUser(realm, "user2"); + user2.setFirstName("John"); + user2.setLastName("Doe"); + + // Search + Assert.assertNull(session.users().getUserByServiceAccountClient(client)); + List users = session.users().searchForUser("John Doe", realm); + Assert.assertEquals(2, users.size()); + Assert.assertTrue(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + // Link service account + user1.setServiceAccountClientLink(client.getId()); + + commit(); + + // Search and assert service account user not found + realm = realmManager.getRealmByName("original"); + UserModel searched = session.users().getUserByServiceAccountClient(client); + Assert.assertEquals(searched, user1); + users = session.users().searchForUser("John Doe", realm); + Assert.assertEquals(1, users.size()); + Assert.assertFalse(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + users = session.users().getUsers(realm, false); + Assert.assertEquals(1, users.size()); + Assert.assertFalse(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + users = session.users().getUsers(realm, true); + Assert.assertEquals(2, users.size()); + Assert.assertTrue(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + Assert.assertEquals(2, session.users().getUsersCount(realm)); + + // Remove client + new ClientManager(realmManager).removeClient(realm, client); + commit(); + + // Assert service account removed as well + realm = realmManager.getRealmByName("original"); + Assert.assertNull(session.users().getUserByUsername("user1", realm)); + } + public static void assertEquals(UserModel expected, UserModel actual) { Assert.assertEquals(expected.getUsername(), actual.getUsername()); Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json index 340d9d3c231..9df4385fa20 100755 --- a/testsuite/integration/src/test/resources/model/testrealm.json +++ b/testsuite/integration/src/test/resources/model/testrealm.json @@ -141,6 +141,11 @@ "userName": "mySocialUser@gmail.com" } ] + }, + { + "username": "my-service-user", + "enabled": true, + "serviceAccountClientId": "OtherApp" } ], "clients": [ @@ -158,6 +163,7 @@ "clientId": "OtherApp", "name": "Other Application", "enabled": true, + "serviceAccountsEnabled": true, "protocolMappers" : [ { "name" : "gss delegation credential",
    SPISPI Providers
    {{spi.name}} -
    - {{provider}} +
    + {{provider.name}} + + + + + + + +
    {{key}}{{value}}
    +