Make sure authz endpoints are documented in openapi spec

Closes: #25259

Signed-off-by: Sophie Tauchert <sophie@999eagle.moe>
This commit is contained in:
Sophie Tauchert 2023-12-08 16:45:13 +01:00 committed by GitHub
parent 80aa1d1367
commit 1d56e0371e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 85 additions and 40 deletions

View file

@ -21,16 +21,19 @@ package org.keycloak.authorization.admin;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class AuthorizationService {
private final AdminPermissionEvaluator auth;
@ -48,7 +51,7 @@ public class AuthorizationService {
}
@Path("/resource-server")
public Object resourceServer() {
public ResourceServerService resourceServer() {
if (resourceServer == null) {
throw new NotFoundException();
}

View file

@ -19,17 +19,20 @@ package org.keycloak.authorization.admin;
import java.util.List;
import java.util.Map;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class PermissionService extends PolicyService {
public PermissionService(ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {

View file

@ -35,6 +35,7 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@ -56,13 +57,15 @@ import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentati
import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class PolicyService {
protected final ResourceServer resourceServer;

View file

@ -31,6 +31,8 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.events.admin.OperationType;
@ -46,6 +48,7 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
@ -54,6 +57,7 @@ import java.util.Collections;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class ResourceServerService {
private final AuthorizationProvider authorization;
@ -91,8 +95,9 @@ public class ResourceServerService {
}
@PUT
@Consumes("application/json")
@Produces("application/json")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@APIResponse(responseCode = "204", description = "No Content")
public Response update(ResourceServerRepresentation server) {
this.auth.realm().requireManageAuthorization();
this.resourceServer.setAllowRemoteResourceManagement(server.isAllowRemoteResourceManagement());
@ -111,15 +116,15 @@ public class ResourceServerService {
}
@GET
@Produces("application/json")
public Response findById() {
@Produces(MediaType.APPLICATION_JSON)
public ResourceServerRepresentation findById() {
this.auth.realm().requireViewAuthorization();
return Response.ok(toRepresentation(this.resourceServer, this.client)).build();
return toRepresentation(this.resourceServer, this.client);
}
@Path("/settings")
@GET
@Produces("application/json")
@Produces(MediaType.APPLICATION_JSON)
public Response exportSettings() {
this.auth.realm().requireManageAuthorization();
return Response.ok(ModelToRepresentation.toResourceServerRepresentation(session, client)).build();
@ -128,6 +133,7 @@ public class ResourceServerService {
@Path("/import")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@APIResponse(responseCode = "204", description = "No Content")
public Response importSettings(ResourceServerRepresentation rep) {
this.auth.realm().requireManageAuthorization();
@ -156,7 +162,7 @@ public class ResourceServerService {
}
@Path("/permission")
public Object getPermissionTypeResource() {
public PermissionService getPermissionTypeResource() {
this.auth.realm().requireViewAuthorization();
return new PermissionService(this.resourceServer, this.authorization, this.auth, adminEvent);
}

View file

@ -44,6 +44,7 @@ import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
@ -67,12 +68,14 @@ import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentatio
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class ResourceSetService {
private final AuthorizationProvider authorization;
@ -131,11 +134,11 @@ public class ResourceSetService {
return toRepresentation(toModel(resource, this.resourceServer, authorization), resourceServer, authorization);
}
@Path("{id}")
@Path("{resource-id}")
@PUT
@Consumes("application/json")
@Produces("application/json")
public Response update(@PathParam("id") String id, ResourceRepresentation resource) {
public Response update(@PathParam("resource-id") String id, ResourceRepresentation resource) {
requireManage();
resource.setId(id);
StoreFactory storeFactory = this.authorization.getStoreFactory();
@ -153,9 +156,9 @@ public class ResourceSetService {
return Response.noContent().build();
}
@Path("{id}")
@Path("{resource-id}")
@DELETE
public Response delete(@PathParam("id") String id) {
public Response delete(@PathParam("resource-id") String id) {
requireManage();
StoreFactory storeFactory = authorization.getStoreFactory();
Resource resource = storeFactory.getResourceStore().findById(resourceServer.getRealm(), resourceServer, id);
@ -174,11 +177,11 @@ public class ResourceSetService {
return Response.noContent().build();
}
@Path("{id}")
@Path("{resource-id}")
@GET
@NoCache
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
public Response findById(@PathParam("resource-id") String id) {
return findById(id, resource -> toRepresentation(resource, resourceServer, authorization, true));
}
@ -194,11 +197,11 @@ public class ResourceSetService {
return Response.ok(toRepresentation.apply(model)).build();
}
@Path("{id}/scopes")
@Path("{resource-id}/scopes")
@GET
@NoCache
@Produces("application/json")
public Response getScopes(@PathParam("id") String id) {
public Response getScopes(@PathParam("resource-id") String id) {
requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
Resource model = storeFactory.getResourceStore().findById(resourceServer.getRealm(), resourceServer, id);
@ -237,11 +240,11 @@ public class ResourceSetService {
return Response.ok(scopes).build();
}
@Path("{id}/permissions")
@Path("{resource-id}/permissions")
@GET
@NoCache
@Produces("application/json")
public Response getPermissions(@PathParam("id") String id) {
public Response getPermissions(@PathParam("resource-id") String id) {
requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
ResourceStore resourceStore = storeFactory.getResourceStore();
@ -295,11 +298,11 @@ public class ResourceSetService {
return Response.ok(representation).build();
}
@Path("{id}/attributes")
@Path("{resource-id}/attributes")
@GET
@NoCache
@Produces("application/json")
public Response getAttributes(@PathParam("id") String id) {
public Response getAttributes(@PathParam("resource-id") String id) {
requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
Resource model = storeFactory.getResourceStore().findById(resourceServer.getRealm(), resourceServer, id);

View file

@ -16,6 +16,12 @@
*/
package org.keycloak.authorization.admin;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@ -33,8 +39,9 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
@ -54,6 +61,7 @@ import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
import static org.keycloak.models.utils.RepresentationToModel.toModel;
@ -61,6 +69,7 @@ import static org.keycloak.models.utils.RepresentationToModel.toModel;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class ScopeService {
private final AuthorizationProvider authorization;
@ -92,11 +101,11 @@ public class ScopeService {
return Response.status(Status.CREATED).entity(scope).build();
}
@Path("{id}")
@Path("{scope-id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response update(@PathParam("id") String id, ScopeRepresentation scope) {
public Response update(@PathParam("scope-id") String id, ScopeRepresentation scope) {
this.auth.realm().requireManageAuthorization();
scope.setId(id);
StoreFactory storeFactory = authorization.getStoreFactory();
@ -113,9 +122,9 @@ public class ScopeService {
return Response.noContent().build();
}
@Path("{id}")
@Path("{scope-id}")
@DELETE
public Response delete(@PathParam("id") String id) {
public Response delete(@PathParam("scope-id") String id) {
this.auth.realm().requireManageAuthorization();
StoreFactory storeFactory = authorization.getStoreFactory();
RealmModel realm = resourceServer.getRealm();
@ -151,11 +160,18 @@ public class ScopeService {
return Response.noContent().build();
}
@Path("{id}")
@Path("{scope-id}")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Response findById(@PathParam("id") String id) {
@APIResponses(value = {
@APIResponse(
responseCode = "200",
content = @Content(schema = @Schema(implementation = ScopeRepresentation.class))
),
@APIResponse(responseCode = "404", description = "Not found")
})
public Response findById(@PathParam("scope-id") String id) {
this.auth.realm().requireViewAuthorization();
Scope model = this.authorization.getStoreFactory().getScopeStore().findById(resourceServer.getRealm(), resourceServer, id);
@ -166,11 +182,18 @@ public class ScopeService {
return Response.ok(toRepresentation(model)).build();
}
@Path("{id}/resources")
@Path("{scope-id}/resources")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Response getResources(@PathParam("id") String id) {
@APIResponses(value = {
@APIResponse(
responseCode = "200",
content = @Content(schema = @Schema(implementation = ResourceRepresentation.class, type = SchemaType.ARRAY))
),
@APIResponse(responseCode = "404", description = "Not found")
})
public Response getResources(@PathParam("scope-id") String id) {
this.auth.realm().requireViewAuthorization();
StoreFactory storeFactory = this.authorization.getStoreFactory();
Scope model = storeFactory.getScopeStore().findById(resourceServer.getRealm(), resourceServer, id);
@ -189,11 +212,18 @@ public class ScopeService {
}).collect(Collectors.toList())).build();
}
@Path("{id}/permissions")
@Path("{scope-id}/permissions")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Response getPermissions(@PathParam("id") String id) {
@APIResponses(value = {
@APIResponse(
responseCode = "200",
content = @Content(schema = @Schema(implementation = PolicyRepresentation.class, type = SchemaType.ARRAY))
),
@APIResponse(responseCode = "404", description = "Not found")
})
public Response getPermissions(@PathParam("scope-id") String id) {
this.auth.realm().requireViewAuthorization();
StoreFactory storeFactory = this.authorization.getStoreFactory();
Scope model = storeFactory.getScopeStore().findById(resourceServer.getRealm(), resourceServer, id);
@ -238,8 +268,8 @@ public class ScopeService {
@GET
@NoCache
@Produces("application/json")
public Response findAll(@QueryParam("scopeId") String id,
@Produces(MediaType.APPLICATION_JSON)
public Stream<ScopeRepresentation> findAll(@QueryParam("scopeId") String id,
@QueryParam("name") String name,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
@ -255,11 +285,8 @@ public class ScopeService {
search.put(Scope.FilterOption.NAME, new String[] {name});
}
return Response.ok(
this.authorization.getStoreFactory().getScopeStore().findByResourceServer(this.resourceServer, search, firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
.map(scope -> toRepresentation(scope))
.collect(Collectors.toList()))
.build();
return this.authorization.getStoreFactory().getScopeStore().findByResourceServer(this.resourceServer, search, firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
.map(scope -> toRepresentation(scope));
}
private void audit(ScopeRepresentation resource, OperationType operation) {