allow specifying format of "permission" parameter in the UMA grant token
endpoint (#15947)
This commit is contained in:
parent
e9accaf387
commit
bd37875a66
7 changed files with 256 additions and 12 deletions
|
@ -134,6 +134,14 @@ public class HttpMethodAuthenticator<R> {
|
|||
if (metadata.getResponseMode() != null) {
|
||||
method.param("response_mode", metadata.getResponseMode());
|
||||
}
|
||||
|
||||
if (metadata.getPermissionResourceFormat() != null) {
|
||||
method.param("permission_resource_format", metadata.getPermissionResourceFormat().toString());
|
||||
}
|
||||
|
||||
if (metadata.getPermissionResourceMatchingUri() != null) {
|
||||
method.param("permission_resource_matching_uri", metadata.getPermissionResourceMatchingUri().toString());
|
||||
}
|
||||
}
|
||||
|
||||
return method;
|
||||
|
|
|
@ -187,6 +187,8 @@ public class AuthorizationRequest {
|
|||
private Boolean includeResourceName;
|
||||
private Integer limit;
|
||||
private String responseMode;
|
||||
private String permissionResourceFormat;
|
||||
private Boolean permissionResourceMatchingUri;
|
||||
|
||||
public Boolean getIncludeResourceName() {
|
||||
if (includeResourceName == null) {
|
||||
|
@ -214,5 +216,21 @@ public class AuthorizationRequest {
|
|||
public String getResponseMode() {
|
||||
return responseMode;
|
||||
}
|
||||
|
||||
public String getPermissionResourceFormat() {
|
||||
return permissionResourceFormat;
|
||||
}
|
||||
|
||||
public void setPermissionResourceFormat(String permissionResourceFormat) {
|
||||
this.permissionResourceFormat = permissionResourceFormat;
|
||||
}
|
||||
|
||||
public Boolean getPermissionResourceMatchingUri() {
|
||||
return permissionResourceMatchingUri;
|
||||
}
|
||||
|
||||
public void setPermissionResourceMatchingUri(Boolean permissionResourceMatchingUri) {
|
||||
this.permissionResourceMatchingUri = permissionResourceMatchingUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,14 @@ This parameter is *optional*. A string representing a set of one or more resourc
|
|||
in order to request permission for multiple resource and scopes. This parameter is an extension to `urn:ietf:params:oauth:grant-type:uma-ticket` grant type in order to allow clients to send authorization requests without a
|
||||
permission ticket. The format of the string must be: `RESOURCE_ID#SCOPE_ID`. For instance: `Resource A#Scope A`, `Resource A#Scope A, Scope B, Scope C`, `Resource A`, `#Scope A`.
|
||||
+
|
||||
* **permission_resource_format**
|
||||
+
|
||||
This parameter is *optional*. A string representing a format indicating the resource in the `permission` parameter. Possible values are `id` and `uri`. `id` indicates the format is `RESOURCE_ID`. `uri` indicates the format is `URI`. If not specified, the default is `id`.
|
||||
+
|
||||
* **permission_resource_matching_uri**
|
||||
+
|
||||
This parameter is *optional*. A boolean value that indicates whether to use path matching when representing resources in URI format in the `permission` parameter. If not specified, the default is false.
|
||||
+
|
||||
* **audience**
|
||||
+
|
||||
This parameter is *optional*. The client identifier of the resource server to which the client is seeking access. This parameter is mandatory
|
||||
|
|
|
@ -19,6 +19,8 @@ package org.keycloak.authorization.authorization;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
|
@ -59,12 +61,14 @@ import org.keycloak.authorization.util.Tokens;
|
|||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.PathMatcher;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
@ -802,5 +806,129 @@ public class AuthorizationTokenService {
|
|||
ClientConnection getClientConnection() {
|
||||
return clientConnection;
|
||||
}
|
||||
|
||||
public void addPermissions(List<String> permissionList, String permissionResourceFormat, boolean matchingUri) {
|
||||
if (permissionResourceFormat == null) {
|
||||
permissionResourceFormat = "id";
|
||||
}
|
||||
|
||||
switch (permissionResourceFormat) {
|
||||
case "id":
|
||||
addPermissionsById(permissionList);
|
||||
break;
|
||||
case "uri":
|
||||
addPermissionsByUri(permissionList, matchingUri);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void addPermissionsById(List<String> permissionList) {
|
||||
for (String permission : permissionList) {
|
||||
String[] parts = permission.split("#");
|
||||
String rsid = parts[0];
|
||||
|
||||
if (parts.length == 1) {
|
||||
addPermission(rsid);
|
||||
} else {
|
||||
String[] scopes = parts[1].split(",");
|
||||
addPermission(rsid, scopes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addPermissionsByUri(List<String> permissionList, boolean matchingUri) {
|
||||
StoreFactory storeFactory = authorization.getStoreFactory();
|
||||
|
||||
for (String permission : permissionList) {
|
||||
String[] parts = permission.split("#");
|
||||
String uri = parts[0];
|
||||
|
||||
if (parts.length == 1) {
|
||||
// only resource uri is specified
|
||||
if (uri.isEmpty()) {
|
||||
CorsErrorResponseException invalidResourceException = new CorsErrorResponseException(getCors(),
|
||||
OAuthErrorException.INVALID_REQUEST, "You must provide the uri", Status.BAD_REQUEST);
|
||||
fireErrorEvent(getEvent(), Errors.INVALID_REQUEST, invalidResourceException);
|
||||
throw invalidResourceException;
|
||||
}
|
||||
|
||||
List<Resource> resources = getResourceListByUri(uri, storeFactory, matchingUri);
|
||||
|
||||
if (resources == null || resources.isEmpty()) {
|
||||
CorsErrorResponseException invalidResourceException = new CorsErrorResponseException(getCors(),
|
||||
"invalid_resource", "Resource with uri [" + uri + "] does not exist.", Status.BAD_REQUEST);
|
||||
fireErrorEvent(getEvent(), Errors.INVALID_REQUEST, invalidResourceException);
|
||||
throw invalidResourceException;
|
||||
}
|
||||
|
||||
resources.stream().forEach(resource -> addPermission(resource.getId()));
|
||||
} else {
|
||||
// resource uri and scopes are specified, or only scopes are specified
|
||||
String[] scopes = parts[1].split(",");
|
||||
|
||||
if (uri.isEmpty()) {
|
||||
// only scopes are specified
|
||||
addPermission("", scopes);
|
||||
return;
|
||||
}
|
||||
|
||||
List<Resource> resources = getResourceListByUri(uri, storeFactory, matchingUri);
|
||||
|
||||
if (resources == null || resources.isEmpty()) {
|
||||
CorsErrorResponseException invalidResourceException = new CorsErrorResponseException(getCors(),
|
||||
"invalid_resource", "Resource with uri [" + uri + "] does not exist.", Status.BAD_REQUEST);
|
||||
fireErrorEvent(getEvent(), Errors.INVALID_REQUEST, invalidResourceException);
|
||||
throw invalidResourceException;
|
||||
}
|
||||
|
||||
resources.stream().forEach(resource -> addPermission(resource.getId(), scopes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Resource> getResourceListByUri(String uri, StoreFactory storeFactory, boolean matchingUri) {
|
||||
Map<Resource.FilterOption, String[]> search = new EnumMap<>(Resource.FilterOption.class);
|
||||
search.put(Resource.FilterOption.URI, new String[] { uri });
|
||||
ResourceServer resourceServer = storeFactory.getResourceServerStore()
|
||||
.findByClient(getRealm().getClientByClientId(getAudience()));
|
||||
List<Resource> resources = storeFactory.getResourceStore().find(getRealm(), resourceServer, search, -1,
|
||||
Constants.DEFAULT_MAX_RESULTS);
|
||||
|
||||
if (!matchingUri || !resources.isEmpty()) {
|
||||
return resources;
|
||||
}
|
||||
|
||||
search = new EnumMap<>(Resource.FilterOption.class);
|
||||
search.put(Resource.FilterOption.URI_NOT_NULL, new String[] { "true" });
|
||||
search.put(Resource.FilterOption.OWNER, new String[] { resourceServer.getClientId() });
|
||||
|
||||
List<Resource> serverResources = storeFactory.getResourceStore().find(getRealm(), resourceServer, search, -1, -1);
|
||||
|
||||
PathMatcher<Map.Entry<String, Resource>> pathMatcher = new PathMatcher<Map.Entry<String, Resource>>() {
|
||||
@Override
|
||||
protected String getPath(Map.Entry<String, Resource> entry) {
|
||||
return entry.getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<Map.Entry<String, Resource>> getPaths() {
|
||||
Map<String, Resource> result = new HashMap<>();
|
||||
serverResources.forEach(resource -> resource.getUris().forEach(uri -> {
|
||||
result.put(uri, resource);
|
||||
}));
|
||||
|
||||
return result.entrySet();
|
||||
}
|
||||
};
|
||||
|
||||
Map.Entry<String, Resource> matches = pathMatcher.matches(uri);
|
||||
|
||||
if (matches != null) {
|
||||
return Collections.singletonList(matches.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -960,17 +960,9 @@ public class TokenEndpoint {
|
|||
|
||||
if (permissions != null) {
|
||||
event.detail(Details.PERMISSION, String.join("|", permissions));
|
||||
for (String permission : permissions) {
|
||||
String[] parts = permission.split("#");
|
||||
String resource = parts[0];
|
||||
|
||||
if (parts.length == 1) {
|
||||
authorizationRequest.addPermission(resource);
|
||||
} else {
|
||||
String[] scopes = parts[1].split(",");
|
||||
authorizationRequest.addPermission(parts[0], scopes);
|
||||
}
|
||||
}
|
||||
String permissionResourceFormat = formParams.getFirst("permission_resource_format");
|
||||
boolean permissionResourceMatchingUri = Boolean.parseBoolean(formParams.getFirst("permission_resource_matching_uri"));
|
||||
authorizationRequest.addPermissions(permissions, permissionResourceFormat, permissionResourceMatchingUri);
|
||||
}
|
||||
|
||||
Metadata metadata = new Metadata();
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.testsuite.authz;
|
|||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
|
@ -35,6 +36,7 @@ import org.keycloak.authorization.client.AuthzClient;
|
|||
import org.keycloak.authorization.client.resource.ProtectionResource;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
|
||||
import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata;
|
||||
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
|
||||
import org.keycloak.representations.idm.authorization.Permission;
|
||||
import org.keycloak.representations.idm.authorization.PermissionRequest;
|
||||
|
@ -165,6 +167,31 @@ public abstract class AbstractResourceServerTest extends AbstractAuthzTest {
|
|||
return authorization.authorize(authorizationRequest);
|
||||
}
|
||||
|
||||
protected AuthorizationResponse authorizeDecision(String accessToken, Boolean matchingUri, PermissionRequest... permissions) {
|
||||
AuthorizationRequest authorizationRequest = new AuthorizationRequest();
|
||||
|
||||
org.keycloak.authorization.client.resource.AuthorizationResource authorization;
|
||||
|
||||
if (accessToken != null) {
|
||||
authorization = getAuthzClient().authorization(accessToken);
|
||||
} else {
|
||||
authorization = getAuthzClient().authorization();
|
||||
}
|
||||
|
||||
for (PermissionRequest permission : permissions)
|
||||
authorizationRequest.addPermission(permission.getResourceId(), new ArrayList<String>(permission.getScopes()));
|
||||
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setResponseMode("decision");
|
||||
metadata.setPermissionResourceFormat("uri");
|
||||
if (matchingUri != null)
|
||||
metadata.setPermissionResourceMatchingUri(matchingUri);
|
||||
|
||||
authorizationRequest.setMetadata(metadata);
|
||||
|
||||
return authorization.authorize(authorizationRequest);
|
||||
}
|
||||
|
||||
protected RealmResource getRealm() {
|
||||
return adminClient.realm("authz-test");
|
||||
}
|
||||
|
@ -209,6 +236,11 @@ public abstract class AbstractResourceServerTest extends AbstractAuthzTest {
|
|||
}
|
||||
|
||||
protected ResourceRepresentation addResource(String resourceName, String owner, boolean ownerManagedAccess, String... scopeNames) throws Exception {
|
||||
return addResource(resourceName, owner, null, ownerManagedAccess, scopeNames);
|
||||
}
|
||||
|
||||
protected ResourceRepresentation addResource(String resourceName, String owner, Set<String> uris,
|
||||
boolean ownerManagedAccess, String... scopeNames) throws Exception {
|
||||
ClientResource client = getClient(getRealm());
|
||||
AuthorizationResource authorization = client.authorization();
|
||||
ResourceRepresentation resource = new ResourceRepresentation(resourceName);
|
||||
|
@ -219,6 +251,9 @@ public abstract class AbstractResourceServerTest extends AbstractAuthzTest {
|
|||
|
||||
resource.setOwnerManagedAccess(ownerManagedAccess);
|
||||
resource.addScope(scopeNames);
|
||||
if (uris != null) {
|
||||
resource.setUris(uris);
|
||||
}
|
||||
|
||||
Response response = authorization.resources().create(resource);
|
||||
ResourceRepresentation temp = response.readEntity(ResourceRepresentation.class);
|
||||
|
|
|
@ -30,6 +30,7 @@ import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
|
|||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -95,7 +96,7 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
|
|||
authorization.policies().js().create(policy).close();
|
||||
|
||||
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
|
||||
resourceA = addResource("Resource A", "ScopeA", "ScopeB", "ScopeC");
|
||||
resourceA = addResource("Resource A", null, Collections.singleton("/resource"), false, "ScopeA", "ScopeB", "ScopeC");
|
||||
|
||||
permission.setName(resourceA.getName() + " Permission");
|
||||
permission.addResource(resourceA.getName());
|
||||
|
@ -371,6 +372,60 @@ public class UmaGrantTypeTest extends AbstractResourceServerTest {
|
|||
assertTrue(permissions.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testObtainDecisionUsingAccessToken() throws Exception {
|
||||
AccessTokenResponse accessTokenResponse = getAuthzClient().obtainAccessToken("marta", "password");
|
||||
|
||||
// use "rsid" as "uri"
|
||||
// uri and scopes exist
|
||||
AuthorizationResponse response = authorizeDecision(accessTokenResponse.getToken(), null,
|
||||
new PermissionRequest("/resource", "ScopeA", "ScopeB"));
|
||||
assertTrue((Boolean) response.getOtherClaims().getOrDefault("result", "false"));
|
||||
|
||||
// uri and scopes are empty
|
||||
try {
|
||||
response = authorizeDecision(accessTokenResponse.getToken(), null, new PermissionRequest(null));
|
||||
fail();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
// uri is empty but scopes exist
|
||||
response = authorizeDecision(accessTokenResponse.getToken(), null, new PermissionRequest(null, "ScopeA", "ScopeB"));
|
||||
assertTrue((Boolean) response.getOtherClaims().getOrDefault("result", "false"));
|
||||
|
||||
// test wild card
|
||||
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
|
||||
ResourceRepresentation resourceB = addResource("Resource B", null, Collections.singleton("/rs/*"), false, "ScopeD",
|
||||
"ScopeE");
|
||||
|
||||
permission.setName(resourceB.getName() + " Permission");
|
||||
permission.addResource(resourceB.getName());
|
||||
permission.addPolicy("Default Policy");
|
||||
|
||||
getClient(getRealm()).authorization().permissions().resource().create(permission).close();
|
||||
|
||||
// matchingUri is null, then result error
|
||||
try {
|
||||
response = authorizeDecision(accessTokenResponse.getToken(), null,
|
||||
new PermissionRequest("/rs/data", "ScopeD", "ScopeE"));
|
||||
fail();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
// matchingUri is true, then result true
|
||||
response = authorizeDecision(accessTokenResponse.getToken(), true,
|
||||
new PermissionRequest("/rs/data", "ScopeD", "ScopeE"));
|
||||
assertTrue((Boolean) response.getOtherClaims().getOrDefault("result", "false"));
|
||||
|
||||
// matchingUri is false, then result error
|
||||
try {
|
||||
response = authorizeDecision(accessTokenResponse.getToken(), false,
|
||||
new PermissionRequest("/rs/data", "ScopeD", "ScopeE"));
|
||||
fail();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCORSHeadersInFailedRptRequest() throws Exception {
|
||||
AccessTokenResponse accessTokenResponse = getAuthzClient().obtainAccessToken("marta", "password");
|
||||
|
|
Loading…
Reference in a new issue