From 967d21dbb5a031d7bb413e35b6d556b91780fe88 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 25 Jul 2019 18:04:00 -0300 Subject: [PATCH] [KEYCLOAK-10713] - Pagination to resources rest api --- .../StoreFactoryCacheManager.java | 1 + .../StoreFactoryCacheSession.java | 15 ++ .../jpa/entities/PermissionTicketEntity.java | 4 +- .../jpa/entities/ResourceEntity.java | 2 + .../jpa/store/JPAPermissionTicketStore.java | 56 ++++ .../jpa/store/JPAResourceStore.java | 36 ++- .../authorization/AuthorizationProvider.java | 5 + .../store/PermissionTicketStore.java | 21 ++ .../authorization/store/ResourceStore.java | 2 + .../broker/provider/util/SimpleHttp.java | 23 +- .../resources/AbstractResourceService.java | 28 +- .../account/resources/ResourceService.java | 23 +- .../account/resources/ResourcesService.java | 121 ++++++++- .../account/ResourcesRestServiceTest.java | 254 ++++++++++++++---- 14 files changed, 499 insertions(+), 92 deletions(-) diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java index e267dbc4c2..1937268fbd 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java @@ -148,6 +148,7 @@ public class StoreFactoryCacheManager extends CacheManager { invalidations.add(StoreFactoryCacheSession.getPermissionTicketByGranted(requester, serverId)); invalidations.add(StoreFactoryCacheSession.getPermissionTicketByGranted(requester, null)); invalidations.add(StoreFactoryCacheSession.getPermissionTicketByResourceNameAndGranted(resourceName, requester, serverId)); + invalidations.add(StoreFactoryCacheSession.getPermissionTicketByResourceNameAndGranted(resourceName, requester, null)); if (scope != null) { invalidations.add(StoreFactoryCacheSession.getPermissionTicketByScope(scope, serverId)); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java index 1f89ed67c4..c01f6688f9 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java @@ -676,6 +676,11 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { (revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId, consumer); } + @Override + public List findByOwner(String ownerId, String resourceServerId, int first, int max) { + return getResourceStoreDelegate().findByOwner(ownerId, resourceServerId, first, max); + } + @Override public List findByUri(String uri, String resourceServerId) { if (uri == null) return null; @@ -1130,6 +1135,16 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider { (revision, permissions) -> new PermissionTicketResourceListQuery(revision, cacheKey, resourceName, permissions.stream().map(permission -> permission.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId); } + @Override + public List findGrantedResources(String requester, int first, int max) { + return getPermissionTicketStoreDelegate().findGrantedResources(requester, first, max); + } + + @Override + public List findGrantedOwnerResources(String owner, int first, int max) { + return getPermissionTicketStoreDelegate().findGrantedOwnerResources(owner, first, max); + } + @Override public List findByOwner(String owner, String resourceServerId) { String cacheKey = getPermissionTicketByOwner(owner, resourceServerId); diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java index 4bf334c951..f563169a94 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java @@ -41,7 +41,9 @@ import javax.persistence.UniqueConstraint; { @NamedQuery(name="findPermissionIdByResource", query="select p.id from PermissionTicketEntity p inner join p.resource r where p.resourceServer.id = :serverId and (r.resourceServer.id = :serverId and r.id = :resourceId)"), @NamedQuery(name="findPermissionIdByScope", query="select p.id from PermissionTicketEntity p inner join p.scope s where p.resourceServer.id = :serverId and (s.resourceServer.id = :serverId and s.id = :scopeId)"), - @NamedQuery(name="findPermissionTicketIdByServerId", query="select p.id from PermissionTicketEntity p where p.resourceServer.id = :serverId ") + @NamedQuery(name="findPermissionTicketIdByServerId", query="select p.id from PermissionTicketEntity p where p.resourceServer.id = :serverId "), + @NamedQuery(name="findGrantedResources", query="select distinct(r.id) from ResourceEntity r inner join PermissionTicketEntity p on r.id = p.resource.id where p.grantedTimestamp is not null and p.requester = :requester order by r.id"), + @NamedQuery(name="findGrantedOwnerResources", query="select distinct(r.id) from ResourceEntity r inner join PermissionTicketEntity p on r.id = p.resource.id where p.grantedTimestamp is not null and p.owner = :owner order by r.id") } ) public class PermissionTicketEntity { diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java index 33640771f2..1f2b88c51c 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java @@ -58,7 +58,9 @@ import org.hibernate.annotations.FetchMode; @NamedQueries( { @NamedQuery(name="findResourceIdByOwner", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.owner = :owner"), + @NamedQuery(name="findResourceIdByOwnerOrdered", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.owner = :owner order by r.id"), @NamedQuery(name="findAnyResourceIdByOwner", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.owner = :owner"), + @NamedQuery(name="findAnyResourceIdByOwnerOrdered", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.owner = :owner order by r.id"), @NamedQuery(name="findResourceIdByUri", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and :uri in elements(r.uris)"), @NamedQuery(name="findResourceIdByName", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.owner = :ownerId and r.name = :name"), @NamedQuery(name="findResourceIdByType", query="select distinct(r) from ResourceEntity r left join fetch r.scopes s where r.resourceServer.id = :serverId and r.owner = :ownerId and r.type = :type"), diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java index 2c5fb12032..9bb44c7c41 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java @@ -36,8 +36,10 @@ import javax.persistence.criteria.Root; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.jpa.entities.PermissionTicketEntity; import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.store.PermissionTicketStore; +import org.keycloak.authorization.store.ResourceStore; import org.keycloak.models.utils.KeycloakModelUtils; /** @@ -262,6 +264,60 @@ public class JPAPermissionTicketStore implements PermissionTicketStore { return find(filters, resourceServerId, -1, -1); } + @Override + public List findGrantedResources(String requester, int first, int max) { + TypedQuery query = entityManager.createNamedQuery("findGrantedResources", String.class); + + query.setFlushMode(FlushModeType.COMMIT); + query.setParameter("requester", requester); + + if (first > -1 && max > -1) { + query.setFirstResult(first); + query.setMaxResults(max); + } + + List result = query.getResultList(); + List list = new LinkedList<>(); + ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + + for (String id : result) { + Resource resource = resourceStore.findById(id, null); + + if (Objects.nonNull(resource)) { + list.add(resource); + } + } + + return list; + } + + @Override + public List findGrantedOwnerResources(String owner, int first, int max) { + TypedQuery query = entityManager.createNamedQuery("findGrantedOwnerResources", String.class); + + query.setFlushMode(FlushModeType.COMMIT); + query.setParameter("owner", owner); + + if (first > -1 && max > -1) { + query.setFirstResult(first); + query.setMaxResults(max); + } + + List result = query.getResultList(); + List list = new LinkedList<>(); + ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + + for (String id : result) { + Resource resource = resourceStore.findById(id, null); + + if (Objects.nonNull(resource)) { + list.add(resource); + } + } + + return list; + } + @Override public List findByOwner(String owner, String resourceServerId) { TypedQuery query = entityManager.createNamedQuery("findPolicyIdByType", String.class); diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java index 2793b2b239..02cabc2755 100644 --- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java @@ -110,10 +110,24 @@ public class JPAResourceStore implements ResourceStore { @Override public void findByOwner(String ownerId, String resourceServerId, Consumer consumer) { - String queryName = "findResourceIdByOwner"; + findByOwnerFilter(ownerId, resourceServerId, consumer, -1, -1); + } + + @Override + public List findByOwner(String ownerId, String resourceServerId, int first, int max) { + List list = new LinkedList<>(); + + findByOwnerFilter(ownerId, resourceServerId, list::add, first, max); + + return list; + } + + private void findByOwnerFilter(String ownerId, String resourceServerId, Consumer consumer, int firstResult, int maxResult) { + boolean pagination = firstResult > -1 && maxResult > -1; + String queryName = pagination ? "findResourceIdByOwnerOrdered" : "findResourceIdByOwner"; if (resourceServerId == null) { - queryName = "findAnyResourceIdByOwner"; + queryName = pagination ? "findAnyResourceIdByOwnerOrdered" : "findAnyResourceIdByOwner"; } TypedQuery query = entityManager.createNamedQuery(queryName, ResourceEntity.class); @@ -125,11 +139,21 @@ public class JPAResourceStore implements ResourceStore { query.setParameter("serverId", resourceServerId); } - StoreFactory storeFactory = provider.getStoreFactory(); + if (pagination) { + query.setFirstResult(firstResult); + query.setMaxResults(maxResult); + } - query.getResultList().stream() - .map(id -> new ResourceAdapter(id, entityManager, storeFactory)) - .forEach(consumer); + ResourceStore resourceStore = provider.getStoreFactory().getResourceStore(); + List result = query.getResultList(); + + for (ResourceEntity entity : result) { + Resource cached = resourceStore.findById(entity.getId(), resourceServerId); + + if (cached != null) { + consumer.accept(cached); + } + } } @Override diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java index 182c34b9d7..c92be9ec4a 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java @@ -491,6 +491,11 @@ public final class AuthorizationProvider implements Provider { delegate.findByOwner(ownerId, resourceServerId, consumer); } + @Override + public List findByOwner(String ownerId, String resourceServerId, int first, int max) { + return delegate.findByOwner(ownerId, resourceServerId, first, max); + } + @Override public List findByUri(String uri, String resourceServerId) { return delegate.findByUri(uri, resourceServerId); diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java index 3362d70bae..3fe5a1f30f 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; /** @@ -109,4 +110,24 @@ public interface PermissionTicketStore { * @return a list of permissions granted for a particular user */ List findGranted(String resourceName, String userId, String resourceServerId); + + /** + * Returns a list of {@link Resource} granted to the given {@code requester} + * + * @param requester the requester + * @param first first result + * @param max max result + * @return a list of {@link Resource} granted to the given {@code requester} + */ + List findGrantedResources(String requester, int first, int max); + + /** + * Returns a list of {@link Resource} granted by the owner to other users + * + * @param owner the owner + * @param first first result + * @param max max result + * @return a list of {@link Resource} granted by the owner + */ + List findGrantedOwnerResources(String owner, int first, int max); } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java index b4b7c58cae..ceb06b12c9 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/ResourceStore.java @@ -77,6 +77,8 @@ public interface ResourceStore { void findByOwner(String ownerId, String resourceServerId, Consumer consumer); + List findByOwner(String ownerId, String resourceServerId, int first, int max); + /** * Finds all {@link Resource} instances with the given uri. * diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java index 858e55151f..5e85b218be 100755 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java @@ -46,9 +46,13 @@ import java.io.StringWriter; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import org.apache.http.client.methods.HttpDelete; @@ -316,7 +320,24 @@ public class SimpleHttp { public String getFirstHeader(String name) throws IOException { readResponse(); - return response.getHeaders(name)[0].getValue(); + Header[] headers = response.getHeaders(name); + + if (headers != null && headers.length > 0) { + return headers[0].getValue(); + } + + return null; + } + + public List getHeader(String name) throws IOException { + readResponse(); + Header[] headers = response.getHeaders(name); + + if (headers != null && headers.length > 0) { + return Stream.of(headers).map(Header::getValue).collect(Collectors.toList()); + } + + return null; } public void close() throws IOException { diff --git a/services/src/main/java/org/keycloak/services/resources/account/resources/AbstractResourceService.java b/services/src/main/java/org/keycloak/services/resources/account/resources/AbstractResourceService.java index a29d6be54c..8e42974b36 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/resources/AbstractResourceService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/resources/AbstractResourceService.java @@ -70,29 +70,6 @@ public abstract class AbstractResourceService { return Cors.add(request, response).auth().allowedOrigins(auth.getToken()).build(); } - protected Collection getPermissions(List tickets, boolean withRequesters) { - Map permissions = new HashMap<>(); - - for (PermissionTicket ticket : tickets) { - ResourcePermission resource = permissions - .computeIfAbsent(ticket.getResource().getId(), s -> new ResourcePermission(ticket, provider)); - - if (withRequesters) { - Permission user = resource.getPermission(ticket.getRequester()); - - if (user == null) { - resource.addPermission(ticket.getRequester(), user = new Permission(ticket.getRequester(), provider)); - } - - user.addScope(ticket.getScope().getName()); - } else { - resource.addScope(new Scope(ticket.getScope())); - } - } - - return permissions.values(); - } - public static class Resource extends ResourceRepresentation { private Client client; @@ -135,6 +112,11 @@ public abstract class AbstractResourceService { setScopes(new HashSet<>()); } + ResourcePermission(org.keycloak.authorization.model.Resource resource, AuthorizationProvider provider) { + super(resource, provider); + setScopes(new HashSet<>()); + } + public Collection getPermissions() { if (permissions == null) { return null; diff --git a/services/src/main/java/org/keycloak/services/resources/account/resources/ResourceService.java b/services/src/main/java/org/keycloak/services/resources/account/resources/ResourceService.java index 5273d2c9b2..2a50849e5b 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/resources/ResourceService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/resources/ResourceService.java @@ -74,14 +74,14 @@ public class ResourceService extends AbstractResourceService { @GET @Path("permissions") @Produces(MediaType.APPLICATION_JSON) - public Response getPermissions() { + public Response toPermissions() { Map filters = new HashMap<>(); filters.put(PermissionTicket.OWNER, user.getId()); filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); filters.put(PermissionTicket.RESOURCE, resource.getId()); - Collection resources = getPermissions(ticketStore.find(filters, null, -1, -1), true); + Collection resources = toPermissions(ticketStore.find(filters, null, -1, -1)); Collection permissions = Collections.EMPTY_LIST; if (!resources.isEmpty()) { @@ -210,4 +210,23 @@ public class ResourceService extends AbstractResourceService { return user; } + + private Collection toPermissions(List tickets) { + Map permissions = new HashMap<>(); + + for (PermissionTicket ticket : tickets) { + ResourcePermission resource = permissions + .computeIfAbsent(ticket.getResource().getId(), s -> new ResourcePermission(ticket, provider)); + + Permission user = resource.getPermission(ticket.getRequester()); + + if (user == null) { + resource.addPermission(ticket.getRequester(), user = new Permission(ticket.getRequester(), provider)); + } + + user.addScope(ticket.getScope().getName()); + } + + return permissions.values(); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/account/resources/ResourcesService.java b/services/src/main/java/org/keycloak/services/resources/account/resources/ResourcesService.java index 458914f7ad..0fbc7b7c2b 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/resources/ResourcesService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/resources/ResourcesService.java @@ -22,13 +22,23 @@ import javax.ws.rs.NotFoundException; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Link; import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.function.BiFunction; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.authorization.model.PermissionTicket; +import org.keycloak.authorization.store.PermissionTicketStore; +import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; import org.keycloak.services.managers.Auth; @@ -46,45 +56,48 @@ public class ResourcesService extends AbstractResourceService { /** * Returns a list of {@link Resource} where the {@link #user} is the resource owner. * + * @param first the first result + * @param max the max result * @return a list of {@link Resource} where the {@link #user} is the resource owner */ @GET @Produces(MediaType.APPLICATION_JSON) - public Response getResources() { - return cors(Response.ok(resourceStore.findByOwner(user.getId(), null).stream() - .map(resource -> new Resource(resource, user, provider)) - .collect(Collectors.toList()))); + public Response getResources(@QueryParam("first") Integer first, @QueryParam("max") Integer max) { + return queryResponse((f, m) -> resourceStore.findByOwner(user.getId(), null, f, m) + .stream().map(resource -> new Resource(resource, user, provider)), first, max); } /** * Returns a list of {@link Resource} shared with the {@link #user} * + * @param first the first result + * @param max the max result * @return a list of {@link Resource} shared with the {@link #user} */ @GET @Path("shared-with-me") @Produces(MediaType.APPLICATION_JSON) - public Response getSharedWithMe() { - return cors(Response.ok(getPermissions(ticketStore.findGranted(user.getId(), null), false))); + public Response getSharedWithMe(@QueryParam("first") Integer first, @QueryParam("max") Integer max) { + return queryResponse((f, m) -> toPermissions(ticketStore.findGrantedResources(auth.getUser().getId(), f, m), false) + .stream(), first, max); } /** * Returns a list of {@link Resource} where the {@link #user} is the resource owner and the resource is * shared with other users. * + * @param first the first result + * @param max the max result * @return a list of {@link Resource} where the {@link #user} is the resource owner and the resource is * * shared with other users */ @GET @Path("shared-with-others") @Produces(MediaType.APPLICATION_JSON) - public Response getSharedWithOthers() { - Map filters = new HashMap<>(); - - filters.put(PermissionTicket.OWNER, user.getId()); - filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); - - return cors(Response.ok(getPermissions(ticketStore.find(filters, null, -1, -1), true))); + public Response getSharedWithOthers(@QueryParam("first") Integer first, @QueryParam("max") Integer max) { + return queryResponse( + (f, m) -> toPermissions(ticketStore.findGrantedOwnerResources(auth.getUser().getId(), f, m), true) + .stream(), first, max); } @Path("{id}") @@ -101,4 +114,86 @@ public class ResourcesService extends AbstractResourceService { return new ResourceService(resource, provider.getKeycloakSession(), user, auth, request); } + + private Collection toPermissions(List resources, boolean withRequesters) { + Collection permissions = new ArrayList<>(); + PermissionTicketStore ticketStore = provider.getStoreFactory().getPermissionTicketStore(); + + for (org.keycloak.authorization.model.Resource resource : resources) { + ResourcePermission permission = new ResourcePermission(resource, provider); + + List tickets; + + if (withRequesters) { + Map filters = new HashMap<>(); + + filters.put(PermissionTicket.OWNER, user.getId()); + filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); + filters.put(PermissionTicket.RESOURCE, resource.getId()); + + tickets = ticketStore.find(filters, null, -1, -1); + } else { + tickets = ticketStore.findGranted(resource.getName(), user.getId(), null); + } + + for (PermissionTicket ticket : tickets) { + if (resource.equals(ticket.getResource())) { + if (withRequesters) { + Permission user = permission.getPermission(ticket.getRequester()); + + if (user == null) { + permission.addPermission(ticket.getRequester(), + user = new Permission(ticket.getRequester(), provider)); + } + + user.addScope(ticket.getScope().getName()); + } else { + permission.addScope(new Scope(ticket.getScope())); + } + } + } + + permissions.add(permission); + } + + return permissions; + } + + private Response queryResponse(BiFunction> query, Integer first, Integer max) { + if (first != null && max != null) { + List result = query.apply(first, max + 1).collect(Collectors.toList()); + int size = result.size(); + + if (size > max) { + result = result.subList(0, size - 1); + } + + return cors(Response.ok().entity(result).links(createPageLinks(first, max, size))); + } + + return cors(Response.ok().entity(query.apply(-1, -1).collect(Collectors.toList()))); + } + + private Link[] createPageLinks(Integer first, Integer max, int resultSize) { + if (resultSize == 0 || (first == 0 && resultSize <= max)) { + return new Link[] {}; + } + + List links = new ArrayList(); + boolean nextPage = resultSize > max; + + if (nextPage) { + links.add(Link.fromUri( + KeycloakUriBuilder.fromUri(request.getUri().getRequestUri()).replaceQuery("first={first}&max={max}") + .build(first + max, max)) + .rel("next").build()); + } + + links.add(Link.fromUri( + KeycloakUriBuilder.fromUri(request.getUri().getRequestUri()).replaceQuery("first={first}&max={max}") + .build(nextPage ? first : first - max, max)) + .rel("prev").build()); + + return links.toArray(new Link[links.size()]); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResourcesRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResourcesRestServiceTest.java index 442cdf44f4..daed9a88a6 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResourcesRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/ResourcesRestServiceTest.java @@ -18,7 +18,9 @@ package org.keycloak.testsuite.account; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import javax.ws.rs.core.Response; import java.io.IOException; @@ -26,9 +28,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import com.fasterxml.jackson.core.type.TypeReference; import org.junit.Test; @@ -42,7 +46,6 @@ import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.AccountRoles; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -152,21 +155,45 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest { List resources = getMyResources(); assertEquals(30, resources.size()); + assertMyResourcesResponse(resources); + } - for (int i = 0; i < 30; i++) { - String uri = "http://resourceServer.com/resources/" + i; - Resource resource = resources.stream() - .filter(rep -> rep.getUris().stream().anyMatch(resourceUri -> resourceUri.equals(uri))).findAny() - .get(); + @Test + public void testGetMyResourcesPagination() { + List resources = getMyResources(0, 10, response -> assertNextPageLink(response, "/realms/test/account/resources", 10, 10)); - assertNotNull(resource.getId()); - assertEquals("Resource " + i, resource.getName()); - assertEquals("Display Name " + i, resource.getDisplayName()); - assertEquals("Icon Uri " + i, resource.getIconUri()); - assertEquals("my-resource-server", resource.getClient().getClientId()); - assertEquals("My Resource Server", resource.getClient().getName()); - assertEquals("http://resourceserver.com", resource.getClient().getBaseUrl()); - } + assertEquals(10, resources.size()); + assertMyResourcesResponse(resources); + + resources = getMyResources(10, 10, response -> assertNextPageLink(response, "/realms/test/account/resources", 20, 10)); + + assertEquals(10, resources.size()); + + resources = getMyResources(20, 10, response -> { + assertNextPageLink(response, "/realms/test/account/resources", 20, 10, true); + }); + + assertEquals(10, resources.size()); + + resources = getMyResources(30, 10); + + assertEquals(0, resources.size()); + + getMyResources(30, 30, response -> { + assertNextPageLink(response, "/realms/test/account/resources", 0, 0, true, true); + }); + + getMyResources(30, 31, response -> { + assertNextPageLink(response, "/realms/test/account/resources", 0, 0, true, true); + }); + + getMyResources(0, 30, response -> { + assertNextPageLink(response, "/realms/test/account/resources", 0, 0, true, true); + }); + + getMyResources(0, 31, response -> { + assertNextPageLink(response, "/realms/test/account/resources", 0, 0, true, true); + }); } @Test @@ -175,19 +202,24 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest { List resources = getSharedWithMe(userName); assertEquals(10, resources.size()); + assertSharedWithMeResponse(resources); + } + } - for (AbstractResourceService.ResourcePermission resource : resources) { - String uri = resource.getUri(); - int id = Integer.parseInt(uri.substring(uri.lastIndexOf('/') + 1)); - assertNotNull(resource.getId()); - assertEquals("Resource " + id, resource.getName()); - assertEquals("Display Name " + id, resource.getDisplayName()); - assertEquals("Icon Uri " + id, resource.getIconUri()); - assertEquals("my-resource-server", resource.getClient().getClientId()); - assertEquals("My Resource Server", resource.getClient().getName()); - assertEquals("http://resourceserver.com", resource.getClient().getBaseUrl()); - assertEquals(2, resource.getScopes().size()); - } + @Test + public void testGetSharedWithMePagination() { + for (String userName : userNames) { + List resources = getSharedWithMe(userName, 0, 3, + response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 3, 3)); + + assertSharedWithMeResponse(resources); + + getSharedWithMe(userName, 3, 3, + response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 6, 3)); + getSharedWithMe(userName, 6, 3, + response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 9, 3)); + getSharedWithMe(userName, 9, 3, + response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 9, 3, true)); } } @@ -198,24 +230,27 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest { }); assertEquals(30, resources.size()); + assertSharedWithOthersResponse(resources); + } - for (AbstractResourceService.ResourcePermission resource : resources) { - String uri = resource.getUri(); - int id = Integer.parseInt(uri.substring(uri.lastIndexOf('/') + 1)); - assertNotNull(resource.getId()); - assertEquals("Resource " + id, resource.getName()); - assertEquals("Display Name " + id, resource.getDisplayName()); - assertEquals("Icon Uri " + id, resource.getIconUri()); - assertEquals("my-resource-server", resource.getClient().getClientId()); - assertEquals("My Resource Server", resource.getClient().getName()); - assertEquals("http://resourceserver.com", resource.getClient().getBaseUrl()); - assertEquals(1, resource.getPermissions().size()); - Permission user = resource.getPermissions().iterator().next(); - - assertTrue(userNames.contains(user.getUsername())); - - assertEquals(2, user.getScopes().size()); - } + @Test + public void testGetSharedWithOthersPagination() { + List resources = doGet("/shared-with-others?first=0&max=5", + new TypeReference>() { + }, response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-others", 5, 5)); + + assertEquals(5, resources.size()); + assertSharedWithOthersResponse(resources); + + doGet("/shared-with-others?first=5&max=5", + new TypeReference>() { + }, response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-others", 10, 5)); + doGet("/shared-with-others?first=20&max=5", + new TypeReference>() { + }, response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-others", 25, 5)); + doGet("/shared-with-others?first=25&max=5", + new TypeReference>() { + }, response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-others", 25, 5, true)); } @Test @@ -254,7 +289,7 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest { assertEquals(2, firstShare.getScopes().size()); List users = new ArrayList<>(userNames); - + users.remove(firstShare.getUsername()); for (String userName : users) { @@ -287,7 +322,7 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest { @Test public void testShareResource() throws Exception { - List users = Arrays.asList("jdoe", "alice"); + List users = new LinkedList<>(Arrays.asList("jdoe", "alice")); List permissions = new ArrayList<>(); AbstractResourceService.ResourcePermission sharedResource = null; @@ -514,14 +549,26 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest { } private List getSharedWithMe(String userName) { + return getSharedWithMe(userName, -1, -1, null); + } + + private List getSharedWithMe(String userName, int first, int max, Consumer responseHandler) { + if (first > -1 && max > -1) { + return doGet("/shared-with-me?first=" + first + "&max=" + max, authzClient.obtainAccessToken(userName, "password").getToken(), + new TypeReference>() {}, responseHandler); + } return doGet("/shared-with-me", authzClient.obtainAccessToken(userName, "password").getToken(), - new TypeReference>() {}); + new TypeReference>() {}, responseHandler); } private R doGet(String resource, TypeReference typeReference) { return doGet(resource, tokenUtil.getToken(), typeReference); } + private R doGet(String resource, TypeReference typeReference, Consumer response) { + return doGet(resource, tokenUtil.getToken(), typeReference, response); + } + private R doGet(String resource, Class type) { return doGet(resource, tokenUtil.getToken(), type); } @@ -534,6 +581,25 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest { } } + private R doGet(String resource, String token, TypeReference typeReference, Consumer responseHandler) { + try { + SimpleHttp http = get(resource, token); + + http.header("Accept", "application/json"); + SimpleHttp.Response response = http.asResponse(); + + if (responseHandler != null) { + responseHandler.accept(response); + } + + R result = JsonSerialization.readValue(response.asString(), typeReference); + + return result; + } catch (IOException cause) { + throw new RuntimeException("Failed to fetch resource", cause); + } + } + private R doGet(String resource, String token, Class type) { try { return get(resource, token).asJson(type); @@ -567,6 +633,102 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest { } private List getMyResources() { + return getMyResources(-1, -1); + } + + private List getMyResources(int first, int max) { + if (first > -1 && max > -1) { + return doGet("?first=" + first + "&max=" + max, new TypeReference>() {}); + } return doGet("", new TypeReference>() {}); } + + private List getMyResources(int first, int max, Consumer response) { + if (first > -1 && max > -1) { + return doGet("?first=" + first + "&max=" + max, new TypeReference>() {}, response); + } + return doGet("", new TypeReference>() {}, response); + } + + private void assertSharedWithOthersResponse(List resources) { + for (AbstractResourceService.ResourcePermission resource : resources) { + String uri = resource.getUri(); + int id = Integer.parseInt(uri.substring(uri.lastIndexOf('/') + 1)); + assertNotNull(resource.getId()); + assertEquals("Resource " + id, resource.getName()); + assertEquals("Display Name " + id, resource.getDisplayName()); + assertEquals("Icon Uri " + id, resource.getIconUri()); + assertEquals("my-resource-server", resource.getClient().getClientId()); + assertEquals("My Resource Server", resource.getClient().getName()); + assertEquals("http://resourceserver.com", resource.getClient().getBaseUrl()); + assertEquals(1, resource.getPermissions().size()); + Permission user = resource.getPermissions().iterator().next(); + + assertTrue(userNames.contains(user.getUsername())); + + assertEquals(2, user.getScopes().size()); + } + } + + private void assertMyResourcesResponse(List resources) { + for (Resource resource : resources) { + String uri = resource.getUri(); + int id = Integer.parseInt(uri.substring(uri.lastIndexOf('/') + 1)); + + assertNotNull(resource.getId()); + assertEquals("Resource " + id, resource.getName()); + assertEquals("Display Name " + id, resource.getDisplayName()); + assertEquals("Icon Uri " + id, resource.getIconUri()); + assertEquals("my-resource-server", resource.getClient().getClientId()); + assertEquals("My Resource Server", resource.getClient().getName()); + assertEquals("http://resourceserver.com", resource.getClient().getBaseUrl()); + } + } + + private void assertSharedWithMeResponse(List resources) { + for (AbstractResourceService.ResourcePermission resource : resources) { + String uri = resource.getUri(); + int id = Integer.parseInt(uri.substring(uri.lastIndexOf('/') + 1)); + assertNotNull(resource.getId()); + assertEquals("Resource " + id, resource.getName()); + assertEquals("Display Name " + id, resource.getDisplayName()); + assertEquals("Icon Uri " + id, resource.getIconUri()); + assertEquals("my-resource-server", resource.getClient().getClientId()); + assertEquals("My Resource Server", resource.getClient().getName()); + assertEquals("http://resourceserver.com", resource.getClient().getBaseUrl()); + assertEquals(2, resource.getScopes().size()); + } + } + + private void assertNextPageLink(SimpleHttp.Response response, String uri, int first, int max) { + assertNextPageLink(response, uri, first, max, false); + } + + private void assertNextPageLink(SimpleHttp.Response response, String uri, int first, int max, boolean lastPage) { + assertNextPageLink(response, uri, first, max, lastPage, false); + } + + private void assertNextPageLink(SimpleHttp.Response response, String uri, int first, int max, boolean lastPage, boolean singlePage) { + try { + List links = response.getHeader("Link"); + + if (singlePage) { + assertNull(links); + return; + } + + assertNotNull(links); + assertEquals(lastPage ? 1 : 2, links.size()); + + for (String link : links) { + if (link.contains("rel=\"next\"")) { + assertEquals("<" + authzClient.getConfiguration().getAuthServerUrl() + uri + "?first=" + first + "&max=" + max + ">; rel=\"next\"", link); + } else { + assertEquals("<" + authzClient.getConfiguration().getAuthServerUrl() + uri + "?first=" + (first - max) + "&max=" + max + ">; rel=\"prev\"", link); + } + } + } catch (IOException e) { + fail("Fail to get link header"); + } + } }