[KEYCLOAK-5726] - Support define enforcement mode for scopes on the adapter configuration

This commit is contained in:
Pedro Igor 2017-10-20 20:51:19 -02:00
parent a4ec32ba66
commit a6e1413d58
8 changed files with 128 additions and 34 deletions

View file

@ -33,6 +33,7 @@ import org.keycloak.authorization.client.ClientAuthorizationContext;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.representations.idm.authorization.Permission;
@ -96,9 +97,9 @@ public abstract class AbstractPolicyEnforcer {
return createEmptyAuthorizationContext(true); return createEmptyAuthorizationContext(true);
} }
Set<String> requiredScopes = getRequiredScopes(pathConfig, request); MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
if (isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) { if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
try { try {
return createAuthorizationContext(accessToken, pathConfig); return createAuthorizationContext(accessToken, pathConfig);
} catch (Exception e) { } catch (Exception e) {
@ -108,7 +109,7 @@ public abstract class AbstractPolicyEnforcer {
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig); LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
if (!challenge(pathConfig, requiredScopes, httpFacade)) { if (!challenge(pathConfig, methodConfig, httpFacade)) {
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig); LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
handleAccessDenied(httpFacade); handleAccessDenied(httpFacade);
} }
@ -118,9 +119,9 @@ public abstract class AbstractPolicyEnforcer {
return createEmptyAuthorizationContext(false); return createEmptyAuthorizationContext(false);
} }
protected abstract boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade); protected abstract boolean challenge(PathConfig pathConfig, MethodConfig methodConfig, OIDCHttpFacade facade);
protected boolean isAuthorized(PathConfig actualPathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) { protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
Request request = httpFacade.getRequest(); Request request = httpFacade.getRequest();
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig(); PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
@ -146,7 +147,7 @@ public abstract class AbstractPolicyEnforcer {
continue; continue;
} }
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) { if (hasResourceScopePermission(methodConfig, permission)) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions); LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) { if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
this.paths.remove(actualPathConfig); this.paths.remove(actualPathConfig);
@ -155,7 +156,7 @@ public abstract class AbstractPolicyEnforcer {
} }
} }
} else { } else {
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) { if (hasResourceScopePermission(methodConfig, permission)) {
hasPermission = true; hasPermission = true;
return true; return true;
} }
@ -166,7 +167,7 @@ public abstract class AbstractPolicyEnforcer {
return true; return true;
} }
LOGGER.debugf("Authorization FAILED for path [%s]. No enough permissions [%s].", actualPathConfig, permissions); LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, permissions);
return false; return false;
} }
@ -186,9 +187,28 @@ public abstract class AbstractPolicyEnforcer {
return false; return false;
} }
private boolean hasResourceScopePermission(Set<String> requiredScopes, Permission permission, PathConfig actualPathConfig) { private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
Set<String> allowedScopes = permission.getScopes(); Set<String> allowedScopes = permission.getScopes();
return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty());
if (allowedScopes.isEmpty()) {
return true;
}
PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode();
if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) {
return allowedScopes.containsAll(methodConfig.getScopes());
}
if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) {
for (String requiredScope : methodConfig.getScopes()) {
if (allowedScopes.contains(requiredScope)) {
return true;
}
}
}
return false;
} }
protected AuthzClient getAuthzClient() { protected AuthzClient getAuthzClient() {
@ -236,20 +256,22 @@ public abstract class AbstractPolicyEnforcer {
return request.getRelativePath(); return request.getRelativePath();
} }
private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) { private MethodConfig getRequiredScopes(PathConfig pathConfig, Request request) {
Set<String> requiredScopes = new HashSet<>();
requiredScopes.addAll(pathConfig.getScopes());
String method = request.getMethod(); String method = request.getMethod();
for (PolicyEnforcerConfig.MethodConfig methodConfig : pathConfig.getMethods()) { for (MethodConfig methodConfig : pathConfig.getMethods()) {
if (methodConfig.getMethod().equals(method)) { if (methodConfig.getMethod().equals(method)) {
requiredScopes.addAll(methodConfig.getScopes()); return methodConfig;
} }
} }
return requiredScopes; MethodConfig methodConfig = new MethodConfig();
methodConfig.setMethod(request.getMethod());
methodConfig.setScopes(pathConfig.getScopes());
methodConfig.setScopesEnforcementMode(PolicyEnforcerConfig.ScopeEnforcementMode.ANY);
return methodConfig;
} }
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) { private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {

View file

@ -17,7 +17,7 @@
*/ */
package org.keycloak.adapters.authorization; package org.keycloak.adapters.authorization;
import java.util.Set; import java.util.HashSet;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.OIDCHttpFacade; import org.keycloak.adapters.OIDCHttpFacade;
@ -26,6 +26,7 @@ import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.representation.PermissionRequest; import org.keycloak.authorization.client.representation.PermissionRequest;
import org.keycloak.authorization.client.resource.PermissionResource; import org.keycloak.authorization.client.resource.PermissionResource;
import org.keycloak.authorization.client.resource.ProtectionResource; import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
/** /**
@ -40,9 +41,9 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
} }
@Override @Override
protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) { protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
if (getEnforcerConfig().getUserManagedAccess() != null) { if (getEnforcerConfig().getUserManagedAccess() != null) {
challengeUmaAuthentication(pathConfig, requiredScopes, facade); challengeUmaAuthentication(pathConfig, methodConfig, facade);
} else { } else {
challengeEntitlementAuthentication(facade); challengeEntitlementAuthentication(facade);
} }
@ -61,10 +62,10 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
} }
} }
private void challengeUmaAuthentication(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) { private void challengeUmaAuthentication(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
HttpFacade.Response response = facade.getResponse(); HttpFacade.Response response = facade.getResponse();
AuthzClient authzClient = getAuthzClient(); AuthzClient authzClient = getAuthzClient();
String ticket = getPermissionTicket(pathConfig, requiredScopes, authzClient); String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient);
String clientId = authzClient.getConfiguration().getResource(); String clientId = authzClient.getConfiguration().getResource();
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize"; String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize";
response.setStatus(401); response.setStatus(401);
@ -74,12 +75,12 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
} }
} }
private String getPermissionTicket(PathConfig pathConfig, Set<String> requiredScopes, AuthzClient authzClient) { private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient) {
ProtectionResource protection = authzClient.protection(); ProtectionResource protection = authzClient.protection();
PermissionResource permission = protection.permission(); PermissionResource permission = protection.permission();
PermissionRequest permissionRequest = new PermissionRequest(); PermissionRequest permissionRequest = new PermissionRequest();
permissionRequest.setResourceSetId(pathConfig.getId()); permissionRequest.setResourceSetId(pathConfig.getId());
permissionRequest.setScopes(requiredScopes); permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
return permission.forResource(permissionRequest).getTicket(); return permission.forResource(permissionRequest).getTicket();
} }
} }

View file

@ -35,6 +35,7 @@ import org.keycloak.authorization.client.representation.EntitlementResponse;
import org.keycloak.authorization.client.representation.PermissionRequest; import org.keycloak.authorization.client.representation.PermissionRequest;
import org.keycloak.authorization.client.representation.PermissionResponse; import org.keycloak.authorization.client.representation.PermissionResponse;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.representations.idm.authorization.Permission;
@ -50,14 +51,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
} }
@Override @Override
protected boolean isAuthorized(PathConfig pathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) { protected boolean isAuthorized(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
AccessToken original = accessToken; AccessToken original = accessToken;
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) { if (super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
return true; return true;
} }
accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade); accessToken = requestAuthorizationToken(pathConfig, methodConfig, httpFacade);
if (accessToken == null) { if (accessToken == null) {
return false; return false;
@ -78,11 +79,11 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
original.setAuthorization(authorization); original.setAuthorization(authorization);
return super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade); return super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade);
} }
@Override @Override
protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) { protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
handleAccessDenied(facade); handleAccessDenied(facade);
return true; return true;
} }
@ -100,7 +101,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
} }
} }
private AccessToken requestAuthorizationToken(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade httpFacade) { private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
try { try {
String accessToken = httpFacade.getSecurityContext().getTokenString(); String accessToken = httpFacade.getSecurityContext().getTokenString();
AuthzClient authzClient = getAuthzClient(); AuthzClient authzClient = getAuthzClient();
@ -111,7 +112,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
PermissionRequest permissionRequest = new PermissionRequest(); PermissionRequest permissionRequest = new PermissionRequest();
permissionRequest.setResourceSetId(pathConfig.getId()); permissionRequest.setResourceSetId(pathConfig.getId());
permissionRequest.setScopes(requiredScopes); permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
PermissionResponse permissionResponse = authzClient.protection().permission().forResource(permissionRequest); PermissionResponse permissionResponse = authzClient.protection().permission().forResource(permissionRequest);
AuthorizationRequest authzRequest = new AuthorizationRequest(permissionResponse.getTicket()); AuthorizationRequest authzRequest = new AuthorizationRequest(permissionResponse.getTicket());

View file

@ -220,6 +220,9 @@ public class PolicyEnforcerConfig {
private String method; private String method;
private List<String> scopes = Collections.emptyList(); private List<String> scopes = Collections.emptyList();
@JsonProperty("scopes-enforcement-mode")
private ScopeEnforcementMode scopesEnforcementMode = ScopeEnforcementMode.ALL;
public String getMethod() { public String getMethod() {
return method; return method;
} }
@ -235,6 +238,14 @@ public class PolicyEnforcerConfig {
public void setScopes(List<String> scopes) { public void setScopes(List<String> scopes) {
this.scopes = scopes; this.scopes = scopes;
} }
public void setScopesEnforcementMode(ScopeEnforcementMode scopesEnforcementMode) {
this.scopesEnforcementMode = scopesEnforcementMode;
}
public ScopeEnforcementMode getScopesEnforcementMode() {
return scopesEnforcementMode;
}
} }
public enum EnforcementMode { public enum EnforcementMode {
@ -243,6 +254,11 @@ public class PolicyEnforcerConfig {
DISABLED DISABLED
} }
public enum ScopeEnforcementMode {
ALL,
ANY
}
public static class UmaProtocolConfig { public static class UmaProtocolConfig {
} }

View file

@ -108,7 +108,7 @@
"redirectUris": [ "redirectUris": [
"/photoz-html5-client/*" "/photoz-html5-client/*"
], ],
"webOrigins": ["*"] "webOrigins": ["http://localhost:8280"]
}, },
{ {
"clientId": "photoz-restful-api", "clientId": "photoz-restful-api",
@ -118,7 +118,7 @@
"redirectUris": [ "redirectUris": [
"/photoz-restful-api/*" "/photoz-restful-api/*"
], ],
"webOrigins" : ["*"], "webOrigins" : ["http://localhost:8280"],
"clientAuthenticatorType": "client-jwt", "clientAuthenticatorType": "client-jwt",
"attributes" : { "attributes" : {
"jwt.credential.certificate" : "MIICqTCCAZECBgFT0Ngs/DANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zZWN1cmUtcG9ydGFsMB4XDTE2MDQwMTA4MDA0MVoXDTI2MDQwMTA4MDIyMVowGDEWMBQGA1UEAwwNc2VjdXJlLXBvcnRhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJa4GixpmzP511AmI0eLPLORyJwXS8908MUvdG3hmh8jMOIhe28XjIFeZSY09vFxh22F2SUMjxU/B2Hw4PDJUkebuNR7rXhOIYCJAo6eEZzjSBY/wngFtfm74zJ/eLCobBtDvIld7jobdHTfE1Oz9+GzvtG0k7cm7ubrLT0J4I1UsFZj3b//3wa+O0vNaTwHC1Jz/m59VbtXqyO4xEzIdl416cnGCmEmk5qd5h1de2UoLi/CTad8HftIJhzN1qhlySzW/9Ha70aYlDH2hiibDsXDTrNaMdaaLik7I8Rv/nIbggysG863PKZo8wknDe62QctH5VYSSktiy4gjSJkGh7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZnnx+AHQ8txugGcFK8gWjildDgk+v31fBHBDvmLQaSzsUaIOJaK4wnlwUI+VfR46HmBXhjlDCobFLUptd+kz0G7xapcIn3b5jLrySUUD7L+LAp1vNOQU4mKhTGS3IEvNB73D3GH9rQ+M3KEcoN3f99fNKqKsUdxbmZqGf4VOQ57PUfLBw4PJJGlROPosBc7ivPRyeYnKekhoCTynq30BAD1FA1BA8ppcY4ZVGADPTAgMJxpglpFY9LiqCwdLAGW1ttnsyIJ7DpT+kybhhk7c+MU7gyQdv8xPnMR0bSCB9hndowgBn5oZ393aMscwMNCzwJ0aWBs1sUyn3X0RIsu9Jg==" "jwt.credential.certificate" : "MIICqTCCAZECBgFT0Ngs/DANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zZWN1cmUtcG9ydGFsMB4XDTE2MDQwMTA4MDA0MVoXDTI2MDQwMTA4MDIyMVowGDEWMBQGA1UEAwwNc2VjdXJlLXBvcnRhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJa4GixpmzP511AmI0eLPLORyJwXS8908MUvdG3hmh8jMOIhe28XjIFeZSY09vFxh22F2SUMjxU/B2Hw4PDJUkebuNR7rXhOIYCJAo6eEZzjSBY/wngFtfm74zJ/eLCobBtDvIld7jobdHTfE1Oz9+GzvtG0k7cm7ubrLT0J4I1UsFZj3b//3wa+O0vNaTwHC1Jz/m59VbtXqyO4xEzIdl416cnGCmEmk5qd5h1de2UoLi/CTad8HftIJhzN1qhlySzW/9Ha70aYlDH2hiibDsXDTrNaMdaaLik7I8Rv/nIbggysG863PKZo8wknDe62QctH5VYSSktiy4gjSJkGh7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZnnx+AHQ8txugGcFK8gWjildDgk+v31fBHBDvmLQaSzsUaIOJaK4wnlwUI+VfR46HmBXhjlDCobFLUptd+kz0G7xapcIn3b5jLrySUUD7L+LAp1vNOQU4mKhTGS3IEvNB73D3GH9rQ+M3KEcoN3f99fNKqKsUdxbmZqGf4VOQ57PUfLBw4PJJGlROPosBc7ivPRyeYnKekhoCTynq30BAD1FA1BA8ppcY4ZVGADPTAgMJxpglpFY9LiqCwdLAGW1ttnsyIJ7DpT+kybhhk7c+MU7gyQdv8xPnMR0bSCB9hndowgBn5oZ393aMscwMNCzwJ0aWBs1sUyn3X0RIsu9Jg=="

View file

@ -46,6 +46,18 @@
"name": "urn:servlet-authz:page:main:actionForPremiumUser" "name": "urn:servlet-authz:page:main:actionForPremiumUser"
} }
] ]
},
{
"name": "Resource A",
"uri": "/protected/scopes.jsp",
"scopes": [
{
"name": "read"
},
{
"name": "write"
}
]
} }
], ],
"policies": [ "policies": [
@ -142,6 +154,37 @@
"scopes": "[\"urn:servlet-authz:page:main:actionForPremiumUser\"]", "scopes": "[\"urn:servlet-authz:page:main:actionForPremiumUser\"]",
"applyPolicies": "[\"Only Premium User Policy\"]" "applyPolicies": "[\"Only Premium User Policy\"]"
} }
},
{
"name": "Deny Policy",
"type": "js",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"code": "// by default, grants any permission associated with this policy\n$evaluation.deny();"
}
},
{
"name": "Resource A Read Permission",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"Resource A\"]",
"scopes": "[\"read\"]",
"applyPolicies": "[\"Any User Policy\"]"
}
},
{
"name": "Resource A Write Permission",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"Resource A\"]",
"scopes": "[\"write\"]",
"applyPolicies": "[\"Deny Policy\"]"
}
} }
] ]
} }

View file

@ -307,4 +307,14 @@ public abstract class AbstractServletAuthzFunctionalAdapterTest extends Abstract
assertFalse(wasDenied()); assertFalse(wasDenied());
}); });
} }
@Test
public void testAccessResourceWithAnyScope() throws Exception {
performTests(() -> {
login("jdoe", "jdoe");
driver.navigate().to(getResourceServerUrl() + "/protected/scopes.jsp");
WaitUtils.waitForPageToLoad();
assertTrue(hasText("Granted"));
});
}
} }