Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2016-02-12 11:38:36 -05:00
commit 24da8288eb
23 changed files with 472 additions and 60 deletions

View file

@ -202,7 +202,28 @@ An Angular JS example using Keycloak to secure it.
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
Step 9: Pure HTML5/Javascript Example
Step 10: Angular2 JS Example
----------------------------------
An Angular2 JS example using Keycloak to secure it. Angular2 is in beta version yet.
To install angular2
```
$ cd keycloak/examples/demo-template/angular2-product-app/src/main/webapp/
$ npm install
```
Transpile TypeScript to JavaScript before running the application.
```
$ npm run tsc
```
[http://localhost:8080/angular2-product](http://localhost:8080/angular2-product)
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
Step 11: Pure HTML5/Javascript Example
----------------------------------
An pure HTML5/Javascript example using Keycloak to secure it.
@ -211,7 +232,7 @@ An pure HTML5/Javascript example using Keycloak to secure it.
If you are already logged in, you will not be asked for a username and password, but you will be redirected to
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.
Step 10: Service Account Example
Step 12: Service Account Example
================================
An example for retrieve service account dedicated to the Client Application itself (not to any user).
@ -222,7 +243,7 @@ Client authentication is done with OAuth2 Client Credentials Grant in out-of-bou
The example also shows different methods of client authentication. There is ProductSAClientSecretServlet using traditional authentication with clientId and client_secret,
but there is also ProductSAClientSignedJWTServlet using client authentication with JWT signed by client private key.
Step 11: Offline Access Example
Step 13: Offline Access Example
===============================
An example for retrieve offline token, which is then saved to the database and can be used by application anytime later. Offline token
is valid even if user is already logged out from SSO. Server restart also won't invalidate offline token. Offline token can be revoked by the user in

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.9.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak.example.demo</groupId>
<artifactId>angular2-product-example</artifactId>
<packaging>war</packaging>
<name>Angular2 Product Portal JS</name>
<description/>
<build>
<finalName>angular2-product</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,78 @@
import {Http, Headers,
RequestOptions, Response} from 'angular2/http';
import {Component} from 'angular2/core';
import {Observable} from 'rxjs/Observable';
import {KeycloakService} from './keycloak';
@Component({
selector: 'my-app',
template:
`
<div id="content-area" class="col-md-9" role="main">
<div id="content">
<h1>Angular2 Product (Beta)</h1>
<h2><span>Products</span></h2>
<button type="button" (click)="logout()">Sign Out</button>
<button type="button" (click)="reloadData()">Reload</button>
<table class="table" [hidden]="!products.length">
<thead>
<tr>
<th>Product Listing</th>
</tr>
</thead>
<tbody>
<tr *ngFor="#p of products">
<td>{{p}}</td>
</tr>
</tbody>
</table>
</div>
</div>
`
})
export class AppComponent {
constructor(private _kc:KeycloakService, private http:Http){ }
products : string[] = [];
logout(){
this._kc.logout();
}
reloadData() {
//angular dont have http interceptor yet
this._kc.getToken().then(
token=>{
let headers = new Headers({
'Accept': 'application/json',
'Authorization': 'Bearer ' + token
});
let options = new RequestOptions({ headers: headers });
this.http.get('/database/products', options)
.map(res => <string[]> res.json())
.subscribe(
prods => this.products = prods,
error => console.log(error));
},
error=>{
console.log(error);
}
);
}
private handleError (error: Response) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
}

View file

@ -0,0 +1,49 @@
import {Injectable} from 'angular2/core';
declare var Keycloak: any;
@Injectable()
export class KeycloakService {
static auth : any = {};
static init() : Promise<any>{
let keycloakAuth : any = new Keycloak('keycloak.json');
KeycloakService.auth.loggedIn = false;
return new Promise((resolve,reject)=>{
keycloakAuth.init({ onLoad: 'login-required' })
.success( () => {
KeycloakService.auth.loggedIn = true;
KeycloakService.auth.authz = keycloakAuth;
KeycloakService.auth.logoutUrl = keycloakAuth.authServerUrl + "/realms/demo/tokens/logout?redirect_uri=/angular2-product/index.html";
resolve(null);
})
.error(()=> {
reject(null);
});
});
}
logout(){
console.log('*** LOGOUT');
KeycloakService.auth.loggedIn = false;
KeycloakService.auth.authz = null;
window.location.href = KeycloakService.auth.logoutUrl;
}
getToken(): Promise<string>{
return new Promise<string>((resolve,reject)=>{
if (KeycloakService.auth.authz.token) {
KeycloakService.auth.authz.updateToken(5).success(function() {
resolve(<string>KeycloakService.auth.authz.token);
})
.error(function() {
reject('Failed to refresh token');
});
}
});
}
}

View file

@ -0,0 +1,14 @@
import 'rxjs/Rx';
import {bootstrap} from 'angular2/platform/browser';
import {HTTP_BINDINGS} from 'angular2/http';
import {KeycloakService} from './keycloak';
import {AppComponent} from './app';
KeycloakService.init().then(
o=>{
bootstrap(AppComponent,[HTTP_BINDINGS, KeycloakService]);
},
x=>{
window.location.reload();
}
);

View file

@ -0,0 +1,49 @@
<!doctype html>
<html>
<head>
<title>Angular 2 QuickStart</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<!-- 3. Display the application -->
<body>
<my-app>Loading...</my-app>
<!-- 1. Load libraries -->
<!-- IE required polyfills, in this exact order -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<script src="node_modules/angular2/bundles/http.js"></script>
<script src="/auth/js/keycloak.js"></script>
<!-- 2. Configure SystemJS -->
<script>
System.config({
packages: {
app: {
format: 'register',
defaultExtension: 'js'
}
}
});
System.import('app/main')
.then(null, console.error.bind(console));
</script>
</body>
</html>

View file

@ -0,0 +1,8 @@
{
"realm": "demo",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "/auth",
"ssl-required": "external",
"resource": "angular2-product",
"public-client": true
}

View file

@ -0,0 +1,25 @@
{
"name": "angular2-product-app",
"version": "1.0.0",
"scripts": {
"tsc": "tsc",
"tsc:w": "tsc -w",
"lite": "lite-server",
"start": "concurrent \"npm run tsc:w\" \"npm run lite\" "
},
"license": "ISC",
"dependencies": {
"angular2": "2.0.0-beta.3",
"systemjs": "0.19.6",
"es6-promise": "^3.0.2",
"es6-shim": "^0.33.3",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.0",
"zone.js": "0.5.11"
},
"devDependencies": {
"concurrently": "^1.0.0",
"lite-server": "^2.0.1",
"typescript": "^1.7.5"
}
}

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es5",
"module": "system",
"moduleResolution": "node",
"sourceMap": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": false
},
"exclude": [
"node_modules"
]
}

View file

@ -147,6 +147,15 @@
"redirectUris": [
"/angular-product/*"
]
},
{
"clientId": "angular2-product",
"enabled": true,
"publicClient": true,
"baseUrl": "/angular2-product/index.html",
"redirectUris": [
"/angular2-product/*"
]
},
{
"clientId": "customer-portal-cli",

View file

@ -24,6 +24,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.entities.*;
import org.keycloak.models.sessions.infinispan.initializer.TimeAwareInitializerState;
import org.keycloak.models.sessions.infinispan.stream.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
@ -410,6 +411,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getEmail()));
}
@Override
public int getClusterStartupTime() {
TimeAwareInitializerState state = (TimeAwareInitializerState) offlineSessionCache.get(InfinispanUserSessionProviderFactory.SESSION_INITIALIZER_STATE_KEY);
int startTime;
if (state == null) {
log.warn("Cluster startup time not yet available. Fallback to local startup time");
startTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
} else {
startTime = state.getClusterStartupTime();
}
return startTime;
}
@Override
public void close() {
}

View file

@ -34,6 +34,9 @@ import org.keycloak.provider.ProviderEventListener;
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
private static final String STATE_KEY_PREFIX = "initializerState";
public static final String SESSION_INITIALIZER_STATE_KEY = STATE_KEY_PREFIX + "::offlineUserSessions";
private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
private Config.Scope config;
@ -84,7 +87,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, "offlineUserSessions");
InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, SESSION_INITIALIZER_STATE_KEY);
initializer.initCache();
initializer.loadPersistentSessions();
}

View file

@ -35,6 +35,7 @@ import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
@ -51,8 +52,6 @@ public class InfinispanUserSessionInitializer {
private static final Logger log = Logger.getLogger(InfinispanUserSessionInitializer.class);
private static final String STATE_KEY_PREFIX = "initializerState";
private final KeycloakSessionFactory sessionFactory;
private final Cache<String, SessionEntity> cache;
private final SessionLoader sessionLoader;
@ -60,21 +59,18 @@ public class InfinispanUserSessionInitializer {
private final int sessionsPerSegment;
private final String stateKey;
private volatile CountDownLatch latch = new CountDownLatch(1);
public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, SessionEntity> cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKeySuffix) {
public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, SessionEntity> cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKey) {
this.sessionFactory = sessionFactory;
this.cache = cache;
this.sessionLoader = sessionLoader;
this.maxErrors = maxErrors;
this.sessionsPerSegment = sessionsPerSegment;
this.stateKey = STATE_KEY_PREFIX + "::" + stateKeySuffix;
this.stateKey = stateKey;
}
public void initCache() {
this.cache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class);
cache.getCacheManager().addListener(new ViewChangeListener());
}
@ -86,7 +82,7 @@ public class InfinispanUserSessionInitializer {
while (!isFinished()) {
if (!isCoordinator()) {
try {
latch.await(500, TimeUnit.MILLISECONDS);
Thread.sleep(1000);
} catch (InterruptedException ie) {
log.error("Interrupted", ie);
}
@ -104,8 +100,10 @@ public class InfinispanUserSessionInitializer {
private InitializerState getOrCreateInitializerState() {
InitializerState state = (InitializerState) cache.get(stateKey);
TimeAwareInitializerState state = (TimeAwareInitializerState) cache.get(stateKey);
if (state == null) {
int startTime = (int)(sessionFactory.getServerStartupTimestamp() / 1000);
final int[] count = new int[1];
// Rather use separate transactions for update and counting
@ -113,7 +111,7 @@ public class InfinispanUserSessionInitializer {
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
sessionLoader.init(session);
sessionLoader.init(session, startTime);
}
});
@ -126,8 +124,9 @@ public class InfinispanUserSessionInitializer {
});
state = new InitializerState();
state = new TimeAwareInitializerState();
state.init(count[0], sessionsPerSegment);
state.setClusterStartupTime(startTime);
saveStateToCache(state);
}
return state;
@ -251,24 +250,6 @@ public class InfinispanUserSessionInitializer {
}
}
@Listener
public class ViewChangeListener {
@ViewChanged
public void viewChanged(ViewChangedEvent event) {
boolean isCoordinator = isCoordinator();
log.debug("View Changed: is coordinator: " + isCoordinator);
if (isCoordinator) {
latch.countDown();
latch = new CountDownLatch(1);
}
}
}
public static class WorkerResult implements Serializable {
private Integer segment;

View file

@ -33,14 +33,13 @@ public class OfflineUserSessionLoader implements SessionLoader {
private static final Logger log = Logger.getLogger(OfflineUserSessionLoader.class);
@Override
public void init(KeycloakSession session) {
public void init(KeycloakSession session, int clusterStartupTime) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
int startTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
log.debugf("Clearing detached sessions from persistent storage and updating timestamps to %d", startTime);
log.debugf("Clearing detached sessions from persistent storage and updating timestamps to %d", clusterStartupTime);
persister.clearDetachedUserSessions();
persister.updateAllTimestamps(startTime);
persister.updateAllTimestamps(clusterStartupTime);
}
@Override

View file

@ -26,7 +26,7 @@ import org.keycloak.models.KeycloakSession;
*/
public interface SessionLoader extends Serializable {
void init(KeycloakSession session);
void init(KeycloakSession session, int clusterStartupTime);
int getSessionsCount(KeycloakSession session);

View file

@ -0,0 +1,34 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.sessions.infinispan.initializer;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class TimeAwareInitializerState extends InitializerState {
private int clusterStartupTime;
public int getClusterStartupTime() {
return clusterStartupTime;
}
public void setClusterStartupTime(int clusterStartupTime) {
this.clusterStartupTime = clusterStartupTime;
}
}

View file

@ -60,21 +60,18 @@
<where>ACCESS_TOKEN_LIFE_IMPLICIT is NULL</where>
</update>
<dropUniqueConstraint tableName="REALM_CLIENT" constraintName="UK_M6QGA3RFME47335JY8JXYXH3I" />
<dropForeignKeyConstraint baseTableName="REALM_CLIENT" constraintName="FK_93S3P0DIUXAWWQQSA528UBY2Q"/>
<dropForeignKeyConstraint baseTableName="REALM_CLIENT" constraintName="FK_M6QGA3RFME47335JY8JXYXH3I"/>
<dropTable tableName="REALM_CLIENT" cascadeConstraints="true"/>
<dropUniqueConstraint tableName="REALM_CLIENT" constraintName="UK_M6QGA3RFME47335JY8JXYXH3I" />
<dropTable tableName="REALM_CLIENT" />
<dropForeignKeyConstraint baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_RLM" />
<dropForeignKeyConstraint baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_CLI"/>
<dropTable tableName="REALM_CLIENT_TEMPLATE" cascadeConstraints="true"/>
<dropTable tableName="REALM_CLIENT_TEMPLATE" />
<dropUniqueConstraint tableName="FED_PROVIDERS" constraintName="UK_DCCIRJLIPU1478VQC89DID88C" />
<dropForeignKeyConstraint baseTableName="FED_PROVIDERS" constraintName="FK_213LYQ09FKXQ8K8NY8DY3737T"/>
<dropForeignKeyConstraint baseTableName="FED_PROVIDERS" constraintName="FK_DCCIRJLIPU1478VQC89DID88C"/>
<dropTable tableName="FED_PROVIDERS" cascadeConstraints="true"/>
<dropUniqueConstraint tableName="FED_PROVIDERS" constraintName="UK_DCCIRJLIPU1478VQC89DID88C" />
<dropTable tableName="FED_PROVIDERS" />
</changeSet>
</databaseChangeLog>

View file

@ -82,6 +82,9 @@ public interface UserSessionProvider extends Provider {
void removeClientInitialAccessModel(RealmModel realm, String id);
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
// Will use startup time of this server in non-cluster environment
int getClusterStartupTime();
void close();
}

View file

@ -193,9 +193,9 @@ public class TokenManager {
int currentTime = Time.currentTime();
if (realm.isRevokeRefreshToken()) {
int serverStartupTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
int clusterStartupTime = session.sessions().getClusterStartupTime();
if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() && (serverStartupTime != validation.clientSession.getTimestamp())) {
if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() && (clusterStartupTime != validation.clientSession.getTimestamp())) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
}

View file

@ -91,7 +91,6 @@ public class KeycloakApplication extends Application {
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
migrateModel();
sessionFactory.publish(new PostMigrationEvent());
boolean bootstrapAdminUser = false;
@ -138,6 +137,8 @@ public class KeycloakApplication extends Application {
session.close();
}
sessionFactory.publish(new PostMigrationEvent());
singletons.add(new WelcomeResource(bootstrapAdminUser));
setupScheduledTasks(sessionFactory);

View file

@ -83,7 +83,7 @@ public class UserSessionInitializerTest {
// Create and persist offline sessions
int started = Time.currentTime();
int serverStartTime = (int)(session.getKeycloakSessionFactory().getServerStartupTimestamp() / 1000);
int serverStartTime = session.sessions().getClusterStartupTime();
for (UserSessionModel origSession : origSessions) {
UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());

View file

@ -100,10 +100,9 @@ public class InfinispanCLI {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line;
System.out.print("$ ");
try {
while ((line = reader.readLine()) != null) {
log.info("Command: " + line);
String[] splits = line.split(" ");
String commandName = splits[0];
Class<? extends AbstractCommand> commandClass = commands.get(commandName);
@ -128,6 +127,8 @@ public class InfinispanCLI {
log.error(ex);
}
}
System.out.print("$ ");
}
} finally {
log.info("Exit infinispan CLI");

View file

@ -18,14 +18,19 @@
package org.keycloak.testsuite.util.cli;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -34,24 +39,59 @@ public class UserCommands {
public static class Create extends AbstractCommand {
private String usernamePrefix;
private String password;
private String realmName;
private String roleNames;
@Override
public String getName() {
return "createUsers";
}
private class StateHolder {
int firstInThisBatch;
int countInThisBatch;
int remaining;
};
@Override
protected void doRunCommand(KeycloakSession session) {
String usernamePrefix = getArg(0);
String password = getArg(1);
String realmName = getArg(2);
usernamePrefix = getArg(0);
password = getArg(1);
realmName = getArg(2);
int first = getIntArg(3);
int count = getIntArg(4);
String roleNames = getArg(5);
int batchCount = getIntArg(5);
roleNames = getArg(6);
final StateHolder state = new StateHolder();
state.firstInThisBatch = first;
state.remaining = count;
state.countInThisBatch = Math.min(batchCount, state.remaining);
while (state.remaining > 0) {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
createUsersInBatch(session, state.firstInThisBatch, state.countInThisBatch);
}
});
// update state
state.firstInThisBatch = state.firstInThisBatch + state.countInThisBatch;
state.remaining = state.remaining - state.countInThisBatch;
state.countInThisBatch = Math.min(batchCount, state.remaining);
}
log.infof("Command finished. All users from %s to %s created", usernamePrefix + first, usernamePrefix + (first + count - 1));
}
private void createUsersInBatch(KeycloakSession session, int first, int count) {
RealmModel realm = session.realms().getRealmByName(realmName);
if (realm == null) {
log.errorf("Unknown realm: %s", realmName);
return;
throw new HandledException();
}
Set<RoleModel> roles = findRoles(realm, roleNames);
@ -74,8 +114,11 @@ public class UserCommands {
@Override
public String printUsage() {
return super.printUsage() + " <username-prefix> <password> <realm-name> <starting-user-offset> <count> <realm-roles-list>. \nRoles list is divided by comma (client roles not yet supported)>\n" +
"Example usage: " + super.printUsage() + " test test demo 0 20 user,admin";
return super.printUsage() + " <username-prefix> <password> <realm-name> <starting-user-offset> <total-count> <batch-size> <realm-roles-list>. " +
"\n'total-count' refers to total count of newly created users. 'batch-size' refers to number of created users in each transaction. 'starting-user-offset' refers to starting username offset." +
"\nFor example if 'starting-user-offset' is 15 and total-count is 10 and username-prefix is 'test', it will create users test15, test16, test17, ... , test24" +
"\nRoles list is divided by comma (client roles are referenced with format <client-id>/<role-name> )>\n" +
"Example usage: " + super.printUsage() + " test test demo 0 500 100 user,admin";
}
private Set<RoleModel> findRoles(RealmModel realm, String rolesList) {
@ -200,10 +243,25 @@ public class UserCommands {
if (user == null) {
log.infof("User '%s' doesn't exist in realm '%s'", username, realmName);
} else {
log.infof("User: ID: '%s', username: '%s', mail: '%s'", user.getId(), user.getUsername(), user.getEmail());
List<String> roleMappings = getRoleMappings(session, realm, user);
log.infof("User: ID: '%s', username: '%s', mail: '%s', roles: '%s'", user.getId(), user.getUsername(), user.getEmail(), roleMappings.toString());
}
}
private List<String> getRoleMappings(KeycloakSession session, RealmModel realm, UserModel user) {
Set<RoleModel> roles = user.getRoleMappings();
List<String> result = new LinkedList<>();
for (RoleModel role : roles) {
if (role.getContainer() instanceof RealmModel) {
result.add(role.getName());
} else {
ClientModel client = (ClientModel) role.getContainer();
result.add(client.getClientId() + "/" + role.getName());
}
}
return result;
}
@Override
public String printUsage() {
return super.printUsage() + " <realm-name> <username>";