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..27d1df077 --- /dev/null +++ b/dataset/src/main/java/org/keycloak/benchmark/dataset/organization/AbstractOrganizationProvisioner.java @@ -0,0 +1,109 @@ +/* + * 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; + +/** + * @author Pedro Igor + */ +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[]