Split OrganizationResource into OrganizationResource and OrganizationsResource

Closes #29574

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik 2024-05-21 10:54:39 +02:00 committed by Pedro Igor
parent 55bf4feebc
commit 1e597cca3e
6 changed files with 247 additions and 220 deletions

View file

@ -47,7 +47,6 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
@Provider @Provider
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") @Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
@ -56,18 +55,16 @@ public class OrganizationIdentityProvidersResource {
private final RealmModel realm; private final RealmModel realm;
private final OrganizationProvider organizationProvider; private final OrganizationProvider organizationProvider;
private final OrganizationModel organization; private final OrganizationModel organization;
private final AdminPermissionEvaluator auth;
public OrganizationIdentityProvidersResource() { public OrganizationIdentityProvidersResource() {
// needed for registering to the JAX-RS stack // 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.realm = session == null ? null : session.getContext().getRealm();
this.organizationProvider = session == null ? null : session.getProvider(OrganizationProvider.class); this.organizationProvider = session == null ? null : session.getProvider(OrganizationProvider.class);
this.organization = organization; this.organization = organization;
this.auth = auth;
} }
@POST @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, " + 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") "or if it is already associated with the organization, an error response is returned")
public Response addIdentityProvider(String id) { public Response addIdentityProvider(String id) {
auth.realm().requireManageRealm();
try { try {
IdentityProviderModel identityProvider = this.realm.getIdentityProvidersStream() IdentityProviderModel identityProvider = this.realm.getIdentityProvidersStream()
.filter(p -> Objects.equals(p.getAlias(), id) || Objects.equals(p.getInternalId(), id)) .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) @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Returns all identity providers associated with the organization") @Operation(summary = "Returns all identity providers associated with the organization")
public Stream<IdentityProviderRepresentation> getIdentityProviders() { public Stream<IdentityProviderRepresentation> getIdentityProviders() {
auth.realm().requireManageRealm();
return organization.getIdentityProviders().map(this::toRepresentation); return organization.getIdentityProviders().map(this::toRepresentation);
} }

View file

@ -53,7 +53,6 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.StringUtil;
@Provider @Provider
@ -64,7 +63,6 @@ public class OrganizationMemberResource {
private final RealmModel realm; private final RealmModel realm;
private final OrganizationProvider provider; private final OrganizationProvider provider;
private final OrganizationModel organization; private final OrganizationModel organization;
private final AdminPermissionEvaluator auth;
private final AdminEventBuilder adminEvent; private final AdminEventBuilder adminEvent;
public OrganizationMemberResource() { public OrganizationMemberResource() {
@ -72,16 +70,14 @@ public class OrganizationMemberResource {
this.realm = null; this.realm = null;
this.provider = null; this.provider = null;
this.organization = null; this.organization = null;
this.auth = null;
this.adminEvent = 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.session = session;
this.realm = session.getContext().getRealm(); this.realm = session.getContext().getRealm();
this.provider = session.getProvider(OrganizationProvider.class); this.provider = session.getProvider(OrganizationProvider.class);
this.organization = organization; this.organization = organization;
this.auth = auth;
this.adminEvent = adminEvent; 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 existing user with the organization. If no user is found, or if it is already associated with the organization, " +
"an error response is returned") "an error response is returned")
public Response addMember(String id) { public Response addMember(String id) {
auth.realm().requireManageRealm();
UserModel user = session.users().getUserById(realm, id); UserModel user = session.users().getUserById(realm, id);
if (user == null) { 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 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 @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); 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," + "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") "an error response with status NOT_FOUND is returned")
public UserRepresentation get(@PathParam("id") String id) { public UserRepresentation get(@PathParam("id") String id) {
auth.realm().requireManageRealm();
if (StringUtil.isBlank(id)) { if (StringUtil.isBlank(id)) {
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST); 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 " + "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") "a member of the organization, an error response is returned")
public Response delete(@PathParam("id") String id) { public Response delete(@PathParam("id") String id) {
auth.realm().requireManageRealm();
if (StringUtil.isBlank(id)) { if (StringUtil.isBlank(id)) {
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST); throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
} }
@ -191,7 +183,6 @@ public class OrganizationMemberResource {
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Returns the organization associated with the user that has the specified id") @Operation(summary = "Returns the organization associated with the user that has the specified id")
public OrganizationRepresentation getOrganization(@PathParam("id") String id) { public OrganizationRepresentation getOrganization(@PathParam("id") String id) {
auth.realm().requireManageRealm();
if (StringUtil.isBlank(id)) { if (StringUtil.isBlank(id)) {
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST); throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
} }

View file

@ -17,44 +17,26 @@
package org.keycloak.organization.admin.resource; 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.Consumes;
import jakarta.ws.rs.DELETE; import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT; import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider; import jakarta.ws.rs.ext.Provider;
import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension; 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.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.NoCache; import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.organization.OrganizationProvider; 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.representations.idm.OrganizationRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder; 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 @Provider
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") @Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
@ -62,214 +44,54 @@ public class OrganizationResource {
private final KeycloakSession session; private final KeycloakSession session;
private final OrganizationProvider provider; private final OrganizationProvider provider;
private final AdminPermissionEvaluator auth;
private final AdminEventBuilder adminEvent; private final AdminEventBuilder adminEvent;
private final OrganizationModel organization;
public OrganizationResource() { public OrganizationResource() {
// needed for registering to the JAX-RS stack // needed for registering to the JAX-RS stack
this(null, null, null); this(null, null, null);
} }
public OrganizationResource(KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { public OrganizationResource(KeycloakSession session, OrganizationModel organization, AdminEventBuilder adminEvent) {
this.session = session; this.session = session;
this.provider = session == null ? null : session.getProvider(OrganizationProvider.class); this.provider = session == null ? null : session.getProvider(OrganizationProvider.class);
this.auth = auth; this.organization = organization;
this.adminEvent = adminEvent; 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<String> 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<OrganizationRepresentation> 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<String, String> 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 @GET
@NoCache @NoCache
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Returns the organization associated with the specified id, or null if no organization is found") @Operation(summary = "Returns the organization representation")
public OrganizationRepresentation get(@PathParam("id") String id) { public OrganizationRepresentation get() {
auth.realm().requireManageRealm(); return Organizations.toRepresentation(organization);
checkOrganizationsEnabled();
if (StringUtil.isBlank(id)) {
throw ErrorResponse.error("Id cannot be null.", Response.Status.BAD_REQUEST);
} }
return toRepresentation(getOrganization(id));
}
@Path("{id}")
@DELETE @DELETE
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Deletes the organization with the specified id") @Operation(summary = "Deletes the organization")
public Response delete(@PathParam("id") String id) { public Response delete() {
auth.realm().requireManageRealm(); provider.remove(organization);
checkOrganizationsEnabled();
if (StringUtil.isBlank(id)) {
throw ErrorResponse.error("Id cannot be null.", Response.Status.BAD_REQUEST);
}
provider.remove(getOrganization(id));
return Response.noContent().build(); return Response.noContent().build();
} }
@Path("{id}")
@PUT @PUT
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Updates the organization with the specified id") @Operation(summary = "Updates the organization")
public Response update(@PathParam("id") String id, OrganizationRepresentation organization) { public Response update(OrganizationRepresentation organizationRep) {
auth.realm().requireManageRealm(); Organizations.toModel(organizationRep, organization);
checkOrganizationsEnabled();
OrganizationModel model = getOrganization(id);
toModel(organization, model);
return Response.noContent().build(); return Response.noContent().build();
} }
@Path("{id}/members") @Path("members")
public OrganizationMemberResource members(@PathParam("id") String id) { public OrganizationMemberResource members() {
checkOrganizationsEnabled(); return new OrganizationMemberResource(session, organization, adminEvent);
return new OrganizationMemberResource(session, getOrganization(id), auth, adminEvent);
} }
@Path("{id}/identity-providers") @Path("identity-providers")
public OrganizationIdentityProvidersResource identityProvider(@PathParam("id") String id) { public OrganizationIdentityProvidersResource identityProvider() {
checkOrganizationsEnabled(); return new OrganizationIdentityProvidersResource(session, organization, adminEvent);
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);
}
} }
} }

View file

@ -27,11 +27,10 @@ public class OrganizationResourceProvider implements AdminRealmResourceProvider
@Override @Override
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
return new OrganizationResource(session, auth, adminEvent); return new OrganizationsResource(session, auth, adminEvent);
} }
@Override @Override
public void close() { public void close() {
} }
} }

View file

@ -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<String> 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<OrganizationRepresentation> 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<String, String> 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);
}
}

View file

@ -17,20 +17,28 @@
package org.keycloak.organization.utils; package org.keycloak.organization.utils;
import static java.util.Optional.ofNullable;
import jakarta.ws.rs.core.Response;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature; import org.keycloak.common.Profile.Feature;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.organization.OrganizationProvider; 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; import org.keycloak.utils.StringUtil;
public class Organizations { 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 // 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());
} }
} }