[KEYCLOAK-10443] - Define a global decision strategy for resource servers

This commit is contained in:
Pedro Igor 2019-06-13 10:50:10 -03:00
parent aca8c89d3e
commit 0cdd23763c
26 changed files with 572 additions and 6 deletions

View File

@ -34,6 +34,7 @@ public class ResourceServerRepresentation {
private List<ResourceRepresentation> resources = emptyList();
private List<PolicyRepresentation> policies = emptyList();
private List<ScopeRepresentation> scopes = emptyList();
private DecisionStrategy decisionStrategy;
public void setId(String id) {
this.id = id;
@ -98,4 +99,12 @@ public class ResourceServerRepresentation {
public List<ScopeRepresentation> getScopes() {
return scopes;
}
public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
this.decisionStrategy = decisionStrategy;
}
public DecisionStrategy getDecisionStrategy() {
return decisionStrategy;
}
}

View File

@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan.authorization;
import org.keycloak.authorization.model.CachedModel;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.models.cache.infinispan.authorization.entities.CachedResourceServer;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
/**
@ -103,6 +104,18 @@ public class ResourceServerAdapter implements ResourceServer, CachedModel<Resour
}
@Override
public DecisionStrategy getDecisionStrategy() {
if (isUpdated()) return updated.getDecisionStrategy();
return cached.getDecisionStrategy();
}
@Override
public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
getDelegateForUpdate();
updated.setDecisionStrategy(decisionStrategy);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -20,6 +20,7 @@ package org.keycloak.models.cache.infinispan.authorization.entities;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
/**
@ -29,11 +30,13 @@ public class CachedResourceServer extends AbstractRevisioned {
private final boolean allowRemoteResourceManagement;
private final PolicyEnforcementMode policyEnforcementMode;
private final DecisionStrategy decisionStrategy;
public CachedResourceServer(Long revision, ResourceServer resourceServer) {
super(revision, resourceServer.getId());
this.allowRemoteResourceManagement = resourceServer.isAllowRemoteResourceManagement();
this.policyEnforcementMode = resourceServer.getPolicyEnforcementMode();
this.decisionStrategy = resourceServer.getDecisionStrategy();
}
public boolean isAllowRemoteResourceManagement() {
@ -43,4 +46,8 @@ public class CachedResourceServer extends AbstractRevisioned {
public PolicyEnforcementMode getPolicyEnforcementMode() {
return this.policyEnforcementMode;
}
public DecisionStrategy getDecisionStrategy() {
return decisionStrategy;
}
}

View File

@ -18,6 +18,7 @@
package org.keycloak.authorization.jpa.entities;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import javax.persistence.Column;
@ -42,6 +43,9 @@ public class ResourceServerEntity {
@Column(name = "POLICY_ENFORCE_MODE")
private PolicyEnforcementMode policyEnforcementMode = PolicyEnforcementMode.ENFORCING;
@Column(name = "DECISION_STRATEGY")
private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS;
public String getId() {
return this.id;
}
@ -66,6 +70,17 @@ public class ResourceServerEntity {
this.policyEnforcementMode = policyEnforcementMode;
}
public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
if (DecisionStrategy.CONSENSUS.equals(decisionStrategy)) {
throw new IllegalArgumentException("Strategy " + decisionStrategy + " not supported");
}
this.decisionStrategy = decisionStrategy;
}
public DecisionStrategy getDecisionStrategy() {
return decisionStrategy;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -21,6 +21,7 @@ import org.keycloak.authorization.model.AbstractAuthorizationModel;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.jpa.JpaModel;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import javax.persistence.EntityManager;
@ -75,6 +76,17 @@ public class ResourceServerAdapter extends AbstractAuthorizationModel implements
}
@Override
public DecisionStrategy getDecisionStrategy() {
return entity.getDecisionStrategy();
}
@Override
public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
throwExceptionIfReadonly();
entity.setDecisionStrategy(decisionStrategy);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ Copyright 2019 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
<changeSet author="psilva@redhat.com" id="authz-7.0.0-KEYCLOAK-10443">
<addColumn tableName="RESOURCE_SERVER">
<column name="DECISION_STRATEGY" type="TINYINT" defaultValue="1">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@ -62,4 +62,5 @@
<include file="META-INF/jpa-changelog-4.6.0.xml"/>
<include file="META-INF/jpa-changelog-4.7.0.xml"/>
<include file="META-INF/jpa-changelog-4.8.0.xml"/>
<include file="META-INF/jpa-changelog-authz-7.0.0.xml"/>
</databaseChangeLog>

View File

@ -18,6 +18,7 @@
package org.keycloak.authorization.model;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
/**
@ -62,4 +63,19 @@ public interface ResourceServer {
* @param enforcementMode one of the available options in {@code PolicyEnforcementMode}
*/
void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode);
/**
* Defines a {@link DecisionStrategy} for this instance, indicating how permissions should be granted depending on the given
* {@code decisionStrategy}.
*
* @param decisionStrategy the decision strategy
*/
void setDecisionStrategy(DecisionStrategy decisionStrategy);
/**
* Returns the {@link DecisionStrategy} configured for this instance.
*
* @return the decision strategy
*/
DecisionStrategy getDecisionStrategy();
}

View File

@ -25,6 +25,7 @@ import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.Permission;
import java.util.ArrayList;
@ -70,6 +71,8 @@ public class DecisionPermissionCollector extends AbstractDecisionCollector {
for (Result.PolicyResult policyResult : result.getResults()) {
Policy policy = policyResult.getPolicy();
Set<Scope> policyScopes = policy.getScopes();
Set<Resource> policyResources = policy.getResources();
boolean containsResource = policyResources.contains(resource);
if (isGranted(policyResult)) {
if (isScopePermission(policy)) {
@ -89,15 +92,21 @@ public class DecisionPermissionCollector extends AbstractDecisionCollector {
userManagedPermissions.add(policyResult);
}
if (!resourceGranted) {
resourceGranted = policy.getResources().contains(resource);
resourceGranted = containsResource;
}
} else {
if (isResourcePermission(policy)) {
if (!resourceGranted) {
// deny all requested scopes if the resource-based permission is associated with the resource or if the
// resource was not granted by any other permission
if (containsResource || !resourceGranted) {
deniedScopes.addAll(requestedScopes);
}
} else {
deniedScopes.addAll(policyScopes);
// deny all scopes associated with the scope-based permission if the permission is associated with the
// resource or if the permission applies to any resource associated with the scopes
if (containsResource || policyResources.isEmpty()) {
deniedScopes.addAll(policyScopes);
}
}
if (!anyDeny) {
anyDeny = true;
@ -105,7 +114,11 @@ public class DecisionPermissionCollector extends AbstractDecisionCollector {
}
}
// remove any scope denied from the list of granted scopes
if (DecisionStrategy.AFFIRMATIVE.equals(resourceServer.getDecisionStrategy())) {
// remove any scope that was granted from the list of denied scopes if the decision strategy is affirmative
deniedScopes.removeAll(grantedScopes);
}
grantedScopes.removeAll(deniedScopes);
if (userManagedPermissions.isEmpty()) {

View File

@ -788,6 +788,7 @@ public class ModelToRepresentation {
server.setName(client.getClientId());
server.setAllowRemoteResourceManagement(model.isAllowRemoteResourceManagement());
server.setPolicyEnforcementMode(model.getPolicyEnforcementMode());
server.setDecisionStrategy(model.getDecisionStrategy());
return server;
}

View File

@ -116,6 +116,7 @@ import org.keycloak.representations.idm.UserFederationMapperRepresentation;
import org.keycloak.representations.idm.UserFederationProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
@ -2068,6 +2069,14 @@ public class RepresentationToModel {
resourceServer.setPolicyEnforcementMode(rep.getPolicyEnforcementMode());
resourceServer.setAllowRemoteResourceManagement(rep.isAllowRemoteResourceManagement());
DecisionStrategy decisionStrategy = rep.getDecisionStrategy();
if (decisionStrategy == null) {
decisionStrategy = DecisionStrategy.UNANIMOUS;
}
resourceServer.setDecisionStrategy(decisionStrategy);
for (ScopeRepresentation scope : rep.getScopes()) {
toModel(scope, resourceServer, authorization);
}

View File

@ -100,6 +100,7 @@ public class ResourceServerService {
this.auth.realm().requireManageAuthorization();
this.resourceServer.setAllowRemoteResourceManagement(server.isAllowRemoteResourceManagement());
this.resourceServer.setPolicyEnforcementMode(server.getPolicyEnforcementMode());
this.resourceServer.setDecisionStrategy(server.getDecisionStrategy());
audit(OperationType.UPDATE, session.getContext().getUri(), false);
return Response.noContent().build();
}

View File

@ -28,6 +28,7 @@ import org.junit.Test;
import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import org.keycloak.util.JsonSerialization;
@ -51,6 +52,7 @@ public class ResourceServerManagementTest extends AbstractAuthorizationTest {
AuthorizationResource settings = clientsResource.get(clientId).authorization();
assertEquals(PolicyEnforcementMode.PERMISSIVE, settings.exportSettings().getPolicyEnforcementMode());
assertEquals(DecisionStrategy.UNANIMOUS, settings.exportSettings().getDecisionStrategy());
assertFalse(settings.resources().findByName("Resource 1").isEmpty());
assertFalse(settings.resources().findByName("Resource 15").isEmpty());

View File

@ -45,6 +45,7 @@ import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
@ -102,6 +103,7 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
ResourceServerRepresentation settings = authorization.getSettings();
settings.setPolicyEnforcementMode(PolicyEnforcementMode.ENFORCING);
settings.setDecisionStrategy(DecisionStrategy.UNANIMOUS);
authorization.update(settings);
@ -129,6 +131,46 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
assertTrue(permissions.isEmpty());
}
/**
* <p>Scope Read on Resource A has two conflicting permissions. One is granting access for Marta and the other for Kolo.
*
* <p>Scope Read should not be granted for Marta.
*/
@Test
public void testMartaCanAccessResourceA() throws Exception {
ClientResource client = getClient(getRealm());
AuthorizationResource authorization = client.authorization();
ResourceServerRepresentation settings = authorization.getSettings();
settings.setPolicyEnforcementMode(PolicyEnforcementMode.ENFORCING);
settings.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
authorization.update(settings);
Collection<Permission> permissions = getEntitlements("marta", "password");
assertEquals(1, permissions.size());
for (Permission permission : new ArrayList<>(permissions)) {
String resourceSetName = permission.getResourceName();
switch (resourceSetName) {
case "Resource A":
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write", "read"));
permissions.remove(permission);
break;
case "Resource C":
assertThat(permission.getScopes(), containsInAnyOrder("execute", "write", "read"));
permissions.remove(permission);
break;
default:
fail("Unexpected permission for resource [" + resourceSetName + "]");
}
}
assertTrue(permissions.isEmpty());
}
@Test
public void testWithPermissiveMode() throws Exception {
ClientResource client = getClient(getRealm());
@ -136,6 +178,7 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
ResourceServerRepresentation settings = authorization.getSettings();
settings.setPolicyEnforcementMode(PolicyEnforcementMode.PERMISSIVE);
settings.setDecisionStrategy(DecisionStrategy.UNANIMOUS);
authorization.update(settings);
@ -174,6 +217,7 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
ResourceServerRepresentation settings = authorization.getSettings();
settings.setPolicyEnforcementMode(PolicyEnforcementMode.DISABLED);
settings.setDecisionStrategy(DecisionStrategy.UNANIMOUS);
authorization.update(settings);

View File

@ -52,6 +52,7 @@ import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.ResourceResource;
import org.keycloak.admin.client.resource.ScopePermissionsResource;
import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
@ -79,6 +80,7 @@ import org.keycloak.representations.idm.authorization.PermissionResponse;
import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
@ -842,6 +844,151 @@ public class EntitlementAPITest extends AbstractAuthzTest {
}
}
@Test
public void testServerDecisionStrategy() throws Exception {
ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST);
AuthorizationResource authorization = client.authorization();
ResourceRepresentation resource = new ResourceRepresentation();
resource.setName(KeycloakModelUtils.generateId());
resource.addScope("read", "write", "delete");
try (Response response = authorization.resources().create(resource)) {
resource = response.readEntity(ResourceRepresentation.class);
}
JSPolicyRepresentation grantPolicy = new JSPolicyRepresentation();
grantPolicy.setName(KeycloakModelUtils.generateId());
grantPolicy.setCode("$evaluation.grant();");
authorization.policies().js().create(grantPolicy).close();
JSPolicyRepresentation denyPolicy = new JSPolicyRepresentation();
denyPolicy.setName(KeycloakModelUtils.generateId());
denyPolicy.setCode("$evaluation.deny();");
authorization.policies().js().create(denyPolicy).close();
ResourcePermissionRepresentation resourcePermission = new ResourcePermissionRepresentation();
resourcePermission.setName(KeycloakModelUtils.generateId());
resourcePermission.addResource(resource.getId());
resourcePermission.addPolicy(denyPolicy.getName());
authorization.permissions().resource().create(resourcePermission).close();
ScopePermissionRepresentation scopePermission1 = new ScopePermissionRepresentation();
scopePermission1.setName(KeycloakModelUtils.generateId());
scopePermission1.addScope("read");
scopePermission1.addPolicy(grantPolicy.getName());
ScopePermissionsResource scopePermissions = authorization.permissions().scope();
scopePermissions.create(scopePermission1).close();
String accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "kolo", "password").getAccessToken();
AuthzClient authzClient = getAuthzClient(AUTHZ_CLIENT_CONFIG);
AuthorizationRequest request = new AuthorizationRequest();
request.addPermission(resource.getName());
try {
authzClient.authorization(accessToken).authorize(request);
fail("kolo can not access the resource");
} catch (RuntimeException expected) {
assertEquals(403, HttpResponseException.class.cast(expected.getCause()).getStatusCode());
assertTrue(HttpResponseException.class.cast(expected.getCause()).toString().contains("access_denied"));
}
ResourceServerRepresentation settings = authorization.getSettings();
settings.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
authorization.update(settings);
assertPermissions(authzClient, accessToken, request, resource, "read");
scopePermission1 = scopePermissions.findByName(scopePermission1.getName());
scopePermission1.addScope("read", "delete");
scopePermissions.findById(scopePermission1.getId()).update(scopePermission1);
assertPermissions(authzClient, accessToken, request, resource, "read", "delete");
ScopePermissionRepresentation scopePermission2 = new ScopePermissionRepresentation();
scopePermission2.setName(KeycloakModelUtils.generateId());
scopePermission2.addScope("write");
scopePermission2.addPolicy(grantPolicy.getName());
scopePermissions.create(scopePermission2).close();
assertPermissions(authzClient, accessToken, request, resource, "read", "delete", "write");
ScopePermissionRepresentation scopePermission3 = new ScopePermissionRepresentation();
scopePermission3.setName(KeycloakModelUtils.generateId());
scopePermission3.addResource(resource.getId());
scopePermission3.addScope("write", "read", "delete");
scopePermission3.addPolicy(grantPolicy.getName());
scopePermissions.create(scopePermission3).close();
assertPermissions(authzClient, accessToken, request, resource, "read", "delete", "write");
scopePermission2 = scopePermissions.findByName(scopePermission2.getName());
scopePermissions.findById(scopePermission2.getId()).remove();
assertPermissions(authzClient, accessToken, request, resource, "read", "delete", "write");
scopePermission1 = scopePermissions.findByName(scopePermission1.getName());
scopePermissions.findById(scopePermission1.getId()).remove();
assertPermissions(authzClient, accessToken, request, resource, "read", "delete", "write");
scopePermission3 = scopePermissions.findByName(scopePermission3.getName());
scopePermission3.addScope("write", "delete");
scopePermissions.findById(scopePermission3.getId()).update(scopePermission3);
assertPermissions(authzClient, accessToken, request, resource, "delete", "write");
scopePermissions.findById(scopePermission3.getId()).remove();
try {
authzClient.authorization(accessToken).authorize(request);
fail("kolo can not access the resource");
} catch (RuntimeException expected) {
assertEquals(403, HttpResponseException.class.cast(expected.getCause()).getStatusCode());
assertTrue(HttpResponseException.class.cast(expected.getCause()).toString().contains("access_denied"));
}
ResourcePermissionRepresentation grantResourcePermission = new ResourcePermissionRepresentation();
grantResourcePermission.setName(KeycloakModelUtils.generateId());
grantResourcePermission.addResource(resource.getId());
grantResourcePermission.addPolicy(grantPolicy.getName());
authorization.permissions().resource().create(grantResourcePermission).close();
assertPermissions(authzClient, accessToken, request, resource, "read", "delete", "write");
settings.setDecisionStrategy(DecisionStrategy.UNANIMOUS);
authorization.update(settings);
try {
authzClient.authorization(accessToken).authorize(request);
fail("kolo can not access the resource");
} catch (RuntimeException expected) {
assertEquals(403, HttpResponseException.class.cast(expected.getCause()).getStatusCode());
assertTrue(HttpResponseException.class.cast(expected.getCause()).toString().contains("access_denied"));
}
}
@Test
public void testObtainAllEntitlementsForResourceType() throws Exception {
ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST);
@ -1226,6 +1373,188 @@ public class EntitlementAPITest extends AbstractAuthzTest {
}
}
@Test
public void testOverrideParentScopePermission() throws Exception {
ClientResource client = getClient(getRealm(), RESOURCE_SERVER_TEST);
AuthorizationResource authorization = client.authorization();
JSPolicyRepresentation onlyOwnerPolicy = createOnlyOwnerPolicy();
authorization.policies().js().create(onlyOwnerPolicy).close();
ResourceRepresentation typedResource = new ResourceRepresentation();
typedResource.setType("resource");
typedResource.setName(KeycloakModelUtils.generateId());
typedResource.addScope("read", "update");
try (Response response = authorization.resources().create(typedResource)) {
typedResource = response.readEntity(ResourceRepresentation.class);
}
ScopePermissionRepresentation typedResourcePermission = new ScopePermissionRepresentation();
typedResourcePermission.setName(KeycloakModelUtils.generateId());
typedResourcePermission.addResource(typedResource.getName());
typedResourcePermission.addPolicy(onlyOwnerPolicy.getName());
typedResourcePermission.addScope("read", "update");
authorization.permissions().scope().create(typedResourcePermission).close();
ResourceRepresentation martaResource = new ResourceRepresentation();
martaResource.setType("resource");
martaResource.setName(KeycloakModelUtils.generateId());
martaResource.addScope("read");
martaResource.setOwner("marta");
try (Response response = authorization.resources().create(martaResource)) {
martaResource = response.readEntity(ResourceRepresentation.class);
}
String accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken();
AuthzClient authzClient = getAuthzClient(AUTHZ_CLIENT_CONFIG);
AuthorizationRequest request = new AuthorizationRequest();
request.addPermission(martaResource.getName());
// marta can access her resource
AuthorizationResponse response = authzClient.authorization(accessToken).authorize(request);
assertNotNull(response.getToken());
Collection<Permission> permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions();
assertEquals(1, permissions.size());
for (Permission grantedPermission : permissions) {
assertEquals(martaResource.getName(), grantedPermission.getResourceName());
Set<String> scopes = grantedPermission.getScopes();
assertEquals(2, scopes.size());
assertThat(scopes, Matchers.containsInAnyOrder("read", "update"));
}
accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "kolo", "password").getAccessToken();
authzClient = getAuthzClient(AUTHZ_CLIENT_CONFIG);
request = new AuthorizationRequest();
request.addPermission(martaResource.getId());
try {
authzClient.authorization(accessToken).authorize(request);
fail("kolo can not access marta resource");
} catch (RuntimeException expected) {
assertEquals(403, HttpResponseException.class.cast(expected.getCause()).getStatusCode());
assertTrue(HttpResponseException.class.cast(expected.getCause()).toString().contains("access_denied"));
}
UserPolicyRepresentation onlyKoloPolicy = new UserPolicyRepresentation();
onlyKoloPolicy.setName(KeycloakModelUtils.generateId());
onlyKoloPolicy.addUser("kolo");
authorization.policies().user().create(onlyKoloPolicy).close();
ResourcePermissionRepresentation martaResourcePermission = new ResourcePermissionRepresentation();
martaResourcePermission.setName(KeycloakModelUtils.generateId());
martaResourcePermission.addResource(martaResource.getId());
martaResourcePermission.addPolicy(onlyKoloPolicy.getName());
try (Response response1 = authorization.permissions().resource().create(martaResourcePermission)) {
martaResourcePermission = response1.readEntity(ResourcePermissionRepresentation.class);
}
response = authzClient.authorization(accessToken).authorize(request);
assertNotNull(response.getToken());
permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions();
assertEquals(1, permissions.size());
for (Permission grantedPermission : permissions) {
assertEquals(martaResource.getName(), grantedPermission.getResourceName());
Set<String> scopes = grantedPermission.getScopes();
assertEquals(2, scopes.size());
assertThat(scopes, Matchers.containsInAnyOrder("read", "update"));
}
ScopePermissionRepresentation martaResourceUpdatePermission = new ScopePermissionRepresentation();
martaResourceUpdatePermission.setName(KeycloakModelUtils.generateId());
martaResourceUpdatePermission.addResource(martaResource.getId());
martaResourceUpdatePermission.addScope("update");
martaResourceUpdatePermission.addPolicy(onlyOwnerPolicy.getName());
try (Response response1 = authorization.permissions().scope().create(martaResourceUpdatePermission)) {
martaResourceUpdatePermission = response1.readEntity(ScopePermissionRepresentation.class);
}
// now kolo can only read, but not update
response = authzClient.authorization(accessToken).authorize(request);
assertNotNull(response.getToken());
permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions();
assertEquals(1, permissions.size());
for (Permission grantedPermission : permissions) {
assertEquals(martaResource.getName(), grantedPermission.getResourceName());
Set<String> scopes = grantedPermission.getScopes();
assertEquals(1, scopes.size());
assertThat(scopes, Matchers.containsInAnyOrder("read"));
}
authorization.permissions().resource().findById(martaResourcePermission.getId()).remove();
try {
// after removing permission to marta resource, kolo can not access any scope in the resource
authzClient.authorization(accessToken).authorize(request);
fail("kolo can not access marta resource");
} catch (RuntimeException expected) {
assertEquals(403, HttpResponseException.class.cast(expected.getCause()).getStatusCode());
assertTrue(HttpResponseException.class.cast(expected.getCause()).toString().contains("access_denied"));
}
martaResourceUpdatePermission.addPolicy(onlyKoloPolicy.getName());
martaResourceUpdatePermission.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
authorization.permissions().scope().findById(martaResourceUpdatePermission.getId()).update(martaResourceUpdatePermission);
// now kolo can access because update permission changed to allow him to access the resource using an affirmative strategy
response = authzClient.authorization(accessToken).authorize(request);
assertNotNull(response.getToken());
permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions();
assertEquals(1, permissions.size());
for (Permission grantedPermission : permissions) {
assertEquals(martaResource.getName(), grantedPermission.getResourceName());
Set<String> scopes = grantedPermission.getScopes();
assertEquals(1, scopes.size());
assertThat(scopes, Matchers.containsInAnyOrder("update"));
}
accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken();
// marta can still access her resource
response = authzClient.authorization(accessToken).authorize(request);
assertNotNull(response.getToken());
permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions();
assertEquals(1, permissions.size());
for (Permission grantedPermission : permissions) {
assertEquals(martaResource.getName(), grantedPermission.getResourceName());
Set<String> scopes = grantedPermission.getScopes();
assertEquals(2, scopes.size());
assertThat(scopes, Matchers.containsInAnyOrder("update", "read"));
}
authorization.permissions().scope().findById(martaResourceUpdatePermission.getId()).remove();
accessToken = new OAuthClient().realm("authz-test").clientId(RESOURCE_SERVER_TEST).doGrantAccessTokenRequest("secret", "kolo", "password").getAccessToken();
try {
// back to original setup, permissions not granted by the type resource
authzClient.authorization(accessToken).authorize(request);
fail("kolo can not access marta resource");
} catch (RuntimeException expected) {
assertEquals(403, HttpResponseException.class.cast(expected.getCause()).getStatusCode());
assertTrue(HttpResponseException.class.cast(expected.getCause()).toString().contains("access_denied"));
}
}
@NotNull
private JSPolicyRepresentation createOnlyOwnerPolicy() {
JSPolicyRepresentation onlyOwnerPolicy = new JSPolicyRepresentation();
@ -1681,4 +2010,17 @@ public class EntitlementAPITest extends AbstractAuthzTest {
client.update(representation);
}
private void assertPermissions(AuthzClient authzClient, String accessToken, AuthorizationRequest request, ResourceRepresentation resource, String... expectedScopes) {
AuthorizationResponse response = authzClient.authorization(accessToken).authorize(request);
assertNotNull(response.getToken());
Collection<Permission> permissions = toAccessToken(response.getToken()).getAuthorization().getPermissions();
assertEquals(1, permissions.size());
for (Permission grantedPermission : permissions) {
assertEquals(resource.getId(), grantedPermission.getResourceId());
assertEquals(expectedScopes.length, grantedPermission.getScopes().size());
assertTrue(grantedPermission.getScopes().containsAll(Arrays.asList(expectedScopes)));
}
}
}

View File

@ -42,8 +42,9 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
@ -239,6 +240,13 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testMicroprofileJWTScopeAddedToClient();
}
private void testDecisionStrategySetOnResourceServer() {
ClientsResource clients = migrationRealm.clients();
ClientRepresentation clientRepresentation = clients.findByClientId("authz-servlet").get(0);
ResourceServerRepresentation settings = clients.get(clientRepresentation.getId()).authorization().getSettings();
assertEquals(DecisionStrategy.UNANIMOUS, settings.getDecisionStrategy());
}
private void testGroupPolicyTypeFineGrainedAdminPermission() {
ClientsResource clients = migrationRealm.clients();
ClientRepresentation clientRepresentation = clients.findByClientId("realm-management").get(0);
@ -600,4 +608,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
protected void testMigrationTo6_x() {
testMigrationTo6_0_0();
}
protected void testMigrationTo7_x(boolean supportedAuthzServices) {
if (supportedAuthzServices) {
testDecisionStrategySetOnResourceServer();
}
}
}

View File

@ -75,6 +75,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo4_x(false, false);
testMigrationTo5_x();
testMigrationTo6_x();
testMigrationTo7_x(false);
}
@Override

View File

@ -68,6 +68,7 @@ public class JsonFileImport255MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo4_x(true, false);
testMigrationTo5_x();
testMigrationTo6_x();
testMigrationTo7_x(true);
}
}

View File

@ -67,6 +67,7 @@ public class JsonFileImport343MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo4_x(true, false);
testMigrationTo5_x();
testMigrationTo6_x();
testMigrationTo7_x(true);
}
}

View File

@ -61,6 +61,7 @@ public class JsonFileImport483MigrationTest extends AbstractJsonFileImportMigrat
checkRealmsImported();
testMigrationTo5_x();
testMigrationTo6_x();
testMigrationTo7_x(true);
}
}

View File

@ -71,6 +71,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigratedData();
testMigrationTo5_x();
testMigrationTo6_x();
testMigrationTo7_x(true);
}
@Test
@ -80,6 +81,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo4_x();
testMigrationTo5_x();
testMigrationTo6_x();
testMigrationTo7_x(true);
}
@Test
@ -90,6 +92,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo4_x();
testMigrationTo5_x();
testMigrationTo6_x();
testMigrationTo7_x(true);
}
@Test
@ -101,6 +104,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo4_x(false, false);
testMigrationTo5_x();
testMigrationTo6_x();
testMigrationTo7_x(false);
}
}

View File

@ -400,7 +400,8 @@
"fullScopeAllowed" : true,
"nodeReRegistrationTimeout" : -1,
"defaultClientScopes" : [ "web-origins", "role_list", "profile", "roles", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access" ]
"optionalClientScopes" : [ "address", "phone", "offline_access" ],
"authorizationServicesEnabled": true
}, {
"id" : "08b72946-628a-48a2-994f-6ccbbf2f5f8e",
"clientId" : "broker",

View File

@ -17,6 +17,7 @@
package org.keycloak.testsuite.console.page.clients.authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.testsuite.console.page.fragment.OnOffSwitch;
import org.keycloak.testsuite.page.Form;
import org.openqa.selenium.support.FindBy;
@ -33,6 +34,9 @@ public class AuthorizationSettingsForm extends Form {
@FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='server.allowRemoteResourceManagement']]")
private OnOffSwitch allowRemoteResourceManagement;
@FindBy(id = "server.decisionStrategy")
private Select decisionStrategy;
public void setEnforcementMode(PolicyEnforcerConfig.EnforcementMode mode) {
enforcementMode.selectByValue(mode.name());
}
@ -48,4 +52,12 @@ public class AuthorizationSettingsForm extends Form {
public boolean isAllowRemoteResourceManagement() {
return allowRemoteResourceManagement.isOn();
}
public DecisionStrategy getDecisionStrategy() {
return DecisionStrategy.valueOf(decisionStrategy.getFirstSelectedOption().getAttribute("value"));
}
public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
enforcementMode.selectByValue(decisionStrategy.name());
}
}

View File

@ -26,6 +26,7 @@ import org.junit.Test;
import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.console.page.clients.authorization.AuthorizationSettingsForm;
@ -56,6 +57,9 @@ public class DefaultAuthorizationSettingsTest extends AbstractAuthorizationSetti
assertEquals(PolicyEnforcerConfig.EnforcementMode.ENFORCING, settings.getEnforcementMode());
assertEquals(true, settings.isAllowRemoteResourceManagement());
assertEquals(DecisionStrategy.UNANIMOUS, settings.getDecisionStrategy());
assertEquals(true, settings.isAllowRemoteResourceManagement());
Resources resources = authorizationPage.authorizationTabs().resources();
ResourceRepresentation resource = resources.resources().findByName("Default Resource");

View File

@ -1291,6 +1291,7 @@ authz-remote-resource-management=Remote Resource Management
authz-remote-resource-management.tooltip=Should resources be managed remotely by the resource server? If false, resources can be managed only from this admin console.
authz-export-settings=Export Settings
authz-export-settings.tooltip=Export and download all authorization settings for this resource server.
authz-server-decision-strategy.tooltip=The decision strategy dictates how permissions are evaluated and how a final decision is obtained. 'Affirmative' means that at least one permission must evaluate to a positive decision in order grant access to a resource and its scopes. 'Unanimous' means that all permissions must evaluate to a positive decision in order for the final decision to be also positive.
# Authz Resource List
authz-no-resources-available=No resources available.
authz-no-scopes-assigned=No scopes assigned.

View File

@ -43,6 +43,20 @@
</div>
<kc-tooltip>{{:: 'authz-policy-enforcement-mode.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="server.decisionStrategy">{{:: 'authz-policy-decision-strategy' | translate}}</label>
<div class="col-sm-2">
<select class="form-control" id="server.decisionStrategy"
data-ng-model="server.decisionStrategy"
ng-change="selectDecisionStrategy()">
<option value="UNANIMOUS">{{:: 'authz-policy-decision-strategy-unanimous' | translate}}</option>
<option value="AFFIRMATIVE">{{:: 'authz-policy-decision-strategy-affirmative' | translate}}</option>
</select>
</div>
<kc-tooltip>{{:: 'authz-server-decision-strategy.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="server.allowRemoteResourceManagement">{{:: 'authz-remote-resource-management' | translate}}</label>
<div class="col-md-6">