Skip to content

Commit f311058

Browse files
ryanemersonahus1
andauthored
Allow the dataset to generate users with a limited number of unique passwords
Closes #1237 Signed-off-by: Ryan Emerson <[email protected]> Signed-off-by: Alexander Schwartz <[email protected]> Co-authored-by: Alexander Schwartz <[email protected]>
1 parent 217d700 commit f311058

File tree

8 files changed

+111
-31
lines changed

8 files changed

+111
-31
lines changed

.github/actions/keycloak-create-dataset/action.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ inputs:
2626
maxWaitEntityCreation:
2727
description: 'Maximum number of seconds to wait for creation of entities'
2828
default: '300'
29+
uniqueCredentials:
30+
description: 'Limits the total number of credentials created to the specified amount in order to speed up dataset initialisation'
31+
default: '-1'
2932

3033
runs:
3134
using: "composite"
@@ -39,7 +42,7 @@ runs:
3942
shell: bash
4043
run: |
4144
./dataset-import.sh -a clear-status-completed -l ${{ env.KEYCLOAK_URL }}/realms/master/dataset
42-
./dataset-import.sh -a create-realms -r ${{ inputs.realms }} -c ${{ inputs.clients }} -u ${{ inputs.users }} -l ${{ env.KEYCLOAK_URL }}/realms/master/dataset -C ${{ inputs.maxWaitEntityCreation }}
45+
./dataset-import.sh -a create-realms -r ${{ inputs.realms }} -c ${{ inputs.clients }} -u ${{ inputs.users }} -U ${{ inputs.uniqueCredentials }} -l ${{ env.KEYCLOAK_URL }}/realms/master/dataset -C ${{ inputs.maxWaitEntityCreation }}
4346
./dataset-import.sh -a status-completed -t ${{ inputs.maxWaitEntityCreation }} -l ${{ env.KEYCLOAK_URL }}/realms/master/dataset
4447
working-directory: dataset
4548

.github/workflows/keycloak-create-dataset.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ on:
2626
description: 'Maximum number of seconds to wait for creation of entities'
2727
type: string
2828
default: '300'
29+
uniqueCredentials:
30+
description: 'Limits the total number of credentials created to the specified amount in order to speed up dataset initialisation'
31+
type: string
32+
default: '-1'
2933

3034
jobs:
3135
prepare:
@@ -56,3 +60,4 @@ jobs:
5660
users: ${{ inputs.users }}
5761
clients: ${{ inputs.clients }}
5862
maxWaitEntityCreation: ${{ inputs.maxWaitEntityCreation }}
63+
uniqueCredentials: ${{ inputs.uniqueCredentials }}

benchmark/src/main/java/org/keycloak/benchmark/Config.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ public class Config {
101101
*/
102102
public static final String basicUrl = System.getProperty("basic-url");
103103

104+
/**
105+
* Used to correctly configure user passwords when users have been created via the dataset using the `unique-credential-count` option.
106+
* In order for the password to be calculated correctly, this value should match the value used when creating the dataset.
107+
*/
108+
public static final int uniqueCredentialCount = Integer.getInteger("unique-credential-count", -1);
109+
104110
public static final Double usersPerSec;
105111

106112
public static final Integer concurrentUsers;

benchmark/src/main/scala/keycloak/scenario/KeycloakScenarioBuilder.scala

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,26 @@ class KeycloakScenarioBuilder {
5959
val serverUrl = Config.serverUrisList.iterator().next()
6060
val realmIndex = String.valueOf(Random.nextInt(Config.numOfRealms))
6161
val clientIndex = String.valueOf(Random.nextInt(Config.numClientsPerRealm))
62-
val userIndex = String.valueOf(Random.nextInt(Config.numUsersPerRealm) + Config.userIndexOffset)
62+
val userIndex = Random.nextInt(Config.numUsersPerRealm) + Config.userIndexOffset
63+
var userIndexStr = String.valueOf(userIndex)
6364
var realmName = Config.realmPrefix.concat(realmIndex)
6465

6566
if (Config.realmName != null) {
6667
realmName = Config.realmName
6768
}
6869

69-
var userName = Config.userNamePrefix.concat(userIndex)
70+
var userName = Config.userNamePrefix.concat(userIndexStr)
7071

7172
if (Config.userName != null) {
7273
userName = Config.userName
7374
}
7475

75-
var userPassword = Config.userPasswordPrefix.concat(userIndex).concat(Config.userPasswordSuffix)
76-
77-
if (Config.userPassword != null) {
78-
userPassword = Config.userPassword
76+
var userPassword = if (Config.userPassword != null) {
77+
Config.userPassword
78+
} else if (Config.uniqueCredentialCount > 0) {
79+
s"password-${userIndex % Config.uniqueCredentialCount}"
80+
} else {
81+
Config.userPasswordPrefix.concat(userIndexStr).concat(Config.userPasswordSuffix)
7982
}
8083

8184
var redirectUri = serverUrl.stripSuffix("/").concat("/realms/").concat(realmName).concat("/account")

dataset/dataset-import.sh

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ set_environment_variables () {
2424
STATUS_TIMEOUT="120"
2525
CREATE_TIMEOUT="3600"
2626
THREADS="-1"
27+
UNIQUE_CREDENTIALS="-1"
2728

28-
while getopts ":a:r:n:c:u:e:o:g:i:p:l:t:C:T:" OPT
29+
while getopts ":a:r:n:c:u:e:o:g:i:p:l:t:C:T:U:" OPT
2930
do
3031
case $OPT in
3132
a)
@@ -70,6 +71,9 @@ set_environment_variables () {
7071
T)
7172
THREADS=$OPTARG
7273
;;
74+
U)
75+
UNIQUE_CREDENTIALS=$OPTARG
76+
;;
7377
?)
7478
echo "Invalid option: $OPT, read the usage carefully -> "
7579
help
@@ -85,7 +89,7 @@ create_clients () {
8589

8690
create_users () {
8791
echo "Creating $1 user/s in realm $2"
88-
execute_command "create-users?count=$1&realm-name=$2"
92+
execute_command "create-users?count=$1&realm-name=$2&unique-credential-count=$3"
8993
}
9094

9195
create_events () {
@@ -193,15 +197,15 @@ main () {
193197
if [ -z "$HASH_ALGORITHM" ]; then HA_PARAM=""; HASH_ALGORITHM="default"; else HA_PARAM="&password-hash-algorithm=$HASH_ALGORITHM"; fi
194198
if [ -z "$HASH_ITERATIONS" ]; then HI_PARAM=""; HASH_ITERATIONS="default"; else HI_PARAM="&password-hash-iterations=$HASH_ITERATIONS"; fi
195199
echo "Creating $REALM_COUNT realms with $CLIENTS_COUNT clients and $USERS_COUNT users with $HASH_ITERATIONS password-hashing iterations using the $HASH_ALGORITHM algorithm."
196-
execute_command "create-realms?count=$REALM_COUNT&clients-per-realm=$CLIENTS_COUNT&users-per-realm=$USERS_COUNT$HI_PARAM$HA_PARAM"
200+
execute_command "create-realms?count=$REALM_COUNT&clients-per-realm=$CLIENTS_COUNT&users-per-realm=$USERS_COUNT&unique-credential-count=$UNIQUE_CREDENTIALS$HI_PARAM$HA_PARAM"
197201
exit 0
198202
;;
199203
create-clients)
200204
create_clients $CLIENTS_COUNT $REALM_NAME $CREATE_TIMEOUT $THREADS
201205
exit 0
202206
;;
203207
create-users)
204-
create_users $USERS_COUNT $REALM_NAME
208+
create_users $USERS_COUNT $REALM_NAME $UNIQUE_CREDENTIALS
205209
exit 0
206210
;;
207211
create-events)

dataset/src/main/java/org/keycloak/benchmark/dataset/DatasetResourceProvider.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.keycloak.benchmark.dataset.config.DatasetException;
3434
import org.keycloak.benchmark.dataset.organization.OrganizationProvisioner;
3535
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
36+
import org.keycloak.credential.hash.PasswordHashProvider;
3637
import org.keycloak.events.Event;
3738
import org.keycloak.events.EventStoreProvider;
3839
import org.keycloak.events.EventType;
@@ -52,6 +53,7 @@
5253
import org.keycloak.models.UserModel;
5354
import org.keycloak.models.UserSessionModel;
5455
import org.keycloak.models.cache.CacheRealmProvider;
56+
import org.keycloak.models.credential.PasswordCredentialModel;
5557
import org.keycloak.models.utils.KeycloakModelUtils;
5658
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
5759
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@@ -65,12 +67,15 @@
6567
import java.util.ArrayList;
6668
import java.util.Collection;
6769
import java.util.Collections;
70+
import java.util.Comparator;
6871
import java.util.HashMap;
6972
import java.util.List;
73+
import java.util.Map;
7074
import java.util.Random;
7175
import java.util.concurrent.atomic.AtomicInteger;
7276
import java.util.regex.Pattern;
7377
import java.util.stream.Collectors;
78+
import java.util.stream.IntStream;
7479

7580
import static org.keycloak.benchmark.dataset.config.DatasetOperation.CREATE_CLIENTS;
7681
import static org.keycloak.benchmark.dataset.config.DatasetOperation.CREATE_EVENTS;
@@ -435,6 +440,8 @@ private void createUsersImpl(Task task, DatasetConfig config, RealmModel realm)
435440
}
436441

437442
private void addUserCreationTasks(RealmContext context, Task task, DatasetConfig config, ExecutorHelper executor, int startIndex, int usersCount) {
443+
// Initialize password credentials if unique-credentials-count is configured
444+
List<PasswordCredentialModel> credentials = initializeCredentials(config, usersCount, context);
438445

439446
for (int i = startIndex; i < (startIndex + usersCount); i += config.getUsersPerTransaction()) {
440447
final int usersStartIndex = i;
@@ -446,7 +453,7 @@ private void addUserCreationTasks(RealmContext context, Task task, DatasetConfig
446453
executor.addTaskRunningInTransaction(session -> {
447454
KeycloakModelUtils.cloneContextRealmClientToSession(baseSession.getContext(), session);
448455

449-
createUsers(context, session, usersStartIndex, endIndex);
456+
createUsers(context, session, usersStartIndex, endIndex, credentials);
450457

451458
task.debug(logger, "Created users in realm %s from %d to %d", context.getRealm().getName(), usersStartIndex, endIndex);
452459

@@ -457,6 +464,24 @@ private void addUserCreationTasks(RealmContext context, Task task, DatasetConfig
457464
}
458465
}
459466

467+
private List<PasswordCredentialModel> initializeCredentials(DatasetConfig config, int usersCount, RealmContext context) {
468+
var map = IntStream.range(0, Math.min(usersCount, config.getUniqueCredentialCount()))
469+
.parallel()
470+
.mapToObj(i -> KeycloakModelUtils.runJobInTransactionWithResult(baseSession.getKeycloakSessionFactory(), session -> {
471+
KeycloakModelUtils.cloneContextRealmClientToSession(baseSession.getContext(), session);
472+
PasswordPolicy policy = context.getRealm().getPasswordPolicy();
473+
PasswordHashProvider hash = policy.getHashAlgorithm() != null ?
474+
baseSession.getProvider(PasswordHashProvider.class, policy.getHashAlgorithm()) :
475+
baseSession.getProvider(PasswordHashProvider.class);
476+
return Map.entry(i, hash.encodedCredential("password-" + i, policy.getHashIterations()));
477+
}))
478+
.collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue));
479+
480+
return map.entrySet().stream()
481+
.sorted(Comparator.comparingInt(Map.Entry::getKey))
482+
.map(Map.Entry::getValue)
483+
.toList();
484+
}
460485

461486
@GET
462487
@Path("/create-events")
@@ -1130,7 +1155,7 @@ private void createClients(RealmContext context, Task task, KeycloakSession sess
11301155
}
11311156

11321157
// Worker task to be triggered by single executor thread
1133-
private void createUsers(RealmContext context, KeycloakSession session, int startIndex, int endIndex) {
1158+
private void createUsers(RealmContext context, KeycloakSession session, int startIndex, int endIndex, List<PasswordCredentialModel> credentials) {
11341159
// Refresh the realm
11351160
RealmModel realm = session.realms().getRealm(context.getRealm().getId());
11361161
DatasetConfig config = context.getConfig();
@@ -1147,8 +1172,13 @@ private void createUsers(RealmContext context, KeycloakSession session, int star
11471172
user.setLastName(username + "-last");
11481173
user.setEmail(username + String.format("@%s.com", realm.getName()));
11491174

1150-
String password = String.format("%s-password", username);
1151-
user.credentialManager().updateCredential(UserCredentialModel.password(password, false));
1175+
if (credentials.isEmpty()) {
1176+
String password = String.format("%s-password", username);
1177+
user.credentialManager().updateCredential(UserCredentialModel.password(password, false));
1178+
} else {
1179+
PasswordCredentialModel password = credentials.get(i % config.getUniqueCredentialCount());
1180+
user.credentialManager().createStoredCredential(password);
1181+
}
11521182

11531183
// Assign a role to a user if any exist in the realm
11541184
if (!context.getRealmRoles().isEmpty()) {

dataset/src/main/java/org/keycloak/benchmark/dataset/config/DatasetConfig.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ public class DatasetConfig {
226226
@QueryParamIntFill(paramName = "identity-provider-mappers-count", operations = CREATE_ORGS)
227227
private int identityProviderMappersCount;
228228

229+
@QueryParamIntFill(paramName = "unique-credential-count", defaultValue = 0, operations = {CREATE_REALMS, CREATE_USERS})
230+
private int uniqueCredentialCount;
231+
229232
// String representation of this configuration (cached here to not be computed in runtime)
230233
private String toString = "DatasetConfig []";
231234

@@ -432,4 +435,8 @@ public int getIdentityProvidersCount() {
432435
public int getIdentityProviderMappersCount() {
433436
return identityProviderMappersCount;
434437
}
438+
439+
public int getUniqueCredentialCount() {
440+
return uniqueCredentialCount;
441+
}
435442
}

doc/dataset/modules/ROOT/pages/using-provider.adoc

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,17 @@ To learn more about the tool, see xref:kubernetes-guide::util/task.adoc[] for de
6868

6969
You need to call this HTTP REST requests.
7070
This request is useful for create 10 realms.
71-
Each realm will contain specified amount of roles, clients, groups and users:
71+
Each realm will contain a set of roles, clients, groups and users:
7272

7373
----
7474
.../realms/master/dataset/create-realms?count=10
7575
----
7676

7777
=== Create many clients
7878

79-
This is request to create 100 new clients in the realm `realm-5` . Each client will have service account enabled and secret like «client_id»-secret (For example `client-156-secret` in case of the client `client-156`):
79+
This is a request to create 200 new clients in the realm `realm-5`.
80+
81+
Each client will have service account enabled and secret like `<client_id>-secret` (For example `client-156-secret` in the case of the client `client-156`):
8082

8183
----
8284
.../realms/master/dataset/create-clients?count=200&realm-name=realm-5
@@ -90,15 +92,35 @@ You can also configure the access-type (`bearer-only`, `confidential` or `public
9092

9193
=== Create many users
9294

93-
This is request to create 500 new users in the `realm-5`.
94-
Each user will have specified amount of roles, client roles and groups, which were already created by `create-realms` endpoint.
95-
Each user will have password like «Username»-password . For example `user-156` will have password like
96-
`user-156-password` :
95+
This is a request to create 1000 new users in the realm `realm-5`:
9796

9897
----
9998
.../realms/master/dataset/create-users?count=1000&realm-name=realm-5
10099
----
101100

101+
Each user will have the specified amount of roles, client roles and groups, which were already created by the `create-realms` endpoint.
102+
103+
Each user will be assigned a password in the format `<username>-password`. For example `user-156` will have the password `user-156-password`.
104+
105+
=== Speed up user creation by limiting the total number of passwords
106+
107+
When creating users, the bottleneck is CPU time to hash the passwords that are by default unique for each user.
108+
109+
To reduce total time to create a large number of users, configure the dataset to only create a limited
110+
pool of hashed passwords and reuse these credentials when creating users. Specify the `unique-credential-count` parameter
111+
in order to limit the number of passwords created.
112+
113+
When limiting the total number of passwords, individual passwords for a given user are calculated as `"password-" + i`
114+
where `i` is the user's number % `unique-credential-count`.
115+
116+
For example, after executing:
117+
118+
----
119+
.../realms/master/dataset/create-users?realm-name=realm-0&count=1000&unique-credential-count=10`
120+
----
121+
122+
the user `user-156` will have the password `password-6` as we calculate `i = 156 % 10`.
123+
102124
=== Create many groups
103125

104126
Groups are created as part of the realm creation.
@@ -115,7 +137,7 @@ With groups-with-hierarchy set to `true` a tree structure of groups is created;
115137
The default value is 3. With the default value, top level groups will have `groups-count-each-level` subgroups and each subgroup will have `groups-count-each-level` themselves.
116138
This parameter is active only when `groups-with-hierarchy` is `true`.
117139

118-
`groups-count-each-level`:: Number of subgroups each created group will have.
140+
`groups-count-each-level`:: The Number of subgroups each created group will have.
119141
This parameter is active only when `groups-with-hierarchy` is `true`.
120142

121143
With the default values, only top-level groups are created.
@@ -137,7 +159,7 @@ You can also create groups in an existing realm by invoking the `create-groups`
137159

138160
=== Create many events
139161

140-
This is request to create 10M new events in the available realms with prefix `realm-`.
162+
This is a request to create 10M new events in the available realms with prefix `realm-`.
141163
For example if we have 100 realms like `realm-0`, `realm-1`, ... `realm-99`, it will create 10M events randomly in them
142164

143165
----
@@ -146,7 +168,7 @@ For example if we have 100 realms like `realm-0`, `realm-1`, ... `realm-99`, it
146168

147169
=== Create many offline sessions
148170

149-
This is request to create 10M new offline sessions in the available realms with prefix `realm-`.
171+
This is a request to create 10M new offline sessions in the available realms with prefix `realm-`.
150172
For example if we have 100 realms like `realm-0`, `realm-1`, … `realm-99`, it will create 10M events randomly in them
151173

152174
----
@@ -178,7 +200,7 @@ For example to create realms with prefix `foo` and with just 1000 hash iteration
178200
.../realms/master/dataset/create-realms?count=10&realm-prefix=foo&password-hash-iterations=1000
179201
----
180202

181-
Another example would be, to specify a particular hashing algorithm in combination with the hashing iterations with the below parameters:
203+
Another example would be to specify a particular hashing algorithm in combination with the hashing iterations with the below parameters:
182204

183205
----
184206
.../realms/master/dataset/create-realms?count=10&realm-prefix=foo&password-hash-algorithm=argon2&password-hash-iterations=1000
@@ -187,7 +209,7 @@ Another example would be, to specify a particular hashing algorithm in combinati
187209
The configuration is written to the server log when HTTP endpoint is triggered, so you can monitor the progress and what parameters were effectively applied.
188210

189211
Note that creation of new objects will automatically start from the next available index.
190-
For example when you trigger endpoint above for creation many clients and you already had 230 clients in your DB (`client-0`, `client-1`, .. `client-229`), then your HTTP request will start creating clients from `client-230` .
212+
For example when you trigger endpoint above for creation many clients, and you already had 230 clients in your DB (`client-0`, `client-1`, ... `client-229`), then your HTTP request will start creating clients from `client-230` .
191213

192214
=== Check if the task is still running
193215

@@ -280,7 +302,7 @@ Alternatively, you can create a single organization with a given name:
280302
realms/realm-0/dataset/orgs/create?name=myorg.com&domains=myorg.com,myorg.org,myorg.net&count=1
281303
----
282304

283-
You can also specify how many members (managed and unmanaged) and how many identity providers should be
305+
You can also specify how many managed and unmanaged members and how many identity providers should be
284306
linked to each organization created:
285307

286308
----
@@ -292,13 +314,13 @@ As a result, 1k organizations with the following configuration:
292314
* 500 unmanaged members
293315
* 10 identity providers
294316

295-
It is also possible te specify a number of identity provider mappers per each identity provider:
317+
It is also possible to specify a number of identity provider mappers per each identity provider:
296318

297319
----
298320
.../realms/realm-0/dataset/orgs/create?count=1000&unmanaged-members-count=500&identity-providers-count=10&identity-provider-mappers-count=3
299321
----
300322

301-
In this case 1k organizations with each having 500 unmanaged members, 10 identity providers and each identity provider having 3 identity provider mnappers
323+
This creates 1k organizations with 500 unmanaged members each, 10 identity providers and with identity provider having 3 identity provider mappers.
302324

303325
You can also provision data to a specific organization. For instance, to provision
304326
more identity providers to a specific organization:
@@ -307,7 +329,7 @@ more identity providers to a specific organization:
307329
.../realms/realm-0/dataset/orgs/org-0/identity-providers/create?count=1000
308330
----
309331

310-
Optionally it's possible to specify a number of identity provider mappers per each identity provider
332+
Optionally, it's possible to specify a number of identity provider mappers per each identity provider
311333

312334
----
313335
.../realms/realm-0/dataset/orgs/org-0/identity-providers/create?count=1000&identity-provider-mappers-count=5
@@ -325,7 +347,7 @@ Or to provision more managed members to a specific organization:
325347
.../realms/realm-0/dataset/orgs/org-0/members/create-managed?count=100
326348
----
327349

328-
When provisioning members make sure you have created enough users in the realm. For managed members, you also need at least
350+
When provisioning members, make sure you have created enough users in the realm. For managed members, you also need at least
329351
a single identity provider linked to an organization.
330352

331353
If you want to remove an organization:

0 commit comments

Comments
 (0)