From 7fce05d5b413cf5720acdedb6eb5acc4f65f0a7a Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 18 Jun 2024 18:07:28 -0300 Subject: [PATCH 1/4] Dataset for organizations Closes #856 Signed-off-by: Pedro Igor --- .../dataset/DatasetResourceProvider.java | 9 +- .../dataset/config/DatasetConfig.java | 53 ++++- .../dataset/config/DatasetOperation.java | 1 + .../AbstractOrganizationProvisioner.java | 106 ++++++++++ ...ganizationIdentityProviderProvisioner.java | 144 +++++++++++++ .../OrganizationMemberProvisioner.java | 140 +++++++++++++ .../organization/OrganizationProvisioner.java | 192 ++++++++++++++++++ .../modules/ROOT/pages/using-provider.adoc | 63 ++++++ 8 files changed, 702 insertions(+), 6 deletions(-) create mode 100644 dataset/src/main/java/org/keycloak/benchmark/dataset/organization/AbstractOrganizationProvisioner.java create mode 100644 dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java create mode 100644 dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationMemberProvisioner.java create mode 100644 dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/DatasetResourceProvider.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/DatasetResourceProvider.java index 3120a80d0..ede2b09b3 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/dataset/DatasetResourceProvider.java +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/DatasetResourceProvider.java @@ -29,6 +29,7 @@ import org.keycloak.benchmark.dataset.config.ConfigUtil; import org.keycloak.benchmark.dataset.config.DatasetConfig; import org.keycloak.benchmark.dataset.config.DatasetException; +import org.keycloak.benchmark.dataset.organization.OrganizationProvisioner; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.events.Event; import org.keycloak.events.EventStoreProvider; @@ -871,6 +872,11 @@ public AuthorizationProvisioner authz() { return new AuthorizationProvisioner(baseSession); } + @Path("/orgs") + public OrganizationProvisioner orgs() { + return new OrganizationProvisioner(baseSession); + } + @GET @Path("/take-dc-down") @NoCache @@ -1103,7 +1109,8 @@ private void createUsers(RealmContext context, KeycloakSession session, int star int groupIndexStartForCurrentUser = (i * config.getGroupsPerUser()); for (int j = groupIndexStartForCurrentUser ; j < groupIndexStartForCurrentUser + config.getGroupsPerUser() ; j++) { int groupIndex = j % context.getGroups().size(); - user.joinGroup(context.getGroups().get(groupIndex)); + GroupModel group = context.getGroups().get(groupIndex); + user.joinGroup(session.groups().getGroupById(realm, group.getId())); logger.tracef("Assigned group %s to the user %s", context.getGroups().get(groupIndex).getName(), user.getUsername()); } diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/config/DatasetConfig.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/config/DatasetConfig.java index bae36b344..d18e4a65c 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/dataset/config/DatasetConfig.java +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/config/DatasetConfig.java @@ -22,6 +22,7 @@ import static org.keycloak.benchmark.dataset.config.DatasetOperation.CREATE_CLIENTS; import static org.keycloak.benchmark.dataset.config.DatasetOperation.CREATE_EVENTS; import static org.keycloak.benchmark.dataset.config.DatasetOperation.CREATE_OFFLINE_SESSIONS; +import static org.keycloak.benchmark.dataset.config.DatasetOperation.CREATE_ORGS; import static org.keycloak.benchmark.dataset.config.DatasetOperation.CREATE_REALMS; import static org.keycloak.benchmark.dataset.config.DatasetOperation.CREATE_USERS; import static org.keycloak.benchmark.dataset.config.DatasetOperation.LAST_CLIENT; @@ -63,7 +64,7 @@ public class DatasetConfig { private Integer start; // Count of entities to be created. Entity is realm, client or user based on the operation - @QueryParamIntFill(paramName = "count", required = true, operations = { CREATE_REALMS, CREATE_CLIENTS, CREATE_USERS, CREATE_EVENTS, CREATE_OFFLINE_SESSIONS, CREATE_AUTHZ_CLIENT }) + @QueryParamIntFill(paramName = "count", required = true, operations = { CREATE_REALMS, CREATE_CLIENTS, CREATE_USERS, CREATE_EVENTS, CREATE_OFFLINE_SESSIONS, CREATE_AUTHZ_CLIENT, CREATE_ORGS }) private Integer count; // Prefix for realm roles to create in every realm (in case of CREATE_REALMS) or to assign to users (in case of CREATE_USERS) @@ -86,7 +87,7 @@ public class DatasetConfig { private Integer clientsPerTransaction; // default number of entries created per DB transaction - @QueryParamIntFill(paramName = "entries-per-transaction", defaultValue = 10, operations = { CREATE_REALMS, CREATE_CLIENTS, CREATE_AUTHZ_CLIENT }) + @QueryParamIntFill(paramName = "entries-per-transaction", defaultValue = 10, operations = { CREATE_REALMS, CREATE_CLIENTS, CREATE_AUTHZ_CLIENT, CREATE_ORGS }) private Integer entriesPerTransaction; // Prefix of clientRoles to be created (in case of CREATE_REALMS and CREATE_CLIENTS). In case of CREATE_USERS it is used to find the clientRoles, which will be assigned to users @@ -164,7 +165,7 @@ public class DatasetConfig { // Transaction timeout used for transactions for creating objects @QueryParamIntFill(paramName = "transaction-timeout", defaultValue = 300, operations = { CREATE_REALMS, CREATE_CLIENTS, CREATE_USERS, - CREATE_EVENTS, CREATE_OFFLINE_SESSIONS, REMOVE_REALMS, CREATE_AUTHZ_CLIENT }) + CREATE_EVENTS, CREATE_OFFLINE_SESSIONS, REMOVE_REALMS, CREATE_AUTHZ_CLIENT, CREATE_ORGS }) private Integer transactionTimeoutInSeconds; // Count of users created in every transaction @@ -173,13 +174,13 @@ public class DatasetConfig { // Count of worker threads concurrently creating entities @QueryParamIntFill(paramName = "threads-count", operations = { CREATE_REALMS, CREATE_CLIENTS, CREATE_USERS, - CREATE_EVENTS, CREATE_OFFLINE_SESSIONS, REMOVE_REALMS, CREATE_AUTHZ_CLIENT }) + CREATE_EVENTS, CREATE_OFFLINE_SESSIONS, REMOVE_REALMS, CREATE_AUTHZ_CLIENT, CREATE_ORGS }) private Integer threadsCount; // Timeout for the whole task. If timeout expires, then the existing task may not be terminated immediatelly. However it will be permitted to start another task // (EG. Send another HTTP request for creating realms), which can cause conflicts @QueryParamIntFill(paramName = "task-timeout", defaultValue = 3600, operations = { CREATE_REALMS, CREATE_CLIENTS, CREATE_USERS, - CREATE_EVENTS, CREATE_OFFLINE_SESSIONS, REMOVE_REALMS, CREATE_AUTHZ_CLIENT }) + CREATE_EVENTS, CREATE_OFFLINE_SESSIONS, REMOVE_REALMS, CREATE_AUTHZ_CLIENT, CREATE_ORGS }) private Integer taskTimeout; // The client id of a client to which data is going to be provisioned @@ -202,6 +203,24 @@ public class DatasetConfig { @QueryParamIntFill(paramName = "users-per-user-policy", defaultValue = 1, operations = CREATE_AUTHZ_CLIENT) private int usersPerUserPolicy; + @QueryParamFill(paramName = "name", operations = { CREATE_ORGS }) + private String name; + + @QueryParamFill(paramName = "domains", operations = { CREATE_ORGS }) + private String domains; + + @QueryParamIntFill(paramName = "domains-count", operations = { CREATE_ORGS }) + private int domainsCount; + + @QueryParamFill(paramName = "org-prefix", defaultValue = "org-", operations = { CREATE_ORGS }) + private String orgPrefix; + + @QueryParamIntFill(paramName = "unmanaged-members-count", operations = CREATE_ORGS) + private int unManagedMembersCount; + + @QueryParamIntFill(paramName = "identity-providers-count", operations = CREATE_ORGS) + private int identityProvidersCount; + // String representation of this configuration (cached here to not be computed in runtime) private String toString = "DatasetConfig []"; @@ -380,4 +399,28 @@ public int getUsersPerUserPolicy() { public String toString() { return toString; } + + public String getName() { + return name; + } + + public String getDomains() { + return domains; + } + + public int getDomainsCount() { + return domainsCount; + } + + public String getOrgPrefix() { + return orgPrefix; + } + + public int getUnManagedMembersCount() { + return unManagedMembersCount; + } + + public int getIdentityProvidersCount() { + return identityProvidersCount; + } } diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/config/DatasetOperation.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/config/DatasetOperation.java index 622ab63f5..e6b440e4f 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/dataset/config/DatasetOperation.java +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/config/DatasetOperation.java @@ -28,6 +28,7 @@ public enum DatasetOperation { CREATE_USERS, CREATE_EVENTS, CREATE_OFFLINE_SESSIONS, + CREATE_ORGS, REMOVE_REALMS, LAST_REALM, LAST_CLIENT, diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/AbstractOrganizationProvisioner.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/AbstractOrganizationProvisioner.java new file mode 100644 index 000000000..5b4b2d255 --- /dev/null +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/AbstractOrganizationProvisioner.java @@ -0,0 +1,106 @@ +/* + * Copyright 2024 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.benchmark.dataset.organization; + +import static org.keycloak.benchmark.dataset.config.DatasetOperation.CREATE_ORGS; + +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; +import org.keycloak.benchmark.dataset.DatasetResourceProvider; +import org.keycloak.benchmark.dataset.ExecutorHelper; +import org.keycloak.benchmark.dataset.Task; +import org.keycloak.benchmark.dataset.TaskManager; +import org.keycloak.benchmark.dataset.TaskResponse; +import org.keycloak.benchmark.dataset.config.ConfigUtil; +import org.keycloak.benchmark.dataset.config.DatasetConfig; +import org.keycloak.benchmark.dataset.config.DatasetException; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.organization.OrganizationProvider; + +public class AbstractOrganizationProvisioner extends DatasetResourceProvider { + + private final String realmName; + + public AbstractOrganizationProvisioner(KeycloakSession session) { + super(session); + realmName = session.getContext().getRealm().getName(); + } + + protected Response start(String name, Runnable runnable) { + DatasetConfig config = getDatasetConfig(); + Task task = Task.start(name); + TaskManager taskManager = new TaskManager(baseSession); + Task existingTask = taskManager.addTaskIfNotInProgress(task, config.getTaskTimeout()); + + if (existingTask != null) { + return Response.status(400).entity(TaskResponse.errorSomeTaskInProgress(existingTask, getStatusUrl())).build(); + } + + try { + new Thread(runnable).start(); + return Response.ok(TaskResponse.taskStarted(task, getStatusUrl())).build(); + } catch (DatasetException de) { + return handleException(handleDatasetException(de)); + } catch (Exception e) { + logger.error("Unexpected error", e); + return handleException(Response.status(500).entity(TaskResponse.error("Unexpected error")).build()); + } + } + + protected OrganizationProvider getOrganizationProvider(KeycloakSession session) { + session.getContext().setRealm(getRealm(session)); + OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class); + + if (orgProvider == null) { + throw new NotFoundException(); + } + + return orgProvider; + } + + protected DatasetConfig getDatasetConfig() { + return ConfigUtil.createConfigFromQueryParams(httpRequest, CREATE_ORGS); + } + + private Response handleException(Response response) { + new TaskManager(baseSession).removeExistingTask(false); + return response; + } + + protected String getRealmName() { + return realmName; + } + + protected RealmModel getRealm(KeycloakSession session) { + RealmModel realm = session.realms().getRealmByName(realmName); + session.getContext().setRealm(realm); + return realm; + } + + protected void runJobInTransactionWithTimeout(KeycloakSessionTask task) { + KeycloakModelUtils.runJobInTransactionWithTimeout(baseSession.getKeycloakSessionFactory(), task::run, getDatasetConfig().getTransactionTimeoutInSeconds()); + } + + protected void addTaskRunningInTransaction(ExecutorHelper executor, KeycloakSessionTask task) { + executor.addTaskRunningInTransaction(task::run); + } +} diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java new file mode 100644 index 000000000..6fb21fead --- /dev/null +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java @@ -0,0 +1,144 @@ +/* + * Copyright 2024 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.benchmark.dataset.organization; + +import java.util.HashMap; +import java.util.concurrent.CountDownLatch; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.reactive.NoCache; +import org.keycloak.benchmark.dataset.ExecutorHelper; +import org.keycloak.benchmark.dataset.TaskManager; +import org.keycloak.benchmark.dataset.config.ConfigUtil; +import org.keycloak.benchmark.dataset.config.DatasetConfig; +import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory; +import org.keycloak.models.ClientModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.OrganizationModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.organization.OrganizationProvider; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; + +public class OrganizationIdentityProviderProvisioner extends AbstractOrganizationProvisioner { + + private final OrganizationModel organization; + private final DatasetConfig config; + + public OrganizationIdentityProviderProvisioner(KeycloakSession session, OrganizationModel organization, DatasetConfig config) { + super(session); + this.organization = organization; + this.config = config; + } + + @Path("create") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Response create() { + return start("Creation of " + config.getCount() + " identity providers in organization " + organization.getName(), addIdentityProviders()); + } + + private Runnable addIdentityProviders() { + return () -> { + String orgId = organization.getId(); + CountDownLatch latch = new CountDownLatch(config.getCount()); + KeycloakSessionFactory sessionFactory = baseSession.getKeycloakSessionFactory(); + ExecutorHelper executor = new ExecutorHelper(config.getThreadsCount(), sessionFactory, config); + + executor.addTask(() -> runJobInTransactionWithTimeout(session -> addIdentityProviders(session, orgId, latch))); + + try { + executor.waitForAllToFinish(); + success(); + logger.infof("Added %d identity providers to organization %s", config.getCount() - latch.getCount(), organization.getName()); + } catch (Exception e) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, session + -> new TaskManager(session).removeExistingTask(false)); + } + }; + } + + protected void addIdentityProviders(KeycloakSession session, String orgId, CountDownLatch latch) { + OrganizationProvider provider = getOrganizationProvider(session); + OrganizationModel organization = provider.getById(orgId); + int startIndex = getLastIndex(session, organization.getName()); + RealmModel realm = session.getContext().getRealm(); + ClientModel client = realm.getClientByClientId("org-broker-client"); + + if (client == null) { + client = realm.addClient("org-broker-client"); + client.setSecret("secret"); + client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + client.setPublicClient(false); + } + + long count = latch.getCount(); + + for (int i = startIndex; i < count; i++) { + String idpAlias = "idp-" + organization.getName() + "-" + i; + + IdentityProviderModel identityProvider = realm.getIdentityProviderByAlias(idpAlias); + + if (identityProvider != null && identityProvider.getInternalId() != null && identityProvider.getOrganizationId() != null) { + continue; + } + + if (identityProvider == null || identityProvider.getInternalId() == null) { + identityProvider = new IdentityProviderModel(); + + identityProvider.setAlias(idpAlias); + identityProvider.setProviderId(KeycloakOIDCIdentityProviderFactory.PROVIDER_ID); + identityProvider.setLoginHint(true); + identityProvider.setEnabled(true); + HashMap idpConfig = new HashMap<>(); + identityProvider.setConfig(idpConfig); + idpConfig.put("issuer", "http://localhost:8180/realms/" + config.getRealmName()); + idpConfig.put("jwksUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/certs"); + idpConfig.put("logoutUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/auth"); + idpConfig.put("metadataDescriptorUrl", "http://localhost:8180/realms/realm-0/.well-known/openid-configuration"); + idpConfig.put("tokenUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/token"); + idpConfig.put("authorizationUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/auth"); + idpConfig.put("useJwksUrl", "true"); + idpConfig.put("userInfoUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/userinfo"); + idpConfig.put("validateSignature", "true"); + idpConfig.put("clientId", "org-broker-client"); + idpConfig.put("clientSecret", "secret"); + idpConfig.put("clientAuthMethod", "client_secret_post"); + realm.addIdentityProvider(identityProvider); + client.addRedirectUri("http://localhost:8180/realms/" + config.getRealmName() + "/broker/" + identityProvider.getAlias() + "/endpoint"); + } + + if (provider.addIdentityProvider(organization, identityProvider)) { + latch.countDown(); + } + } + } + + private int getLastIndex(KeycloakSession session, String orgName) { + RealmModel realm = session.getContext().getRealm(); + return ConfigUtil.findFreeEntityIndex(index -> realm.getIdentityProvidersStream().anyMatch(idp -> idp.getAlias().equals("idp-" + orgName + "-" + index))); + } +} diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationMemberProvisioner.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationMemberProvisioner.java new file mode 100644 index 000000000..aa1b88bc1 --- /dev/null +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationMemberProvisioner.java @@ -0,0 +1,140 @@ +/* + * Copyright 2024 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.benchmark.dataset.organization; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.reactive.NoCache; +import org.keycloak.benchmark.dataset.ExecutorHelper; +import org.keycloak.benchmark.dataset.TaskManager; +import org.keycloak.benchmark.dataset.config.DatasetConfig; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.OrganizationModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.organization.OrganizationProvider; + +public class OrganizationMemberProvisioner extends AbstractOrganizationProvisioner { + + private final OrganizationModel organization; + private final DatasetConfig config; + + public OrganizationMemberProvisioner(KeycloakSession session, OrganizationModel organization, DatasetConfig config) { + super(session); + if (organization == null) { + throw new BadRequestException("Organization not set"); + } + this.organization = organization; + this.config = config; + } + + @Path("create-unmanaged") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Response createUnmanaged() { + return start("Creation of " + config.getCount() + " unmanaged members in organization " + organization.getName(), () -> { + KeycloakSessionFactory sessionFactory = baseSession.getKeycloakSessionFactory(); + ExecutorHelper executor = new ExecutorHelper(config.getThreadsCount(), sessionFactory, config); + String orgId = organization.getId(); + CountDownLatch latch = new CountDownLatch(config.getCount()); + + executor.addTask(() -> runJobInTransactionWithTimeout(session -> addUnmanagedMembers(session, orgId, latch))); + + try { + executor.waitForAllToFinish(); + success(); + logger.infof("Added %d members to organization %s", config.getCount() - latch.getCount(), organization.getName()); + } catch (Exception e) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, session + -> new TaskManager(session).removeExistingTask(false)); + } + }); + } + + @Path("create-managed") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Response createManaged() { + return start("Creation of " + config.getCount() + " managed members in organization " + organization.getName(), () -> { + KeycloakSessionFactory sessionFactory = baseSession.getKeycloakSessionFactory(); + ExecutorHelper executor = new ExecutorHelper(config.getThreadsCount(), sessionFactory, config); + String orgId = organization.getId(); + CountDownLatch latch = new CountDownLatch(config.getCount()); + + executor.addTask(() -> runJobInTransactionWithTimeout(session -> addManagedMembers(session, orgId, latch))); + + try { + executor.waitForAllToFinish(); + success(); + logger.infof("Added %d members to organization %s", config.getCount() - latch.getCount(), organization.getName()); + } catch (Exception e) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, session + -> new TaskManager(session).removeExistingTask(false)); + } + }); + } + + protected void addUnmanagedMembers(KeycloakSession session, String orgId, CountDownLatch latch) { + OrganizationProvider provider = getOrganizationProvider(session); + RealmModel realm = session.getContext().getRealm(); + OrganizationModel organization = provider.getById(orgId); + session.users().searchForUserStream(realm, Map.of(UserModel.INCLUDE_SERVICE_ACCOUNT, Boolean.FALSE.toString())) + .filter((u) -> provider.getByMember(u) == null) + .takeWhile(userModel -> { + return latch.getCount() > 0; + }) + .forEach(userModel -> { + provider.addMember(organization, userModel); + latch.countDown(); + }); + } + + protected void addManagedMembers(KeycloakSession session, String orgId, CountDownLatch latch) { + OrganizationProvider provider = getOrganizationProvider(session); + RealmModel realm = session.getContext().getRealm(); + OrganizationModel organization = provider.getById(orgId); + session.users().searchForUserStream(realm, Map.of(UserModel.INCLUDE_SERVICE_ACCOUNT, Boolean.FALSE.toString())) + .filter((u) -> provider.getByMember(u) == null) + .takeWhile(userModel -> latch.getCount() > 0) + .forEach(userModel -> { + Optional broker = organization.getIdentityProviders().findAny(); + + if (broker.isPresent()) { + FederatedIdentityModel federatedIdentity = new FederatedIdentityModel(broker.get().getAlias(), userModel.getId(), userModel.getUsername()); + session.users().addFederatedIdentity(realm, userModel, federatedIdentity); + provider.addMember(organization, userModel); + latch.countDown(); + } + }); + } +} diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java new file mode 100644 index 000000000..0468e6e12 --- /dev/null +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java @@ -0,0 +1,192 @@ +/* + * Copyright 2024 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.benchmark.dataset.organization; + +import static org.keycloak.utils.StringUtil.isBlank; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.reactive.NoCache; +import org.keycloak.benchmark.dataset.ExecutorHelper; +import org.keycloak.benchmark.dataset.TaskResponse; +import org.keycloak.benchmark.dataset.config.ConfigUtil; +import org.keycloak.benchmark.dataset.config.DatasetConfig; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.OrganizationDomainModel; +import org.keycloak.models.OrganizationModel; +import org.keycloak.models.RealmModel; +import org.keycloak.organization.OrganizationProvider; +import org.keycloak.utils.StringUtil; + +public class OrganizationProvisioner extends AbstractOrganizationProvisioner { + + private OrganizationModel organization; + + public OrganizationProvisioner(KeycloakSession session) { + super(session); + enableOrganization(); + } + + @Path("create") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Response create() { + return start("Creation of " + getDatasetConfig().getCount() + " organizations", createOrganizations()); + } + + @Path("remove") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Response remove() { + OrganizationProvider provider = getOrganizationProvider(baseSession); + RealmModel realm = getRealm(baseSession); + organization.getIdentityProviders().map(IdentityProviderModel::getAlias).forEach(realm::removeIdentityProviderByAlias); + provider.remove(organization); + return Response.noContent().build(); + } + + @Path("{org-alias}") + public Object getOrganization(@PathParam("org-alias") String orgAlias) { + OrganizationProvider provider = getOrganizationProvider(baseSession); + + organization = provider.getAllStream(orgAlias, true, -1, -1).findAny().orElse(null); + + if (organization == null) { + return Response.status(400).entity(TaskResponse.error("Organization " + orgAlias + " does not exist")).build(); + } + + return this; + } + + @Path("members") + public Object members() { + return new OrganizationMemberProvisioner(baseSession, organization, getDatasetConfig()); + } + + @Path("identity-providers") + public Object identityProviders() { + return new OrganizationIdentityProviderProvisioner(baseSession, organization, getDatasetConfig()); + } + + private Runnable createOrganizations() { + return () -> { + DatasetConfig config = getDatasetConfig(); + KeycloakSessionFactory sessionFactory = baseSession.getKeycloakSessionFactory(); + ExecutorHelper executor = new ExecutorHelper(config.getThreadsCount(), sessionFactory, config); + + try { + Integer count = config.getCount(); + + executor.addTask(() -> runJobInTransactionWithTimeout(session -> { + int startIndex = getLastIndex(session); + + if (isBlank(config.getName())) { + for (int i = startIndex; i < (startIndex + count); i += config.getEntriesPerTransaction()) { + int orgStartIndex = i; + int endIndex = Math.min(orgStartIndex + config.getEntriesPerTransaction(), startIndex + count); + + addTaskRunningInTransaction(executor, s -> { + for (int j = orgStartIndex; j < endIndex; j++) { + createOrganization(config.getOrgPrefix() + j, s, config); + } + + logger.infof("Created organizations in realm %s from %d to %d", getRealmName(), orgStartIndex, endIndex); + + if (((endIndex - startIndex) / config.getEntriesPerTransaction()) % 20 == 0) { + logger.infof("Created %d organizations in realm %s", count, getRealmName()); + } + }); + } + } else { + createOrganization(config.getName(), session, config); + } + })); + + executor.waitForAllToFinish(); + success(); + logger.infof("Created %d organizations in realm %s", count, getRealmName()); + } catch (Exception e) { + cleanup(executor); + } + }; + } + + private void createOrganization(String name, KeycloakSession session, DatasetConfig config) { + OrganizationProvider orgProvider = getOrganizationProvider(session); + + if (orgProvider.getAllStream(name, true, -1, -1).findAny().isPresent()) { + RealmModel realm = session.getContext().getRealm(); + logger.infof("Not creating %s organization in realm %s because it already exists", name, realm.getName()); + return; + } + + OrganizationModel organization = orgProvider.create(name); + int domainsCount = config.getDomainsCount(); + String domains = Optional.ofNullable(config.getDomains()).filter(StringUtil::isNotBlank).orElse(name); + + if (domainsCount > 0) { + Set domainSet = new HashSet<>(); + + for (int i = 0; i < domainsCount; i++) { + domainSet.add(name + "." + "d" + i); + } + + domains = String.join(",", domainSet); + } + + organization.setDomains(Stream.of(domains.split(",")).map(OrganizationDomainModel::new).collect(Collectors.toSet())); + + int orgUnmanagedMembersCount = config.getUnManagedMembersCount(); + + if (orgUnmanagedMembersCount > 0) { + new OrganizationMemberProvisioner(baseSession, organization, config).addUnmanagedMembers(session, organization.getId(), new CountDownLatch(orgUnmanagedMembersCount)); + } + + int orgIdentityProvidersCount = config.getIdentityProvidersCount(); + + if (orgIdentityProvidersCount > 0) { + new OrganizationIdentityProviderProvisioner(baseSession, organization, config).addIdentityProviders(session, organization.getId(), new CountDownLatch(orgIdentityProvidersCount)); + } + } + + private int getLastIndex(KeycloakSession session) { + String orgPrefix = getDatasetConfig().getOrgPrefix(); + return ConfigUtil.findFreeEntityIndex(index -> getOrganizationProvider(session).getAllStream(orgPrefix + index, true, -1, -1).findAny().isPresent()); + } + + private void enableOrganization() { + RealmModel realm = baseSession.getContext().getRealm(); + realm.setOrganizationsEnabled(true); + } +} diff --git a/doc/dataset/modules/ROOT/pages/using-provider.adoc b/doc/dataset/modules/ROOT/pages/using-provider.adoc index 570681c0d..47ccec282 100644 --- a/doc/dataset/modules/ROOT/pages/using-provider.adoc +++ b/doc/dataset/modules/ROOT/pages/using-provider.adoc @@ -253,6 +253,69 @@ To see the last created user in a given realm .../realms/master/dataset/last-user?realm-name=realm5 ---- +=== Provisioning organizations + +Before provisioning organizations, make sure to manually create or provision a realm. For example, +provision a `realm-0` realm as follows: + +---- +.../realms/master/dataset/create-realms?count=1&users-per-realm=5000 +---- + +As a result, you have a realm `realm-0` with 5k users in it. + +This is the request to create 1000 organizations in a realm with prefix `org-`: + +---- +.../realms/realm-0/dataset/orgs/create?count=1000 +---- + +Alternatively, you can create a single organization with a given name: + +---- +realms/realm-0/dataset/orgs/create?name=myorg.com&domains=myorg.com,myorg.org,myorg.net&count=1 +---- + +You can also specify how many members (managed and unmanaged) and how many identity providers should be +linked to each organization created: + +---- +.../realms/realm-0/dataset/orgs/create?count=1000&unmanaged-members-count=500&identity-providers-count=10 +---- + +As a result, 1k organizations with the following configuration: + +* 500 unmanaged members +* 10 identity providers + +You can also provision data to a specific organization. For instance, to provision +more identity providers to a specific organization: + +---- +.../realms/realm-0/dataset/orgs/org-0/identity-providers/create?count=1000 +---- + +Or to provision more unmanaged members to a specific organization: + +---- +.../realms/realm-0/dataset/orgs/org-0/members/create-unmanaged?count=100 +---- + +Or to provision more managed members to a specific organization: + +---- +.../realms/realm-0/dataset/orgs/org-0/members/create-managed?count=100 +---- + +When provisioning members make sure you have created enough users in the realm. For managed members, you also need at least +a single identity provider linked to an organization. + +If you want to remove an organization: + +---- +.../realms/realm-0/dataset/orgs/org-0/remove +---- + == Further reading * xref:clearing-caches.adoc[] From db917d6b2037633a5549ee3dab40f7360fb171f4 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 20 Jun 2024 18:54:56 -0300 Subject: [PATCH 2/4] improving logging Signed-off-by: Pedro Igor --- .../main/java/org/keycloak/benchmark/dataset/Task.java | 10 +++++++++- .../organization/OrganizationMemberProvisioner.java | 4 +--- .../dataset/organization/OrganizationProvisioner.java | 10 ++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/Task.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/Task.java index 77ad83871..5118ba83b 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/dataset/Task.java +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/Task.java @@ -18,6 +18,7 @@ package org.keycloak.benchmark.dataset; +import java.time.Duration; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -90,7 +91,14 @@ public String getTaskMessage() { @Override public String toString() { - return String.format("%s, started: %s", taskMessage, new Date(startTimeMs)); + boolean running = endTimeMs == null; + Long endTimeMs = this.endTimeMs; + + if (running) { + endTimeMs = Time.currentTimeMillis(); + } + + return String.format("%s, running: %s, time: %ss, started: %s, ended: %s", taskMessage, running, Duration.ofMillis(endTimeMs - startTimeMs).toSeconds(), new Date(startTimeMs), new Date(endTimeMs)); } public Boolean isSuccess() { diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationMemberProvisioner.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationMemberProvisioner.java index aa1b88bc1..f59c1d80a 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationMemberProvisioner.java +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationMemberProvisioner.java @@ -110,9 +110,7 @@ protected void addUnmanagedMembers(KeycloakSession session, String orgId, CountD OrganizationModel organization = provider.getById(orgId); session.users().searchForUserStream(realm, Map.of(UserModel.INCLUDE_SERVICE_ACCOUNT, Boolean.FALSE.toString())) .filter((u) -> provider.getByMember(u) == null) - .takeWhile(userModel -> { - return latch.getCount() > 0; - }) + .takeWhile(userModel -> latch.getCount() > 0) .forEach(userModel -> { provider.addMember(organization, userModel); latch.countDown(); diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java index 0468e6e12..adb2b07ce 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java @@ -121,11 +121,9 @@ private Runnable createOrganizations() { createOrganization(config.getOrgPrefix() + j, s, config); } - logger.infof("Created organizations in realm %s from %d to %d", getRealmName(), orgStartIndex, endIndex); + int createdCount = endIndex - orgStartIndex; - if (((endIndex - startIndex) / config.getEntriesPerTransaction()) % 20 == 0) { - logger.infof("Created %d organizations in realm %s", count, getRealmName()); - } + logger.infof("Created %d organizations in realm %s from %d to %d", createdCount, getRealmName(), orgStartIndex, endIndex); }); } } else { @@ -135,9 +133,9 @@ private Runnable createOrganizations() { executor.waitForAllToFinish(); success(); - logger.infof("Created %d organizations in realm %s", count, getRealmName()); } catch (Exception e) { cleanup(executor); + logger.error("Failed to provision organizations", e); } }; } @@ -151,7 +149,7 @@ private void createOrganization(String name, KeycloakSession session, DatasetCon return; } - OrganizationModel organization = orgProvider.create(name); + OrganizationModel organization = orgProvider.create(name, name); int domainsCount = config.getDomainsCount(); String domains = Optional.ofNullable(config.getDomains()).filter(StringUtil::isNotBlank).orElse(name); From e70cd6bafbefe5210fe801f57f8606705566a8a3 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 21 Jun 2024 09:42:15 -0300 Subject: [PATCH 3/4] fixing broker urls Signed-off-by: Pedro Igor --- .../OrganizationIdentityProviderProvisioner.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java index 6fb21fead..209376608 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java @@ -93,6 +93,7 @@ protected void addIdentityProviders(KeycloakSession session, String orgId, Count client.setSecret("secret"); client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); client.setPublicClient(false); + client.addRedirectUri("http://localhost:8180/realms/" + realm.getName() + "/broker/*"); } long count = latch.getCount(); @@ -115,20 +116,19 @@ protected void addIdentityProviders(KeycloakSession session, String orgId, Count identityProvider.setEnabled(true); HashMap idpConfig = new HashMap<>(); identityProvider.setConfig(idpConfig); - idpConfig.put("issuer", "http://localhost:8180/realms/" + config.getRealmName()); - idpConfig.put("jwksUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/certs"); - idpConfig.put("logoutUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/auth"); - idpConfig.put("metadataDescriptorUrl", "http://localhost:8180/realms/realm-0/.well-known/openid-configuration"); - idpConfig.put("tokenUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/token"); - idpConfig.put("authorizationUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/auth"); + idpConfig.put("issuer", "http://localhost:8180/realms/" + realm.getName()); + idpConfig.put("jwksUrl", "http://localhost:8180/realms/" + realm.getName() + "/protocol/openid-connect/certs"); + idpConfig.put("logoutUrl", "http://localhost:8180/realms/" + realm.getName() + "/protocol/openid-connect/auth"); + idpConfig.put("metadataDescriptorUrl", "http://localhost:8180/realms/" + realm.getName() + "/.well-known/openid-configuration"); + idpConfig.put("tokenUrl", "http://localhost:8180/realms/" + realm.getName() + "/protocol/openid-connect/token"); + idpConfig.put("authorizationUrl", "http://localhost:8180/realms/" + realm.getName() + "/protocol/openid-connect/auth"); idpConfig.put("useJwksUrl", "true"); - idpConfig.put("userInfoUrl", "http://localhost:8180/realms/realm-0/protocol/openid-connect/userinfo"); + idpConfig.put("userInfoUrl", "http://localhost:8180/realms/" + realm.getName() + "/protocol/openid-connect/userinfo"); idpConfig.put("validateSignature", "true"); idpConfig.put("clientId", "org-broker-client"); idpConfig.put("clientSecret", "secret"); idpConfig.put("clientAuthMethod", "client_secret_post"); realm.addIdentityProvider(identityProvider); - client.addRedirectUri("http://localhost:8180/realms/" + config.getRealmName() + "/broker/" + identityProvider.getAlias() + "/endpoint"); } if (provider.addIdentityProvider(organization, identityProvider)) { From a62624568672284f5fbcc12ae214f971bc7b15d8 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 21 Jun 2024 11:01:23 -0300 Subject: [PATCH 4/4] fixing creation of brokers Signed-off-by: Pedro Igor --- ...ganizationIdentityProviderProvisioner.java | 25 ++++++++++--------- .../organization/OrganizationProvisioner.java | 13 ++++++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java index 209376608..224613bc3 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationIdentityProviderProvisioner.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -86,19 +87,10 @@ protected void addIdentityProviders(KeycloakSession session, String orgId, Count OrganizationModel organization = provider.getById(orgId); int startIndex = getLastIndex(session, organization.getName()); RealmModel realm = session.getContext().getRealm(); - ClientModel client = realm.getClientByClientId("org-broker-client"); - - if (client == null) { - client = realm.addClient("org-broker-client"); - client.setSecret("secret"); - client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); - client.setPublicClient(false); - client.addRedirectUri("http://localhost:8180/realms/" + realm.getName() + "/broker/*"); - } long count = latch.getCount(); - for (int i = startIndex; i < count; i++) { + for (int i = startIndex; i < startIndex + count; i++) { String idpAlias = "idp-" + organization.getName() + "-" + i; IdentityProviderModel identityProvider = realm.getIdentityProviderByAlias(idpAlias); @@ -129,6 +121,8 @@ protected void addIdentityProviders(KeycloakSession session, String orgId, Count idpConfig.put("clientSecret", "secret"); idpConfig.put("clientAuthMethod", "client_secret_post"); realm.addIdentityProvider(identityProvider); + AtomicInteger lastIndex = (AtomicInteger) session.getAttribute("idpLastIndex"); + lastIndex.incrementAndGet(); } if (provider.addIdentityProvider(organization, identityProvider)) { @@ -138,7 +132,14 @@ protected void addIdentityProviders(KeycloakSession session, String orgId, Count } private int getLastIndex(KeycloakSession session, String orgName) { - RealmModel realm = session.getContext().getRealm(); - return ConfigUtil.findFreeEntityIndex(index -> realm.getIdentityProvidersStream().anyMatch(idp -> idp.getAlias().equals("idp-" + orgName + "-" + index))); + AtomicInteger lastIndex = (AtomicInteger) session.getAttribute("idpLastIndex"); + + if (lastIndex == null) { + RealmModel realm = session.getContext().getRealm(); + lastIndex = new AtomicInteger(ConfigUtil.findFreeEntityIndex(index -> realm.getIdentityProvidersStream().anyMatch(idp -> idp.getAlias().equals("idp-" + orgName + "-" + index)))); + session.setAttribute("idpLastIndex", lastIndex); + } + + return lastIndex.get(); } } diff --git a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java index adb2b07ce..f9fa4f1b8 100644 --- a/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/OrganizationProvisioner.java @@ -38,6 +38,7 @@ import org.keycloak.benchmark.dataset.TaskResponse; import org.keycloak.benchmark.dataset.config.ConfigUtil; import org.keycloak.benchmark.dataset.config.DatasetConfig; +import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -45,6 +46,7 @@ import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; import org.keycloak.organization.OrganizationProvider; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.utils.StringUtil; public class OrganizationProvisioner extends AbstractOrganizationProvisioner { @@ -109,6 +111,17 @@ private Runnable createOrganizations() { Integer count = config.getCount(); executor.addTask(() -> runJobInTransactionWithTimeout(session -> { + RealmModel realm = getRealm(session); + ClientModel client = realm.getClientByClientId("org-broker-client"); + + if (client == null) { + client = realm.addClient("org-broker-client"); + client.setSecret("secret"); + client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + client.setPublicClient(false); + client.addRedirectUri("http://localhost:8180/realms/" + realm.getName() + "/broker/*"); + } + int startIndex = getLastIndex(session); if (isBlank(config.getName())) {