[KEYCLOAK-10714] - Add filtering support in My Resources endpoint by name

This commit is contained in:
Pedro Igor 2019-09-26 19:43:24 -03:00 committed by Stian Thorgersen
parent ab2bb31505
commit 17785dac08
6 changed files with 100 additions and 35 deletions

View file

@ -1136,8 +1136,8 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
} }
@Override @Override
public List<Resource> findGrantedResources(String requester, int first, int max) { public List<Resource> findGrantedResources(String requester, String name, int first, int max) {
return getPermissionTicketStoreDelegate().findGrantedResources(requester, first, max); return getPermissionTicketStoreDelegate().findGrantedResources(requester, name, first, max);
} }
@Override @Override

View file

@ -43,6 +43,7 @@ import javax.persistence.UniqueConstraint;
@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="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="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="findGrantedResourcesByName", 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 and lower(r.name) like :resourceName 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") @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")
} }
) )

View file

@ -266,12 +266,18 @@ public class JPAPermissionTicketStore implements PermissionTicketStore {
} }
@Override @Override
public List<Resource> findGrantedResources(String requester, int first, int max) { public List<Resource> findGrantedResources(String requester, String name, int first, int max) {
TypedQuery<String> query = entityManager.createNamedQuery("findGrantedResources", String.class); TypedQuery<String> query = name == null ?
entityManager.createNamedQuery("findGrantedResources", String.class) :
entityManager.createNamedQuery("findGrantedResourcesByName", String.class);
query.setFlushMode(FlushModeType.COMMIT); query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("requester", requester); query.setParameter("requester", requester);
if (name != null) {
query.setParameter("resourceName", "%" + name.toLowerCase() + "%");
}
if (first > -1 && max > -1) { if (first > -1 && max > -1) {
query.setFirstResult(first); query.setFirstResult(first);
query.setMaxResults(max); query.setMaxResults(max);

View file

@ -115,11 +115,12 @@ public interface PermissionTicketStore {
* Returns a list of {@link Resource} granted to the given {@code requester} * Returns a list of {@link Resource} granted to the given {@code requester}
* *
* @param requester the requester * @param requester the requester
* @param name the keyword to query resources by name or null if any resource
* @param first first result * @param first first result
* @param max max result * @param max max result
* @return a list of {@link Resource} granted to the given {@code requester} * @return a list of {@link Resource} granted to the given {@code requester}
*/ */
List<Resource> findGrantedResources(String requester, int first, int max); List<Resource> findGrantedResources(String requester, String name, int first, int max);
/** /**
* Returns a list of {@link Resource} granted by the owner to other users * Returns a list of {@link Resource} granted by the owner to other users

View file

@ -27,7 +27,6 @@ import javax.ws.rs.core.Link;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -57,14 +56,24 @@ public class ResourcesService extends AbstractResourceService {
* Returns a list of {@link Resource} where the {@link #user} is the resource owner. * Returns a list of {@link Resource} where the {@link #user} is the resource owner.
* *
* @param first the first result * @param first the first result
* @param max the max result * @param max the max result
* @return a list of {@link Resource} where the {@link #user} is the resource owner * @return a list of {@link Resource} where the {@link #user} is the resource owner
*/ */
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response getResources(@QueryParam("first") Integer first, @QueryParam("max") Integer max) { public Response getResources(@QueryParam("name") String name,
return queryResponse((f, m) -> resourceStore.findByOwner(user.getId(), null, f, m) @QueryParam("first") Integer first,
.stream().map(resource -> new Resource(resource, user, provider)), first, max); @QueryParam("max") Integer max) {
Map<String, String[]> filters = new HashMap<>();
filters.put("owner", new String[] { user.getId() });
if (name != null) {
filters.put("name", new String[] { name });
}
return queryResponse((f, m) -> resourceStore.findByResourceServer(filters, null, f, m).stream()
.map(resource -> new Resource(resource, user, provider)), first, max);
} }
/** /**
@ -77,8 +86,10 @@ public class ResourcesService extends AbstractResourceService {
@GET @GET
@Path("shared-with-me") @Path("shared-with-me")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response getSharedWithMe(@QueryParam("first") Integer first, @QueryParam("max") Integer max) { public Response getSharedWithMe(@QueryParam("name") String name,
return queryResponse((f, m) -> toPermissions(ticketStore.findGrantedResources(auth.getUser().getId(), f, m), false) @QueryParam("first") Integer first,
@QueryParam("max") Integer max) {
return queryResponse((f, m) -> toPermissions(ticketStore.findGrantedResources(auth.getUser().getId(), name, f, m), false)
.stream(), first, max); .stream(), first, max);
} }
@ -189,10 +200,12 @@ public class ResourcesService extends AbstractResourceService {
.rel("next").build()); .rel("next").build());
} }
links.add(Link.fromUri( if (first > 0) {
KeycloakUriBuilder.fromUri(request.getUri().getRequestUri()).replaceQuery("first={first}&max={max}") links.add(Link.fromUri(
.build(nextPage ? first : first - max, max)) KeycloakUriBuilder.fromUri(request.getUri().getRequestUri()).replaceQuery("first={first}&max={max}")
.rel("prev").build()); .build(nextPage ? first : first - max, max))
.rel("prev").build());
}
return links.toArray(new Link[links.size()]); return links.toArray(new Link[links.size()]);
} }

View file

@ -42,6 +42,7 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.authorization.client.AuthzClient; import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration; import org.keycloak.authorization.client.Configuration;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AccountRoles; import org.keycloak.models.AccountRoles;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -158,6 +159,13 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest {
assertMyResourcesResponse(resources); assertMyResourcesResponse(resources);
} }
@Test
public void testGetMyResourcesByName() {
assertEquals(11, getMyResources("Resource 1").size());
assertEquals(0, getMyResources("non-existent\n").size());
assertEquals(1, getMyResources("Resource 23").size());
}
@Test @Test
public void testGetMyResourcesPagination() { public void testGetMyResourcesPagination() {
List<Resource> resources = getMyResources(0, 10, response -> assertNextPageLink(response, "/realms/test/account/resources", 10, 10)); List<Resource> resources = getMyResources(0, 10, response -> assertNextPageLink(response, "/realms/test/account/resources", 10, 10));
@ -206,19 +214,26 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest {
} }
} }
@Test
public void testGetSharedWithMeByName() {
assertEquals(5, getSharedWithMe("jdoe", "Resource 1", -1, -1, null).size());
assertEquals(0, getSharedWithMe("jdoe", "non-existent", -1, -1, null).size());
assertEquals(10, getSharedWithMe("jdoe", "resource", -1, -1, null).size());
}
@Test @Test
public void testGetSharedWithMePagination() { public void testGetSharedWithMePagination() {
for (String userName : userNames) { for (String userName : userNames) {
List<AbstractResourceService.ResourcePermission> resources = getSharedWithMe(userName, 0, 3, List<AbstractResourceService.ResourcePermission> resources = getSharedWithMe(userName, null, 0, 3,
response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 3, 3)); response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 3, 3));
assertSharedWithMeResponse(resources); assertSharedWithMeResponse(resources);
getSharedWithMe(userName, 3, 3, getSharedWithMe(userName, null, 3, 3,
response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 6, 3)); response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 6, 3));
getSharedWithMe(userName, 6, 3, getSharedWithMe(userName, null, 6, 3,
response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 9, 3)); response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 9, 3));
getSharedWithMe(userName, 9, 3, getSharedWithMe(userName, null, 9, 3,
response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 9, 3, true)); response -> assertNextPageLink(response, "/realms/test/account/resources/shared-with-me", 9, 3, true));
} }
} }
@ -549,15 +564,22 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest {
} }
private List<AbstractResourceService.ResourcePermission> getSharedWithMe(String userName) { private List<AbstractResourceService.ResourcePermission> getSharedWithMe(String userName) {
return getSharedWithMe(userName, -1, -1, null); return getSharedWithMe(userName, null, -1, -1, null);
} }
private List<AbstractResourceService.ResourcePermission> getSharedWithMe(String userName, int first, int max, Consumer<SimpleHttp.Response> responseHandler) { private List<AbstractResourceService.ResourcePermission> getSharedWithMe(String userName, String name, int first, int max, Consumer<SimpleHttp.Response> responseHandler) {
if (first > -1 && max > -1) { KeycloakUriBuilder uri = KeycloakUriBuilder.fromUri("/shared-with-me");
return doGet("/shared-with-me?first=" + first + "&max=" + max, authzClient.obtainAccessToken(userName, "password").getToken(),
new TypeReference<List<AbstractResourceService.ResourcePermission>>() {}, responseHandler); if (name != null) {
uri.queryParam("name", name);
} }
return doGet("/shared-with-me", authzClient.obtainAccessToken(userName, "password").getToken(),
if (first > -1 && max > -1) {
uri.queryParam("first", first);
uri.queryParam("max", max);
}
return doGet(uri.build().toString(), authzClient.obtainAccessToken(userName, "password").getToken(),
new TypeReference<List<AbstractResourceService.ResourcePermission>>() {}, responseHandler); new TypeReference<List<AbstractResourceService.ResourcePermission>>() {}, responseHandler);
} }
@ -636,18 +658,35 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest {
return getMyResources(-1, -1); return getMyResources(-1, -1);
} }
private List<Resource> getMyResources(String name) {
return getMyResources(name, -1, -1);
}
private List<Resource> getMyResources(int first, int max) { private List<Resource> getMyResources(int first, int max) {
if (first > -1 && max > -1) { return getMyResources(null, first, max);
return doGet("?first=" + first + "&max=" + max, new TypeReference<List<Resource>>() {}); }
private List<Resource> getMyResources(String name, int first, int max) {
KeycloakUriBuilder uri = KeycloakUriBuilder.fromUri("");
if (name != null) {
uri.queryParam("name", name);
} }
return doGet("", new TypeReference<List<Resource>>() {});
if (first > -1 && max > -1) {
uri.queryParam("first", first);
uri.queryParam("max", max);
}
return doGet(uri.build().toString(), new TypeReference<List<Resource>>() {});
} }
private List<Resource> getMyResources(int first, int max, Consumer<SimpleHttp.Response> response) { private List<Resource> getMyResources(int first, int max, Consumer<SimpleHttp.Response> response) {
String query = "";
if (first > -1 && max > -1) { if (first > -1 && max > -1) {
return doGet("?first=" + first + "&max=" + max, new TypeReference<List<Resource>>() {}, response); query = "?first=" + first + "&max=" + max;
} }
return doGet("", new TypeReference<List<Resource>>() {}, response); return doGet(query, new TypeReference<List<Resource>>() {}, response);
} }
private void assertSharedWithOthersResponse(List<AbstractResourceService.ResourcePermission> resources) { private void assertSharedWithOthersResponse(List<AbstractResourceService.ResourcePermission> resources) {
@ -708,7 +747,7 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest {
assertNextPageLink(response, uri, first, max, lastPage, false); assertNextPageLink(response, uri, first, max, lastPage, false);
} }
private void assertNextPageLink(SimpleHttp.Response response, String uri, int first, int max, boolean lastPage, boolean singlePage) { private void assertNextPageLink(SimpleHttp.Response response, String uri, int nextPage, int max, boolean lastPage, boolean singlePage) {
try { try {
List<String> links = response.getHeader("Link"); List<String> links = response.getHeader("Link");
@ -718,13 +757,18 @@ public class ResourcesRestServiceTest extends AbstractRestServiceTest {
} }
assertNotNull(links); assertNotNull(links);
assertEquals(lastPage ? 1 : 2, links.size());
if (max - nextPage == 0) {
assertEquals(1, links.size());
} else {
assertEquals(lastPage ? 1 : 2, links.size());
}
for (String link : links) { for (String link : links) {
if (link.contains("rel=\"next\"")) { if (link.contains("rel=\"next\"")) {
assertEquals("<" + authzClient.getConfiguration().getAuthServerUrl() + uri + "?first=" + first + "&max=" + max + ">; rel=\"next\"", link); assertEquals("<" + authzClient.getConfiguration().getAuthServerUrl() + uri + "?first=" + nextPage + "&max=" + max + ">; rel=\"next\"", link);
} else { } else {
assertEquals("<" + authzClient.getConfiguration().getAuthServerUrl() + uri + "?first=" + (first - max) + "&max=" + max + ">; rel=\"prev\"", link); assertEquals("<" + authzClient.getConfiguration().getAuthServerUrl() + uri + "?first=" + (nextPage - max) + "&max=" + max + ">; rel=\"prev\"", link);
} }
} }
} catch (IOException e) { } catch (IOException e) {