diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java index b6628a9ac8..b073fdb896 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java @@ -94,6 +94,41 @@ public class PermissionResource { } } + /** + * Creates a new uma permission for a single resource and scope(s). + * + * @param ticket the {@link PermissionTicketRepresentation} representing the resource and scope(s) (not {@code null}) + * @return a permission response holding the permission ticket representation + */ + public PermissionTicketRepresentation create(final PermissionTicketRepresentation ticket) { + if (ticket == null) { + throw new IllegalArgumentException("Permission ticket must not be null or empty"); + } + if (ticket.getRequester() == null || ticket.getRequesterName() == null) { + throw new IllegalArgumentException("Permission ticket must have a requester"); + } + if (ticket.getResource() == null || ticket.getResourceName() == null) { + throw new IllegalArgumentException("Permission ticket must have a resource"); + } + if (ticket.getScope() == null || ticket.getScopeName() == null) { + throw new IllegalArgumentException("Permission ticket must have a scope"); + } + Callable callable = new Callable() { + @Override + public PermissionTicketRepresentation call() throws Exception { + return http.post(serverConfiguration.getPermissionEndpoint()+"/ticket") + .json(JsonSerialization.writeValueAsBytes(ticket)) + .authorizationBearer(pat.call()) + .response().json(new TypeReference(){}).execute(); + } + }; + try { + return callable.call(); + } catch (Exception cause) { + return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error updating permission ticket", cause); + } + } + /** * Query the server for any permission ticket associated with the given scopeId. * @@ -107,7 +142,7 @@ public class PermissionResource { Callable> callable = new Callable>() { @Override public List call() throws Exception { - return http.>get(serverConfiguration.getPermissionEndpoint()) + return http.>get(serverConfiguration.getPermissionEndpoint()+"/ticket") .authorizationBearer(pat.call()) .param("scopeId", scopeId) .response().json(new TypeReference>(){}).execute(); @@ -133,7 +168,7 @@ public class PermissionResource { Callable> callable = new Callable>() { @Override public List call() throws Exception { - return http.>get(serverConfiguration.getPermissionEndpoint()) + return http.>get(serverConfiguration.getPermissionEndpoint()+"/ticket") .authorizationBearer(pat.call()) .param("resourceId", resourceId) .response().json(new TypeReference>(){}).execute(); @@ -170,7 +205,7 @@ public class PermissionResource { Callable> callable = new Callable>() { @Override public List call() throws Exception { - return http.>get(serverConfiguration.getPermissionEndpoint()) + return http.>get(serverConfiguration.getPermissionEndpoint()+"/ticket") .authorizationBearer(pat.call()) .param("resourceId", resourceId) .param("scopeId", scopeId) @@ -205,7 +240,7 @@ public class PermissionResource { Callable callable = new Callable() { @Override public Object call() throws Exception { - http.put(serverConfiguration.getPermissionEndpoint()) + http.put(serverConfiguration.getPermissionEndpoint()+"/ticket") .json(JsonSerialization.writeValueAsBytes(ticket)) .authorizationBearer(pat.call()) .response().json(List.class).execute(); diff --git a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java index 7b6b29ea04..ce4dff6c5e 100644 --- a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java +++ b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java @@ -37,6 +37,7 @@ import org.keycloak.services.resources.admin.AdminEventBuilder; import javax.ws.rs.Path; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response.Status; +import org.keycloak.authorization.protection.permission.PermissionTicketService; /** * @author Pedro Igor @@ -82,6 +83,17 @@ public class ProtectionService { return resource; } + + @Path("/permission/ticket") + public Object ticket() { + KeycloakIdentity identity = createIdentity(false); + + PermissionTicketService resource = new PermissionTicketService(identity, getResourceServer(identity), this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } private KeycloakIdentity createIdentity(boolean checkProtectionScope) { KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession()); diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java index 6434f19b54..3adea4b26f 100644 --- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java @@ -17,31 +17,16 @@ */ package org.keycloak.authorization.protection.permission; -import org.keycloak.OAuthErrorException; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.common.KeycloakIdentity; -import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.representations.idm.authorization.PermissionRequest; -import org.keycloak.authorization.store.PermissionTicketStore; -import org.keycloak.models.Constants; -import org.keycloak.models.utils.ModelToRepresentation; -import org.keycloak.models.utils.RepresentationToModel; -import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation; -import org.keycloak.services.ErrorResponseException; import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; import javax.ws.rs.POST; -import javax.ws.rs.PUT; import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; /** * @author Pedro Igor @@ -64,82 +49,4 @@ public class PermissionService extends AbstractPermissionService { return super.create(request); } - @PUT - @Consumes("application/json") - public Response update(PermissionTicketRepresentation representation) { - if (representation == null || representation.getId() == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST); - } - - PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore(); - PermissionTicket ticket = ticketStore.findById(representation.getId(), resourceServer.getId()); - - if (ticket == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST); - } - - RepresentationToModel.toModel(representation, resourceServer.getId(), authorization); - - return Response.noContent().build(); - } - - @DELETE - @Consumes("application/json") - public Response delete(String id) { - if (id == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST); - } - - PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore(); - PermissionTicket ticket = ticketStore.findById(id, resourceServer.getId()); - - if (ticket == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST); - } - - ticketStore.delete(id); - - return Response.noContent().build(); - } - - @GET - @Produces("application/json") - public Response find(@QueryParam("scopeId") String scopeId, - @QueryParam("resourceId") String resourceId, - @QueryParam("owner") String owner, - @QueryParam("requester") String requester, - @QueryParam("granted") Boolean granted, - @QueryParam("returnNames") Boolean returnNames, - @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResult) { - PermissionTicketStore permissionTicketStore = authorization.getStoreFactory().getPermissionTicketStore(); - - Map filters = new HashMap<>(); - - if (resourceId != null) { - filters.put(PermissionTicket.RESOURCE, resourceId); - } - - if (scopeId != null) { - filters.put(PermissionTicket.SCOPE, scopeId); - } - - if (owner != null) { - filters.put(PermissionTicket.OWNER, owner); - } - - if (requester != null) { - filters.put(PermissionTicket.REQUESTER, requester); - } - - if (granted != null) { - filters.put(PermissionTicket.GRANTED, granted.toString()); - } - - return Response.ok().entity(permissionTicketStore.find(filters, resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS) - .stream() - .map(permissionTicket -> ModelToRepresentation.toRepresentation(permissionTicket, authorization, returnNames == null ? false : returnNames)) - .collect(Collectors.toList())) - .build(); - } } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java new file mode 100644 index 0000000000..951a780dc9 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java @@ -0,0 +1,215 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.permission; + +import org.keycloak.OAuthErrorException; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.PermissionTicketStore; +import org.keycloak.models.Constants; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation; +import org.keycloak.services.ErrorResponseException; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.models.UserModel; + +/** + * @author Pedro Igor + */ +public class PermissionTicketService { + + private final AuthorizationProvider authorization; + private final KeycloakIdentity identity; + private final ResourceServer resourceServer; + + public PermissionTicketService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization) { + this.identity = identity; + this.resourceServer = resourceServer; + this.authorization = authorization; + } + + @POST + @Consumes("application/json") + @Produces("application/json") + public Response create(PermissionTicketRepresentation representation) { + PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore(); + if (representation == null) + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_permission", Response.Status.BAD_REQUEST); + if (representation.getId() != null) + throw new ErrorResponseException("invalid_permission", "created permissions should not have id", Response.Status.BAD_REQUEST); + if (representation.getResource() == null) + throw new ErrorResponseException("invalid_permission", "created permissions should have resource", Response.Status.BAD_REQUEST); + if (representation.getScope() == null && representation.getScopeName() == null) + throw new ErrorResponseException("invalid_permission", "created permissions should have scope or scopeName", Response.Status.BAD_REQUEST); + if (representation.getRequester() == null && representation.getRequesterName() == null) + throw new ErrorResponseException("invalid_permission", "created permissions should have requester or requesterName", Response.Status.BAD_REQUEST); + + ResourceStore rstore = this.authorization.getStoreFactory().getResourceStore(); + Resource resource = rstore.findById(representation.getResource(), resourceServer.getId()); + if (resource == null ) throw new ErrorResponseException("invalid_resource_id", "Resource set with id [" + representation.getResource() + "] does not exists in this server.", Response.Status.BAD_REQUEST); + + if (!resource.getOwner().equals(this.identity.getId())) + throw new ErrorResponseException("not_authorised", "permissions for [" + representation.getResource() + "] can be only created by the owner", Response.Status.FORBIDDEN); + + UserModel user = null; + if(representation.getRequester() != null) + user = this.authorization.getKeycloakSession().userStorageManager().getUserById(representation.getRequester(), this.authorization.getRealm()); + else + user = this.authorization.getKeycloakSession().userStorageManager().getUserByUsername(representation.getRequesterName(), this.authorization.getRealm()); + + if (user == null) + throw new ErrorResponseException("invalid_permission", "Requester does not exists in this server as user.", Response.Status.BAD_REQUEST); + + Scope scope = null; + ScopeStore sstore = this.authorization.getStoreFactory().getScopeStore(); + + if(representation.getScopeName() != null) + scope = sstore.findByName(representation.getScopeName(), resourceServer.getId()); + else + scope = sstore.findById(representation.getScope(), resourceServer.getId()); + + if (scope == null && representation.getScope() !=null ) + throw new ErrorResponseException("invalid_scope", "Scope [" + representation.getScope() + "] is invalid", Response.Status.BAD_REQUEST); + if (scope == null && representation.getScopeName() !=null ) + throw new ErrorResponseException("invalid_scope", "Scope [" + representation.getScopeName() + "] is invalid", Response.Status.BAD_REQUEST); + + boolean match = resource.getScopes().contains(scope); + + if (!match) + throw new ErrorResponseException("invalid_resource_id", "Resource set with id [" + representation.getResource() + "] does not have Scope [" + scope.getName() + "]", Response.Status.BAD_REQUEST); + + Map attributes = new HashMap(); + attributes.put(PermissionTicket.RESOURCE, resource.getId()); + attributes.put(PermissionTicket.SCOPE, scope.getId()); + attributes.put(PermissionTicket.REQUESTER, user.getId()); + + if (!ticketStore.find(attributes, resourceServer.getId(), -1, -1).isEmpty()) + throw new ErrorResponseException("invalid_permission", "Permission already exists", Response.Status.BAD_REQUEST); + + PermissionTicket ticket = ticketStore.create(resource.getId(), scope.getId(), user.getId(), resourceServer); + representation = ModelToRepresentation.toRepresentation(ticket, authorization); + return Response.ok(representation).build(); + } + + @PUT + @Consumes("application/json") + public Response update(PermissionTicketRepresentation representation) { + if (representation == null || representation.getId() == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST); + } + + PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore(); + PermissionTicket ticket = ticketStore.findById(representation.getId(), resourceServer.getId()); + + if (ticket == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST); + } + + if (!ticket.getOwner().equals(this.identity.getId()) && !this.identity.isResourceServer()) + throw new ErrorResponseException("not_authorised", "permissions for [" + representation.getResource() + "] can be updated only by the owner or by the resource server", Response.Status.FORBIDDEN); + + RepresentationToModel.toModel(representation, resourceServer.getId(), authorization); + + return Response.noContent().build(); + } + + + @Path("{id}") + @DELETE + @Consumes("application/json") + public Response delete(@PathParam("id") String id) { + if (id == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST); + } + + PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore(); + PermissionTicket ticket = ticketStore.findById(id, resourceServer.getId()); + + if (ticket == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST); + } + + if (!ticket.getOwner().equals(this.identity.getId()) && !this.identity.isResourceServer() && !ticket.getRequester().equals(this.identity.getId())) + throw new ErrorResponseException("not_authorised", "permissions for [" + ticket.getResource() + "] can be deleted only by the owner, the requester, or the resource server", Response.Status.FORBIDDEN); + + ticketStore.delete(id); + + return Response.noContent().build(); + } + + @GET + @Produces("application/json") + public Response find(@QueryParam("scopeId") String scopeId, + @QueryParam("resourceId") String resourceId, + @QueryParam("owner") String owner, + @QueryParam("requester") String requester, + @QueryParam("granted") Boolean granted, + @QueryParam("returnNames") Boolean returnNames, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResult) { + PermissionTicketStore permissionTicketStore = authorization.getStoreFactory().getPermissionTicketStore(); + + Map filters = new HashMap<>(); + + if (resourceId != null) { + filters.put(PermissionTicket.RESOURCE, resourceId); + } + + if (scopeId != null) { + filters.put(PermissionTicket.SCOPE, scopeId); + } + + if (owner != null) { + filters.put(PermissionTicket.OWNER, owner); + } + + if (requester != null) { + filters.put(PermissionTicket.REQUESTER, requester); + } + + if (granted != null) { + filters.put(PermissionTicket.GRANTED, granted.toString()); + } + + return Response.ok().entity(permissionTicketStore.find(filters, resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS) + .stream() + .map(permissionTicket -> ModelToRepresentation.toRepresentation(permissionTicket, authorization, returnNames == null ? false : returnNames)) + .collect(Collectors.toList())) + .build(); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java deleted file mode 100644 index 37944d25ca..0000000000 --- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.permission; - -import org.keycloak.authorization.AuthorizationProvider; -import org.keycloak.authorization.common.KeycloakIdentity; -import org.keycloak.authorization.model.ResourceServer; -import org.keycloak.representations.idm.authorization.PermissionRequest; - -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Response; -import java.util.List; - -/** - * @author Pedro Igor - */ -public class PermissionsService extends AbstractPermissionService { - - public PermissionsService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization) { - super(identity, resourceServer, authorization); - } - - @POST - @Consumes("application/json") - @Produces("application/json") - public Response create(List request) { - return super.create(request); - } -} \ No newline at end of file