[KEYCLOAK-7354] split ticket management and permission endpoint

see (https://issues.jboss.org/browse/KEYCLOAK-7354)

* created new endpoint for ticket management /permission/ticket
* removed unused class
* support for direct creation of ticket by resource owner
* fix DELETE ticket
This commit is contained in:
Federico M. Facca 2018-05-15 14:32:40 +02:00
parent 5cbe595fe3
commit 76076cdb3c
5 changed files with 266 additions and 143 deletions

View file

@ -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<PermissionTicketRepresentation> callable = new Callable<PermissionTicketRepresentation>() {
@Override
public PermissionTicketRepresentation call() throws Exception {
return http.<PermissionTicketRepresentation>post(serverConfiguration.getPermissionEndpoint()+"/ticket")
.json(JsonSerialization.writeValueAsBytes(ticket))
.authorizationBearer(pat.call())
.response().json(new TypeReference<PermissionTicketRepresentation>(){}).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 <code>scopeId</code>. * Query the server for any permission ticket associated with the given <code>scopeId</code>.
* *
@ -107,7 +142,7 @@ public class PermissionResource {
Callable<List<PermissionTicketRepresentation>> callable = new Callable<List<PermissionTicketRepresentation>>() { Callable<List<PermissionTicketRepresentation>> callable = new Callable<List<PermissionTicketRepresentation>>() {
@Override @Override
public List<PermissionTicketRepresentation> call() throws Exception { public List<PermissionTicketRepresentation> call() throws Exception {
return http.<List<PermissionTicketRepresentation>>get(serverConfiguration.getPermissionEndpoint()) return http.<List<PermissionTicketRepresentation>>get(serverConfiguration.getPermissionEndpoint()+"/ticket")
.authorizationBearer(pat.call()) .authorizationBearer(pat.call())
.param("scopeId", scopeId) .param("scopeId", scopeId)
.response().json(new TypeReference<List<PermissionTicketRepresentation>>(){}).execute(); .response().json(new TypeReference<List<PermissionTicketRepresentation>>(){}).execute();
@ -133,7 +168,7 @@ public class PermissionResource {
Callable<List<PermissionTicketRepresentation>> callable = new Callable<List<PermissionTicketRepresentation>>() { Callable<List<PermissionTicketRepresentation>> callable = new Callable<List<PermissionTicketRepresentation>>() {
@Override @Override
public List<PermissionTicketRepresentation> call() throws Exception { public List<PermissionTicketRepresentation> call() throws Exception {
return http.<List<PermissionTicketRepresentation>>get(serverConfiguration.getPermissionEndpoint()) return http.<List<PermissionTicketRepresentation>>get(serverConfiguration.getPermissionEndpoint()+"/ticket")
.authorizationBearer(pat.call()) .authorizationBearer(pat.call())
.param("resourceId", resourceId) .param("resourceId", resourceId)
.response().json(new TypeReference<List<PermissionTicketRepresentation>>(){}).execute(); .response().json(new TypeReference<List<PermissionTicketRepresentation>>(){}).execute();
@ -170,7 +205,7 @@ public class PermissionResource {
Callable<List<PermissionTicketRepresentation>> callable = new Callable<List<PermissionTicketRepresentation>>() { Callable<List<PermissionTicketRepresentation>> callable = new Callable<List<PermissionTicketRepresentation>>() {
@Override @Override
public List<PermissionTicketRepresentation> call() throws Exception { public List<PermissionTicketRepresentation> call() throws Exception {
return http.<List<PermissionTicketRepresentation>>get(serverConfiguration.getPermissionEndpoint()) return http.<List<PermissionTicketRepresentation>>get(serverConfiguration.getPermissionEndpoint()+"/ticket")
.authorizationBearer(pat.call()) .authorizationBearer(pat.call())
.param("resourceId", resourceId) .param("resourceId", resourceId)
.param("scopeId", scopeId) .param("scopeId", scopeId)
@ -205,7 +240,7 @@ public class PermissionResource {
Callable callable = new Callable() { Callable callable = new Callable() {
@Override @Override
public Object call() throws Exception { public Object call() throws Exception {
http.<List>put(serverConfiguration.getPermissionEndpoint()) http.<List>put(serverConfiguration.getPermissionEndpoint()+"/ticket")
.json(JsonSerialization.writeValueAsBytes(ticket)) .json(JsonSerialization.writeValueAsBytes(ticket))
.authorizationBearer(pat.call()) .authorizationBearer(pat.call())
.response().json(List.class).execute(); .response().json(List.class).execute();

View file

@ -37,6 +37,7 @@ import org.keycloak.services.resources.admin.AdminEventBuilder;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import org.keycloak.authorization.protection.permission.PermissionTicketService;
/** /**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -82,6 +83,17 @@ public class ProtectionService {
return resource; 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) { private KeycloakIdentity createIdentity(boolean checkProtectionScope) {
KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession()); KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession());

View file

@ -17,31 +17,16 @@
*/ */
package org.keycloak.authorization.protection.permission; package org.keycloak.authorization.protection.permission;
import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.common.KeycloakIdentity; import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.representations.idm.authorization.PermissionRequest; 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.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/** /**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -64,82 +49,4 @@ public class PermissionService extends AbstractPermissionService {
return super.create(request); 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<String, String> 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();
}
} }

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
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<String, String> attributes = new HashMap<String, String>();
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<String, String> 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();
}
}

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
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<PermissionRequest> request) {
return super.create(request);
}
}