[KEYCLOAK-10020] - Add ability to request user-managed (ticket) permissions by name
This commit is contained in:
parent
e7deb77725
commit
ebcfeb20a3
11 changed files with 155 additions and 18 deletions
|
@ -42,9 +42,9 @@ public class PermissionTicketAdapter implements PermissionTicket, CachedModel<Pe
|
|||
@Override
|
||||
public PermissionTicket getDelegateForUpdate() {
|
||||
if (updated == null) {
|
||||
cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getRequester(), cached.getResourceId(), cached.getScopeId(), cached.getResourceServerId());
|
||||
updated = cacheSession.getPermissionTicketStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
|
||||
if (updated == null) throw new IllegalStateException("Not found in database");
|
||||
cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getRequester(), cached.getResourceId(), updated.getResource().getName(), cached.getScopeId(), cached.getResourceServerId());
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ public class PermissionTicketAdapter implements PermissionTicket, CachedModel<Pe
|
|||
@Override
|
||||
public void setGrantedTimestamp(Long millis) {
|
||||
getDelegateForUpdate();
|
||||
cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getRequester(), cached.getResourceId(), cached.getScopeId(), cached.getResourceServerId());
|
||||
cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getRequester(), cached.getResourceId(), updated.getResource().getName(), cached.getScopeId(), cached.getResourceServerId());
|
||||
updated.setGrantedTimestamp(millis);
|
||||
}
|
||||
|
||||
|
@ -132,7 +132,7 @@ public class PermissionTicketAdapter implements PermissionTicket, CachedModel<Pe
|
|||
@Override
|
||||
public void setPolicy(Policy policy) {
|
||||
getDelegateForUpdate();
|
||||
cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getRequester(), cached.getResourceId(), cached.getScopeId(), cached.getResourceServerId());
|
||||
cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getRequester(), cached.getResourceId(), updated.getResource().getName(), cached.getScopeId(), cached.getResourceServerId());
|
||||
updated.setPolicy(policy);
|
||||
}
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
|
|||
@Override
|
||||
public void setDisplayName(String name) {
|
||||
getDelegateForUpdate();
|
||||
cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUris(modelSupplier), cached.getScopesIds(modelSupplier), cached.getResourceServerId(), cached.getOwner());
|
||||
cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(modelSupplier), cached.getScopesIds(modelSupplier), cached.getResourceServerId(), cached.getOwner());
|
||||
updated.setDisplayName(name);
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,7 @@ public class StoreFactoryCacheManager extends CacheManager {
|
|||
invalidations.add(StoreFactoryCacheSession.getResourceByOwnerCacheKey(owner, serverId));
|
||||
invalidations.add(StoreFactoryCacheSession.getResourceByOwnerCacheKey(owner, null));
|
||||
invalidations.add(StoreFactoryCacheSession.getPermissionTicketByResource(id, serverId));
|
||||
addInvalidations(InResourcePredicate.create().resource(name), invalidations);
|
||||
|
||||
if (type != null) {
|
||||
invalidations.add(StoreFactoryCacheSession.getResourceByTypeCacheKey(type, serverId));
|
||||
|
@ -137,11 +138,12 @@ public class StoreFactoryCacheManager extends CacheManager {
|
|||
}
|
||||
}
|
||||
|
||||
public void permissionTicketUpdated(String id, String owner, String requester, String resource, String scope, String serverId, Set<String> invalidations) {
|
||||
public void permissionTicketUpdated(String id, String owner, String requester, String resource, String resourceName, String scope, String serverId, Set<String> invalidations) {
|
||||
invalidations.add(id);
|
||||
invalidations.add(StoreFactoryCacheSession.getPermissionTicketByOwner(owner, serverId));
|
||||
invalidations.add(StoreFactoryCacheSession.getPermissionTicketByResource(resource, serverId));
|
||||
invalidations.add(StoreFactoryCacheSession.getPermissionTicketByGranted(requester, serverId));
|
||||
invalidations.add(StoreFactoryCacheSession.getPermissionTicketByResourceNameAndGranted(resourceName, requester, serverId));
|
||||
if (scope != null) {
|
||||
invalidations.add(StoreFactoryCacheSession.getPermissionTicketByScope(scope, serverId));
|
||||
}
|
||||
|
@ -151,8 +153,8 @@ public class StoreFactoryCacheManager extends CacheManager {
|
|||
policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations);
|
||||
}
|
||||
|
||||
public void permissionTicketRemoval(String id, String owner, String requester, String resource, String scope, String serverId, Set<String> invalidations) {
|
||||
permissionTicketUpdated(id, owner, requester, resource, scope, serverId, invalidations);
|
||||
public void permissionTicketRemoval(String id, String owner, String requester, String resource, String resourceName, String scope, String serverId, Set<String> invalidations) {
|
||||
permissionTicketUpdated(id, owner, requester, resource, resourceName, scope, serverId, invalidations);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -295,12 +295,12 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
|
|||
invalidationEvents.add(PolicyUpdatedEvent.create(id, name, resources, resourceTypes, scopes, serverId));
|
||||
}
|
||||
|
||||
public void registerPermissionTicketInvalidation(String id, String owner, String requester, String resource, String scope, String serverId) {
|
||||
cache.permissionTicketUpdated(id, owner, requester, resource, scope, serverId, invalidations);
|
||||
public void registerPermissionTicketInvalidation(String id, String owner, String requester, String resource, String resourceName, String scope, String serverId) {
|
||||
cache.permissionTicketUpdated(id, owner, requester, resource, resourceName, scope, serverId, invalidations);
|
||||
PermissionTicketAdapter adapter = managedPermissionTickets.get(id);
|
||||
if (adapter != null) adapter.invalidateFlag();
|
||||
|
||||
invalidationEvents.add(PermissionTicketUpdatedEvent.create(id, owner, requester, resource, scope, serverId));
|
||||
invalidationEvents.add(PermissionTicketUpdatedEvent.create(id, owner, requester, resource, resourceName, scope, serverId));
|
||||
}
|
||||
|
||||
private Set<String> getResourceTypes(Set<String> resources, String serverId) {
|
||||
|
@ -400,6 +400,10 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
|
|||
return "permission.ticket.granted." + userId + "." + serverId;
|
||||
}
|
||||
|
||||
public static String getPermissionTicketByResourceNameAndGranted(String resourceName, String userId, String serverId) {
|
||||
return "permission.ticket.granted." + resourceName + "." + userId + "." + serverId;
|
||||
}
|
||||
|
||||
public static String getPermissionTicketByOwner(String owner, String serverId) {
|
||||
return "permission.ticket.owner." + owner + "." + serverId;
|
||||
}
|
||||
|
@ -993,7 +997,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
|
|||
@Override
|
||||
public PermissionTicket create(String resourceId, String scopeId, String requester, ResourceServer resourceServer) {
|
||||
PermissionTicket created = getPermissionTicketStoreDelegate().create(resourceId, scopeId, requester, resourceServer);
|
||||
registerPermissionTicketInvalidation(created.getId(), created.getOwner(), created.getRequester(), created.getResource().getId(), scopeId, created.getResourceServer().getId());
|
||||
registerPermissionTicketInvalidation(created.getId(), created.getOwner(), created.getRequester(), created.getResource().getId(), created.getResource().getName(), scopeId, created.getResourceServer().getId());
|
||||
return created;
|
||||
}
|
||||
|
||||
|
@ -1008,8 +1012,8 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
|
|||
if (permission.getScope() != null) {
|
||||
scopeId = permission.getScope().getId();
|
||||
}
|
||||
invalidationEvents.add(PermissionTicketRemovedEvent.create(id, permission.getOwner(), permission.getRequester(), permission.getResource().getId(), scopeId, permission.getResourceServer().getId()));
|
||||
cache.permissionTicketRemoval(id, permission.getOwner(), permission.getRequester(), permission.getResource().getId(), scopeId, permission.getResourceServer().getId(), invalidations);
|
||||
invalidationEvents.add(PermissionTicketRemovedEvent.create(id, permission.getOwner(), permission.getRequester(), permission.getResource().getId(), permission.getResource().getName(), scopeId, permission.getResourceServer().getId()));
|
||||
cache.permissionTicketRemoval(id, permission.getOwner(), permission.getRequester(), permission.getResource().getId(), permission.getResource().getName(),scopeId, permission.getResourceServer().getId(), invalidations);
|
||||
getPermissionTicketStoreDelegate().delete(id);
|
||||
UserManagedPermissionUtil.removePolicy(permission, StoreFactoryCacheSession.this);
|
||||
|
||||
|
@ -1075,6 +1079,13 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
|
|||
(revision, permissions) -> new PermissionTicketListQuery(revision, cacheKey, permissions.stream().map(permission -> permission.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PermissionTicket> findGranted(String resourceName, String userId, String resourceServerId) {
|
||||
String cacheKey = getPermissionTicketByResourceNameAndGranted(resourceName, userId, resourceServerId);
|
||||
return cacheQuery(cacheKey, PermissionTicketListQuery.class, () -> getPermissionTicketStoreDelegate().findGranted(resourceName, userId, resourceServerId),
|
||||
(revision, permissions) -> new PermissionTicketResourceListQuery(revision, cacheKey, resourceName, permissions.stream().map(permission -> permission.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PermissionTicket> findByOwner(String owner, String resourceServerId) {
|
||||
String cacheKey = getPermissionTicketByOwner(owner, resourceServerId);
|
||||
|
|
|
@ -33,13 +33,15 @@ public class PermissionTicketRemovedEvent extends InvalidationEvent implements A
|
|||
private String scope;
|
||||
private String serverId;
|
||||
private String requester;
|
||||
private String resourceName;
|
||||
|
||||
public static PermissionTicketRemovedEvent create(String id, String owner, String requester, String resource, String scope, String serverId) {
|
||||
public static PermissionTicketRemovedEvent create(String id, String owner, String requester, String resource, String resourceName, String scope, String serverId) {
|
||||
PermissionTicketRemovedEvent event = new PermissionTicketRemovedEvent();
|
||||
event.id = id;
|
||||
event.owner = owner;
|
||||
event.requester = requester;
|
||||
event.resource = resource;
|
||||
event.resourceName = resourceName;
|
||||
event.scope = scope;
|
||||
event.serverId = serverId;
|
||||
return event;
|
||||
|
@ -57,6 +59,6 @@ public class PermissionTicketRemovedEvent extends InvalidationEvent implements A
|
|||
|
||||
@Override
|
||||
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
|
||||
cache.permissionTicketRemoval(id, owner, requester, resource, scope, serverId, invalidations);
|
||||
cache.permissionTicketRemoval(id, owner, requester, resource, resourceName, scope, serverId, invalidations);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,13 +33,15 @@ public class PermissionTicketUpdatedEvent extends InvalidationEvent implements A
|
|||
private String scope;
|
||||
private String serverId;
|
||||
private String requester;
|
||||
private String resourceName;
|
||||
|
||||
public static PermissionTicketUpdatedEvent create(String id, String owner, String requester, String resource, String scope, String serverId) {
|
||||
public static PermissionTicketUpdatedEvent create(String id, String owner, String requester, String resource, String resourceName, String scope, String serverId) {
|
||||
PermissionTicketUpdatedEvent event = new PermissionTicketUpdatedEvent();
|
||||
event.id = id;
|
||||
event.owner = owner;
|
||||
event.requester = requester;
|
||||
event.resource = resource;
|
||||
event.resourceName = resourceName;
|
||||
event.scope = scope;
|
||||
event.serverId = serverId;
|
||||
return event;
|
||||
|
@ -57,6 +59,6 @@ public class PermissionTicketUpdatedEvent extends InvalidationEvent implements A
|
|||
|
||||
@Override
|
||||
public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
|
||||
cache.permissionTicketUpdated(id, owner, requester, resource, scope, serverId, invalidations);
|
||||
cache.permissionTicketUpdated(id, owner, requester, resource, resourceName, scope, serverId, invalidations);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -192,6 +192,8 @@ public class JPAPermissionTicketStore implements PermissionTicketStore {
|
|||
}
|
||||
} else if (PermissionTicket.RESOURCE.equals(name)) {
|
||||
predicates.add(root.join("resource").get("id").in(value));
|
||||
} else if (PermissionTicket.RESOURCE_NAME.equals(name)) {
|
||||
predicates.add(root.join("resource").get("name").in(value));
|
||||
} else if (PermissionTicket.OWNER.equals(name)) {
|
||||
predicates.add(builder.equal(root.get("owner"), value));
|
||||
} else if (PermissionTicket.REQUESTER.equals(name)) {
|
||||
|
@ -249,6 +251,17 @@ public class JPAPermissionTicketStore implements PermissionTicketStore {
|
|||
return find(filters, resourceServerId, -1, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PermissionTicket> findGranted(String resourceName, String userId, String resourceServerId) {
|
||||
HashMap<String, String> filters = new HashMap<>();
|
||||
|
||||
filters.put(PermissionTicket.RESOURCE_NAME, resourceName);
|
||||
filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString());
|
||||
filters.put(PermissionTicket.REQUESTER, userId);
|
||||
|
||||
return find(filters, resourceServerId, -1, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PermissionTicket> findByOwner(String owner, String resourceServerId) {
|
||||
TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByType", String.class);
|
||||
|
|
|
@ -23,6 +23,7 @@ public interface PermissionTicket {
|
|||
|
||||
String ID = "id";
|
||||
String RESOURCE = "resource.id";
|
||||
String RESOURCE_NAME = "resource.name";
|
||||
String SCOPE = "scope.id";
|
||||
String SCOPE_IS_NULL = "scope_is_null";
|
||||
String OWNER = "owner";
|
||||
|
|
|
@ -99,4 +99,14 @@ public interface PermissionTicketStore {
|
|||
* @return a list of permissions granted for a particular user
|
||||
*/
|
||||
List<PermissionTicket> findGranted(String userId, String resourceServerId);
|
||||
|
||||
/**
|
||||
* Returns a list of {@link PermissionTicket} with name equal to {@code resourceName} granted to the given {@code userId}.
|
||||
*
|
||||
* @param resourceName the name of a resource
|
||||
* @param userId the user id
|
||||
* @param resourceServerId the resource server id
|
||||
* @return a list of permissions granted for a particular user
|
||||
*/
|
||||
List<PermissionTicket> findGranted(String resourceName, String userId, String resourceServerId);
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import org.keycloak.authorization.common.KeycloakIdentity;
|
|||
import org.keycloak.authorization.model.Resource;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.authorization.model.Scope;
|
||||
import org.keycloak.authorization.model.PermissionTicket;
|
||||
import org.keycloak.authorization.permission.ResourcePermission;
|
||||
import org.keycloak.authorization.policy.evaluation.EvaluationContext;
|
||||
import org.keycloak.authorization.policy.evaluation.PermissionTicketAwareDecisionResultCollector;
|
||||
|
@ -419,6 +420,11 @@ public class AuthorizationTokenService {
|
|||
}
|
||||
|
||||
if (!identity.isResourceServer()) {
|
||||
List<PermissionTicket> tickets = storeFactory.getPermissionTicketStore().findGranted(resourceName, identity.getId(), resourceServer.getId());
|
||||
for (PermissionTicket permissionTicket : tickets) {
|
||||
requestedResources.add(permissionTicket.getResource());
|
||||
}
|
||||
|
||||
Resource serverResource = resourceStore.findByName(resourceName, resourceServer.getId());
|
||||
|
||||
if (serverResource != null) {
|
||||
|
|
|
@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotNull;
|
|||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
@ -34,7 +35,9 @@ import org.keycloak.admin.client.resource.AuthorizationResource;
|
|||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
||||
import org.keycloak.authorization.client.resource.PermissionResource;
|
||||
import org.keycloak.authorization.client.util.HttpResponseException;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
|
||||
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
|
||||
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.Permission;
|
||||
|
@ -284,7 +287,7 @@ public class UserManagedAccessTest extends AbstractResourceServerTest {
|
|||
|
||||
try {
|
||||
response = authorize("kolo", "password", resource.getId(), new String[] {});
|
||||
fail("User should have access to resource from another user");
|
||||
fail("User should not have access to resource from another user");
|
||||
} catch (AuthorizationDeniedException ade) {
|
||||
|
||||
}
|
||||
|
@ -330,6 +333,86 @@ public class UserManagedAccessTest extends AbstractResourceServerTest {
|
|||
assertTrue(permissions.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserGrantedAccessConsideredWhenRequestingAuthorizationByResourceName() throws Exception {
|
||||
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
|
||||
resource = addResource("Resource A", "marta", true, "ScopeA", "ScopeB");
|
||||
|
||||
permission.setName(resource.getName() + " Permission");
|
||||
permission.addResource(resource.getId());
|
||||
permission.addPolicy("Only Owner Policy");
|
||||
|
||||
getClient(getRealm()).authorization().permissions().resource().create(permission).close();
|
||||
|
||||
try {
|
||||
AuthorizationResponse response = authorize("kolo", "password", resource.getId(), new String[] {});
|
||||
fail("User should not have access to resource from another user");
|
||||
} catch (AuthorizationDeniedException ade) {
|
||||
|
||||
}
|
||||
|
||||
PermissionResource permissionResource = getAuthzClient().protection().permission();
|
||||
List<PermissionTicketRepresentation> permissionTickets = permissionResource.findByResource(resource.getId());
|
||||
|
||||
assertFalse(permissionTickets.isEmpty());
|
||||
assertEquals(2, permissionTickets.size());
|
||||
|
||||
for (PermissionTicketRepresentation ticket : permissionTickets) {
|
||||
assertFalse(ticket.isGranted());
|
||||
|
||||
ticket.setGranted(true);
|
||||
|
||||
permissionResource.update(ticket);
|
||||
}
|
||||
|
||||
permissionTickets = permissionResource.findByResource(resource.getId());
|
||||
|
||||
assertFalse(permissionTickets.isEmpty());
|
||||
assertEquals(2, permissionTickets.size());
|
||||
|
||||
for (PermissionTicketRepresentation ticket : permissionTickets) {
|
||||
assertTrue(ticket.isGranted());
|
||||
}
|
||||
|
||||
AuthorizationRequest request = new AuthorizationRequest();
|
||||
// No resource id used in request, only name
|
||||
request.addPermission("Resource A", "ScopeA", "ScopeB");
|
||||
|
||||
List<Permission> permissions = authorize("kolo", "password", request);
|
||||
|
||||
assertEquals(1, permissions.size());
|
||||
Permission koloPermission = permissions.get(0);
|
||||
assertEquals("Resource A", koloPermission.getResourceName());
|
||||
assertTrue(koloPermission.getScopes().containsAll(Arrays.asList("ScopeA", "ScopeB")));
|
||||
|
||||
ResourceRepresentation resourceRep = getAuthzClient().protection().resource().findById(resource.getId());
|
||||
|
||||
resourceRep.setName("Resource A Changed");
|
||||
|
||||
getAuthzClient().protection().resource().update(resourceRep);
|
||||
|
||||
request = new AuthorizationRequest();
|
||||
// Try to use the old name
|
||||
request.addPermission("Resource A", "ScopeA", "ScopeB");
|
||||
|
||||
try {
|
||||
authorize("kolo", "password", request);
|
||||
fail("User should not have access to resource from another user");
|
||||
} catch (RuntimeException ade) {
|
||||
assertTrue(ade.getCause().toString().contains("invalid_resource"));
|
||||
}
|
||||
|
||||
request = new AuthorizationRequest();
|
||||
request.addPermission(resourceRep.getName(), "ScopeA", "ScopeB");
|
||||
|
||||
permissions = authorize("kolo", "password", request);
|
||||
|
||||
assertEquals(1, permissions.size());
|
||||
koloPermission = permissions.get(0);
|
||||
assertEquals(resourceRep.getName(), koloPermission.getResourceName());
|
||||
assertTrue(koloPermission.getScopes().containsAll(Arrays.asList("ScopeA", "ScopeB")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserGrantsAccessToResourceWithoutScopes() throws Exception {
|
||||
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
|
||||
|
@ -547,4 +630,11 @@ public class UserManagedAccessTest extends AbstractResourceServerTest {
|
|||
assertPermissions(permissions, "Resource A");
|
||||
assertTrue(permissions.isEmpty());
|
||||
}
|
||||
|
||||
private List<Permission> authorize(String userName, String password, AuthorizationRequest request) {
|
||||
AuthorizationResponse response = getAuthzClient().authorization(userName, password).authorize(request);
|
||||
AccessToken token = toAccessToken(response.getToken());
|
||||
AccessToken.Authorization authorization = token.getAuthorization();
|
||||
return new ArrayList<>(authorization.getPermissions());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue