keycloak-uncached
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java 27(+16 -11)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java 2(+1 -1)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java 2(+0 -2)
core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java 3(+3 -0)
examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 7(+6 -1)
server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java 50(+37 -13)
services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java 18(+7 -11)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 7(+6 -1)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
index 6b1fe19..0c0fc23 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
@@ -126,11 +126,13 @@ public abstract class AbstractPolicyEnforcer {
List<Permission> permissions = authorization.getPermissions();
for (Permission permission : permissions) {
- Set<String> allowedScopes = permission.getScopes();
-
if (permission.getResourceSetId() != null) {
if (isResourcePermission(actualPathConfig, permission)) {
- if (((allowedScopes == null || allowedScopes.isEmpty()) && requiredScopes.isEmpty()) || allowedScopes.containsAll(requiredScopes)) {
+ if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) {
+ continue;
+
+ }
+ if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
this.paths.remove(actualPathConfig);
@@ -138,11 +140,6 @@ public abstract class AbstractPolicyEnforcer {
return true;
}
}
- } else {
- if ((allowedScopes.isEmpty() && requiredScopes.isEmpty()) || allowedScopes.containsAll(requiredScopes)) {
- LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
- return true;
- }
}
}
@@ -151,6 +148,11 @@ public abstract class AbstractPolicyEnforcer {
return false;
}
+ private boolean hasResourceScopePermission(Set<String> requiredScopes, Permission permission, PathConfig actualPathConfig) {
+ Set<String> allowedScopes = permission.getScopes();
+ return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty());
+ }
+
protected AuthzClient getAuthzClient() {
return this.authzClient;
}
@@ -210,7 +212,6 @@ public abstract class AbstractPolicyEnforcer {
config.setPath(targetResource.getUri());
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
- config.setInstance(true);
config.setParentConfig(originalConfig);
this.paths.add(config);
@@ -244,13 +245,17 @@ public abstract class AbstractPolicyEnforcer {
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
// first we try a match using resource id
- boolean resourceMatch = permission.getResourceSetId().equals(actualPathConfig.getId());
+ boolean resourceMatch = matchResourcePermission(actualPathConfig, permission);
// as a fallback, check if the current path is an instance and if so, check if parent's id matches the permission
if (!resourceMatch && actualPathConfig.isInstance()) {
- resourceMatch = permission.getResourceSetId().equals(actualPathConfig.getParentConfig().getId());
+ resourceMatch = matchResourcePermission(actualPathConfig.getParentConfig(), permission);
}
return resourceMatch;
}
+
+ private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
+ return permission.getResourceSetId().equals(actualPathConfig.getId());
+ }
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
index e151f76..a776762 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
@@ -50,7 +50,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
int retry = 2;
AccessToken original = accessToken;
- while (retry >= 0) {
+ while (retry > 0) {
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
original.setAuthorization(accessToken.getAuthorization());
return true;
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index 5c26124..aa6d3d2 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -45,7 +45,6 @@ public class PolicyEnforcer {
private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class);
private final KeycloakDeployment deployment;
- private final PathMatcher pathMatcher;
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
private final List<PathConfig> paths;
@@ -54,7 +53,6 @@ public class PolicyEnforcer {
this.deployment = deployment;
this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
this.authzClient = AuthzClient.create(new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient()));
- this.pathMatcher = new PathMatcher();
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
if (LOGGER.isDebugEnabled()) {
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
index b2c5757..9cf710a 100644
--- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -109,7 +109,6 @@ public class PolicyEnforcerConfig {
private List<MethodConfig> methods = new ArrayList<>();
private List<String> scopes = Collections.emptyList();
private String id;
- private boolean instance;
@JsonIgnore
private PathConfig parentConfig;
@@ -178,11 +177,7 @@ public class PolicyEnforcerConfig {
}
public boolean isInstance() {
- return instance;
- }
-
- public void setInstance(boolean instance) {
- this.instance = instance;
+ return this.parentConfig != null;
}
public void setParentConfig(PathConfig parentConfig) {
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java
index dd21901..9a51031 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java
@@ -16,6 +16,8 @@
*/
package org.keycloak.representations.idm.authorization;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -35,6 +37,7 @@ public class PolicyRepresentation {
private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS;
private Map<String, String> config = new HashMap();
private List<PolicyRepresentation> dependentPolicies;
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<PolicyRepresentation> associatedPolicies = new ArrayList<>();
public String getId() {
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
index c28bca3..a5d7f16 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -109,7 +109,12 @@ public class AlbumService {
private void createProtectedResource(Album album) {
try {
- ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), new HashSet(), "/album/" + album.getId(), "http://photoz.com/album");
+ HashSet<ScopeRepresentation> scopes = new HashSet<>();
+
+ scopes.add(new ScopeRepresentation(SCOPE_ALBUM_VIEW));
+ scopes.add(new ScopeRepresentation(SCOPE_ALBUM_DELETE));
+
+ ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album");
albumResource.setOwner(album.getUserId());
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
index 95fb58b..6849d07 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
@@ -9,17 +9,18 @@
"secret": "secret"
},
"policy-enforcer": {
+ "user-managed-access" : {},
"paths": [
{
"path" : "/album/*",
"methods" : [
{
- "method": "GET",
- "scopes" : ["urn:photoz.com:scopes:album:view"]
- },
- {
"method": "POST",
"scopes" : ["urn:photoz.com:scopes:album:create"]
+ },
+ {
+ "method": "GET",
+ "scopes" : ["urn:photoz.com:scopes:album:view"]
}
]
},
@@ -30,6 +31,10 @@
{
"method": "DELETE",
"scopes" : ["urn:photoz.com:scopes:album:delete"]
+ },
+ {
+ "method": "GET",
+ "scopes" : ["urn:photoz.com:scopes:album:view"]
}
]
},
diff --git a/examples/authz/photoz/photoz-restful-api-authz-service.json b/examples/authz/photoz/photoz-restful-api-authz-service.json
index 6547d2f..455948c 100644
--- a/examples/authz/photoz/photoz-restful-api-authz-service.json
+++ b/examples/authz/photoz/photoz-restful-api-authz-service.json
@@ -52,7 +52,6 @@
"sessionName": "MainOwnerSession",
"mavenArtifactGroupId": "org.keycloak",
"moduleName": "PhotozAuthzOwnerPolicy",
- "applyPolicies": "[]",
"scannerPeriod": "1",
"scannerPeriodUnit": "Hours"
}
@@ -64,7 +63,6 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "applyPolicies": "[]",
"roles": "[{\"id\":\"admin\",\"required\":true}]"
}
},
@@ -75,7 +73,6 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "applyPolicies": "[]",
"roles": "[{\"id\":\"user\"},{\"id\":\"manage-albums\",\"required\":true}]"
}
},
@@ -86,7 +83,6 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "applyPolicies": "[]",
"code": "var contextAttributes = $evaluation.getContext().getAttributes();\n\nif (contextAttributes.containsValue('kc.client.network.ip_address', '127.0.0.1')) {\n $evaluation.grant();\n}"
}
},
@@ -117,7 +113,6 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "applyPolicies": "[]",
"code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}"
}
},
diff --git a/examples/authz/servlet-authz/servlet-authz-app-config.json b/examples/authz/servlet-authz/servlet-authz-app-config.json
index d5fb1cb..43ebde4 100644
--- a/examples/authz/servlet-authz/servlet-authz-app-config.json
+++ b/examples/authz/servlet-authz/servlet-authz-app-config.json
@@ -54,7 +54,7 @@
"description": "Defines that adminsitrators can do something",
"type": "role",
"config": {
- "roles": "[\"admin\"]"
+ "roles": "[{\"id\":\"admin\"}]"
}
},
{
@@ -62,7 +62,7 @@
"description": "Defines that any user can do something",
"type": "role",
"config": {
- "roles": "[\"user\"]"
+ "roles": "[{\"id\":\"user\"}]"
}
},
{
@@ -71,7 +71,7 @@
"type": "role",
"logic": "POSITIVE",
"config": {
- "roles": "[\"user_premium\"]"
+ "roles": "[{\"id\":\"user_premium\"}]"
}
},
{
diff --git a/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java
index a70d3c0..a0a7b6c 100644
--- a/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java
+++ b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java
@@ -67,7 +67,8 @@ public interface Attributes {
* @return true if any attribute with <code>name</code> and <code>value</code> exist. Otherwise, returns false.
*/
default boolean containsValue(String name, String value) {
- return toMap().getOrDefault(name, emptyList()).stream().anyMatch(value::equals);
+ Collection<String> values = toMap().get(name);
+ return values != null && values.stream().anyMatch(value::equals);
}
/**
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
index e2ef2f9..d5fa9cc 100644
--- a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
@@ -32,8 +32,10 @@ import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -130,27 +132,49 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator {
return true;
}
- if (policy.getScopes().isEmpty()) {
- return true;
- }
+ Set<Scope> scopes = new HashSet<>(policy.getScopes());
- boolean hasScope = true;
+ if (scopes.isEmpty()) {
+ Set<Resource> resources = new HashSet<>();
- for (Scope givenScope : policy.getScopes()) {
- boolean hasGivenScope = false;
+ resources.addAll(policy.getResources());
- for (Scope scope : permission.getScopes()) {
- if (givenScope.getId().equals(scope.getId())) {
- hasGivenScope = true;
- break;
- }
+ for (Resource resource : resources) {
+ scopes.addAll(resource.getScopes());
}
- if (!hasGivenScope) {
+ if (!resources.isEmpty() && scopes.isEmpty()) {
return false;
}
+
+ if (scopes.isEmpty()) {
+ Resource resource = permission.getResource();
+ String type = resource.getType();
+
+ if (type != null) {
+ List<Resource> resourcesByType = authorization.getStoreFactory().getResourceStore().findByType(type);
+
+ for (Resource resourceType : resourcesByType) {
+ if (resourceType.getOwner().equals(resource.getResourceServer().getClientId())) {
+ resources.add(resourceType);
+ }
+ }
+ }
+ }
+
+ for (Resource resource : resources) {
+ scopes.addAll(resource.getScopes());
+ }
+ }
+
+ for (Scope givenScope : scopes) {
+ for (Scope scope : permission.getScopes()) {
+ if (givenScope.getId().equals(scope.getId())) {
+ return true;
+ }
+ }
}
- return hasScope;
+ return false;
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
index 8e4ec5e..6712a9d 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
@@ -240,10 +240,16 @@ public class PolicyEvaluationService {
}
}
- accessToken.addAccess(clientModel.getClientId());
- AccessToken.Access resourceAccess = accessToken.getResourceAccess(clientModel.getClientId());
+ AccessToken.Access clientAccess = accessToken.addAccess(clientModel.getClientId());
+ clientAccess.roles(new HashSet<>());
- userModel.getClientRoleMappings(clientModel).stream().map(RoleModel::getName).forEach(roleName -> resourceAccess.addRole(roleName));
+ userModel.getClientRoleMappings(clientModel).stream().map(RoleModel::getName).forEach(roleName -> clientAccess.addRole(roleName));
+
+ ClientModel resourceServerClient = realm.getClientById(resourceServer.getClientId());
+ AccessToken.Access resourceServerAccess = accessToken.addAccess(resourceServerClient.getClientId());
+ resourceServerAccess.roles(new HashSet<>());
+
+ userModel.getClientRoleMappings(resourceServerClient).stream().map(RoleModel::getName).forEach(roleName -> resourceServerAccess.addRole(roleName));
}
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
index 405675a..0fd3474 100644
--- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
+++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
@@ -17,6 +17,7 @@
package org.keycloak.authorization.authorization;
import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.authorization.representation.AuthorizationRequest;
@@ -50,8 +51,6 @@ import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -104,8 +103,12 @@ public class AuthorizationTokenService {
List<Permission> entitlements = Permissions.allPermits(results);
if (entitlements.isEmpty()) {
+ HashMap<Object, Object> error = new HashMap<>();
+
+ error.put(OAuth2Constants.ERROR, "not_authorized");
+
asyncResponse.resume(Cors.add(httpRequest, Response.status(Status.FORBIDDEN)
- .entity(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN)))
+ .entity(error))
.allowedOrigins(identity.getAccessToken())
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
} else {
@@ -193,14 +196,7 @@ public class AuthorizationTokenService {
return permissionsToEvaluate.entrySet().stream()
.flatMap((Function<Entry<String, Set<String>>, Stream<ResourcePermission>>) entry -> {
Resource entryResource = storeFactory.getResourceStore().findById(entry.getKey());
- if (entry.getValue().isEmpty()) {
- return Arrays.asList(new ResourcePermission(entryResource, Collections.emptyList(), entryResource.getResourceServer())).stream();
- } else {
- return entry.getValue().stream()
- .map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, entryResource.getResourceServer().getId()))
- .filter(scope -> scope != null)
- .map(scope -> new ResourcePermission(entryResource, Arrays.asList(scope), entryResource.getResourceServer()));
- }
+ return Permissions.createResourcePermissions(entryResource, entry.getValue(), authorization).stream();
}).collect(Collectors.toList());
}
diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
index ccc457d..e5986b7 100644
--- a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
+++ b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
@@ -18,6 +18,7 @@
package org.keycloak.authorization.entitlement;
import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.common.KeycloakEvaluationContext;
@@ -55,8 +56,6 @@ import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -119,8 +118,12 @@ public class EntitlementService {
List<Permission> entitlements = Permissions.allPermits(results);
if (entitlements.isEmpty()) {
+ HashMap<Object, Object> error = new HashMap<>();
+
+ error.put(OAuth2Constants.ERROR, "not_authorized");
+
asyncResponse.resume(Cors.add(request, Response.status(Status.FORBIDDEN)
- .entity(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN)))
+ .entity(error))
.allowedOrigins(identity.getAccessToken())
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
} else {
@@ -249,15 +252,7 @@ public class EntitlementService {
return permissionsToEvaluate.entrySet().stream()
.flatMap((Function<Map.Entry<String, Set<String>>, Stream<ResourcePermission>>) entry -> {
Resource entryResource = storeFactory.getResourceStore().findById(entry.getKey());
-
- if (entry.getValue().isEmpty()) {
- return Arrays.asList(new ResourcePermission(entryResource, Collections.emptyList(), entryResource.getResourceServer())).stream();
- } else {
- return entry.getValue().stream()
- .map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, entryResource.getResourceServer().getId()))
- .filter(scope -> scope != null)
- .map(scope -> new ResourcePermission(entryResource, Arrays.asList(scope), entryResource.getResourceServer()));
- }
+ return Permissions.createResourcePermissions(entryResource, entry.getValue(), authorization).stream();
}).collect(Collectors.toList());
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
index ed49697..ebc57c2 100644
--- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java
+++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
@@ -27,6 +27,7 @@ import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.representations.idm.authorization.Permission;
@@ -61,91 +62,95 @@ public final class Permissions {
StoreFactory storeFactory = authorization.getStoreFactory();
ResourceStore resourceStore = storeFactory.getResourceStore();
- resourceStore.findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resourceServer, authorization)));
- resourceStore.findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resourceServer, authorization)));
+ resourceStore.findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resource.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()), authorization)));
+ resourceStore.findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resource.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()), authorization)));
return permissions;
}
- public static List<ResourcePermission> createResourcePermissions(Resource resource, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ public static List<ResourcePermission> createResourcePermissions(Resource resource, Set<String> requestedScopes, AuthorizationProvider authorization) {
List<ResourcePermission> permissions = new ArrayList<>();
- List<Scope> scopes = resource.getScopes();
-
- if (scopes.isEmpty()) {
- String type = resource.getType();
+ String type = resource.getType();
+ ResourceServer resourceServer = resource.getResourceServer();
+ List<Scope> scopes;
+ if (requestedScopes.isEmpty()) {
+ scopes = resource.getScopes();
// check if there is a typed resource whose scopes are inherited by the resource being requested. In this case, we assume that parent resource
// is owned by the resource server itself
- if (type != null) {
+ if (type != null && !resource.getOwner().equals(resourceServer.getClientId())) {
StoreFactory storeFactory = authorization.getStoreFactory();
ResourceStore resourceStore = storeFactory.getResourceStore();
resourceStore.findByType(type).forEach(resource1 -> {
if (resource1.getOwner().equals(resourceServer.getClientId())) {
- scopes.addAll(resource1.getScopes());
+ for (Scope typeScope : resource1.getScopes()) {
+ if (!scopes.contains(typeScope)) {
+ scopes.add(typeScope);
+ }
+ }
}
});
}
+ } else {
+ ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore();
+ scopes = requestedScopes.stream().map(scopeName -> {
+ Scope byName = scopeStore.findByName(scopeName, resource.getResourceServer().getId());
+ return byName;
+ }).collect(Collectors.toList());
}
- if (scopes.size() > 1) {
+ if (scopes.isEmpty()) {
+ permissions.add(new ResourcePermission(resource, Collections.emptyList(), resource.getResourceServer()));
+ } else {
for (Scope scope : scopes) {
permissions.add(new ResourcePermission(resource, Arrays.asList(scope), resource.getResourceServer()));
}
- } else {
- permissions.add(new ResourcePermission(resource, Collections.emptyList(), resource.getResourceServer()));
}
return permissions;
}
public static List<Permission> allPermits(List<Result> evaluation) {
- List<Permission> permissions = evaluation.stream()
- .filter(evaluationResult -> evaluationResult.getEffect().equals(Effect.PERMIT))
- .map(evaluationResult -> {
- ResourcePermission permission = evaluationResult.getPermission();
- String resourceId = null;
- String resourceName = null;
-
- Resource resource = permission.getResource();
-
- if (resource != null) {
- resourceId = resource.getId();
- resourceName = resource.getName();
- }
+ Map<String, Permission> permissions = new HashMap<>();
- Set<String> scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet());
-
- return new Permission(resourceId, resourceName, scopes);
- }).collect(Collectors.toList());
-
- Map<String, Permission> perms = new HashMap<>();
-
- permissions.forEach(permission -> {
- Permission evalPermission = perms.get(permission.getResourceSetId());
-
- if (evalPermission == null) {
- evalPermission = permission;
- perms.put(permission.getResourceSetId(), evalPermission);
+ for (Result evaluationResult : evaluation) {
+ ResourcePermission permission = evaluationResult.getPermission();
+ Set<String> scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet());
+ if (evaluationResult.getEffect().equals(Effect.DENY)) {
+ continue;
}
+ Resource resource = permission.getResource();
- Set<String> permissionScopes = permission.getScopes();
-
- if (permissionScopes != null && !permissionScopes.isEmpty()) {
- Set<String> scopes = evalPermission.getScopes();
+ if (resource != null) {
+ String resourceId = resource.getId();
+ String resourceName = resource.getName();
+ Permission evalPermission = permissions.get(resource.getId());
- if (scopes == null) {
- scopes = new HashSet();
- evalPermission.setScopes(scopes);
+ if (evalPermission == null) {
+ evalPermission = new Permission(resourceId, resourceName, scopes);
+ permissions.put(resourceId, evalPermission);
}
- for (String scopeName : permissionScopes) {
- if (!scopes.contains(scopeName)) {
- scopes.add(scopeName);
+ if (scopes != null && !scopes.isEmpty()) {
+ Set<String> finalScopes = evalPermission.getScopes();
+
+ if (finalScopes == null) {
+ finalScopes = new HashSet();
+ evalPermission.setScopes(finalScopes);
+ }
+
+ for (String scopeName : scopes) {
+ if (!finalScopes.contains(scopeName)) {
+ finalScopes.add(scopeName);
+ }
}
}
+ } else {
+ Permission scopePermission = new Permission(null, null, scopes);
+ permissions.put(scopePermission.toString(), scopePermission);
}
- });
+ }
- return perms.values().stream().collect(Collectors.toList());
+ return permissions.values().stream().collect(Collectors.toList());
}
}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
index c28bca3..a5d7f16 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -109,7 +109,12 @@ public class AlbumService {
private void createProtectedResource(Album album) {
try {
- ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), new HashSet(), "/album/" + album.getId(), "http://photoz.com/album");
+ HashSet<ScopeRepresentation> scopes = new HashSet<>();
+
+ scopes.add(new ScopeRepresentation(SCOPE_ALBUM_VIEW));
+ scopes.add(new ScopeRepresentation(SCOPE_ALBUM_DELETE));
+
+ ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album");
albumResource.setOwner(album.getUserId());
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
index 95fb58b..a3ac697 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
@@ -1,7 +1,7 @@
{
"realm": "photoz",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
- "auth-server-url": "http://localhost:8080/auth",
+ "auth-server-url": "http://localhost:8180/auth",
"ssl-required": "external",
"resource": "photoz-restful-api",
"bearer-only" : true,
@@ -9,17 +9,18 @@
"secret": "secret"
},
"policy-enforcer": {
+ "user-managed-access" : {},
"paths": [
{
"path" : "/album/*",
"methods" : [
{
- "method": "GET",
- "scopes" : ["urn:photoz.com:scopes:album:view"]
- },
- {
"method": "POST",
"scopes" : ["urn:photoz.com:scopes:album:create"]
+ },
+ {
+ "method": "GET",
+ "scopes" : ["urn:photoz.com:scopes:album:view"]
}
]
},
@@ -30,6 +31,10 @@
{
"method": "DELETE",
"scopes" : ["urn:photoz.com:scopes:album:delete"]
+ },
+ {
+ "method": "GET",
+ "scopes" : ["urn:photoz.com:scopes:album:view"]
}
]
},
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
index 8a8d483..28662fa 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
@@ -94,6 +94,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
importResourceServerSettings();
}
+ @Test
public void testCreateDeleteAlbum() throws Exception {
try {
this.deployer.deploy(RESOURCE_SERVER_ID);
@@ -115,6 +116,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
}
}
+ @Test
public void testOnlyOwnerCanDeleteAlbum() throws Exception {
try {
this.deployer.deploy(RESOURCE_SERVER_ID);
@@ -160,6 +162,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
}
}
+ @Test
public void testRegularUserCanNotAccessAdminResources() throws Exception {
try {
this.deployer.deploy(RESOURCE_SERVER_ID);
@@ -172,6 +175,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
}
}
+ @Test
public void testAdminOnlyFromSpecificAddress() throws Exception {
try {
this.deployer.deploy(RESOURCE_SERVER_ID);
@@ -264,6 +268,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
}
}
+ @Test
public void testAdminWithoutPermissionsToDeleteScopePermission() throws Exception {
try {
this.deployer.deploy(RESOURCE_SERVER_ID);
@@ -327,6 +332,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
}
}
+ @Test
public void testClientRoleRepresentingUserConsent() throws Exception {
try {
this.deployer.deploy(RESOURCE_SERVER_ID);
@@ -366,6 +372,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
}
}
+ @Test
public void testClientRoleNotRequired() throws Exception {
try {
this.deployer.deploy(RESOURCE_SERVER_ID);