Add OpenAPI documentation for the Organization API

Closes #29479

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2024-05-16 14:20:37 -03:00 committed by Pedro Igor
parent 8151c93bc7
commit bfa4660ecd
3 changed files with 74 additions and 27 deletions

View file

@ -32,6 +32,10 @@ import jakarta.ws.rs.ext.Provider;
import java.util.Objects;
import java.util.stream.Stream;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
@ -41,10 +45,12 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
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 = "")
public class OrganizationIdentityProvidersResource {
private final RealmModel realm;
@ -66,6 +72,10 @@ public class OrganizationIdentityProvidersResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Adds the identity provider with the specified id to the organization",
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();
@ -90,6 +100,9 @@ public class OrganizationIdentityProvidersResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
@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);
@ -98,6 +111,11 @@ public class OrganizationIdentityProvidersResource {
@Path("{alias}")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Returns the identity provider associated with the organization that has the specified alias",
description = "Searches for an identity provider with the given alias. If one is found and is associated with the " +
"organization, it is returned. Otherwise, an error response with status NOT_FOUND is returned")
public IdentityProviderRepresentation getIdentityProvider(@PathParam("alias") String alias) {
IdentityProviderModel broker = realm.getIdentityProviderByAlias(alias);
@ -111,6 +129,10 @@ public class OrganizationIdentityProvidersResource {
@Path("{alias}")
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Removes the identity provider with the specified alias from the organization",
description = "Breaks the association between the identity provider and the organization. The provider itself is not deleted. " +
"If no provider is found, or if it is not currently associated with the org, an error response is returned")
public Response delete(@PathParam("alias") String alias) {
IdentityProviderModel broker = realm.getIdentityProviderByAlias(alias);

View file

@ -17,8 +17,6 @@
package org.keycloak.organization.admin.resource;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import jakarta.ws.rs.Consumes;
@ -27,7 +25,6 @@ import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
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;
@ -38,37 +35,28 @@ import jakarta.ws.rs.core.Response.Status;
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.keycloak.OAuth2Constants;
import org.keycloak.common.util.Time;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.authentication.actiontoken.inviteorg.InviteOrgActionToken;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.*;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.UserResource;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.utils.StringUtil;
@Provider
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class OrganizationMemberResource {
private final KeycloakSession session;
@ -98,6 +86,10 @@ public class OrganizationMemberResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Adds the user with the specified id as a member of the organization", description = "Adds, or associates, " +
"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);
@ -119,6 +111,8 @@ public class OrganizationMemberResource {
@Path("invite-user")
@POST
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Invites a new user to the organization using the specified e-mail address")
public Response inviteUser(String email) {
return new OrganizationInvitationResource(session, organization, adminEvent).inviteUser(email);
}
@ -126,13 +120,17 @@ public class OrganizationMemberResource {
@POST
@Path("invite-existing-user")
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Invites an existing user to the organization, using the specified user id")
public Response inviteExistingUser(String id) {
return new OrganizationInvitationResource(session, organization, adminEvent).inviteExistingUser(id);
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation( summary = "Return a paginated list of organization members filtered according to the specified parameters")
@NoCache
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation( summary = "Returns a paginated list of organization members filtered according to the specified parameters")
public Stream<UserRepresentation> search(
@Parameter(description = "A String representing either a member's username, e-mail, first name, or last name.") @QueryParam("search") String search,
@Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact,
@ -146,6 +144,11 @@ public class OrganizationMemberResource {
@Path("{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation( summary = "Returns the member of the organization with the specified id", description = "Searches for a" +
"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)) {
@ -157,6 +160,10 @@ public class OrganizationMemberResource {
@Path("{id}")
@DELETE
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Removes the user with the specified id from the organization", description = "Breaks the association " +
"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)) {
@ -172,17 +179,12 @@ public class OrganizationMemberResource {
throw ErrorResponse.error("Not a member of the organization", Status.BAD_REQUEST);
}
@Path("{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response update(@PathParam("id") String id, UserRepresentation user) {
auth.realm().requireManageRealm();
return new UserResource(session, getMember(id), auth, adminEvent).updateUser(user);
}
@Path("{id}/organization")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
@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)) {

View file

@ -39,6 +39,7 @@ 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;
@ -56,6 +57,7 @@ import org.keycloak.utils.SearchQueryUtils;
import org.keycloak.utils.StringUtil;
@Provider
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class OrganizationResource {
private final KeycloakSession session;
@ -75,8 +77,16 @@ public class OrganizationResource {
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();
if (organization == null) {
@ -107,7 +117,7 @@ public class OrganizationResource {
@Produces(MediaType.APPLICATION_JSON)
@NoCache
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation( summary = "Return a paginated list of organizations filtered according to the specified parameters")
@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,
@ -126,9 +136,18 @@ public class OrganizationResource {
}
}
/**
* 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();
if (StringUtil.isBlank(id)) {
@ -140,6 +159,8 @@ public class OrganizationResource {
@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();
if (StringUtil.isBlank(id)) {
@ -154,6 +175,8 @@ public class OrganizationResource {
@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();
OrganizationModel model = getOrganization(id);