From 1e597cca3e00eeab0328e1adcbc4a8f3b2194bd6 Mon Sep 17 00:00:00 2001 From: vramik Date: Tue, 21 May 2024 10:54:39 +0200 Subject: [PATCH] Split OrganizationResource into OrganizationResource and OrganizationsResource Closes #29574 Signed-off-by: vramik --- ...OrganizationIdentityProvidersResource.java | 10 +- .../resource/OrganizationMemberResource.java | 11 +- .../admin/resource/OrganizationResource.java | 216 ++---------------- .../OrganizationResourceProvider.java | 3 +- .../admin/resource/OrganizationsResource.java | 161 +++++++++++++ .../organization/utils/Organizations.java | 66 +++++- 6 files changed, 247 insertions(+), 220 deletions(-) create mode 100644 services/src/main/java/org/keycloak/organization/admin/resource/OrganizationsResource.java diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationIdentityProvidersResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationIdentityProvidersResource.java index 16140783f8..b8e4776a52 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationIdentityProvidersResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationIdentityProvidersResource.java @@ -47,7 +47,6 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.AdminEventBuilder; -import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; @Provider @Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") @@ -56,18 +55,16 @@ public class OrganizationIdentityProvidersResource { private final RealmModel realm; private final OrganizationProvider organizationProvider; private final OrganizationModel organization; - private final AdminPermissionEvaluator auth; public OrganizationIdentityProvidersResource() { // needed for registering to the JAX-RS stack - this(null, null, null, null); + this(null, null, null); } - public OrganizationIdentityProvidersResource(KeycloakSession session, OrganizationModel organization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { + public OrganizationIdentityProvidersResource(KeycloakSession session, OrganizationModel organization, AdminEventBuilder adminEvent) { this.realm = session == null ? null : session.getContext().getRealm(); this.organizationProvider = session == null ? null : session.getProvider(OrganizationProvider.class); this.organization = organization; - this.auth = auth; } @POST @@ -77,8 +74,6 @@ public class OrganizationIdentityProvidersResource { description = "Adds, or associates, an existing identity provider with the organization. If no identity provider is found, " + "or if it is already associated with the organization, an error response is returned") public Response addIdentityProvider(String id) { - auth.realm().requireManageRealm(); - try { IdentityProviderModel identityProvider = this.realm.getIdentityProvidersStream() .filter(p -> Objects.equals(p.getAlias(), id) || Objects.equals(p.getInternalId(), id)) @@ -104,7 +99,6 @@ public class OrganizationIdentityProvidersResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) @Operation(summary = "Returns all identity providers associated with the organization") public Stream getIdentityProviders() { - auth.realm().requireManageRealm(); return organization.getIdentityProviders().map(this::toRepresentation); } diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java index 5d96612356..081bed8ceb 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationMemberResource.java @@ -53,7 +53,6 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.AdminEventBuilder; -import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.utils.StringUtil; @Provider @@ -64,7 +63,6 @@ public class OrganizationMemberResource { private final RealmModel realm; private final OrganizationProvider provider; private final OrganizationModel organization; - private final AdminPermissionEvaluator auth; private final AdminEventBuilder adminEvent; public OrganizationMemberResource() { @@ -72,16 +70,14 @@ public class OrganizationMemberResource { this.realm = null; this.provider = null; this.organization = null; - this.auth = null; this.adminEvent = null; } - public OrganizationMemberResource(KeycloakSession session, OrganizationModel organization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { + public OrganizationMemberResource(KeycloakSession session, OrganizationModel organization, AdminEventBuilder adminEvent) { this.session = session; this.realm = session.getContext().getRealm(); this.provider = session.getProvider(OrganizationProvider.class); this.organization = organization; - this.auth = auth; this.adminEvent = adminEvent; } @@ -92,7 +88,6 @@ public class OrganizationMemberResource { "an existing user with the organization. If no user is found, or if it is already associated with the organization, " + "an error response is returned") public Response addMember(String id) { - auth.realm().requireManageRealm(); UserModel user = session.users().getUserById(realm, id); if (user == null) { @@ -142,7 +137,6 @@ public class OrganizationMemberResource { @Parameter(description = "The position of the first result to be processed (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer first, @Parameter(description = "The maximum number of results to be returned. Defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max ) { - auth.realm().requireManageRealm(); return provider.getMembersStream(organization, search, exact, first, max).map(this::toRepresentation); } @@ -155,7 +149,6 @@ public class OrganizationMemberResource { "user with the given id. If one is found, and is currently a member of the organization, returns it. Otherwise," + "an error response with status NOT_FOUND is returned") public UserRepresentation get(@PathParam("id") String id) { - auth.realm().requireManageRealm(); if (StringUtil.isBlank(id)) { throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST); } @@ -170,7 +163,6 @@ public class OrganizationMemberResource { "between the user and organization. The user itself is not deleted. If no user is found, or if they are not " + "a member of the organization, an error response is returned") public Response delete(@PathParam("id") String id) { - auth.realm().requireManageRealm(); if (StringUtil.isBlank(id)) { throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST); } @@ -191,7 +183,6 @@ public class OrganizationMemberResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) @Operation(summary = "Returns the organization associated with the user that has the specified id") public OrganizationRepresentation getOrganization(@PathParam("id") String id) { - auth.realm().requireManageRealm(); if (StringUtil.isBlank(id)) { throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST); } diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java index 69dcd99559..51796e8ae5 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java @@ -17,44 +17,26 @@ package org.keycloak.organization.admin.resource; -import static java.util.Optional.ofNullable; - -import java.util.Map; -import java.util.Set; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.extensions.Extension; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.reactive.NoCache; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.OrganizationDomainModel; import org.keycloak.models.OrganizationModel; import org.keycloak.organization.OrganizationProvider; -import org.keycloak.representations.idm.OrganizationDomainRepresentation; +import org.keycloak.organization.utils.Organizations; import org.keycloak.representations.idm.OrganizationRepresentation; -import org.keycloak.services.ErrorResponse; import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.AdminEventBuilder; -import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; -import org.keycloak.utils.SearchQueryUtils; -import org.keycloak.utils.StringUtil; @Provider @Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") @@ -62,214 +44,54 @@ public class OrganizationResource { private final KeycloakSession session; private final OrganizationProvider provider; - private final AdminPermissionEvaluator auth; private final AdminEventBuilder adminEvent; + private final OrganizationModel organization; public OrganizationResource() { // needed for registering to the JAX-RS stack this(null, null, null); } - public OrganizationResource(KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { + public OrganizationResource(KeycloakSession session, OrganizationModel organization, AdminEventBuilder adminEvent) { this.session = session; this.provider = session == null ? null : session.getProvider(OrganizationProvider.class); - this.auth = auth; + this.organization = organization; this.adminEvent = adminEvent; } - /** - * Creates a new organization based on the specified {@link OrganizationRepresentation}. - * - * @param organization the representation containing the organization data. - * @return a {@link Response} containing the status of the operation. - */ - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) - @Operation( summary = "Creates a new organization") - public Response create(OrganizationRepresentation organization) { - auth.realm().requireManageRealm(); - checkOrganizationsEnabled(); - if (organization == null) { - throw ErrorResponse.error("Organization cannot be null.", Response.Status.BAD_REQUEST); - } - - Set domains = ofNullable(organization.getDomains()).orElse(Set.of()).stream().map(OrganizationDomainRepresentation::getName).collect(Collectors.toSet()); - OrganizationModel model = provider.create(organization.getName(), domains); - - toModel(organization, model); - - return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build(); - } - - /** - * Returns a stream of organizations, filtered according to query parameters. - * - * @param search a {@code String} representing either an organization name or domain. - * @param searchQuery a query to search for organization attributes, in the format 'key1:value2 key2:value2'. - * @param exact if {@code true}, the organizations will be searched using exact match for the {@code search} param - i.e. - * either the organization name or one of its domains must match exactly the {@code search} param. If false, - * the method returns all organizations whose name or (domains) partially match the {@code search} param. - * @param first the position of the first result to be processed (pagination offset). Ignored if negative or {@code null}. - * @param max the maximum number of results to be returned. Ignored if negative or {@code null}. - * @return a non-null {@code Stream} of matched organizations. - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - @NoCache - @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) - @Operation( summary = "Returns a paginated list of organizations filtered according to the specified parameters") - public Stream search( - @Parameter(description = "A String representing either an organization name or domain") @QueryParam("search") String search, - @Parameter(description = "A query to search for custom attributes, in the format 'key1:value2 key2:value2'") @QueryParam("q") String searchQuery, - @Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact, - @Parameter(description = "The position of the first result to be processed (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer first, - @Parameter(description = "The maximum number of results to be returned - defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max - ) { - auth.realm().requireManageRealm(); - checkOrganizationsEnabled(); - - // check if are searching orgs by attribute. - if(StringUtil.isNotBlank(searchQuery)) { - Map attributes = SearchQueryUtils.getFields(searchQuery); - return provider.getAllStream(attributes, first, max).map(this::toRepresentation); - } else { - return provider.getAllStream(search, exact, first, max).map(this::toRepresentation); - } - } - - /** - * Returns the organization associated with the specified {@code id}. - * - * @param id the organization id. - * @return the organization associated with the specified id, or {@code null} if no organization is found. - */ - @Path("{id}") @GET @NoCache @Produces(MediaType.APPLICATION_JSON) @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) - @Operation(summary = "Returns the organization associated with the specified id, or null if no organization is found") - public OrganizationRepresentation get(@PathParam("id") String id) { - auth.realm().requireManageRealm(); - checkOrganizationsEnabled(); - if (StringUtil.isBlank(id)) { - throw ErrorResponse.error("Id cannot be null.", Response.Status.BAD_REQUEST); - } - - return toRepresentation(getOrganization(id)); + @Operation(summary = "Returns the organization representation") + public OrganizationRepresentation get() { + return Organizations.toRepresentation(organization); } - @Path("{id}") @DELETE @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) - @Operation(summary = "Deletes the organization with the specified id") - public Response delete(@PathParam("id") String id) { - auth.realm().requireManageRealm(); - checkOrganizationsEnabled(); - if (StringUtil.isBlank(id)) { - throw ErrorResponse.error("Id cannot be null.", Response.Status.BAD_REQUEST); - } - - provider.remove(getOrganization(id)); - + @Operation(summary = "Deletes the organization") + public Response delete() { + provider.remove(organization); return Response.noContent().build(); } - @Path("{id}") @PUT @Consumes(MediaType.APPLICATION_JSON) @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) - @Operation(summary = "Updates the organization with the specified id") - public Response update(@PathParam("id") String id, OrganizationRepresentation organization) { - auth.realm().requireManageRealm(); - checkOrganizationsEnabled(); - OrganizationModel model = getOrganization(id); - toModel(organization, model); - + @Operation(summary = "Updates the organization") + public Response update(OrganizationRepresentation organizationRep) { + Organizations.toModel(organizationRep, organization); return Response.noContent().build(); } - @Path("{id}/members") - public OrganizationMemberResource members(@PathParam("id") String id) { - checkOrganizationsEnabled(); - return new OrganizationMemberResource(session, getOrganization(id), auth, adminEvent); + @Path("members") + public OrganizationMemberResource members() { + return new OrganizationMemberResource(session, organization, adminEvent); } - @Path("{id}/identity-providers") - public OrganizationIdentityProvidersResource identityProvider(@PathParam("id") String id) { - checkOrganizationsEnabled(); - return new OrganizationIdentityProvidersResource(session, getOrganization(id), auth, adminEvent); - } - - private OrganizationModel getOrganization(String id) { - //checking permissions before trying to find organization to be able to return 403 (instead of 400 or 404) - auth.realm().requireManageRealm(); - - if (id == null) { - throw ErrorResponse.error("Id cannot be null.", Response.Status.BAD_REQUEST); - } - - OrganizationModel model = provider.getById(id); - - if (model == null) { - throw ErrorResponse.error("Organization not found.", Response.Status.NOT_FOUND); - } - - session.setAttribute(OrganizationModel.class.getName(), model); - - return model; - } - - private OrganizationRepresentation toRepresentation(OrganizationModel model) { - if (model == null) { - return null; - } - - OrganizationRepresentation rep = new OrganizationRepresentation(); - - rep.setId(model.getId()); - rep.setName(model.getName()); - rep.setEnabled(model.isEnabled()); - rep.setDescription(model.getDescription()); - rep.setAttributes(model.getAttributes()); - model.getDomains().filter(Objects::nonNull).map(this::toRepresentation) - .forEach(rep::addDomain); - - return rep; - } - - private OrganizationDomainRepresentation toRepresentation(OrganizationDomainModel model) { - OrganizationDomainRepresentation representation = new OrganizationDomainRepresentation(); - representation.setName(model.getName()); - representation.setVerified(model.getVerified()); - return representation; - } - - private OrganizationModel toModel(OrganizationRepresentation rep, OrganizationModel model) { - if (rep == null) { - return null; - } - - model.setName(rep.getName()); - model.setEnabled(rep.isEnabled()); - model.setDescription(rep.getDescription()); - model.setAttributes(rep.getAttributes()); - model.setDomains(ofNullable(rep.getDomains()).orElse(Set.of()).stream() - .filter(Objects::nonNull) - .map(this::toModel) - .collect(Collectors.toSet())); - - return model; - } - - private OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) { - return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified()); - } - - private void checkOrganizationsEnabled() { - if (provider != null && !provider.isEnabled()) { - throw ErrorResponse.error("Organizations not enabled for this realm.", Response.Status.NOT_FOUND); - } + @Path("identity-providers") + public OrganizationIdentityProvidersResource identityProvider() { + return new OrganizationIdentityProvidersResource(session, organization, adminEvent); } } diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResourceProvider.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResourceProvider.java index 4a6865a656..ff6433ff95 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResourceProvider.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResourceProvider.java @@ -27,11 +27,10 @@ public class OrganizationResourceProvider implements AdminRealmResourceProvider @Override public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { - return new OrganizationResource(session, auth, adminEvent); + return new OrganizationsResource(session, auth, adminEvent); } @Override public void close() { - } } diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationsResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationsResource.java new file mode 100644 index 0000000000..52e73b9b9f --- /dev/null +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationsResource.java @@ -0,0 +1,161 @@ +/* + * 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.organization.admin.resource; + +import static java.util.Optional.ofNullable; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.resteasy.reactive.NoCache; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OrganizationModel; +import org.keycloak.organization.OrganizationProvider; +import org.keycloak.organization.utils.Organizations; +import org.keycloak.representations.idm.OrganizationDomainRepresentation; +import org.keycloak.representations.idm.OrganizationRepresentation; +import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.KeycloakOpenAPI; +import org.keycloak.services.resources.admin.AdminEventBuilder; +import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +import org.keycloak.utils.SearchQueryUtils; +import org.keycloak.utils.StringUtil; + +@Provider +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") +public class OrganizationsResource { + + private final KeycloakSession session; + private final OrganizationProvider provider; + private final AdminPermissionEvaluator auth; + private final AdminEventBuilder adminEvent; + + public OrganizationsResource() { + // needed for registering to the JAX-RS stack + this(null, null, null); + } + + public OrganizationsResource(KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { + this.session = session; + this.provider = session == null ? null : session.getProvider(OrganizationProvider.class); + this.auth = auth; + this.adminEvent = adminEvent; + } + + /** + * Creates a new organization based on the specified {@link OrganizationRepresentation}. + * + * @param organization the representation containing the organization data. + * @return a {@link Response} containing the status of the operation. + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) + @Operation( summary = "Creates a new organization") + public Response create(OrganizationRepresentation organization) { + auth.realm().requireManageRealm(); + Organizations.checkEnabled(provider); + + if (organization == null) { + throw ErrorResponse.error("Organization cannot be null.", Response.Status.BAD_REQUEST); + } + + Set domains = ofNullable(organization.getDomains()).orElse(Set.of()).stream().map(OrganizationDomainRepresentation::getName).collect(Collectors.toSet()); + OrganizationModel model = provider.create(organization.getName(), domains); + + Organizations.toModel(organization, model); + + return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build(); + } + + /** + * Returns a stream of organizations, filtered according to query parameters. + * + * @param search a {@code String} representing either an organization name or domain. + * @param searchQuery a query to search for organization attributes, in the format 'key1:value2 key2:value2'. + * @param exact if {@code true}, the organizations will be searched using exact match for the {@code search} param - i.e. + * either the organization name or one of its domains must match exactly the {@code search} param. If false, + * the method returns all organizations whose name or (domains) partially match the {@code search} param. + * @param first the position of the first result to be processed (pagination offset). Ignored if negative or {@code null}. + * @param max the maximum number of results to be returned. Ignored if negative or {@code null}. + * @return a non-null {@code Stream} of matched organizations. + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) + @Operation( summary = "Returns a paginated list of organizations filtered according to the specified parameters") + public Stream search( + @Parameter(description = "A String representing either an organization name or domain") @QueryParam("search") String search, + @Parameter(description = "A query to search for custom attributes, in the format 'key1:value2 key2:value2'") @QueryParam("q") String searchQuery, + @Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact, + @Parameter(description = "The position of the first result to be processed (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer first, + @Parameter(description = "The maximum number of results to be returned - defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max + ) { + auth.realm().requireManageRealm(); + Organizations.checkEnabled(provider); + + // check if are searching orgs by attribute. + if (StringUtil.isNotBlank(searchQuery)) { + Map attributes = SearchQueryUtils.getFields(searchQuery); + return provider.getAllStream(attributes, first, max).map(Organizations::toRepresentation); + } else { + return provider.getAllStream(search, exact, first, max).map(Organizations::toRepresentation); + } + } + + /** + * Base path for the admin REST API for one particular organization. + */ + @Path("{id}") + public OrganizationResource get(@PathParam("id") String id) { + auth.realm().requireManageRealm(); + Organizations.checkEnabled(provider); + + if (StringUtil.isBlank(id)) { + throw ErrorResponse.error("Id cannot be null.", Response.Status.BAD_REQUEST); + } + + OrganizationModel organizationModel = provider.getById(id); + + if (organizationModel == null) { + throw ErrorResponse.error("Organization not found.", Response.Status.NOT_FOUND); + } + + session.setAttribute(OrganizationModel.class.getName(), organizationModel); + + return new OrganizationResource(session, organizationModel, adminEvent); + } +} diff --git a/services/src/main/java/org/keycloak/organization/utils/Organizations.java b/services/src/main/java/org/keycloak/organization/utils/Organizations.java index 29e81442c6..3c58201fc3 100644 --- a/services/src/main/java/org/keycloak/organization/utils/Organizations.java +++ b/services/src/main/java/org/keycloak/organization/utils/Organizations.java @@ -17,20 +17,28 @@ package org.keycloak.organization.utils; +import static java.util.Optional.ofNullable; + +import jakarta.ws.rs.core.Response; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; - +import java.util.stream.Collectors; import org.keycloak.common.Profile; import org.keycloak.common.Profile.Feature; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OrganizationDomainModel; import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.organization.OrganizationProvider; +import org.keycloak.representations.idm.OrganizationDomainRepresentation; +import org.keycloak.representations.idm.OrganizationRepresentation; +import org.keycloak.services.ErrorResponse; import org.keycloak.utils.StringUtil; public class Organizations { @@ -115,8 +123,60 @@ public class Organizations { }; } - public static boolean isEnabledAndOrganizationsPresent(OrganizationProvider organizationProvider) { + public static boolean isEnabledAndOrganizationsPresent(OrganizationProvider provider) { // todo replace getAllStream().findAny().isPresent() with count query - return organizationProvider != null && organizationProvider.isEnabled() && organizationProvider.getAllStream().findAny().isPresent(); + return provider != null && provider.isEnabled() && provider.getAllStream().findAny().isPresent(); + } + + public static void checkEnabled(OrganizationProvider provider) { + if (provider == null || !provider.isEnabled()) { + throw ErrorResponse.error("Organizations not enabled for this realm.", Response.Status.NOT_FOUND); + } + } + + public static OrganizationRepresentation toRepresentation(OrganizationModel model) { + if (model == null) { + return null; + } + + OrganizationRepresentation rep = new OrganizationRepresentation(); + + rep.setId(model.getId()); + rep.setName(model.getName()); + rep.setEnabled(model.isEnabled()); + rep.setDescription(model.getDescription()); + rep.setAttributes(model.getAttributes()); + model.getDomains().filter(Objects::nonNull).map(Organizations::toRepresentation) + .forEach(rep::addDomain); + + return rep; + } + + public static OrganizationDomainRepresentation toRepresentation(OrganizationDomainModel model) { + OrganizationDomainRepresentation representation = new OrganizationDomainRepresentation(); + representation.setName(model.getName()); + representation.setVerified(model.getVerified()); + return representation; + } + + public static OrganizationModel toModel(OrganizationRepresentation rep, OrganizationModel model) { + if (rep == null) { + return null; + } + + model.setName(rep.getName()); + model.setEnabled(rep.isEnabled()); + model.setDescription(rep.getDescription()); + model.setAttributes(rep.getAttributes()); + model.setDomains(ofNullable(rep.getDomains()).orElse(Set.of()).stream() + .filter(Objects::nonNull) + .map(Organizations::toModel) + .collect(Collectors.toSet())); + + return model; + } + + public static OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) { + return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified()); } }