Split OrganizationResource into OrganizationResource and OrganizationsResource
Closes #29574 Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
parent
55bf4feebc
commit
1e597cca3e
6 changed files with 247 additions and 220 deletions
|
@ -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<IdentityProviderRepresentation> getIdentityProviders() {
|
||||
auth.realm().requireManageRealm();
|
||||
return organization.getIdentityProviders().map(this::toRepresentation);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue