keycloak-aplcache
Changes
connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java 116(+71 -45)
connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java 5(+3 -2)
connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java 18(+17 -1)
connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java 4(+2 -2)
examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java 10(+1 -9)
export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java 11(+10 -1)
export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java 2(+1 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js 22(+1 -21)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js 24(+20 -4)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js 43(+22 -21)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html 7(+7 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html 113(+113 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html 28(+0 -28)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html 6(+3 -3)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html 146(+92 -54)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info-providers.html 55(+55 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html 6(+3 -3)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java 13(+9 -4)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java 6(+6 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java 13(+9 -4)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java 12(+12 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java 28(+22 -6)
services/src/main/java/org/keycloak/services/resources/admin/info/MemoryInfoRepresentation.java 54(+54 -0)
services/src/main/java/org/keycloak/services/resources/admin/info/ProviderRepresentation.java 17(+17 -0)
services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java 200(+38 -162)
services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoRepresentation.java 108(+108 -0)
services/src/main/java/org/keycloak/services/resources/admin/info/SpiInfoRepresentation.java 39(+39 -0)
services/src/main/java/org/keycloak/services/resources/admin/info/SystemInfoRepresentation.java 217(+217 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java 4(+2 -2)
Details
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 61bbafa..a79c82b 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<String,String> operationalInfo;
@Override
public JpaConnectionProvider create(KeycloakSession session) {
@@ -120,56 +124,73 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
- if (databaseSchema != null) {
- logger.trace("Updating database");
-
- 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);
- }
+ 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");
+
+ } finally {
+ // Close after creating EntityManagerFactory to prevent in-mem databases from closing
+ if (connection != null) {
+ try {
+ connection.close();
+ } catch (SQLException e) {
+ logger.warn(e);
+ }
+ }
}
}
}
}
}
+
+ protected void prepareOperationalInfo(Connection connection) {
+ try {
+ operationalInfo = new LinkedHashMap<>();
+ DatabaseMetaData md = connection.getMetaData();
+ operationalInfo.put("databaseUrl",md.getURL());
+ operationalInfo.put("databaseUser", md.getUserName());
+ operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion());
+ operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion());
+ } catch (SQLException e) {
+ logger.warn("Unable to prepare operational info due database exception: " + e.getMessage());
+ }
+ }
private Connection getConnection() {
try {
@@ -185,5 +206,10 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
throw new RuntimeException("Failed to connect to database", e);
}
}
+
+ @Override
+ public Map<String,String> getOperationalInfo() {
+ return operationalInfo;
+ }
}
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 1cf4a5f..288e403 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public interface JpaConnectionProviderFactory extends ProviderFactory<JpaConnectionProvider> {
+public interface JpaConnectionProviderFactory extends ServerInfoAwareProviderFactory<JpaConnectionProvider> {
+
}
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
index 103c7ce..5fc0f23 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
@@ -142,6 +142,7 @@
<dropColumn tableName="CLIENT_SESSION" columnName="ACTION"/>
<addColumn tableName="USER_ENTITY">
<column name="CREATED_TIMESTAMP" type="BIGINT"/>
+ <column name="SERVICE_ACCOUNT_CLIENT_LINK" type="VARCHAR(36)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
index 36be680..8da15b6 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -57,6 +60,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
private MongoStore mongoStore;
private DB db;
protected Config.Scope config;
+
+ private Map<String,String> operationalInfo;
@Override
public MongoConnectionProvider create(KeycloakSession session) {
@@ -159,7 +164,13 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
} else {
client = new MongoClient(new ServerAddress(host, port), clientOptions);
}
-
+
+ operationalInfo = new LinkedHashMap<>();
+ operationalInfo.put("mongoServerAddress", client.getAddress().toString());
+ operationalInfo.put("mongoDatabaseName", dbName);
+ operationalInfo.put("mongoUser", user);
+ operationalInfo.put("mongoDriverVersion", client.getVersion());
+
logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName);
return client;
}
@@ -206,5 +217,10 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
}
}
}
+
+ @Override
+ public Map<String,String> getOperationalInfo() {
+ return operationalInfo;
+ }
}
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 e787ce6..bce5fe4 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public interface MongoConnectionProviderFactory extends ProviderFactory<MongoConnectionProvider> {
+public interface MongoConnectionProviderFactory extends ServerInfoAwareProviderFactory<MongoConnectionProvider> {
}
diff --git a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java
index 928f62d..561a5d0 100644
--- a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java
+++ b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java
@@ -8,7 +8,6 @@ public interface ServiceAccountConstants {
String CLIENT_AUTH = "client_auth";
String SERVICE_ACCOUNT_USER_PREFIX = "service-account-";
- String SERVICE_ACCOUNT_CLIENT_ATTRIBUTE = "serviceAccountClient";
String CLIENT_ID_PROTOCOL_MAPPER = "Client ID";
String CLIENT_HOST_PROTOCOL_MAPPER = "Client Host";
diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
index 1d2bee3..ea20afc 100755
--- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
@@ -26,6 +26,7 @@ public class UserRepresentation {
protected String lastName;
protected String email;
protected String federationLink;
+ protected String serviceAccountClientId; // For rep, it points to clientId (not DB ID)
// Currently there is Map<String, List<String>> but for backwards compatibility, we also need to support Map<String, String>
protected Map<String, Object> attributes;
@@ -218,4 +219,12 @@ public class UserRepresentation {
public void setFederationLink(String federationLink) {
this.federationLink = federationLink;
}
+
+ public String getServiceAccountClientId() {
+ return serviceAccountClientId;
+ }
+
+ public void setServiceAccountClientId(String serviceAccountClientId) {
+ this.serviceAccountClientId = serviceAccountClientId;
+ }
}
diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml
index fe6ee0c..29a49a7 100755
--- a/docbook/reference/en/en-US/master.xml
+++ b/docbook/reference/en/en-US/master.xml
@@ -28,6 +28,7 @@
<!ENTITY Email SYSTEM "modules/email.xml">
<!ENTITY Roles SYSTEM "modules/roles.xml">
<!ENTITY DirectAccess SYSTEM "modules/direct-access.xml">
+ <!ENTITY ServiceAccounts SYSTEM "modules/service-accounts.xml">
<!ENTITY CORS SYSTEM "modules/cors.xml">
<!ENTITY Timeouts SYSTEM "modules/timeouts.xml">
<!ENTITY Events SYSTEM "modules/events.xml">
@@ -122,6 +123,7 @@ This one is short
&AccessTypes;
&Roles;
&DirectAccess;
+ &ServiceAccounts;
&CORS;
&Timeouts;
&AdminApi;
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 0000000..e641988
--- /dev/null
+++ b/docbook/reference/en/en-US/modules/service-accounts.xml
@@ -0,0 +1,56 @@
+<chapter id="service-accounts">
+ <title>Service Accounts</title>
+ <para>
+ Keycloak allows you to obtain an access token dedicated to some Client Application (not to any user).
+ See <ulink url="http://tools.ietf.org/html/rfc6749#section-4.4">Client Credentials Grant</ulink>
+ from OAuth 2.0 spec.
+ </para>
+ <para>
+ To use it you must have
+ registered a valid confidential Client and you need to check the switch <literal>Service Accounts Enabled</literal> in Keycloak
+ admin console for this client. In tab <literal>Service Account Roles</literal> you can configure the roles available to the service account retrieved on behalf of this client.
+ Don't forget that you need those roles to be available in Scopes of this client as well (unless you have <literal>Full Scope Allowed</literal> on).
+ As in normal login, roles from access token are intersection of scopes and the service account roles.
+ </para>
+
+ <para>
+ The REST URL to invoke on is <literal>/{keycloak-root}/realms/{realm-name}/protocol/openid-connect/token</literal>.
+ Invoking on this URL is a POST request and requires you to post the clientId and clientSecret of the client in <literal>Authorization: Basic</literal> header.
+ Later we want to add more mechanisms for authenticating clients. You also need to use parameter <literal>grant_type=client_credentials</literal> as per OAuth2 specification.
+ </para>
+ <para>
+ For example the POST invocation to retrieve service account can look like this:
+ <programlisting><![CDATA[
+ POST /auth/realms/demo/protocol/openid-connect/token
+ Authorization: Basic cHJvZHVjdC1zYS1jbGllbnQ6cGFzc3dvcmQ=
+ Content-Type: application/x-www-form-urlencoded
+
+ grant_type=client_credentials]]>
+ </programlisting>
+ The response would be this <ulink url="http://tools.ietf.org/html/rfc6749#section-4.4.3">standard JSON document</ulink> from the OAuth 2.0 specification.
+ <programlisting><![CDATA[
+HTTP/1.1 200 OK
+Content-Type: application/json;charset=UTF-8
+Cache-Control: no-store
+Pragma: no-cache
+
+{
+ "access_token":"2YotnFZFEjr1zCsicMWpAA",
+ "token_type":"bearer",
+ "expires_in":60,
+ "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
+ "refresh_expires_in":600,
+ "id_token":"tGzv3JOkF0XG5Qx2TlKWIA",
+ "not-before-policy":0,
+ "session-state":"234234-234234-234234"
+}]]>
+ </programlisting>
+ </para>
+ <para>
+ The retrieved access token can be refreshed or logged out by out-of-bound request.
+ </para>
+ <para>
+ See the example application <literal>service-account</literal>
+ from the main Keycloak <literal>demo</literal> example.
+ </para>
+</chapter>
\ 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 f9dc9f1..d03654d 100644
--- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java
+++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java
@@ -140,15 +140,7 @@ public class ProductServiceAccountServlet extends HttpServlet {
int status = response.getStatusLine().getStatusCode();
if (status != 200) {
String json = getContent(entity);
- String error = "Failed retrieve products.";
-
- if (status == 401) {
- error = error + " You need to login first with the service account.";
- } else if (status == 403) {
- error = error + " Maybe service account user doesn't have needed role? Assign role 'user' in Keycloak admin console to user '" +
- ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + getKeycloakDeployment().getResourceName() + "' and then logout and login again.";
- }
- error = error + " Status: " + status + ", Response: " + json;
+ String error = "Failed retrieve products. Status: " + status + ", Response: " + json;
req.setAttribute(ERROR, error);
} else if (entity == null) {
req.setAttribute(ERROR, "No entity");
diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json
index a26a058..d669e6b 100755
--- a/examples/demo-template/testrealm.json
+++ b/examples/demo-template/testrealm.json
@@ -71,6 +71,13 @@
"clientRoles": {
"realm-management": [ "realm-admin" ]
}
+ },
+ {
+ "username" : "service-account-product-sa-client",
+ "enabled": true,
+ "email" : "service-account-product-sa-client@placeholder.org",
+ "serviceAccountClientId": "product-sa-client",
+ "realmRoles": [ "user" ]
}
],
"roles" : {
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index a3396e3..601ddaf 100755
--- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -123,7 +123,7 @@ public class ExportUtils {
// Finally users if needed
if (includeUsers) {
- List<UserModel> allUsers = session.users().getUsers(realm);
+ List<UserModel> allUsers = session.users().getUsers(realm, true);
List<UserRepresentation> users = new ArrayList<UserRepresentation>();
for (UserModel user : allUsers) {
UserRepresentation userRep = exportUser(session, realm, user);
@@ -286,6 +286,15 @@ public class ExportUtils {
userRep.setClientConsents(consentReps);
}
+ // Service account
+ if (user.getServiceAccountClientLink() != null) {
+ String clientInternalId = user.getServiceAccountClientLink();
+ ClientModel client = realm.getClientById(clientInternalId);
+ if (client != null) {
+ userRep.setServiceAccountClientId(client.getClientId());
+ }
+ }
+
return userRep;
}
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java
index c72708b..0ecc10a 100755
--- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java
@@ -92,7 +92,7 @@ public abstract class MultipleStepsExportProvider implements ExportProvider {
@Override
protected void runExportImportTask(KeycloakSession session) throws IOException {
RealmModel realm = session.realms().getRealmByName(realmName);
- usersHolder.users = session.users().getUsers(realm, usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart);
+ usersHolder.users = session.users().getUsers(realm, usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart, true);
writeUsers(realmName + "-users-" + (usersHolder.currentPageStart / countPerPage) + ".json", session, realm, usersHolder.users);
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 5ddf659..5e01598 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
@@ -82,8 +82,8 @@ module.config([ '$routeProvider', function($routeProvider) {
realm : function(RealmLoader) {
return RealmLoader();
},
- serverInfo : function(ServerInfoLoader) {
- return ServerInfoLoader();
+ serverInfo : function(ServerInfo) {
+ return ServerInfo.delay;
}
},
controller : 'RealmLoginSettingsCtrl'
@@ -368,6 +368,9 @@ module.config([ '$routeProvider', function($routeProvider) {
},
clients : function(ClientListLoader) {
return ClientListLoader();
+ },
+ client : function() {
+ return {};
}
},
controller : 'UserRoleMappingCtrl'
@@ -762,17 +765,23 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'ClientInstallationCtrl'
})
- .when('/realms/:realm/clients/:client/service-accounts', {
- templateUrl : resourceUrl + '/partials/client-service-accounts.html',
+ .when('/realms/:realm/clients/:client/service-account-roles', {
+ templateUrl : resourceUrl + '/partials/client-service-account-roles.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
+ user : function(ClientServiceAccountUserLoader) {
+ return ClientServiceAccountUserLoader();
+ },
+ clients : function(ClientListLoader) {
+ return ClientListLoader();
+ },
client : function(ClientLoader) {
return ClientLoader();
}
},
- controller : 'ClientServiceAccountsCtrl'
+ controller : 'UserRoleMappingCtrl'
})
.when('/create/client/:realm', {
templateUrl : resourceUrl + '/partials/client-detail.html',
@@ -1124,7 +1133,22 @@ module.config([ '$routeProvider', function($routeProvider) {
controller : 'AuthenticationConfigCreateCtrl'
})
.when('/server-info', {
- templateUrl : resourceUrl + '/partials/server-info.html'
+ templateUrl : resourceUrl + '/partials/server-info.html',
+ resolve : {
+ serverInfo : function(ServerInfoLoader) {
+ return ServerInfoLoader();
+ }
+ },
+ controller : 'ServerInfoCtrl'
+ })
+ .when('/server-info/providers', {
+ templateUrl : resourceUrl + '/partials/server-info-providers.html',
+ resolve : {
+ serverInfo : function(ServerInfoLoader) {
+ return ServerInfoLoader();
+ }
+ },
+ controller : 'ServerInfoCtrl'
})
.when('/logout', {
templateUrl : resourceUrl + '/partials/home.html',
@@ -1858,4 +1882,4 @@ module.directive( 'kcOpen', function ( $location ) {
});
});
};
-});
\ 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 db21798..8ad8ae6 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
@@ -549,7 +549,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
"bearer-only"
];
- $scope.protocols = serverInfo.protocols;
+ $scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers);
$scope.signatureAlgorithms = [
"RSA_SHA1",
@@ -1323,25 +1323,5 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv
});
-module.controller('ClientServiceAccountsCtrl', function($scope, $http, realm, client, Notifications, Client) {
- $scope.realm = realm;
- $scope.client = angular.copy(client);
-
- $scope.serviceAccountsEnabledChanged = function() {
- if (client.serviceAccountsEnabled != $scope.client.serviceAccountsEnabled) {
- Client.update({
- realm : realm.realm,
- client : client.id
- }, $scope.client, function() {
- $scope.changed = false;
- client = angular.copy($scope.client);
- Notifications.success("Service Account settings updated.");
- });
- }
- }
-
-});
-
-
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 85ad18b..15a8fa9 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
@@ -7,9 +7,6 @@ module.controller('GlobalCtrl', function($scope, $http, Auth, WhoAmI, Current, $
$scope.resourceUrl = resourceUrl;
$scope.auth = Auth;
$scope.serverInfo = ServerInfo.get();
- $scope.serverInfoUpdate = function() {
- $scope.serverInfo = ServerInfo.get();
- };
function hasAnyAccess() {
var realmAccess = Auth.user && Auth.user['realm_access'];
@@ -125,6 +122,25 @@ module.controller('RealmTabCtrl', function(Dialog, $scope, Current, Realm, Notif
};
});
+module.controller('ServerInfoCtrl', function($scope, ServerInfo) {
+ ServerInfo.reload();
+
+ $scope.serverInfo = ServerInfo.get();
+
+ $scope.$watch($scope.serverInfo, function() {
+ $scope.providers = [];
+ for(var spi in $scope.serverInfo.providers) {
+ var p = angular.copy($scope.serverInfo.providers[spi]);
+ p.name = spi;
+ $scope.providers.push(p)
+ }
+ });
+
+ $scope.serverInfoReload = function() {
+ ServerInfo.reload();
+ }
+});
+
module.controller('RealmListCtrl', function($scope, Realm, Current) {
$scope.realms = Realm.query();
Current.realms = $scope.realms;
@@ -1217,7 +1233,7 @@ module.controller('RealmEventsConfigCtrl', function($scope, eventsConfig, RealmE
}
});
- $scope.eventListeners = serverInfo.eventListeners;
+ $scope.eventListeners = Object.keys(serverInfo.providers.eventsListener.providers);
$scope.eventSelectOptions = {
'multiple': true,
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 3bdd551..175a3b3 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -1,4 +1,4 @@
-module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, clients, Notifications, RealmRoleMapping,
+module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, clients, client, Notifications, RealmRoleMapping,
ClientRoleMapping, AvailableRealmRoleMapping, AvailableClientRoleMapping,
CompositeRealmRoleMapping, CompositeClientRoleMapping) {
$scope.realm = realm;
@@ -7,6 +7,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
$scope.selectedRealmMappings = [];
$scope.realmMappings = [];
$scope.clients = clients;
+ $scope.client = client;
$scope.clientRoles = [];
$scope.clientComposite = [];
$scope.selectedClientRoles = [];
@@ -28,11 +29,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
$scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id});
$scope.selectedRealmMappings = [];
$scope.selectRealmRoles = [];
- if ($scope.client) {
+ if ($scope.targetClient) {
console.log('load available');
- $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
+ $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
}
@@ -49,11 +50,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
$scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id});
$scope.selectedRealmMappings = [];
$scope.selectRealmRoles = [];
- if ($scope.client) {
+ if ($scope.targetClient) {
console.log('load available');
- $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
+ $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
}
@@ -62,11 +63,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
};
$scope.addClientRole = function() {
- $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.client.id,
+ $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id,
$scope.selectedClientRoles).success(function() {
- $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
+ $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
$scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id});
@@ -76,11 +77,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
};
$scope.deleteClientRole = function() {
- $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.client.id,
+ $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id,
{data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() {
- $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
+ $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
$scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id});
@@ -92,11 +93,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl
$scope.changeClient = function() {
console.log('changeClient');
- if ($scope.client) {
+ if ($scope.targetClient) {
console.log('load available');
- $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
- $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id});
+ $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
+ $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id});
} else {
$scope.clientRoles = null;
$scope.clientMappings = null;
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index 773f6f0..c347759 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
@@ -35,8 +35,10 @@ module.factory('RealmListLoader', function(Loader, Realm, $q) {
return Loader.get(Realm);
});
-module.factory('ServerInfoLoader', function(Loader, ServerInfo, $q) {
- return Loader.get(ServerInfo);
+module.factory('ServerInfoLoader', function(Loader, ServerInfo) {
+ return function() {
+ return ServerInfo.promise;
+ };
});
module.factory('RealmLoader', function(Loader, Realm, $route, $q) {
@@ -282,6 +284,15 @@ module.factory('ClientListLoader', function(Loader, Client, $route, $q) {
});
});
+module.factory('ClientServiceAccountUserLoader', function(Loader, ClientServiceAccountUser, $route, $q) {
+ return Loader.get(ClientServiceAccountUser, function() {
+ return {
+ realm : $route.current.params.realm,
+ client : $route.current.params.client
+ }
+ });
+});
+
module.factory('RoleMappingLoader', function(Loader, RoleMapping, $route, $q) {
var realm = $route.current.params.realm || $route.current.params.client;
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 1d6a6be..04929ef 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
@@ -215,10 +215,27 @@ module.factory('RealmLDAPConnectionTester', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection');
});
-module.factory('ServerInfo', function($resource) {
- return $resource(authUrl + '/admin/serverinfo');
-});
+module.service('ServerInfo', function($resource, $q, $http) {
+ var info = {};
+ var delay = $q.defer();
+
+ $http.get(authUrl + '/admin/serverinfo').success(function(data) {
+ info = data;
+ delay.resolve(info);
+ });
+ return {
+ get: function() {
+ return info;
+ },
+ reload: function() {
+ $http.get(authUrl + '/admin/serverinfo').success(function(data) {
+ angular.copy(data, info);
+ });
+ },
+ promise: delay.promise
+ }
+});
module.factory('ClientProtocolMapper', function($resource) {
@@ -897,6 +914,13 @@ module.factory('ClientOrigins', function($resource) {
});
});
+module.factory('ClientServiceAccountUser', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/clients/:client/service-account-user', {
+ realm : '@realm',
+ client : '@client'
+ });
+});
+
module.factory('Current', function(Realm, $route, $rootScope) {
var current = {
realms: {},
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 49a1997..727c66d 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -72,6 +72,13 @@
</div>
<kc-tooltip>'Confidential' clients require a secret to initiate login protocol. 'Public' clients do not require a secret. 'Bearer-only' clients are web services that never initiate a login.</kc-tooltip>
</div>
+ <div class="form-group" data-ng-show="protocol == 'openid-connect' && !client.publicClient && !client.bearerOnly">
+ <label class="col-md-2 control-label" for="serviceAccountsEnabled">Service Accounts Enabled</label>
+ <kc-tooltip>Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client.</kc-tooltip>
+ <div class="col-md-6">
+ <input ng-model="client.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch />
+ </div>
+ </div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlServerSignature">Include AuthnStatement</label>
<div class="col-sm-6">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html
new file mode 100644
index 0000000..03a38d9
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html
@@ -0,0 +1,113 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
+ <li>{{client.clientId}}</li>
+ </ol>
+
+ <h1>{{client.clientId|capitalize}}</h1>
+
+ <kc-tabs-client></kc-tabs-client>
+
+ <h2><span>{{client.clientId}}</span> Service Accounts </h2>
+ <p class="subtitle"></p>
+
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-show="client.serviceAccountsEnabled">
+ <div class="form-group">
+ <label class="col-md-2 control-label" class="control-label">Realm Roles</label>
+ <div class="col-md-10">
+ <div class="row">
+ <div class="col-md-3">
+ <label class="control-label" for="available">Available Roles</label>
+ <kc-tooltip>Realm level roles that can be assigned to service account.</kc-tooltip>
+
+ <select id="available" class="form-control" multiple size="5"
+ ng-multiple="true"
+ ng-model="selectedRealmRoles"
+ ng-options="r.name for r in realmRoles">
+ </select>
+ <button ng-disabled="selectedRealmRoles.length == 0" class="btn btn-default" type="submit" ng-click="addRealmRole()">
+ Add selected <i class="fa fa-angle-double-right"></i>
+ </button>
+ </div>
+ <div class="col-md-3">
+ <label class="control-label" for="assigned">Assigned Roles</label>
+ <kc-tooltip>Realm level roles assigned to service account.</kc-tooltip>
+ <select id="assigned" class="form-control" multiple size=5
+ ng-multiple="true"
+ ng-model="selectedRealmMappings"
+ ng-options="r.name for r in realmMappings">
+ </select>
+ <button ng-disabled="selectedRealmMappings.length == 0" class="btn btn-default" type="submit" ng-click="deleteRealmRole()">
+ <i class="fa fa-angle-double-left"></i> Remove selected
+ </button>
+ </div>
+ <div class="col-md-3">
+ <label class="control-label" for="realm-composite">Effective Roles </label>
+ <kc-tooltip>Assigned realm level roles that may have been inherited from a composite role.</kc-tooltip>
+ <select id="realm-composite" class="form-control" multiple size=5
+ disabled="true"
+ ng-model="dummymodel"
+ ng-options="r.name for r in realmComposite">
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label class="col-md-2 control-label" class="control-label">
+ <span>Client Roles</span>
+ <select class="form-control" id="clients" name="clients" ng-change="changeClient()" ng-model="targetClient" ng-options="a.clientId for a in clients" ng-disabled="false"></select>
+ </label>
+
+ <div class="col-md-10">
+ <div class="row" data-ng-hide="targetClient">
+ <div class="col-md-4"><span class="text-muted">Select client to view roles for client</span></div>
+ </div>
+ <div class="row" data-ng-show="targetClient">
+ <div class="col-md-3">
+ <label class="control-label" for="client-available">Available Roles</label>
+ <kc-tooltip>Client roles available to be assigned.</kc-tooltip>
+ <select id="client-available" class="form-control" multiple size="5"
+ ng-multiple="true"
+ ng-model="selectedClientRoles"
+ ng-options="r.name for r in clientRoles">
+ </select>
+ <button ng-disabled="selectedClientRoles.length == 0" class="btn btn-default" type="submit" ng-click="addClientRole()">
+ Add selected <i class="fa fa-angle-double-right"></i>
+ </button>
+ </div>
+ <div class="col-md-3">
+ <label class="control-label" for="client-assigned">Assigned Roles</label>
+ <kc-tooltip>Assigned client roles.</kc-tooltip>
+ <select id="client-assigned" class="form-control" multiple size=5
+ ng-multiple="true"
+ ng-model="selectedClientMappings"
+ ng-options="r.name for r in clientMappings">
+ </select>
+ <button ng-disabled="selectedClientMappings.length == 0" class="btn btn-default" type="submit" ng-click="deleteClientRole()">
+ <i class="fa fa-angle-double-left"></i> Remove selected
+ </button>
+ </div>
+ <div class="col-md-3">
+ <label class="control-label" for="client-composite">Effective Roles</label>
+ <kc-tooltip>Assigned client roles that may have been inherited from a composite role.</kc-tooltip>
+ <select id="client-composite" class="form-control" multiple size=5
+ disabled="true"
+ ng-model="dummymodel"
+ ng-options="r.name for r in clientComposite">
+ </select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+
+ <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-show="!client.serviceAccountsEnabled">
+ <legend><span class="text">Service account is not enabled for {{client.clientId}}.</span></legend>
+ </form>
+
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
index 8223936..3228be5 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
@@ -52,13 +52,13 @@
<div class="form-group">
<label class="col-md-2 control-label" class="control-label">
<span>Client Roles</span>
- <select class="form-control" id="clients" name="clients" ng-change="changeClient()" ng-model="client" ng-options="a.clientId for a in clients" ng-disabled="false"></select>
+ <select class="form-control" id="clients" name="clients" ng-change="changeClient()" ng-model="targetClient" ng-options="a.clientId for a in clients" ng-disabled="false"></select>
</label>
<div class="col-md-10" kc-read-only="!access.manageUsers">
- <div class="row" data-ng-hide="client">
+ <div class="row" data-ng-hide="targetClient">
<div class="col-md-4"><span class="text-muted">Select client to view roles for client</span></div>
</div>
- <div class="row" data-ng-show="client">
+ <div class="row" data-ng-show="targetClient">
<div class="col-md-3">
<label class="control-label" for="available-client">Available Roles</label>
<kc-tooltip>Assignable roles from this client.</kc-tooltip>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html
index 34c9a84..a92c774 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,66 +1,104 @@
-<div class="col-md-12">
- <h1>Server Info</h1>
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <h1>
+ Server Info
+ <i id="serverInfoReload" class="pficon pficon-restart clickable" data-ng-click="serverInfoReload()"></i>
+ </h1>
+
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#/server-info">Info</a></li>
+ <li><a href="#/server-info/providers">Providers</a></li>
+ </ul>
<table class="table table-striped table-bordered">
<tr>
- <td>Version</td>
- <td>{{serverInfo.version}}</td>
+ <td width="20%">Keycloak Version</td>
+ <td>{{serverInfo.systemInfo.version}}</td>
</tr>
<tr>
<td>Server Time</td>
- <td>{{serverInfo.serverTime}} (<a data-ng-click="serverInfoUpdate()">update</a>)</td>
+ <td>{{serverInfo.systemInfo.serverTime}}</td>
+ </tr>
+ <tr>
+ <td>Server Uptime</td>
+ <td>{{serverInfo.systemInfo.uptime}}</td>
</tr>
</table>
<fieldset>
- <legend collapsed>Providers</legend>
-
- <div class="form-group">
- <h3>Public SPIs</h3>
- <kc-tooltip>For public SPIs there are built-in providers, but it's also supported to write your own custom providers.</kc-tooltip>
-
- <table class="table table-striped table-bordered">
- <thead>
- <tr>
- <th>SPI</th>
- <th>Providers</th>
- </tr>
- </thead>
- <tbody>
- <tr data-ng-repeat="spi in (serverInfo.providers | filter:{internal:false} | orderBy:'name')">
- <td>{{spi.name}}</td>
- <td>
- <div data-ng-repeat="provider in (spi.implementations | orderBy:'toString()')">
- {{provider}}
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
-
- <div class="form-group">
- <h3>Internal SPIs</h3>
- <kc-tooltip>For internal SPIs there are only built-in providers. It's not recommended to write your own custom providers as internal SPIs may change or be removed without notice.</kc-tooltip>
+ <legend>Memory</legend>
+ <table class="table table-striped table-bordered" style="margin-top: 0;">
+ <tr>
+ <td width="20%">Total Memory</td>
+ <td>{{serverInfo.memoryInfo.totalFormated}}</td>
+ </tr>
+ <tr>
+ <td>Free Memory</td>
+ <td>{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)</td>
+ </tr>
+ <tr>
+ <td>Used Memory</td>
+ <td>{{serverInfo.memoryInfo.usedFormated}}</td>
+ </tr>
+ </table>
+ </fieldset>
- <table class="table table-striped table-bordered">
- <thead>
- <tr>
- <th>SPI</th>
- <th>Providers</th>
- </tr>
- </thead>
- <tbody>
- <tr data-ng-repeat="spi in (serverInfo.providers | filter:{internal:true} | orderBy:'name')">
- <td>{{spi.name}}</td>
- <td>
- <div data-ng-repeat="provider in (spi.implementations | orderBy:'toString()')">
- {{provider}}
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
+ <fieldset>
+ <legend>System</legend>
+ <table class="table table-striped table-bordered" style="margin-top: 0;">
+ <tr>
+ <td width="20%">Current Working Directory</td>
+ <td>{{serverInfo.systemInfo.userDir}}</td>
+ </tr>
+ <tr>
+ <td>Java Version</td>
+ <td>{{serverInfo.systemInfo.javaVersion}}</td>
+ </tr>
+ <tr>
+ <td>Java Vendor</td>
+ <td>{{serverInfo.systemInfo.javaVendor}}</td>
+ </tr>
+ <tr>
+ <td>Java Runtime</td>
+ <td>{{serverInfo.systemInfo.javaRuntime}}</td>
+ </tr>
+ <tr>
+ <td>Java VM</td>
+ <td>{{serverInfo.systemInfo.javaVm}}</td>
+ </tr>
+ <tr>
+ <td>Java VM Version</td>
+ <td>{{serverInfo.systemInfo.javaVmVersion}}</td>
+ </tr>
+ <tr>
+ <td>Java Home</td>
+ <td>{{serverInfo.systemInfo.javaHome}}</td>
+ </tr>
+ <tr>
+ <td>User Name</td>
+ <td>{{serverInfo.systemInfo.userName}}</td>
+ </tr>
+ <tr>
+ <td>User Timezone</td>
+ <td>{{serverInfo.systemInfo.userTimezone}}</td>
+ </tr>
+ <tr>
+ <td>User Locale</td>
+ <td>{{serverInfo.systemInfo.userLocale}}</td>
+ </tr>
+ <tr>
+ <td>System Encoding</td>
+ <td>{{serverInfo.systemInfo.fileEncoding}}</td>
+ </tr>
+ <tr>
+ <td>Operating System</td>
+ <td>{{serverInfo.systemInfo.osName}} {{serverInfo.systemInfo.osVersion}}</td>
+ </tr>
+ <tr>
+ <td>OS Architecture</td>
+ <td>{{serverInfo.systemInfo.osArchitecture}}</td>
+ </tr>
+ </table>
</fieldset>
-</div>
\ No newline at end of file
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info-providers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info-providers.html
new file mode 100755
index 0000000..6f8bbb4
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info-providers.html
@@ -0,0 +1,55 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <h1>
+ Server Info
+ <i id="serverInfoReload" class="pficon pficon-restart clickable" data-ng-click="serverInfoReload()"></i>
+ </h1>
+
+ <ul class="nav nav-tabs">
+ <li><a href="#/server-info">Info</a></li>
+ <li class="active"><a href="#/server-info/providers">Providers</a></li>
+ </ul>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+ <div class="form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="Search..." data-ng-model="search" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ </div>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr>
+ <th width="20%">SPI</th>
+ <th>Providers</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr data-ng-repeat="spi in (providers | orderBy:'name' | filter:search)">
+ <td>{{spi.name}}</td>
+ <td>
+ <div data-ng-repeat="(providerName, provider) in spi.providers">
+ {{providerName}}
+ <span ng-show="provider.operationalInfo">
+ <button type="button" class="btn btn-default btn-xs" ng-click="collapseRep = !collapseRep">
+ <span class="glyphicon glyphicon-plus" data-ng-show="!collapseRep"></span>
+ <span class="glyphicon glyphicon-minus" data-ng-show="collapseRep"></span>
+ </button>
+ <table ng-show="collapseRep" class="table table-striped table-bordered" style="margin-top: 0px;">
+ <tr ng-repeat="(key, value) in provider.operationalInfo">
+ <td width="20%">{{key}}</td>
+ <td>{{value}}</td>
+ </tr>
+ </table>
+ </span>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index c82d99f..2aad2d5 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -33,9 +33,9 @@
<kc-tooltip>Helper utility for generating various client adapter configuration formats which you can download or cut and paste to configure your clients.</kc-tooltip>
</li>
- <li ng-class="{active: path[4] == 'service-accounts'}" data-ng-show="!client.publicClient && !client.bearerOnly">
- <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/service-accounts">Service Accounts</a>
- <kc-tooltip>Allows you to authenticate this client to Keycloak and retrieve access tokens dedicated to this client.</kc-tooltip>
+ <li ng-class="{active: path[4] == 'service-account-roles'}" data-ng-show="client.serviceAccountsEnabled">
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/service-account-roles">Service Account Roles</a>
+ <kc-tooltip>Allows you to authenticate role mappings for the service account dedicated to this client.</kc-tooltip>
</li>
</ul>
</div>
\ No newline at end of file
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java
index e2ef2f6..5a8a6e0 100755
--- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java
@@ -32,7 +32,7 @@ public class MigrateTo1_4_0 {
}
public void migrateUsers(KeycloakSession session, RealmModel realm) {
- List<UserModel> users = session.userStorage().getUsers(realm);
+ List<UserModel> users = session.userStorage().getUsers(realm, false);
for (UserModel user : users) {
String email = user.getEmail();
email = KeycloakModelUtils.toLowerCaseSafe(email);
diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
index eeae34f..8c82a8e 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
@@ -27,6 +27,7 @@ public class UserEntity extends AbstractIdentifiableEntity {
private List<CredentialEntity> credentials = new ArrayList<CredentialEntity>();
private List<FederatedIdentityEntity> federatedIdentities;
private String federationLink;
+ private String serviceAccountClientLink;
public String getUsername() {
return username;
@@ -148,5 +149,13 @@ public class UserEntity extends AbstractIdentifiableEntity {
public void setFederationLink(String federationLink) {
this.federationLink = federationLink;
}
+
+ public String getServiceAccountClientLink() {
+ return serviceAccountClientLink;
+ }
+
+ public void setServiceAccountClientLink(String serviceAccountClientLink) {
+ this.serviceAccountClientLink = serviceAccountClientLink;
+ }
}
diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java b/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java
index cbb3da2..8266550 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 {
<T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String id);
List<ProviderFactory> getProviderFactories(Class<? extends Provider> 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 23aaf1b..ee18d79 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -204,8 +204,17 @@ public class UserFederationManager implements UserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm) {
- return getUsers(realm, 0, Integer.MAX_VALUE - 1);
+ public UserModel getUserByServiceAccountClient(ClientModel client) {
+ UserModel user = session.userStorage().getUserByServiceAccountClient(client);
+ if (user != null) {
+ user = validateAndProxyUser(client.getRealm(), user);
+ }
+ return user;
+ }
+
+ @Override
+ public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+ return getUsers(realm, 0, Integer.MAX_VALUE - 1, includeServiceAccounts);
}
@@ -242,11 +251,11 @@ public class UserFederationManager implements UserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
+ public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) {
return query(new PaginatedQuery() {
@Override
public List<UserModel> query(RealmModel realm, int first, int max) {
- return session.userStorage().getUsers(realm, first, max);
+ return session.userStorage().getUsers(realm, first, max, includeServiceAccounts);
}
}, realm, firstResult, maxResults);
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index 19fdad2..94c2ffc 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -104,6 +104,9 @@ public interface UserModel {
String getFederationLink();
void setFederationLink(String link);
+ String getServiceAccountClientLink();
+ void setServiceAccountClientLink(String clientInternalId);
+
void addConsent(UserConsentModel consent);
UserConsentModel getConsentByClient(String clientInternalId);
List<UserConsentModel> getConsents();
diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java
index f48062f..1690b7a 100755
--- a/model/api/src/main/java/org/keycloak/models/UserProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java
@@ -25,9 +25,12 @@ public interface UserProvider extends Provider {
UserModel getUserByUsername(String username, RealmModel realm);
UserModel getUserByEmail(String email, RealmModel realm);
UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm);
- List<UserModel> getUsers(RealmModel realm);
+ UserModel getUserByServiceAccountClient(ClientModel client);
+ List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts);
+
+ // Service account is included for counts
int getUsersCount(RealmModel realm);
- List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults);
+ List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts);
List<UserModel> searchForUser(String search, RealmModel realm);
List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults);
List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index f5261a0..cfe0852 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -2,6 +2,7 @@ package org.keycloak.models.utils;
import org.bouncycastle.openssl.PEMWriter;
import org.keycloak.constants.KerberosConstants;
+import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -350,6 +351,8 @@ public final class KeycloakModelUtils {
return mapperModel;
}
+ // END USER FEDERATION RELATED STUFF
+
public static String toLowerCaseSafe(String str) {
return str==null ? null : str.toLowerCase();
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index a38b305..83c8273 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -902,6 +902,14 @@ public class RepresentationToModel {
user.addConsent(consentModel);
}
}
+ if (userRep.getServiceAccountClientId() != null) {
+ String clientId = userRep.getServiceAccountClientId();
+ ClientModel client = clientMap.get(clientId);
+ if (client == null) {
+ throw new RuntimeException("Unable to find client specified for service account link. Client: " + clientId);
+ }
+ user.setServiceAccountClientLink(client.getId());;
+ }
return user;
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
index 9599ab9..3c1edb3 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
@@ -208,6 +208,16 @@ public class UserModelDelegate implements UserModel {
}
@Override
+ public String getServiceAccountClientLink() {
+ return delegate.getServiceAccountClientLink();
+ }
+
+ @Override
+ public void setServiceAccountClientLink(String clientInternalId) {
+ delegate.setServiceAccountClientLink(clientInternalId);
+ }
+
+ @Override
public void addConsent(UserConsentModel consent) {
delegate.addConsent(consent);
}
diff --git a/model/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 0000000..97d17b1
--- /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<T extends Provider> extends ProviderFactory<T> {
+
+ /**
+ * Get operational info about given provider. This info contains informations about providers configuration and operational conditions (eg. errors in connection to remote systems etc) which is
+ * shown on "Server Info" page.
+ *
+ * @return Map with keys describing value and relevant values itself
+ */
+ public Map<String, String> getOperationalInfo();
+
+}
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index 5db8f93..98e4254 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
@@ -478,6 +478,16 @@ public class UserAdapter implements UserModel, Comparable {
}
@Override
+ public String getServiceAccountClientLink() {
+ return user.getServiceAccountClientLink();
+ }
+
+ @Override
+ public void setServiceAccountClientLink(String clientInternalId) {
+ user.setServiceAccountClientLink(clientInternalId);
+ }
+
+ @Override
public void addConsent(UserConsentModel consent) {
// TODO
}
diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
index ff152f8..0bcc37a 100755
--- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
+++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
@@ -107,8 +107,18 @@ public class FileUserProvider implements UserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm) {
- return getUsers(realm, -1, -1);
+ public UserModel getUserByServiceAccountClient(ClientModel client) {
+ for (UserModel user : inMemoryModel.getUsers(client.getRealm().getId())) {
+ if (client.getId().equals(user.getServiceAccountClientLink())) {
+ return user;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+ return getUsers(realm, -1, -1, includeServiceAccounts);
}
@Override
@@ -117,12 +127,27 @@ public class FileUserProvider implements UserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
- List users = new ArrayList(inMemoryModel.getUsers(realm.getId()));
+ public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
+ List<UserModel> users = new ArrayList<>(inMemoryModel.getUsers(realm.getId()));
+
+ if (!includeServiceAccounts) {
+ users = filterServiceAccountUsers(users);
+ }
+
List<UserModel> sortedList = sortedSubList(users, firstResult, maxResults);
return sortedList;
}
+ private List<UserModel> filterServiceAccountUsers(List<UserModel> users) {
+ List<UserModel> result = new ArrayList<>();
+ for (UserModel user : users) {
+ if (user.getServiceAccountClientLink() == null) {
+ result.add(user);
+ }
+ }
+ return result;
+ }
+
protected List<UserModel> sortedSubList(List list, int firstResult, int maxResults) {
if (list.isEmpty()) return list;
@@ -183,6 +208,9 @@ public class FileUserProvider implements UserProvider {
}
}
+ // Remove users with service account link
+ found = filterServiceAccountUsers(found);
+
return sortedSubList(found, firstResult, maxResults);
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
index 4e99e44..aed1394 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
@@ -207,8 +207,13 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm) {
- return getDelegate().getUsers(realm);
+ public UserModel getUserByServiceAccountClient(ClientModel client) {
+ return getDelegate().getUserByServiceAccountClient(client);
+ }
+
+ @Override
+ public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+ return getDelegate().getUsers(realm, includeServiceAccounts);
}
@Override
@@ -217,8 +222,8 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
- return getDelegate().getUsers(realm, firstResult, maxResults);
+ public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
+ return getDelegate().getUsers(realm, firstResult, maxResults, includeServiceAccounts);
}
@Override
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
index 3d1395b..853677b 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
@@ -31,6 +31,7 @@ public class CachedUser implements Serializable {
private boolean enabled;
private boolean totp;
private String federationLink;
+ private String serviceAccountClientLink;
private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
private Set<String> requiredActions = new HashSet<>();
private Set<String> roleMappings = new HashSet<String>();
@@ -49,6 +50,7 @@ public class CachedUser implements Serializable {
this.enabled = user.isEnabled();
this.totp = user.isTotp();
this.federationLink = user.getFederationLink();
+ this.serviceAccountClientLink = user.getServiceAccountClientLink();
this.requiredActions.addAll(user.getRequiredActions());
for (RoleModel role : user.getRoleMappings()) {
roleMappings.add(role.getId());
@@ -114,4 +116,8 @@ public class CachedUser implements Serializable {
public String getFederationLink() {
return federationLink;
}
+
+ public String getServiceAccountClientLink() {
+ return serviceAccountClientLink;
+ }
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
index 3abe72f..8ed8b6a 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java
@@ -74,8 +74,13 @@ public class NoCacheUserProvider implements CacheUserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm) {
- return getDelegate().getUsers(realm);
+ public UserModel getUserByServiceAccountClient(ClientModel client) {
+ return getDelegate().getUserByServiceAccountClient(client);
+ }
+
+ @Override
+ public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+ return getDelegate().getUsers(realm, includeServiceAccounts);
}
@Override
@@ -84,8 +89,8 @@ public class NoCacheUserProvider implements CacheUserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
- return getDelegate().getUsers(realm, firstResult, maxResults);
+ public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
+ return getDelegate().getUsers(realm, firstResult, maxResults, includeServiceAccounts);
}
@Override
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
index b075ea1..f2b5e33 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
@@ -244,6 +244,18 @@ public class UserAdapter implements UserModel {
}
@Override
+ public String getServiceAccountClientLink() {
+ if (updated != null) return updated.getServiceAccountClientLink();
+ return cached.getServiceAccountClientLink();
+ }
+
+ @Override
+ public void setServiceAccountClientLink(String clientInternalId) {
+ getDelegateForUpdate();
+ updated.setServiceAccountClientLink(clientInternalId);
+ }
+
+ @Override
public Set<RoleModel> getRealmRoleMappings() {
if (updated != null) return updated.getRealmRoleMappings();
Set<RoleModel> roleMappings = getRoleMappings();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
index 5e0769a..2da1641 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
@@ -21,12 +21,15 @@ import java.util.Collection;
*/
@NamedQueries({
@NamedQuery(name="getAllUsersByRealm", query="select u from UserEntity u where u.realmId = :realmId order by u.username"),
- @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and ( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"),
+ @NamedQuery(name="getAllUsersByRealmExcludeServiceAccount", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) order by u.username"),
+ @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) and " +
+ "( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"),
@NamedQuery(name="getRealmUserById", query="select u from UserEntity u where u.id = :id and u.realmId = :realmId"),
@NamedQuery(name="getRealmUserByUsername", query="select u from UserEntity u where u.username = :username and u.realmId = :realmId"),
@NamedQuery(name="getRealmUserByEmail", query="select u from UserEntity u where u.email = :email and u.realmId = :realmId"),
@NamedQuery(name="getRealmUserByLastName", query="select u from UserEntity u where u.lastName = :lastName and u.realmId = :realmId"),
@NamedQuery(name="getRealmUserByFirstLastName", query="select u from UserEntity u where u.firstName = :first and u.lastName = :last and u.realmId = :realmId"),
+ @NamedQuery(name="getRealmUserByServiceAccount", query="select u from UserEntity u where u.serviceAccountClientLink = :clientInternalId and u.realmId = :realmId"),
@NamedQuery(name="getRealmUserCount", query="select count(u) from UserEntity u where u.realmId = :realmId"),
@NamedQuery(name="deleteUsersByRealm", query="delete from UserEntity u where u.realmId = :realmId"),
@NamedQuery(name="deleteUsersByRealmAndLink", query="delete from UserEntity u where u.realmId = :realmId and u.federationLink=:link")
@@ -77,6 +80,9 @@ public class UserEntity {
@Column(name="federation_link")
protected String federationLink;
+ @Column(name="SERVICE_ACCOUNT_CLIENT_LINK")
+ protected String serviceAccountClientLink;
+
public String getId() {
return id;
}
@@ -198,6 +204,14 @@ public class UserEntity {
this.federationLink = federationLink;
}
+ public String getServiceAccountClientLink() {
+ return serviceAccountClientLink;
+ }
+
+ public void setServiceAccountClientLink(String serviceAccountClientLink) {
+ this.serviceAccountClientLink = serviceAccountClientLink;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index ae04a5f..4f02f6c 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -272,13 +272,29 @@ public class JpaUserProvider implements UserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm) {
- return getUsers(realm, -1, -1);
+ public UserModel getUserByServiceAccountClient(ClientModel client) {
+ TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserByServiceAccount", UserEntity.class);
+ query.setParameter("realmId", client.getRealm().getId());
+ query.setParameter("clientInternalId", client.getId());
+ List<UserEntity> results = query.getResultList();
+ if (results.isEmpty()) {
+ return null;
+ } else if (results.size() > 1) {
+ throw new IllegalStateException("More service account linked users found for client=" + client.getClientId() +
+ ", results=" + results);
+ } else {
+ UserEntity user = results.get(0);
+ return new UserAdapter(client.getRealm(), em, user);
+ }
+ }
+
+ @Override
+ public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+ return getUsers(realm, -1, -1, includeServiceAccounts);
}
@Override
public int getUsersCount(RealmModel realm) {
- // TODO: named query?
Object count = em.createNamedQuery("getRealmUserCount")
.setParameter("realmId", realm.getId())
.getSingleResult();
@@ -286,8 +302,10 @@ public class JpaUserProvider implements UserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
- TypedQuery<UserEntity> query = em.createNamedQuery("getAllUsersByRealm", UserEntity.class);
+ public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
+ String queryName = includeServiceAccounts ? "getAllUsersByRealm" : "getAllUsersByRealmExcludeServiceAccount" ;
+
+ TypedQuery<UserEntity> query = em.createNamedQuery(queryName, UserEntity.class);
query.setParameter("realmId", realm.getId());
if (firstResult != -1) {
query.setFirstResult(firstResult);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index ca0c284..e603777 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -544,6 +544,16 @@ public class UserAdapter implements UserModel {
}
@Override
+ public String getServiceAccountClientLink() {
+ return user.getServiceAccountClientLink();
+ }
+
+ @Override
+ public void setServiceAccountClientLink(String clientInternalId) {
+ user.setServiceAccountClientLink(clientInternalId);
+ }
+
+ @Override
public void addConsent(UserConsentModel consent) {
String clientId = consent.getClient().getId();
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index cc720c5..a433fea 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -105,6 +105,16 @@ public class MongoUserProvider implements UserProvider {
return userEntity == null ? null : new UserAdapter(session, realm, userEntity, invocationContext);
}
+ @Override
+ public UserModel getUserByServiceAccountClient(ClientModel client) {
+ DBObject query = new QueryBuilder()
+ .and("serviceAccountClientLink").is(client.getId())
+ .and("realmId").is(client.getRealm().getId())
+ .get();
+ MongoUserEntity userEntity = getMongoStore().loadSingleEntity(MongoUserEntity.class, query, invocationContext);
+ return userEntity == null ? null : new UserAdapter(session, client.getRealm(), userEntity, invocationContext);
+ }
+
protected List<UserModel> convertUserEntities(RealmModel realm, List<MongoUserEntity> userEntities) {
List<UserModel> userModels = new ArrayList<UserModel>();
for (MongoUserEntity user : userEntities) {
@@ -115,8 +125,8 @@ public class MongoUserProvider implements UserProvider {
@Override
- public List<UserModel> getUsers(RealmModel realm) {
- return getUsers(realm, -1, -1);
+ public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
+ return getUsers(realm, -1, -1, includeServiceAccounts);
}
@Override
@@ -128,10 +138,15 @@ public class MongoUserProvider implements UserProvider {
}
@Override
- public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
- DBObject query = new QueryBuilder()
- .and("realmId").is(realm.getId())
- .get();
+ public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
+ QueryBuilder queryBuilder = new QueryBuilder()
+ .and("realmId").is(realm.getId());
+
+ if (!includeServiceAccounts) {
+ queryBuilder = queryBuilder.and("serviceAccountClientLink").is(null);
+ }
+
+ DBObject query = queryBuilder.get();
DBObject sort = new BasicDBObject("username", 1);
List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, query, sort, firstResult, maxResults, invocationContext);
return convertUserEntities(realm, users);
@@ -170,6 +185,7 @@ public class MongoUserProvider implements UserProvider {
QueryBuilder builder = new QueryBuilder().and(
new QueryBuilder().and("realmId").is(realm.getId()).get(),
+ new QueryBuilder().and("serviceAccountClientLink").is(null).get(),
new QueryBuilder().or(
new QueryBuilder().put("username").regex(caseInsensitivePattern).get(),
new QueryBuilder().put("email").regex(caseInsensitivePattern).get(),
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index a840813..6dae14b 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -187,7 +187,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
@Override
public Map<String, List<String>> getAttributes() {
- return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes());
+ return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map) user.getAttributes());
}
public MongoUserEntity getUser() {
@@ -461,6 +461,17 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
}
@Override
+ public String getServiceAccountClientLink() {
+ return user.getServiceAccountClientLink();
+ }
+
+ @Override
+ public void setServiceAccountClientLink(String clientInternalId) {
+ user.setServiceAccountClientLink(clientInternalId);
+ updateUser();
+ }
+
+ @Override
public void addConsent(UserConsentModel consent) {
String clientId = consent.getClient().getId();
if (getConsentEntityByClientId(clientId) != null) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java
index 3c8b8ad..1a8ad0b 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java
@@ -106,20 +106,13 @@ public class ServiceAccountManager {
protected Response finishClientAuthorization() {
event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH);
- Map<String, String> search = new HashMap<>();
- search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
- List<UserModel> users = session.users().searchForUserByUserAttributes(search, realm);
+ clientUser = session.users().getUserByServiceAccountClient(client);
- if (users.size() == 0) {
+ if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
// May need to handle bootstrap here as well
- logger.warnf("Service account user for client '%s' not found. Creating now", client.getClientId());
+ logger.infof("Service account user for client '%s' not found or default protocol mapper for service account not found. Creating now", client.getClientId());
new ClientManager(new RealmManager(session)).enableServiceAccount(client);
- users = session.users().searchForUserByUserAttributes(search, realm);
- clientUser = users.get(0);
- } else if (users.size() == 1) {
- clientUser = users.get(0);
- } else {
- throw new ModelDuplicateException("Multiple service account users found for client '" + client.getClientId() + "' . Check your DB");
+ clientUser = session.users().getUserByServiceAccountClient(client);
}
String clientUsername = clientUser.getUsername();
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
index e312aa0..c5be21b 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<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<Class<? extends Provider>, Map<String, ProviderFactory>>();
protected CopyOnWriteArrayList<ProviderEventListener> listeners = new CopyOnWriteArrayList<ProviderEventListener>();
+ protected long serverStartupTimestamp;
+
@Override
public void register(ProviderEventListener listener) {
listeners.add(listener);
@@ -46,6 +48,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
}
public void init() {
+ serverStartupTimestamp = System.currentTimeMillis();
+
ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers"));
for (Spi spi : ServiceLoader.load(Spi.class, getClass().getClassLoader())) {
@@ -148,4 +152,12 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
return factory.getClass().getPackage().getName().startsWith("org.keycloak");
}
+ /**
+ * @return timestamp of Keycloak server startup
+ */
+ @Override
+ public long getServerStartupTimestamp() {
+ return serverStartupTimestamp;
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
index a7f9079..1b5a4e8 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
@@ -51,6 +51,12 @@ public class ClientManager {
if (sessions != null) {
sessions.onClientRemoved(realm, client);
}
+
+ UserModel serviceAccountUser = realmManager.getSession().users().getUserByServiceAccountClient(client);
+ if (serviceAccountUser != null) {
+ realmManager.getSession().users().removeUser(realm, serviceAccountUser);
+ }
+
return true;
} else {
return false;
@@ -93,18 +99,15 @@ public class ClientManager {
client.setServiceAccountsEnabled(true);
// Add dedicated user for this service account
- RealmModel realm = client.getRealm();
- Map<String, String> search = new HashMap<>();
- search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
- List<UserModel> serviceAccountUsers = realmManager.getSession().users().searchForUserByUserAttributes(search, realm);
- if (serviceAccountUsers.size() == 0) {
+ if (realmManager.getSession().users().getUserByServiceAccountClient(client) == null) {
String username = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + client.getClientId();
logger.infof("Creating service account user '%s'", username);
- UserModel user = realmManager.getSession().users().addUser(realm, username);
+ // Don't use federation for service account user
+ UserModel user = realmManager.getSession().userStorage().addUser(client.getRealm(), username);
user.setEnabled(true);
user.setEmail(username + "@placeholder.org");
- user.setSingleAttribute(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId());
+ user.setServiceAccountClientLink(client.getId());
}
// Add protocol mappers to retrieve clientId in access token
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
index e1d1fa3..97c1472 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
@@ -20,6 +20,7 @@ import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
+import org.keycloak.services.resources.admin.info.ServerInfoAdminResource;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 67cfb65..b85aa39 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -19,6 +19,7 @@ import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
@@ -293,6 +294,31 @@ public class ClientResource {
}
/**
+ * Returns user dedicated to this service account
+ *
+ * @return
+ */
+ @Path("service-account-user")
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public UserRepresentation getServiceAccountUser() {
+ auth.requireView();
+
+ UserModel user = session.users().getUserByServiceAccountClient(client);
+ if (user == null) {
+ if (client.isServiceAccountsEnabled()) {
+ new ClientManager(new RealmManager(session)).enableServiceAccount(client);
+ user = session.users().getUserByServiceAccountClient(client);
+ } else {
+ throw new BadRequestException("Service account not enabled for the client '" + client.getClientId() + "'");
+ }
+ }
+
+ return ModelToRepresentation.toRepresentation(user);
+ }
+
+ /**
* If the client has an admin URL, push the client's revocation policy to it.
*
*/
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
index 83e6c2f..eeffe5d 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
@@ -109,7 +109,7 @@ public class IdentityProviderResource {
// Admin changed the ID (alias) of identity provider. We must update all clients and users
logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId);
- updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm), oldProviderId, newProviderId);
+ updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm, false), oldProviderId, newProviderId);
}
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(providerRep).success();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/MemoryInfoRepresentation.java b/services/src/main/java/org/keycloak/services/resources/admin/info/MemoryInfoRepresentation.java
new file mode 100644
index 0000000..56a1110
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/MemoryInfoRepresentation.java
@@ -0,0 +1,54 @@
+package org.keycloak.services.resources.admin.info;
+
+public class MemoryInfoRepresentation {
+
+ protected long total;
+ protected long used;
+
+ public static MemoryInfoRepresentation create() {
+ MemoryInfoRepresentation rep = new MemoryInfoRepresentation();
+ Runtime runtime = Runtime.getRuntime();
+ rep.total = runtime.maxMemory();
+ rep.used = runtime.totalMemory() - runtime.freeMemory();
+ return rep;
+ }
+
+ public long getTotal() {
+ return total;
+ }
+
+ public String getTotalFormated() {
+ return formatMemory(getTotal());
+ }
+
+ public long getFree() {
+ return getTotal() - getUsed();
+ }
+
+ public String getFreeFormated() {
+ return formatMemory(getFree());
+ }
+
+ public long getUsed() {
+ return used;
+ }
+
+ public String getUsedFormated() {
+ return formatMemory(getUsed());
+ }
+
+ public long getFreePercentage() {
+ return getFree() * 100 / getTotal();
+ }
+
+ private String formatMemory(long bytes) {
+ if (bytes > 1024L * 1024L) {
+ return bytes / (1024L * 1024L) + " MB";
+ } else if (bytes > 1024L) {
+ return bytes / (1024L) + " kB";
+ } else {
+ return bytes + " B";
+ }
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ProviderRepresentation.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ProviderRepresentation.java
new file mode 100644
index 0000000..ffcaeb1
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ProviderRepresentation.java
@@ -0,0 +1,17 @@
+package org.keycloak.services.resources.admin.info;
+
+import java.util.Map;
+
+public class ProviderRepresentation {
+
+ private Map<String, String> operationalInfo;
+
+ public Map<String, String> getOperationalInfo() {
+ return operationalInfo;
+ }
+
+ public void setOperationalInfo(Map<String, String> operationalInfo) {
+ this.operationalInfo = operationalInfo;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoRepresentation.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoRepresentation.java
new file mode 100644
index 0000000..9b7f4f5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoRepresentation.java
@@ -0,0 +1,108 @@
+package org.keycloak.services.resources.admin.info;
+
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ServerInfoRepresentation {
+
+ private SystemInfoRepresentation systemInfo;
+ private MemoryInfoRepresentation memoryInfo;
+
+ private Map<String, List<String>> themes;
+
+ private List<Map<String, String>> socialProviders;
+ private List<Map<String, String>> identityProviders;
+ private List<Map<String, String>> clientImporters;
+
+ private Map<String, SpiInfoRepresentation> providers;
+
+ private Map<String, List<ProtocolMapperTypeRepresentation>> protocolMapperTypes;
+ private Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers;
+
+ private Map<String, List<String>> enums;
+
+ public SystemInfoRepresentation getSystemInfo() {
+ return systemInfo;
+ }
+
+ public void setSystemInfo(SystemInfoRepresentation systemInfo) {
+ this.systemInfo = systemInfo;
+ }
+
+ public MemoryInfoRepresentation getMemoryInfo() {
+ return memoryInfo;
+ }
+
+ public void setMemoryInfo(MemoryInfoRepresentation memoryInfo) {
+ this.memoryInfo = memoryInfo;
+ }
+ public Map<String, List<String>> getThemes() {
+ return themes;
+ }
+
+ public void setThemes(Map<String, List<String>> themes) {
+ this.themes = themes;
+ }
+
+ public List<Map<String, String>> getSocialProviders() {
+ return socialProviders;
+ }
+
+ public void setSocialProviders(List<Map<String, String>> socialProviders) {
+ this.socialProviders = socialProviders;
+ }
+
+ public List<Map<String, String>> getIdentityProviders() {
+ return identityProviders;
+ }
+
+ public void setIdentityProviders(List<Map<String, String>> identityProviders) {
+ this.identityProviders = identityProviders;
+ }
+
+ public List<Map<String, String>> getClientImporters() {
+ return clientImporters;
+ }
+
+ public void setClientImporters(List<Map<String, String>> clientImporters) {
+ this.clientImporters = clientImporters;
+ }
+
+ public Map<String, SpiInfoRepresentation> getProviders() {
+ return providers;
+ }
+
+ public void setProviders(Map<String, SpiInfoRepresentation> providers) {
+ this.providers = providers;
+ }
+
+ public Map<String, List<ProtocolMapperTypeRepresentation>> getProtocolMapperTypes() {
+ return protocolMapperTypes;
+ }
+
+ public void setProtocolMapperTypes(Map<String, List<ProtocolMapperTypeRepresentation>> protocolMapperTypes) {
+ this.protocolMapperTypes = protocolMapperTypes;
+ }
+
+ public Map<String, List<ProtocolMapperRepresentation>> getBuiltinProtocolMappers() {
+ return builtinProtocolMappers;
+ }
+
+ public void setBuiltinProtocolMappers(Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers) {
+ this.builtinProtocolMappers = builtinProtocolMappers;
+ }
+
+ public Map<String, List<String>> getEnums() {
+ return enums;
+ }
+
+ public void setEnums(Map<String, List<String>> enums) {
+ this.enums = enums;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/SpiInfoRepresentation.java b/services/src/main/java/org/keycloak/services/resources/admin/info/SpiInfoRepresentation.java
new file mode 100644
index 0000000..3f94a5a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/SpiInfoRepresentation.java
@@ -0,0 +1,39 @@
+package org.keycloak.services.resources.admin.info;
+
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class SpiInfoRepresentation {
+
+ private boolean internal;
+ private boolean systemInfo;
+
+ private Map<String, ProviderRepresentation> providers;
+
+ public boolean isInternal() {
+ return internal;
+ }
+
+ public void setInternal(boolean internal) {
+ this.internal = internal;
+ }
+
+ public boolean isSystemInfo() {
+ return systemInfo;
+ }
+
+ public void setSystemInfo(boolean systemInfo) {
+ this.systemInfo = systemInfo;
+ }
+
+ public Map<String, ProviderRepresentation> getProviders() {
+ return providers;
+ }
+
+ public void setProviders(Map<String, ProviderRepresentation> providers) {
+ this.providers = providers;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/SystemInfoRepresentation.java b/services/src/main/java/org/keycloak/services/resources/admin/info/SystemInfoRepresentation.java
new file mode 100644
index 0000000..bc0329a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/SystemInfoRepresentation.java
@@ -0,0 +1,217 @@
+package org.keycloak.services.resources.admin.info;
+
+import org.keycloak.Version;
+import org.keycloak.models.KeycloakSession;
+
+import java.util.Date;
+import java.util.Locale;
+
+public class SystemInfoRepresentation {
+
+ private String version;
+ private String serverTime;
+ private String uptime;
+ private long uptimeMillis;
+ private String javaVersion;
+ private String javaVendor;
+ private String javaVm;
+ private String javaVmVersion;
+ private String javaRuntime;
+ private String javaHome;
+ private String osName;
+ private String osArchitecture;
+ private String osVersion;
+ private String fileEncoding;
+ private String userName;
+ private String userDir;
+ private String userTimezone;
+ private String userLocale;
+
+ public static SystemInfoRepresentation create(KeycloakSession session) {
+ SystemInfoRepresentation rep = new SystemInfoRepresentation();
+ rep.version = Version.VERSION;
+ rep.serverTime = new Date().toString();
+ rep.uptimeMillis = System.currentTimeMillis() - session.getKeycloakSessionFactory().getServerStartupTimestamp();
+ rep.uptime = formatUptime(rep.uptimeMillis);
+ rep.javaVersion = System.getProperty("java.version");
+ rep.javaVendor = System.getProperty("java.vendor");
+ rep.javaVm = System.getProperty("java.vm.name");
+ rep.javaVmVersion = System.getProperty("java.vm.version");
+ rep.javaRuntime = System.getProperty("java.runtime.name");
+ rep.javaHome = System.getProperty("java.home");
+ rep.osName = System.getProperty("os.name");
+ rep.osArchitecture = System.getProperty("os.arch");
+ rep.osVersion = System.getProperty("os.version");
+ rep.fileEncoding = System.getProperty("file.encoding");
+ rep.userName = System.getProperty("user.name");
+ rep.userDir = System.getProperty("user.dir");
+ rep.userTimezone = System.getProperty("user.timezone");
+ rep.userLocale = (new Locale(System.getProperty("user.country"), System.getProperty("user.language")).toString());
+ return rep;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public String getServerTime() {
+ return serverTime;
+ }
+
+ public void setServerTime(String serverTime) {
+ this.serverTime = serverTime;
+ }
+
+ public String getUptime() {
+ return uptime;
+ }
+
+ public void setUptime(String uptime) {
+ this.uptime = uptime;
+ }
+
+ public long getUptimeMillis() {
+ return uptimeMillis;
+ }
+
+ public void setUptimeMillis(long uptimeMillis) {
+ this.uptimeMillis = uptimeMillis;
+ }
+
+ public String getJavaVersion() {
+ return javaVersion;
+ }
+
+ public void setJavaVersion(String javaVersion) {
+ this.javaVersion = javaVersion;
+ }
+
+ public String getJavaVendor() {
+ return javaVendor;
+ }
+
+ public void setJavaVendor(String javaVendor) {
+ this.javaVendor = javaVendor;
+ }
+
+ public String getJavaVm() {
+ return javaVm;
+ }
+
+ public void setJavaVm(String javaVm) {
+ this.javaVm = javaVm;
+ }
+
+ public String getJavaVmVersion() {
+ return javaVmVersion;
+ }
+
+ public void setJavaVmVersion(String javaVmVersion) {
+ this.javaVmVersion = javaVmVersion;
+ }
+
+ public String getJavaRuntime() {
+ return javaRuntime;
+ }
+
+ public void setJavaRuntime(String javaRuntime) {
+ this.javaRuntime = javaRuntime;
+ }
+
+ public String getJavaHome() {
+ return javaHome;
+ }
+
+ public void setJavaHome(String javaHome) {
+ this.javaHome = javaHome;
+ }
+
+ public String getOsName() {
+ return osName;
+ }
+
+ public void setOsName(String osName) {
+ this.osName = osName;
+ }
+
+ public String getOsArchitecture() {
+ return osArchitecture;
+ }
+
+ public void setOsArchitecture(String osArchitecture) {
+ this.osArchitecture = osArchitecture;
+ }
+
+ public String getOsVersion() {
+ return osVersion;
+ }
+
+ public void setOsVersion(String osVersion) {
+ this.osVersion = osVersion;
+ }
+
+ public String getFileEncoding() {
+ return fileEncoding;
+ }
+
+ public void setFileEncoding(String fileEncoding) {
+ this.fileEncoding = fileEncoding;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public String getUserDir() {
+ return userDir;
+ }
+
+ public void setUserDir(String userDir) {
+ this.userDir = userDir;
+ }
+
+ public String getUserTimezone() {
+ return userTimezone;
+ }
+
+ public void setUserTimezone(String userTimezone) {
+ this.userTimezone = userTimezone;
+ }
+
+ public String getUserLocale() {
+ return userLocale;
+ }
+
+ public void setUserLocale(String userLocale) {
+ this.userLocale = userLocale;
+ }
+
+ private static String formatUptime(long uptime) {
+ long diffInSeconds = uptime / 1000;
+ long diff[] = new long[]{0, 0, 0, 0}; // sec
+ diff[3] = (diffInSeconds >= 60 ? diffInSeconds % 60 : diffInSeconds); // min
+ diff[2] = (diffInSeconds = (diffInSeconds / 60)) >= 60 ? diffInSeconds % 60 : diffInSeconds; // hours
+ diff[1] = (diffInSeconds = (diffInSeconds / 60)) >= 24 ? diffInSeconds % 24 : diffInSeconds; // days
+ diff[0] = (diffInSeconds = (diffInSeconds / 24));
+
+ return String.format(
+ "%d day%s, %d hour%s, %d minute%s, %d second%s",
+ diff[0],
+ diff[0] != 1 ? "s" : "",
+ diff[1],
+ diff[1] != 1 ? "s" : "",
+ diff[2],
+ diff[2] != 1 ? "s" : "",
+ diff[3],
+ diff[3] != 1 ? "s" : "");
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 8348037..1a1aa29 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -554,7 +554,7 @@ public class UsersResource {
}
userModels = session.users().searchForUserByAttributes(attributes, realm, firstResult, maxResults);
} else {
- userModels = session.users().getUsers(realm, firstResult, maxResults);
+ userModels = session.users().getUsers(realm, firstResult, maxResults, false);
}
for (UserModel user : userModels) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
index 3bde1cb..33ead31 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 b883226..8ce5c7c 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -814,7 +814,7 @@ public abstract class AbstractIdentityProviderTest {
private void removeTestUsers() {
RealmModel realm = getRealm();
- List<UserModel> users = this.session.users().getUsers(realm);
+ List<UserModel> users = this.session.users().getUsers(realm, true);
for (UserModel user : users) {
Set<FederatedIdentityModel> identities = this.session.users().getFederatedIdentities(user, realm);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java
index ee6c5c6..8eb05b6 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java
@@ -288,14 +288,14 @@ public abstract class AbstractKerberosTest {
RealmManager manager = new RealmManager(session);
RealmModel appRealm = manager.getRealm("test");
- List<UserModel> users = session.userStorage().getUsers(appRealm);
+ List<UserModel> users = session.userStorage().getUsers(appRealm, true);
for (UserModel user : users) {
if (!user.getUsername().equals(AssertEvents.DEFAULT_USERNAME)) {
session.userStorage().removeUser(appRealm, user);
}
}
- Assert.assertEquals(1, session.userStorage().getUsers(appRealm).size());
+ Assert.assertEquals(1, session.userStorage().getUsers(appRealm, true).size());
} finally {
keycloakRule.stopSession(session, true);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
index e50caf8..9b03d5b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
@@ -227,7 +227,7 @@ public class SyncProvidersTest {
RealmModel testRealm = session.realms().getRealm("test");
// Remove all users from model
- for (UserModel user : session.userStorage().getUsers(testRealm)) {
+ for (UserModel user : session.userStorage().getUsers(testRealm, true)) {
session.userStorage().removeUser(testRealm, user);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
index 238184e..48ed318 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
@@ -433,7 +433,7 @@ public class AdapterTest extends AbstractModelTest {
RealmModel otherRealm = adapter.createRealm("other");
realmManager.getSession().users().addUser(otherRealm, "bburke");
- Assert.assertEquals(1, realmManager.getSession().users().getUsers(otherRealm).size());
+ Assert.assertEquals(1, realmManager.getSession().users().getUsers(otherRealm, false).size());
Assert.assertEquals(1, realmManager.getSession().users().searchForUser("bu", otherRealm).size());
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index 31a9574..332c94b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -304,6 +304,14 @@ public class ImportTest extends AbstractModelTest {
Assert.assertTrue(otherAppAdminConsent.isRoleGranted(realm.getRole("admin")));
Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin")));
Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper));
+
+ // Test service accounts
+ Assert.assertFalse(application.isServiceAccountsEnabled());
+ Assert.assertTrue(otherApp.isServiceAccountsEnabled());
+ Assert.assertNull(session.users().getUserByServiceAccountClient(application));
+ UserModel linked = session.users().getUserByServiceAccountClient(otherApp);
+ Assert.assertNotNull(linked);
+ Assert.assertEquals("my-service-user", linked.getUsername());
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
index d0c9d00..9455271 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
@@ -7,6 +7,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.services.managers.ClientManager;
import static org.junit.Assert.assertNotNull;
@@ -226,6 +227,61 @@ public class UserModelTest extends AbstractModelTest {
Assert.assertEquals(0, users.size());
}
+ @Test
+ public void testServiceAccountLink() throws Exception {
+ RealmModel realm = realmManager.createRealm("original");
+ ClientModel client = realm.addClient("foo");
+
+ UserModel user1 = session.users().addUser(realm, "user1");
+ user1.setFirstName("John");
+ user1.setLastName("Doe");
+
+ UserModel user2 = session.users().addUser(realm, "user2");
+ user2.setFirstName("John");
+ user2.setLastName("Doe");
+
+ // Search
+ Assert.assertNull(session.users().getUserByServiceAccountClient(client));
+ List<UserModel> users = session.users().searchForUser("John Doe", realm);
+ Assert.assertEquals(2, users.size());
+ Assert.assertTrue(users.contains(user1));
+ Assert.assertTrue(users.contains(user2));
+
+ // Link service account
+ user1.setServiceAccountClientLink(client.getId());
+
+ commit();
+
+ // Search and assert service account user not found
+ realm = realmManager.getRealmByName("original");
+ UserModel searched = session.users().getUserByServiceAccountClient(client);
+ Assert.assertEquals(searched, user1);
+ users = session.users().searchForUser("John Doe", realm);
+ Assert.assertEquals(1, users.size());
+ Assert.assertFalse(users.contains(user1));
+ Assert.assertTrue(users.contains(user2));
+
+ users = session.users().getUsers(realm, false);
+ Assert.assertEquals(1, users.size());
+ Assert.assertFalse(users.contains(user1));
+ Assert.assertTrue(users.contains(user2));
+
+ users = session.users().getUsers(realm, true);
+ Assert.assertEquals(2, users.size());
+ Assert.assertTrue(users.contains(user1));
+ Assert.assertTrue(users.contains(user2));
+
+ Assert.assertEquals(2, session.users().getUsersCount(realm));
+
+ // Remove client
+ new ClientManager(realmManager).removeClient(realm, client);
+ commit();
+
+ // Assert service account removed as well
+ realm = realmManager.getRealmByName("original");
+ Assert.assertNull(session.users().getUserByUsername("user1", realm));
+ }
+
public static void assertEquals(UserModel expected, UserModel actual) {
Assert.assertEquals(expected.getUsername(), actual.getUsername());
Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp());
diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json
index 340d9d3..9df4385 100755
--- a/testsuite/integration/src/test/resources/model/testrealm.json
+++ b/testsuite/integration/src/test/resources/model/testrealm.json
@@ -141,6 +141,11 @@
"userName": "mySocialUser@gmail.com"
}
]
+ },
+ {
+ "username": "my-service-user",
+ "enabled": true,
+ "serviceAccountClientId": "OtherApp"
}
],
"clients": [
@@ -158,6 +163,7 @@
"clientId": "OtherApp",
"name": "Other Application",
"enabled": true,
+ "serviceAccountsEnabled": true,
"protocolMappers" : [
{
"name" : "gss delegation credential",