[KEYCLOAK-5726] - Support define enforcement mode for scopes on the adapter configuration
This commit is contained in:
parent
a4ec32ba66
commit
a6e1413d58
8 changed files with 128 additions and 34 deletions
|
@ -33,6 +33,7 @@ import org.keycloak.authorization.client.ClientAuthorizationContext;
|
|||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||
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.idm.authorization.Permission;
|
||||
|
||||
|
@ -96,9 +97,9 @@ public abstract class AbstractPolicyEnforcer {
|
|||
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 {
|
||||
return createAuthorizationContext(accessToken, pathConfig);
|
||||
} catch (Exception e) {
|
||||
|
@ -108,7 +109,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||
|
||||
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);
|
||||
handleAccessDenied(httpFacade);
|
||||
}
|
||||
|
@ -118,9 +119,9 @@ public abstract class AbstractPolicyEnforcer {
|
|||
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();
|
||||
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
|
||||
|
||||
|
@ -146,7 +147,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
|
||||
if (hasResourceScopePermission(methodConfig, permission)) {
|
||||
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
|
||||
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
|
||||
this.paths.remove(actualPathConfig);
|
||||
|
@ -155,7 +156,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
|
||||
if (hasResourceScopePermission(methodConfig, permission)) {
|
||||
hasPermission = true;
|
||||
return true;
|
||||
}
|
||||
|
@ -166,7 +167,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||
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;
|
||||
}
|
||||
|
@ -186,9 +187,28 @@ public abstract class AbstractPolicyEnforcer {
|
|||
return false;
|
||||
}
|
||||
|
||||
private boolean hasResourceScopePermission(Set<String> requiredScopes, Permission permission, PathConfig actualPathConfig) {
|
||||
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
|
||||
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() {
|
||||
|
@ -236,20 +256,22 @@ public abstract class AbstractPolicyEnforcer {
|
|||
return request.getRelativePath();
|
||||
}
|
||||
|
||||
private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) {
|
||||
Set<String> requiredScopes = new HashSet<>();
|
||||
|
||||
requiredScopes.addAll(pathConfig.getScopes());
|
||||
|
||||
private MethodConfig getRequiredScopes(PathConfig pathConfig, Request request) {
|
||||
String method = request.getMethod();
|
||||
|
||||
for (PolicyEnforcerConfig.MethodConfig methodConfig : pathConfig.getMethods()) {
|
||||
for (MethodConfig methodConfig : pathConfig.getMethods()) {
|
||||
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) {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
package org.keycloak.adapters.authorization;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
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.resource.PermissionResource;
|
||||
import org.keycloak.authorization.client.resource.ProtectionResource;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
|
||||
/**
|
||||
|
@ -40,9 +41,9 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||
}
|
||||
|
||||
@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) {
|
||||
challengeUmaAuthentication(pathConfig, requiredScopes, facade);
|
||||
challengeUmaAuthentication(pathConfig, methodConfig, facade);
|
||||
} else {
|
||||
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();
|
||||
AuthzClient authzClient = getAuthzClient();
|
||||
String ticket = getPermissionTicket(pathConfig, requiredScopes, authzClient);
|
||||
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient);
|
||||
String clientId = authzClient.getConfiguration().getResource();
|
||||
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize";
|
||||
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();
|
||||
PermissionResource permission = protection.permission();
|
||||
PermissionRequest permissionRequest = new PermissionRequest();
|
||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
||||
permissionRequest.setScopes(requiredScopes);
|
||||
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
||||
return permission.forResource(permissionRequest).getTicket();
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ import org.keycloak.authorization.client.representation.EntitlementResponse;
|
|||
import org.keycloak.authorization.client.representation.PermissionRequest;
|
||||
import org.keycloak.authorization.client.representation.PermissionResponse;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
import org.keycloak.representations.idm.authorization.Permission;
|
||||
|
||||
|
@ -50,14 +51,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||
}
|
||||
|
||||
@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;
|
||||
|
||||
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
|
||||
if (super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
|
||||
accessToken = requestAuthorizationToken(pathConfig, methodConfig, httpFacade);
|
||||
|
||||
if (accessToken == null) {
|
||||
return false;
|
||||
|
@ -78,11 +79,11 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||
|
||||
original.setAuthorization(authorization);
|
||||
|
||||
return super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade);
|
||||
return super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
|
||||
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
||||
handleAccessDenied(facade);
|
||||
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 {
|
||||
String accessToken = httpFacade.getSecurityContext().getTokenString();
|
||||
AuthzClient authzClient = getAuthzClient();
|
||||
|
@ -111,7 +112,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||
PermissionRequest permissionRequest = new PermissionRequest();
|
||||
|
||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
||||
permissionRequest.setScopes(requiredScopes);
|
||||
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
||||
|
||||
PermissionResponse permissionResponse = authzClient.protection().permission().forResource(permissionRequest);
|
||||
AuthorizationRequest authzRequest = new AuthorizationRequest(permissionResponse.getTicket());
|
||||
|
|
|
@ -220,6 +220,9 @@ public class PolicyEnforcerConfig {
|
|||
private String method;
|
||||
private List<String> scopes = Collections.emptyList();
|
||||
|
||||
@JsonProperty("scopes-enforcement-mode")
|
||||
private ScopeEnforcementMode scopesEnforcementMode = ScopeEnforcementMode.ALL;
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
@ -235,6 +238,14 @@ public class PolicyEnforcerConfig {
|
|||
public void setScopes(List<String> scopes) {
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
public void setScopesEnforcementMode(ScopeEnforcementMode scopesEnforcementMode) {
|
||||
this.scopesEnforcementMode = scopesEnforcementMode;
|
||||
}
|
||||
|
||||
public ScopeEnforcementMode getScopesEnforcementMode() {
|
||||
return scopesEnforcementMode;
|
||||
}
|
||||
}
|
||||
|
||||
public enum EnforcementMode {
|
||||
|
@ -243,6 +254,11 @@ public class PolicyEnforcerConfig {
|
|||
DISABLED
|
||||
}
|
||||
|
||||
public enum ScopeEnforcementMode {
|
||||
ALL,
|
||||
ANY
|
||||
}
|
||||
|
||||
public static class UmaProtocolConfig {
|
||||
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
"redirectUris": [
|
||||
"/photoz-html5-client/*"
|
||||
],
|
||||
"webOrigins": ["*"]
|
||||
"webOrigins": ["http://localhost:8280"]
|
||||
},
|
||||
{
|
||||
"clientId": "photoz-restful-api",
|
||||
|
@ -118,7 +118,7 @@
|
|||
"redirectUris": [
|
||||
"/photoz-restful-api/*"
|
||||
],
|
||||
"webOrigins" : ["*"],
|
||||
"webOrigins" : ["http://localhost:8280"],
|
||||
"clientAuthenticatorType": "client-jwt",
|
||||
"attributes" : {
|
||||
"jwt.credential.certificate" : "MIICqTCCAZECBgFT0Ngs/DANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1zZWN1cmUtcG9ydGFsMB4XDTE2MDQwMTA4MDA0MVoXDTI2MDQwMTA4MDIyMVowGDEWMBQGA1UEAwwNc2VjdXJlLXBvcnRhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJa4GixpmzP511AmI0eLPLORyJwXS8908MUvdG3hmh8jMOIhe28XjIFeZSY09vFxh22F2SUMjxU/B2Hw4PDJUkebuNR7rXhOIYCJAo6eEZzjSBY/wngFtfm74zJ/eLCobBtDvIld7jobdHTfE1Oz9+GzvtG0k7cm7ubrLT0J4I1UsFZj3b//3wa+O0vNaTwHC1Jz/m59VbtXqyO4xEzIdl416cnGCmEmk5qd5h1de2UoLi/CTad8HftIJhzN1qhlySzW/9Ha70aYlDH2hiibDsXDTrNaMdaaLik7I8Rv/nIbggysG863PKZo8wknDe62QctH5VYSSktiy4gjSJkGh7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZnnx+AHQ8txugGcFK8gWjildDgk+v31fBHBDvmLQaSzsUaIOJaK4wnlwUI+VfR46HmBXhjlDCobFLUptd+kz0G7xapcIn3b5jLrySUUD7L+LAp1vNOQU4mKhTGS3IEvNB73D3GH9rQ+M3KEcoN3f99fNKqKsUdxbmZqGf4VOQ57PUfLBw4PJJGlROPosBc7ivPRyeYnKekhoCTynq30BAD1FA1BA8ppcY4ZVGADPTAgMJxpglpFY9LiqCwdLAGW1ttnsyIJ7DpT+kybhhk7c+MU7gyQdv8xPnMR0bSCB9hndowgBn5oZ393aMscwMNCzwJ0aWBs1sUyn3X0RIsu9Jg=="
|
||||
|
|
|
@ -46,6 +46,18 @@
|
|||
"name": "urn:servlet-authz:page:main:actionForPremiumUser"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Resource A",
|
||||
"uri": "/protected/scopes.jsp",
|
||||
"scopes": [
|
||||
{
|
||||
"name": "read"
|
||||
},
|
||||
{
|
||||
"name": "write"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"policies": [
|
||||
|
@ -142,6 +154,37 @@
|
|||
"scopes": "[\"urn:servlet-authz:page:main:actionForPremiumUser\"]",
|
||||
"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\"]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Granted
|
|
@ -307,4 +307,14 @@ public abstract class AbstractServletAuthzFunctionalAdapterTest extends Abstract
|
|||
assertFalse(wasDenied());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessResourceWithAnyScope() throws Exception {
|
||||
performTests(() -> {
|
||||
login("jdoe", "jdoe");
|
||||
driver.navigate().to(getResourceServerUrl() + "/protected/scopes.jsp");
|
||||
WaitUtils.waitForPageToLoad();
|
||||
assertTrue(hasText("Granted"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue