ResourcesService.java
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other 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.services.resources.account.resources;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Link;
import jakarta.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
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.keycloak.http.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;
import org.keycloak.utils.MediaType;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ResourcesService extends AbstractResourceService {
public ResourcesService(KeycloakSession session, UserModel user, Auth auth, HttpRequest request) {
super(session, user, auth, request);
}
/**
* 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(@QueryParam("name") String name,
@QueryParam("first") Integer first,
@QueryParam("max") Integer max) {
Map<org.keycloak.authorization.model.Resource.FilterOption, String[]> filters =
new EnumMap<>(org.keycloak.authorization.model.Resource.FilterOption.class);
filters.put(org.keycloak.authorization.model.Resource.FilterOption.OWNER, new String[] { user.getId() });
if (name != null) {
filters.put(org.keycloak.authorization.model.Resource.FilterOption.NAME, new String[] { name });
}
return queryResponse((f, m) -> resourceStore.find(auth.getRealm(), null, filters, 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(@QueryParam("name") String name,
@QueryParam("first") Integer first,
@QueryParam("max") Integer max) {
return queryResponse((f, m) -> toPermissions(ticketStore.findGrantedResources(auth.getRealm(), auth.getUser().getId(), name, 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(@QueryParam("first") Integer first, @QueryParam("max") Integer max) {
return queryResponse(
(f, m) -> toPermissions(ticketStore.findGrantedOwnerResources(auth.getRealm(), auth.getUser().getId(), f, m), true)
.stream(), first, max);
}
/**
*/
@GET
@Path("pending-requests")
@Produces(MediaType.APPLICATION_JSON)
public Response getPendingRequests() {
Map<PermissionTicket.FilterOption, String> filters = new EnumMap<>(PermissionTicket.FilterOption.class);
filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId());
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString());
final List<PermissionTicket> permissionTickets = ticketStore.find(auth.getRealm(), null, filters, null, null);
final List<ResourcePermission> resourceList = new ArrayList<>(permissionTickets.size());
for (PermissionTicket ticket : permissionTickets) {
ResourcePermission resourcePermission = new ResourcePermission(ticket.getResource(), provider);
resourcePermission.addScope(new Scope(ticket.getScope()));
resourceList.add(resourcePermission);
}
return queryResponse(
(f, m) -> resourceList.stream(), -1, resourceList.size());
}
@Path("{id}")
public Object getResource(@PathParam("id") String id) {
org.keycloak.authorization.model.Resource resource = resourceStore.findById(auth.getRealm(), null, id);
if (resource == null) {
throw new NotFoundException("resource_not_found");
}
if (!resource.getOwner().equals(user.getId())) {
throw new BadRequestException("invalid_resource");
}
return new ResourceService(resource, provider.getKeycloakSession(), user, auth, request);
}
private Collection<ResourcePermission> toPermissions(List<org.keycloak.authorization.model.Resource> resources, boolean withRequesters) {
Collection<ResourcePermission> permissions = new ArrayList<>();
PermissionTicketStore ticketStore = provider.getStoreFactory().getPermissionTicketStore();
for (org.keycloak.authorization.model.Resource resource : resources) {
ResourcePermission permission = new ResourcePermission(resource, provider);
List<PermissionTicket> tickets;
if (withRequesters) {
Map<PermissionTicket.FilterOption, String> filters = new EnumMap<>(PermissionTicket.FilterOption.class);
filters.put(PermissionTicket.FilterOption.OWNER, user.getId());
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString());
filters.put(PermissionTicket.FilterOption.RESOURCE_ID, resource.getId());
tickets = ticketStore.find(auth.getRealm(), resource.getResourceServer(), filters, null, null);
} else {
tickets = ticketStore.findGranted(resource.getResourceServer(), resource.getName(), user.getId());
}
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<Integer, Integer, Stream<?>> 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 Response.ok().entity(result).links(createPageLinks(first, max, size)).build();
}
return Response.ok().entity(query.apply(-1, -1).collect(Collectors.toList())).build();
}
private Link[] createPageLinks(Integer first, Integer max, int resultSize) {
if (resultSize == 0 || (first == 0 && resultSize <= max)) {
return new Link[] {};
}
List<Link> links = new ArrayList();
boolean nextPage = resultSize > max;
if (nextPage) {
links.add(Link.fromUri(
KeycloakUriBuilder.fromUri(uriInfo.getRequestUri()).replaceQuery("first={first}&max={max}")
.build(first + max, max))
.rel("next").build());
}
if (first > 0) {
links.add(Link.fromUri(
KeycloakUriBuilder.fromUri(uriInfo.getRequestUri()).replaceQuery("first={first}&max={max}")
.build(Math.max(first - max, 0), max))
.rel("prev").build());
}
return links.toArray(new Link[links.size()]);
}
}