keycloak-aplcache
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java 75(+39 -36)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java 55(+25 -30)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java 76(+38 -38)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java 6(+3 -3)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java 62(+37 -25)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java 4(+4 -0)
adapters/oidc/js/src/main/resources/keycloak-authz.js 172(+105 -67)
authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java 48(+0 -48)
authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java 84(+0 -84)
authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementResponse.java 42(+0 -42)
authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java 60(+0 -60)
authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java 79(+0 -79)
authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java 49(+0 -49)
authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java 24(+22 -2)
authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java 244(+109 -135)
authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java 4(+2 -2)
authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java 76(+62 -14)
authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java 43(+0 -43)
authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java 191(+180 -11)
authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java 202(+164 -38)
authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java 34(+27 -7)
authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java 84(+80 -4)
authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java 24(+22 -2)
core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java 49(+10 -39)
core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java 190(+190 -0)
core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationResponse.java 37(+22 -15)
core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java 57(+31 -26)
core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketRepresentation.java 87(+87 -0)
core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java 80(+54 -26)
core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java 8(+8 -0)
core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java 27(+27 -0)
examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java 97(+28 -69)
examples/authz/photoz/photoz-realm.json 17(+12 -5)
examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 76(+54 -22)
examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/SharedAlbum.java 47(+47 -0)
examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java 14(+14 -0)
examples/authz/photoz/photoz-restful-api/src/main/resources/photoz-restful-api-authz-service.json 147(+58 -89)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java 83(+83 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java 12(+12 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedScope.java 7(+6 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketListQuery.java 42(+42 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketQuery.java 32(+7 -25)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketResourceListQuery.java 31(+17 -14)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketScopeListQuery.java 37(+14 -23)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java 60(+60 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java 60(+60 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java 139(+139 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java 4(+3 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java 41(+40 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ScopeAdapter.java 12(+12 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java 15(+15 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java 146(+145 -1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 6(+6 -0)
model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java 161(+161 -0)
model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java 229(+229 -0)
model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PermissionTicketAdapter.java 132(+132 -0)
server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java 416(+276 -140)
server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java 3(+2 -1)
server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java 2(+1 -1)
server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java 12(+10 -2)
server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java 17(+15 -2)
server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java 181(+181 -0)
server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java 93(+93 -0)
services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java 10(+9 -1)
services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java 414(+280 -134)
services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java 50(+0 -50)
services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java 43(+0 -43)
services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java 78(+0 -78)
services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java 42(+0 -42)
services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java 47(+23 -24)
services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java 126(+72 -54)
services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java 106(+102 -4)
services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java 2(+1 -1)
services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java 11(+11 -0)
services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java 121(+30 -91)
services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java 27(+20 -7)
services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java 349(+349 -0)
services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java 1(+0 -1)
testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/index.jsp 4(+2 -2)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html 3(+2 -1)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js 68(+62 -6)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js 5(+5 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html 1(+1 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html 2(+1 -1)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java 2(+1 -1)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 20(+14 -6)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java 2(+1 -1)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java 12(+12 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json 20(+4 -16)
testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-permissive-authz-service.json 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java 114(+106 -8)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java 154(+132 -22)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java 6(+2 -4)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java 6(+5 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractAuthzTest.java 1(+0 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java 229(+229 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthorizationAPITest.java 58(+10 -48)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java 67(+25 -42)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java 9(+4 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java 127(+60 -67)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java 36(+12 -24)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java 32(+11 -21)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java 23(+10 -13)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java 370(+370 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RolePolicyTest.java 42(+16 -26)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaDiscoveryDocumentTest.java 77(+77 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java 276(+276 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedAccessTest.java 358(+358 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/default-keycloak-uma2.json 8(+8 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java 5(+5 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scope.java 5(+5 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java 14(+14 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java 20(+17 -3)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java 5(+5 -0)
testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java 10(+10 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html 14(+14 -0)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
index 15aa1e1..96fbe5d 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
@@ -18,7 +18,6 @@
package org.keycloak.adapters.authorization;
import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -65,54 +64,58 @@ public abstract class AbstractPolicyEnforcer {
return createEmptyAuthorizationContext(true);
}
+ Request request = httpFacade.getRequest();
+ String path = getPath(request);
+ PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
- if (securityContext != null) {
- AccessToken accessToken = securityContext.getToken();
+ if (securityContext == null) {
+ if (pathConfig != null) {
+ challenge(pathConfig, getRequiredScopes(pathConfig, request), httpFacade);
+ }
+ return createEmptyAuthorizationContext(false);
+ }
- if (accessToken != null) {
- Request request = httpFacade.getRequest();
- String path = getPath(request);
- PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
+ AccessToken accessToken = securityContext.getToken();
- LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
+ if (accessToken != null) {
+ LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
- if (pathConfig == null) {
- if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
- return createAuthorizationContext(accessToken, null);
- }
+ if (pathConfig == null) {
+ if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
+ return createAuthorizationContext(accessToken, null);
+ }
- LOGGER.debugf("Could not find a configuration for path [%s]", path);
+ LOGGER.debugf("Could not find a configuration for path [%s]", path);
- if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
- return createAuthorizationContext(accessToken, null);
- }
+ if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
+ return createAuthorizationContext(accessToken, null);
+ }
- handleAccessDenied(httpFacade);
+ handleAccessDenied(httpFacade);
- return createEmptyAuthorizationContext(false);
- }
+ return createEmptyAuthorizationContext(false);
+ }
- if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
- return createEmptyAuthorizationContext(true);
- }
+ if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
+ return createEmptyAuthorizationContext(true);
+ }
- MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
+ MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
- if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
- try {
- return createAuthorizationContext(accessToken, pathConfig);
- } catch (Exception e) {
- throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
- }
+ if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
+ try {
+ return createAuthorizationContext(accessToken, pathConfig);
+ } catch (Exception e) {
+ throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
}
+ }
- LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
+ LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
- if (!challenge(pathConfig, methodConfig, httpFacade)) {
- LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
- handleAccessDenied(httpFacade);
- }
+ if (!challenge(pathConfig, methodConfig, httpFacade)) {
+ LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
+ handleAccessDenied(httpFacade);
}
}
@@ -139,7 +142,7 @@ public abstract class AbstractPolicyEnforcer {
boolean hasPermission = false;
for (Permission permission : permissions) {
- if (permission.getResourceSetId() != null) {
+ if (permission.getResourceId() != null) {
if (isResourcePermission(actualPathConfig, permission)) {
hasPermission = true;
@@ -292,6 +295,6 @@ public abstract class AbstractPolicyEnforcer {
}
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
- return permission.getResourceSetId().equals(actualPathConfig.getId());
+ return permission.getResourceId().equals(actualPathConfig.getId());
}
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java
index 172c745..9e29735 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java
@@ -23,11 +23,11 @@ import org.jboss.logging.Logger;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.spi.HttpFacade;
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;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -42,45 +42,40 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
@Override
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
- if (getEnforcerConfig().getUserManagedAccess() != null) {
- challengeUmaAuthentication(pathConfig, methodConfig, facade);
- } else {
- challengeEntitlementAuthentication(facade);
- }
- return true;
- }
-
- private void challengeEntitlementAuthentication(OIDCHttpFacade facade) {
HttpFacade.Response response = facade.getResponse();
AuthzClient authzClient = getAuthzClient();
- String clientId = authzClient.getConfiguration().getResource();
- String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/entitlement";
- response.setStatus(401);
- response.setHeader("WWW-Authenticate", "KC_ETT realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\"");
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("Sending Entitlement challenge");
+ String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient);
+
+ if (ticket == null) {
+ response.setStatus(403);
+ return true;
}
- }
- private void challengeUmaAuthentication(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
- HttpFacade.Response response = facade.getResponse();
- AuthzClient authzClient = getAuthzClient();
- String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient);
- String clientId = authzClient.getConfiguration().getResource();
- String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize";
+ String realm = authzClient.getConfiguration().getRealm();
+ String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString();
response.setStatus(401);
- response.setHeader("WWW-Authenticate", "UMA realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\",ticket=\"" + ticket + "\"");
+ StringBuilder wwwAuthenticate = new StringBuilder("UMA realm=\"").append(realm).append("\"").append(",as_uri=\"").append(authorizationServerUri).append("\"");
+
+ if (ticket != null) {
+ wwwAuthenticate.append(",ticket=\"").append(ticket).append("\"");
+ }
+
+ response.setHeader("WWW-Authenticate", wwwAuthenticate.toString());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Sending UMA challenge");
}
+ return true;
}
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(new HashSet<>(methodConfig.getScopes()));
- return permission.forResource(permissionRequest).getTicket();
+ if (getEnforcerConfig().getUserManagedAccess() != null) {
+ ProtectionResource protection = authzClient.protection();
+ PermissionResource permission = protection.permission();
+ PermissionRequest permissionRequest = new PermissionRequest();
+ permissionRequest.setResourceId(pathConfig.getId());
+ permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
+ return permission.create(permissionRequest).getTicket();
+ }
+ return null;
}
}
\ No newline at end of file
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
index 65fdc1e..0732ee9 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
@@ -19,25 +19,23 @@ package org.keycloak.adapters.authorization;
import java.util.ArrayList;
import java.util.HashSet;
-import java.util.Set;
import org.jboss.logging.Logger;
+import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
-import org.keycloak.authorization.client.representation.AuthorizationRequest;
-import org.keycloak.authorization.client.representation.AuthorizationResponse;
-import org.keycloak.authorization.client.representation.EntitlementRequest;
-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.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -90,6 +88,12 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
@Override
protected void handleAccessDenied(OIDCHttpFacade facade) {
+ KeycloakSecurityContext securityContext = facade.getSecurityContext();
+
+ if (securityContext == null) {
+ return;
+ }
+
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
HttpFacade.Response response = facade.getResponse();
@@ -103,45 +107,41 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
try {
- String accessToken = httpFacade.getSecurityContext().getTokenString();
+ KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
+ String accessTokenString = securityContext.getTokenString();
AuthzClient authzClient = getAuthzClient();
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
+ PermissionRequest permissionRequest = new PermissionRequest();
- if (getEnforcerConfig().getUserManagedAccess() != null) {
- LOGGER.debug("Obtaining authorization for authenticated user.");
- PermissionRequest permissionRequest = new PermissionRequest();
-
- permissionRequest.setResourceSetId(pathConfig.getId());
- permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
-
- PermissionResponse permissionResponse = authzClient.protection().permission().forResource(permissionRequest);
- AuthorizationRequest authzRequest = new AuthorizationRequest(permissionResponse.getTicket());
- AuthorizationResponse authzResponse = authzClient.authorization(accessToken).authorize(authzRequest);
+ permissionRequest.setResourceId(pathConfig.getId());
+ permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
- if (authzResponse != null) {
- return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
- }
+ AccessToken accessToken = securityContext.getToken();
+ AuthorizationRequest authzRequest;
- return null;
+ if (getEnforcerConfig().getUserManagedAccess() != null) {
+ PermissionResponse permissionResponse = authzClient.protection().permission().create(permissionRequest);
+ authzRequest = new AuthorizationRequest();
+ authzRequest.setTicket(permissionResponse.getTicket());
} else {
- LOGGER.debug("Obtaining entitlements for authenticated user.");
- AccessToken token = httpFacade.getSecurityContext().getToken();
-
- if (token.getAuthorization() == null) {
- EntitlementResponse authzResponse = authzClient.entitlement(accessToken).getAll(authzClient.getConfiguration().getResource());
- return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
- } else {
- EntitlementRequest request = new EntitlementRequest();
- PermissionRequest permissionRequest = new PermissionRequest();
- permissionRequest.setResourceSetId(pathConfig.getId());
- permissionRequest.setResourceSetName(pathConfig.getName());
- permissionRequest.setScopes(new HashSet<>(pathConfig.getScopes()));
- LOGGER.debugf("Sending entitlements request: resource_set_id [%s], resource_set_name [%s], scopes [%s].", permissionRequest.getResourceSetId(), permissionRequest.getResourceSetName(), permissionRequest.getScopes());
- request.addPermission(permissionRequest);
- EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getResource(), request);
- return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
+ authzRequest = new AuthorizationRequest();
+ if (accessToken.getAuthorization() != null) {
+ authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
}
}
+
+ if (accessToken.getAuthorization() != null) {
+ authzRequest.setRpt(accessTokenString);
+ }
+
+ LOGGER.debug("Obtaining authorization for authenticated user.");
+ AuthorizationResponse authzResponse = authzClient.authorization(accessTokenString).authorize(authzRequest);
+
+ if (authzResponse != null) {
+ return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment);
+ }
+
+ return null;
} catch (AuthorizationDeniedException e) {
LOGGER.debug("Authorization denied", e);
return null;
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
index c8bce94..8e83de1 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
@@ -18,8 +18,8 @@
package org.keycloak.adapters.authorization;
import java.util.Arrays;
+import java.util.List;
import java.util.Map;
-import java.util.Set;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
@@ -221,11 +221,11 @@ class PathMatcher {
private PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
if (originalConfig.hasPattern()) {
ProtectedResource resource = this.authzClient.protection().resource();
- Set<String> search = resource.findByFilter("uri=" + path);
+ List<ResourceRepresentation> search = resource.findByUri(path);
if (!search.isEmpty()) {
// resource does exist on the server, cache it
- ResourceRepresentation targetResource = resource.findById(search.iterator().next()).getResourceDescription();
+ ResourceRepresentation targetResource = search.get(0);
PathConfig config = PolicyEnforcer.createPathConfig(targetResource);
config.setScopes(originalConfig.getScopes());
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index 7f21eef..fe8aa1a 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -17,6 +17,15 @@
*/
package org.keycloak.adapters.authorization;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.adapters.KeycloakDeployment;
@@ -25,7 +34,6 @@ import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.RegistrationResponse;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.representation.ScopeRepresentation;
import org.keycloak.authorization.client.resource.ProtectedResource;
@@ -34,14 +42,6 @@ import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -58,10 +58,15 @@ public class PolicyEnforcer {
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
this.deployment = deployment;
this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
- this.authzClient = AuthzClient.create(new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient()), new ClientAuthenticator() {
+ Configuration configuration = new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient());
+ this.authzClient = AuthzClient.create(configuration, new ClientAuthenticator() {
@Override
- public void configureClientCredentials(HashMap<String, String> requestParams, HashMap<String, String> requestHeaders) {
- ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, requestParams);
+ public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
+ Map<String, String> formparams = new HashMap<>();
+ ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, formparams);
+ for (Entry<String, String> param : formparams.entrySet()) {
+ requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
+ }
}
});
this.pathMatcher = new PathMatcher(this.authzClient);
@@ -142,26 +147,34 @@ public class PolicyEnforcer {
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
- Set<String> search;
+ ResourceRepresentation resource;
String resourceName = pathConfig.getName();
String path = pathConfig.getPath();
if (resourceName != null) {
LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path);
- search = protectedResource.findByFilter("name=" + resourceName);
+ resource = protectedResource.findByName(resourceName);
} else {
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
- search = protectedResource.findByFilter("uri=" + path);
+ List<ResourceRepresentation> resources = protectedResource.findByUri(path);
+
+ if (resources.size() == 1) {
+ resource = resources.get(0);
+ } else if (resources.size() > 1) {
+ throw new RuntimeException("Multiple resources found with the same uri");
+ } else {
+ resource = null;
+ }
}
- if (search.isEmpty()) {
+ if (resource == null) {
if (enforcerConfig.isCreateResources()) {
LOGGER.debugf("Creating resource on server for path [%s].", pathConfig);
- ResourceRepresentation resource = new ResourceRepresentation();
+ ResourceRepresentation representation = new ResourceRepresentation();
- resource.setName(resourceName);
- resource.setType(pathConfig.getType());
- resource.setUri(path);
+ representation.setName(resourceName);
+ representation.setType(pathConfig.getType());
+ representation.setUri(path);
HashSet<ScopeRepresentation> scopes = new HashSet<>();
@@ -173,16 +186,16 @@ public class PolicyEnforcer {
scopes.add(scope);
}
- resource.setScopes(scopes);
+ representation.setScopes(scopes);
- RegistrationResponse registrationResponse = protectedResource.create(resource);
+ ResourceRepresentation registrationResponse = protectedResource.create(representation);
pathConfig.setId(registrationResponse.getId());
} else {
throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + "]. Make sure you have created a resource on the server that matches with the path configuration.");
}
} else {
- pathConfig.setId(search.iterator().next());
+ pathConfig.setId(resource.getId());
}
PathConfig existingPath = null;
@@ -210,8 +223,7 @@ public class PolicyEnforcer {
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
for (String id : protectedResource.findAll()) {
- RegistrationResponse response = protectedResource.findById(id);
- ResourceRepresentation resourceDescription = response.getResourceDescription();
+ ResourceRepresentation resourceDescription = protectedResource.findById(id);
if (resourceDescription.getUri() != null) {
PathConfig pathConfig = createPathConfig(resourceDescription);
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java
index bb68409..966f8c0 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java
@@ -174,6 +174,10 @@ public class BearerTokenRequestAuthenticator {
@Override
public boolean challenge(HttpFacade facade) {
+ if (deployment.getPolicyEnforcer() != null) {
+ deployment.getPolicyEnforcer().enforce(OIDCHttpFacade.class.cast(facade));
+ return true;
+ }
OIDCAuthenticationError error = new OIDCAuthenticationError(reason, description);
facade.getRequest().setError(error);
facade.getResponse().addHeader("WWW-Authenticate", challenge);
adapters/oidc/js/src/main/resources/keycloak-authz.js 172(+105 -67)
diff --git a/adapters/oidc/js/src/main/resources/keycloak-authz.js b/adapters/oidc/js/src/main/resources/keycloak-authz.js
index 843b11d..5237273 100644
--- a/adapters/oidc/js/src/main/resources/keycloak-authz.js
+++ b/adapters/oidc/js/src/main/resources/keycloak-authz.js
@@ -18,14 +18,14 @@
(function( window, undefined ) {
- var KeycloakAuthorization = function (keycloak) {
+ var KeycloakAuthorization = function (keycloak, options) {
var _instance = this;
this.rpt = null;
this.init = function () {
var request = new XMLHttpRequest();
- request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/.well-known/uma-configuration');
+ request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/.well-known/uma2-configuration');
request.onreadystatechange = function () {
if (request.readyState == 4) {
if (request.status == 200) {
@@ -47,68 +47,61 @@
* necessary information to ask a Keycloak server for authorization data using both UMA and Entitlement protocol,
* depending on how the policy enforcer at the resource server was configured.
*/
- this.authorize = function (wwwAuthenticateHeader) {
+ this.authorize = function (authorizationRequest) {
this.then = function (onGrant, onDeny, onError) {
- if (wwwAuthenticateHeader.indexOf('UMA') != -1) {
- var params = wwwAuthenticateHeader.split(',');
-
- for (i = 0; i < params.length; i++) {
- var param = params[i].split('=');
-
- if (param[0] == 'ticket') {
- var request = new XMLHttpRequest();
-
- request.open('POST', _instance.config.rpt_endpoint, true);
- request.setRequestHeader('Content-Type', 'application/json')
- request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token)
-
- request.onreadystatechange = function () {
- if (request.readyState == 4) {
- var status = request.status;
-
- if (status >= 200 && status < 300) {
- var rpt = JSON.parse(request.responseText).rpt;
- _instance.rpt = rpt;
- onGrant(rpt);
- } else if (status == 403) {
- if (onDeny) {
- onDeny();
- } else {
- console.error('Authorization request was denied by the server.');
- }
- } else {
- if (onError) {
- onError();
- } else {
- console.error('Could not obtain authorization data from server.');
- }
- }
+ if (authorizationRequest && authorizationRequest.ticket) {
+ var request = new XMLHttpRequest();
+
+ request.open('POST', _instance.config.token_endpoint, true);
+ request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+ request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
+
+ request.onreadystatechange = function () {
+ if (request.readyState == 4) {
+ var status = request.status;
+
+ if (status >= 200 && status < 300) {
+ var rpt = JSON.parse(request.responseText).access_token;
+ _instance.rpt = rpt;
+ onGrant(rpt);
+ } else if (status == 403) {
+ if (onDeny) {
+ onDeny();
+ } else {
+ console.error('Authorization request was denied by the server.');
}
- };
-
- var ticket = param[1].substring(1, param[1].length - 1).trim();
-
- request.send(JSON.stringify(
- {
- ticket: ticket,
- rpt: _instance.rpt
+ } else {
+ if (onError) {
+ onError();
+ } else {
+ console.error('Could not obtain authorization data from server.');
}
- ));
+ }
}
+ };
+
+ var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId + "&ticket=" + authorizationRequest.ticket;
+
+ if (authorizationRequest.submitRequest != undefined) {
+ params += "&submit_request=" + authorizationRequest.submitRequest;
}
- } else if (wwwAuthenticateHeader.indexOf('KC_ETT') != -1) {
- var params = wwwAuthenticateHeader.substring('KC_ETT'.length).trim().split(',');
- var clientId = null;
- for (i = 0; i < params.length; i++) {
- var param = params[i].split('=');
+ var metadata = authorizationRequest.metadata;
- if (param[0] == 'realm') {
- clientId = param[1].substring(1, param[1].length - 1).trim();
+ if (metadata) {
+ if (metadata.responseIncludeResourceName) {
+ params += "&response_include_resource_name=" + metadata.responseIncludeResourceName;
}
+ if (metadata.responsePermissionsLimit) {
+ params += "&response_permissions_limit=" + metadata.responsePermissionsLimit;
+ }
+ }
+
+ if (_instance.rpt && (authorizationRequest.incrementalAuthorization == undefined || authorizationRequest.incrementalAuthorization)) {
+ params += "&rpt=" + _instance.rpt;
}
- _instance.entitlement(clientId).then(onGrant, onDeny, onError);
+ request.send(params);
}
};
@@ -116,20 +109,22 @@
};
/**
- * Obtains all entitlements from a Keycloak Server based on a give resourceServerId.
+ * Obtains all entitlements from a Keycloak Server based on a given resourceServerId.
*/
- this.entitlement = function (resourceSeververId, entitlementRequest ) {
+ this.entitlement = function (resourceServerId, authorizationRequest) {
this.then = function (onGrant, onDeny, onError) {
var request = new XMLHttpRequest();
-
+ request.open('POST', _instance.config.token_endpoint, true);
+ request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+ request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
request.onreadystatechange = function () {
if (request.readyState == 4) {
var status = request.status;
if (status >= 200 && status < 300) {
- var rpt = JSON.parse(request.responseText).rpt;
+ var rpt = JSON.parse(request.responseText).access_token;
_instance.rpt = rpt;
onGrant(rpt);
} else if (status == 403) {
@@ -148,19 +143,62 @@
}
};
- var erJson = null
+ if (!authorizationRequest) {
+ authorizationRequest = {};
+ }
+
+ var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId;
- if(entitlementRequest) {
- request.open('POST', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true);
- request.setRequestHeader("Content-type", "application/json");
- erJson = JSON.stringify(entitlementRequest)
- } else {
- request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true);
+ if (authorizationRequest.claimToken) {
+ params += "&claim_token=" + authorizationRequest.claimToken;
+
+ if (authorizationRequest.claimTokenFormat) {
+ params += "&claim_token_format=" + authorizationRequest.claimTokenFormat;
+ }
}
- request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token)
- request.send(erJson);
+ params += "&audience=" + resourceServerId;
+
+ var permissions = authorizationRequest.permissions;
+
+ if (!permissions) {
+ permissions = [];
+ }
+
+ for (i = 0; i < permissions.length; i++) {
+ var resource = permissions[i];
+ var permission = resource.id;
+
+ if (resource.scopes && resource.scopes.length > 0) {
+ permission += "#";
+ for (j = 0; j < resource.scopes.length; j++) {
+ var scope = resource.scopes[j];
+ if (permission.indexOf('#') != permission.length - 1) {
+ permission += ",";
+ }
+ permission += scope;
+ }
+ }
+
+ params += "&permission=" + permission;
+ }
+
+ var metadata = authorizationRequest.metadata;
+
+ if (metadata) {
+ if (metadata.responseIncludeResourceName) {
+ params += "&response_include_resource_name=" + metadata.responseIncludeResourceName;
+ }
+ if (metadata.responsePermissionsLimit) {
+ params += "&response_permissions_limit=" + metadata.responsePermissionsLimit;
+ }
+ }
+
+ if (_instance.rpt) {
+ params += "&rpt=" + _instance.rpt;
+ }
+ request.send(params);
};
return this;
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
index 1113fd2..1bf5c86 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
@@ -17,32 +17,40 @@
*/
package org.keycloak.authorization.client;
+import java.io.IOException;
+import java.io.InputStream;
+
import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.resource.AuthorizationResource;
-import org.keycloak.authorization.client.resource.EntitlementResource;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.authorization.client.util.Http;
-import org.keycloak.jose.jws.JWSInput;
-import org.keycloak.representations.AccessToken;
+import org.keycloak.authorization.client.util.TokenCallable;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.JsonSerialization;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.util.concurrent.Callable;
-
/**
* <p>This is class serves as an entry point for clients looking for access to Keycloak Authorization Services.
*
+ * <p>When creating a new instances make sure you have a Keycloak Server running at the location specified in the client
+ * configuration. The client tries to obtain server configuration by invoking the UMA Discovery Endpoint, usually available
+ * from the server at <i>http(s)://{server}:{port}/auth/realms/{realm}/.well-known/uma-configuration</i>.
+ *
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class AuthzClient {
private final Http http;
- private Callable<String> patSupplier;
-
- public static AuthzClient create() {
+ private TokenCallable patSupplier;
+
+ /**
+ * <p>Creates a new instance.
+ *
+ * <p>This method expects a <code>keycloak.json</code> in the classpath, otherwise an exception will be thrown.
+ *
+ * @return a new instance
+ * @throws RuntimeException in case there is no <code>keycloak.json</code> file in the classpath or the file could not be parsed
+ */
+ public static AuthzClient create() throws RuntimeException {
InputStream configStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("keycloak.json");
if (configStream == null) {
@@ -56,65 +64,118 @@ public class AuthzClient {
}
}
+ /**
+ * <p>Creates a new instance.
+ *
+ * @param configuration the client configuration
+ * @return a new instance
+ */
public static AuthzClient create(Configuration configuration) {
return new AuthzClient(configuration, configuration.getClientAuthenticator());
}
+ /**
+ * <p>Creates a new instance.
+ *
+ * @param configuration the client configuration
+ * @param authenticator the client authenticator
+ * @return a new instance
+ */
public static AuthzClient create(Configuration configuration, ClientAuthenticator authenticator) {
return new AuthzClient(configuration, authenticator);
}
private final ServerConfiguration serverConfiguration;
- private final Configuration deployment;
-
- private AuthzClient(Configuration configuration, ClientAuthenticator authenticator) {
- if (configuration == null) {
- throw new IllegalArgumentException("Client configuration can not be null.");
- }
-
- String configurationUrl = configuration.getAuthServerUrl();
-
- if (configurationUrl == null) {
- throw new IllegalArgumentException("Configuration URL can not be null.");
- }
-
- configurationUrl += "/realms/" + configuration.getRealm() + "/.well-known/uma-configuration";
-
- this.deployment = configuration;
-
- this.http = new Http(configuration, authenticator != null ? authenticator : configuration.getClientAuthenticator());
-
- try {
- this.serverConfiguration = this.http.<ServerConfiguration>get(URI.create(configurationUrl))
- .response().json(ServerConfiguration.class)
- .execute();
- } catch (Exception e) {
- throw new RuntimeException("Could not obtain configuration from server [" + configurationUrl + "].", e);
- }
-
- this.http.setServerConfiguration(this.serverConfiguration);
+ private final Configuration configuration;
+
+ /**
+ * <p>Creates a {@link ProtectionResource} instance which can be used to access the Protection API.
+ *
+ * <p>When using this method, the PAT (the access token with the uma_protection scope) is obtained for the client
+ * itself, using any of the supported credential types (client/secret, jwt, etc).
+ *
+ * @return a {@link ProtectionResource}
+ */
+ public ProtectionResource protection() {
+ return new ProtectionResource(this.http, this.serverConfiguration, createPatSupplier());
}
- private AuthzClient(Configuration configuration) {
- this(configuration, null);
+ /**
+ * <p>Creates a {@link ProtectionResource} instance which can be used to access the Protection API.
+ *
+ * @param the PAT (the access token with the uma_protection scope)
+ * @return a {@link ProtectionResource}
+ */
+ public ProtectionResource protection(final String accessToken) {
+ return new ProtectionResource(this.http, this.serverConfiguration, new TokenCallable(http, configuration, serverConfiguration) {
+ @Override
+ public String call() {
+ return accessToken;
+ }
+
+ @Override
+ protected boolean isRetry() {
+ return false;
+ }
+ });
}
- public ProtectionResource protection() {
- return new ProtectionResource(this.http, createPatSupplier());
+ /**
+ * <p>Creates a {@link ProtectionResource} instance which can be used to access the Protection API.
+ *
+ * <p>When using this method, the PAT (the access token with the uma_protection scope) is obtained for a given user.
+ *
+ * @return a {@link ProtectionResource}
+ */
+ public ProtectionResource protection(String userName, String password) {
+ return new ProtectionResource(this.http, this.serverConfiguration, createPatSupplier(userName, password));
}
- public AuthorizationResource authorization(String accesstoken) {
- return new AuthorizationResource(this.http, accesstoken);
+ /**
+ * <p>Creates a {@link AuthorizationResource} instance which can be used to obtain permissions from the server.
+ *
+ * @return a {@link AuthorizationResource}
+ */
+ public AuthorizationResource authorization() {
+ return new AuthorizationResource(configuration, serverConfiguration, this.http, null);
}
- public AuthorizationResource authorization(String userName, String password) {
- return new AuthorizationResource(this.http, obtainAccessToken(userName, password).getToken());
+ /**
+ * <p>Creates a {@link AuthorizationResource} instance which can be used to obtain permissions from the server.
+ *
+ * @param accessToken the Access Token that will be used as a bearer to access the token endpoint
+ * @return a {@link AuthorizationResource}
+ */
+ public AuthorizationResource authorization(final String accessToken) {
+ return new AuthorizationResource(configuration, serverConfiguration, this.http, new TokenCallable(http, configuration, serverConfiguration) {
+ @Override
+ public String call() {
+ return accessToken;
+ }
+
+ @Override
+ protected boolean isRetry() {
+ return false;
+ }
+ });
}
- public EntitlementResource entitlement(String eat) {
- return new EntitlementResource(this.http, eat);
+ /**
+ * <p>Creates a {@link AuthorizationResource} instance which can be used to obtain permissions from the server.
+ *
+ * @param userName an ID Token or Access Token representing an identity and/or access context
+ * @param password
+ * @return a {@link AuthorizationResource}
+ */
+ public AuthorizationResource authorization(final String userName, final String password) {
+ return new AuthorizationResource(configuration, serverConfiguration, this.http, createRefreshableAccessTokenSupplier(userName, password));
}
+ /**
+ * Obtains an access token using the client credentials.
+ *
+ * @return an {@link AccessTokenResponse}
+ */
public AccessTokenResponse obtainAccessToken() {
return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint())
.authentication()
@@ -124,6 +185,11 @@ public class AuthzClient {
.execute();
}
+ /**
+ * Obtains an access token using the resource owner credentials.
+ *
+ * @return an {@link AccessTokenResponse}
+ */
public AccessTokenResponse obtainAccessToken(String userName, String password) {
return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint())
.authentication()
@@ -133,47 +199,64 @@ public class AuthzClient {
.execute();
}
+ /**
+ * Returns the configuration obtained from the server at the UMA Discovery Endpoint.
+ *
+ * @return the {@link ServerConfiguration}
+ */
public ServerConfiguration getServerConfiguration() {
return this.serverConfiguration;
}
+ /**
+ * Obtains the client configuration
+ *
+ * @return the {@link Configuration}
+ */
public Configuration getConfiguration() {
- return this.deployment;
+ return this.configuration;
}
- private Callable<String> createPatSupplier() {
+ private AuthzClient(Configuration configuration, ClientAuthenticator authenticator) {
+ if (configuration == null) {
+ throw new IllegalArgumentException("Client configuration can not be null.");
+ }
+
+ String configurationUrl = configuration.getAuthServerUrl();
+
+ if (configurationUrl == null) {
+ throw new IllegalArgumentException("Configuration URL can not be null.");
+ }
+
+ configurationUrl += "/realms/" + configuration.getRealm() + "/.well-known/uma2-configuration";
+
+ this.configuration = configuration;
+
+ this.http = new Http(configuration, authenticator != null ? authenticator : configuration.getClientAuthenticator());
+
+ try {
+ this.serverConfiguration = this.http.<ServerConfiguration>get(configurationUrl)
+ .response().json(ServerConfiguration.class)
+ .execute();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not obtain configuration from server [" + configurationUrl + "].", e);
+ }
+
+ this.http.setServerConfiguration(this.serverConfiguration);
+ }
+
+ private TokenCallable createPatSupplier(String userName, String password) {
if (patSupplier == null) {
- patSupplier = new Callable<String>() {
- AccessTokenResponse clientToken = obtainAccessToken();
-
- @Override
- public String call() {
- String token = clientToken.getToken();
-
- try {
- AccessToken accessToken = JsonSerialization.readValue(new JWSInput(token).getContent(), AccessToken.class);
-
- if (accessToken.isActive()) {
- return token;
- }
-
- clientToken = http.<AccessTokenResponse>post(serverConfiguration.getTokenEndpoint())
- .authentication().client()
- .form()
- .param("grant_type", "refresh_token")
- .param("refresh_token", clientToken.getRefreshToken())
- .response()
- .json(AccessTokenResponse.class)
- .execute();
- } catch (Exception e) {
- patSupplier = null;
- throw new RuntimeException(e);
- }
-
- return clientToken.getToken();
- }
- };
+ patSupplier = createRefreshableAccessTokenSupplier(userName, password);
}
return patSupplier;
}
-}
+
+ private TokenCallable createPatSupplier() {
+ return createPatSupplier(null, null);
+ }
+
+ private TokenCallable createRefreshableAccessTokenSupplier(final String userName, final String password) {
+ return new TokenCallable(userName, password, http, configuration, serverConfiguration);
+ }
+}
\ No newline at end of file
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
index 076c2db..d9077e5 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
@@ -17,11 +17,12 @@
*/
package org.keycloak.authorization.client;
-import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface ClientAuthenticator {
- void configureClientCredentials(HashMap<String, String> requestParams, HashMap<String, String> requestHeaders);
+ void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders);
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
index 647891f..7e6802e 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
@@ -17,14 +17,14 @@
*/
package org.keycloak.authorization.client;
-import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.BasicAuthHelper;
-import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -34,10 +34,22 @@ public class Configuration extends AdapterConfig {
@JsonIgnore
private HttpClient httpClient;
+ @JsonIgnore
+ private ClientAuthenticator clientAuthenticator = createDefaultClientAuthenticator();
+
public Configuration() {
}
+ /**
+ * Creates a new instance.
+ *
+ * @param authServerUrl the server's URL. E.g.: http://{server}:{port}/auth.(not {@code null})
+ * @param realm the realm name (not {@code null})
+ * @param clientId the client id (not {@code null})
+ * @param clientCredentials a map with the client credentials (not {@code null})
+ * @param httpClient the {@link HttpClient} instance that should be used when sending requests to the server, or {@code null} if a default instance should be created
+ */
public Configuration(String authServerUrl, String realm, String clientId, Map<String, Object> clientCredentials, HttpClient httpClient) {
this.authServerUrl = authServerUrl;
setAuthServerUrl(authServerUrl);
@@ -47,29 +59,34 @@ public class Configuration extends AdapterConfig {
this.httpClient = httpClient;
}
- @JsonIgnore
- private ClientAuthenticator clientAuthenticator = new ClientAuthenticator() {
- @Override
- public void configureClientCredentials(HashMap<String, String> requestParams, HashMap<String, String> requestHeaders) {
- String secret = (String) getCredentials().get("secret");
-
- if (secret == null) {
- throw new RuntimeException("Client secret not provided.");
- }
-
- requestHeaders.put("Authorization", BasicAuthHelper.createHeader(getResource(), secret));
- }
- };
-
public HttpClient getHttpClient() {
if (this.httpClient == null) {
this.httpClient = HttpClients.createDefault();
}
-
return httpClient;
}
- public ClientAuthenticator getClientAuthenticator() {
+ ClientAuthenticator getClientAuthenticator() {
return this.clientAuthenticator;
}
+
+ /**
+ * Creates a default client authenticator which uses HTTP BASIC and client id and secret to authenticate the client.
+ *
+ * @return the default client authenticator
+ */
+ private ClientAuthenticator createDefaultClientAuthenticator() {
+ return new ClientAuthenticator() {
+ @Override
+ public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
+ String secret = (String) getCredentials().get("secret");
+
+ if (secret == null) {
+ throw new RuntimeException("Client secret not provided.");
+ }
+
+ requestHeaders.put("Authorization", BasicAuthHelper.createHeader(getResource(), secret));
+ }
+ };
+ }
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
index 65ec87c..c2e8f8a 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
@@ -17,14 +17,14 @@
*/
package org.keycloak.authorization.client.representation;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
/**
* <p>One or more resources that the resource server manages as a set of protected resources.
*
@@ -38,13 +38,17 @@ public class ResourceRepresentation {
private String id;
private String name;
+ private String displayName;
private String uri;
private String type;
+
+ @JsonProperty("resource_scopes")
private Set<ScopeRepresentation> scopes;
@JsonProperty("icon_uri")
private String iconUri;
private String owner;
+ private Boolean ownerManagedAccess;
/**
* Creates a new instance.
@@ -106,6 +110,10 @@ public class ResourceRepresentation {
return this.name;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
public String getUri() {
return this.uri;
}
@@ -129,6 +137,10 @@ public class ResourceRepresentation {
this.name = name;
}
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
public void setUri(String uri) {
this.uri = uri;
}
@@ -153,6 +165,14 @@ public class ResourceRepresentation {
this.owner = owner;
}
+ public void setOwnerManagedAccess(Boolean ownerManagedAccess) {
+ this.ownerManagedAccess = ownerManagedAccess;
+ }
+
+ public Boolean getOwnerManagedAccess() {
+ return ownerManagedAccess;
+ }
+
public void addScope(ScopeRepresentation scopeRepresentation) {
if (this.scopes == null) {
this.scopes = new HashSet<>();
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java
index 6716165..f708e52 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java
@@ -1,13 +1,12 @@
/*
- * JBoss, Home of Professional Open Source
- *
- * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ * Copyright 2018 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
+ * 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,
@@ -17,219 +16,194 @@
*/
package org.keycloak.authorization.client.representation;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
-import java.net.URI;
-import java.util.Set;
+import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ServerConfiguration {
- private String version;
- private URI issuer;
+ @JsonProperty("issuer")
+ private String issuer;
- @JsonProperty("pat_profiles_supported")
- private Set<String> patProfiles;
+ @JsonProperty("authorization_endpoint")
+ private String authorizationEndpoint;
- @JsonProperty("pat_grant_types_supported")
- private Set<String> patGrantTypes;
+ @JsonProperty("token_endpoint")
+ private String tokenEndpoint;
- @JsonProperty("aat_profiles_supported")
- private Set<String> aatProfiles;
+ @JsonProperty("token_introspection_endpoint")
+ private String tokenIntrospectionEndpoint;
- @JsonProperty("aat_grant_types_supported")
- private Set<String> aatGrantTypes;
+ @JsonProperty("userinfo_endpoint")
+ private String userinfoEndpoint;
- @JsonProperty("rpt_profiles_supported")
- private Set<String> rptProfiles;
+ @JsonProperty("end_session_endpoint")
+ private String logoutEndpoint;
- @JsonProperty("claim_token_profiles_supported")
- private Set<String> claimTokenProfiles;
+ @JsonProperty("jwks_uri")
+ private String jwksUri;
- @JsonProperty("dynamic_client_endpoint")
- private URI dynamicClientEndpoint;
+ @JsonProperty("check_session_iframe")
+ private String checkSessionIframe;
- @JsonProperty("token_endpoint")
- private URI tokenEndpoint;
+ @JsonProperty("grant_types_supported")
+ private List<String> grantTypesSupported;
- @JsonProperty("authorization_endpoint")
- private URI authorizationEndpoint;
+ @JsonProperty("response_types_supported")
+ private List<String> responseTypesSupported;
- @JsonProperty("requesting_party_claims_endpoint")
- private URI requestingPartyClaimsEndpoint;
+ @JsonProperty("subject_types_supported")
+ private List<String> subjectTypesSupported;
- @JsonProperty("resource_set_registration_endpoint")
- private URI resourceSetRegistrationEndpoint;
+ @JsonProperty("id_token_signing_alg_values_supported")
+ private List<String> idTokenSigningAlgValuesSupported;
- @JsonProperty("introspection_endpoint")
- private URI introspectionEndpoint;
+ @JsonProperty("userinfo_signing_alg_values_supported")
+ private List<String> userInfoSigningAlgValuesSupported;
- @JsonProperty("permission_registration_endpoint")
- private URI permissionRegistrationEndpoint;
+ @JsonProperty("request_object_signing_alg_values_supported")
+ private List<String> requestObjectSigningAlgValuesSupported;
- @JsonProperty("rpt_endpoint")
- private URI rptEndpoint;
+ @JsonProperty("response_modes_supported")
+ private List<String> responseModesSupported;
- /**
- * Non-standard, Keycloak specific configuration options
- */
- private String realm;
+ @JsonProperty("registration_endpoint")
+ private String registrationEndpoint;
- private String realmPublicKey;
+ @JsonProperty("token_endpoint_auth_methods_supported")
+ private List<String> tokenEndpointAuthMethodsSupported;
- private URI serverUrl;
+ @JsonProperty("token_endpoint_auth_signing_alg_values_supported")
+ private List<String> tokenEndpointAuthSigningAlgValuesSupported;
- public String getVersion() {
- return this.version;
- }
+ @JsonProperty("claims_supported")
+ private List<String> claimsSupported;
- void setVersion(final String version) {
- this.version = version;
- }
+ @JsonProperty("claim_types_supported")
+ private List<String> claimTypesSupported;
- public URI getIssuer() {
- return this.issuer;
- }
+ @JsonProperty("claims_parameter_supported")
+ private Boolean claimsParameterSupported;
- void setIssuer(final URI issuer) {
- this.issuer = issuer;
- }
+ @JsonProperty("scopes_supported")
+ private List<String> scopesSupported;
- public Set<String> getPatProfiles() {
- return this.patProfiles;
- }
+ @JsonProperty("request_parameter_supported")
+ private Boolean requestParameterSupported;
- void setPatProfiles(final Set<String> patProfiles) {
- this.patProfiles = patProfiles;
- }
-
- public Set<String> getPatGrantTypes() {
- return this.patGrantTypes;
- }
+ @JsonProperty("request_uri_parameter_supported")
+ private Boolean requestUriParameterSupported;
- void setPatGrantTypes(final Set<String> patGrantTypes) {
- this.patGrantTypes = patGrantTypes;
- }
-
- public Set<String> getAatProfiles() {
- return this.aatProfiles;
- }
+ @JsonProperty("resource_registration_endpoint")
+ private String resourceRegistrationEndpoint;
- void setAatProfiles(final Set<String> aatProfiles) {
- this.aatProfiles = aatProfiles;
- }
-
- public Set<String> getAatGrantTypes() {
- return this.aatGrantTypes;
- }
-
- void setAatGrantTypes(final Set<String> aatGrantTypes) {
- this.aatGrantTypes = aatGrantTypes;
- }
+ @JsonProperty("permission_endpoint")
+ private String permissionEndpoint;
- public Set<String> getRptProfiles() {
- return this.rptProfiles;
+ public String getIssuer() {
+ return issuer;
}
- void setRptProfiles(final Set<String> rptProfiles) {
- this.rptProfiles = rptProfiles;
+ public String getAuthorizationEndpoint() {
+ return authorizationEndpoint;
}
- public Set<String> getClaimTokenProfiles() {
- return this.claimTokenProfiles;
+ public String getTokenEndpoint() {
+ return tokenEndpoint;
}
- void setClaimTokenProfiles(final Set<String> claimTokenProfiles) {
- this.claimTokenProfiles = claimTokenProfiles;
+ public String getTokenIntrospectionEndpoint() {
+ return tokenIntrospectionEndpoint;
}
- public URI getDynamicClientEndpoint() {
- return this.dynamicClientEndpoint;
+ public String getUserinfoEndpoint() {
+ return userinfoEndpoint;
}
- void setDynamicClientEndpoint(final URI dynamicClientEndpoint) {
- this.dynamicClientEndpoint = dynamicClientEndpoint;
+ public String getLogoutEndpoint() {
+ return logoutEndpoint;
}
- public URI getTokenEndpoint() {
- return this.tokenEndpoint;
+ public String getJwksUri() {
+ return jwksUri;
}
- void setTokenEndpoint(final URI tokenEndpoint) {
- this.tokenEndpoint = tokenEndpoint;
+ public String getCheckSessionIframe() {
+ return checkSessionIframe;
}
- public URI getAuthorizationEndpoint() {
- return this.authorizationEndpoint;
+ public List<String> getGrantTypesSupported() {
+ return grantTypesSupported;
}
- void setAuthorizationEndpoint(final URI authorizationEndpoint) {
- this.authorizationEndpoint = authorizationEndpoint;
+ public List<String> getResponseTypesSupported() {
+ return responseTypesSupported;
}
- public URI getRequestingPartyClaimsEndpoint() {
- return this.requestingPartyClaimsEndpoint;
+ public List<String> getSubjectTypesSupported() {
+ return subjectTypesSupported;
}
- void setRequestingPartyClaimsEndpoint(final URI requestingPartyClaimsEndpoint) {
- this.requestingPartyClaimsEndpoint = requestingPartyClaimsEndpoint;
+ public List<String> getIdTokenSigningAlgValuesSupported() {
+ return idTokenSigningAlgValuesSupported;
}
- public URI getResourceSetRegistrationEndpoint() {
- return this.resourceSetRegistrationEndpoint;
+ public List<String> getUserInfoSigningAlgValuesSupported() {
+ return userInfoSigningAlgValuesSupported;
}
- void setResourceSetRegistrationEndpoint(final URI resourceSetRegistrationEndpoint) {
- this.resourceSetRegistrationEndpoint = resourceSetRegistrationEndpoint;
+ public List<String> getRequestObjectSigningAlgValuesSupported() {
+ return requestObjectSigningAlgValuesSupported;
}
- public URI getIntrospectionEndpoint() {
- return this.introspectionEndpoint;
+ public List<String> getResponseModesSupported() {
+ return responseModesSupported;
}
- void setIntrospectionEndpoint(final URI introspectionEndpoint) {
- this.introspectionEndpoint = introspectionEndpoint;
+ public String getRegistrationEndpoint() {
+ return registrationEndpoint;
}
- public URI getPermissionRegistrationEndpoint() {
- return this.permissionRegistrationEndpoint;
+ public List<String> getTokenEndpointAuthMethodsSupported() {
+ return tokenEndpointAuthMethodsSupported;
}
- void setPermissionRegistrationEndpoint(final URI permissionRegistrationEndpoint) {
- this.permissionRegistrationEndpoint = permissionRegistrationEndpoint;
+ public List<String> getTokenEndpointAuthSigningAlgValuesSupported() {
+ return tokenEndpointAuthSigningAlgValuesSupported;
}
- public URI getRptEndpoint() {
- return this.rptEndpoint;
+ public List<String> getClaimsSupported() {
+ return claimsSupported;
}
- void setRptEndpoint(final URI rptEndpoint) {
- this.rptEndpoint = rptEndpoint;
+ public List<String> getClaimTypesSupported() {
+ return claimTypesSupported;
}
- public String getRealm() {
- return this.realm;
+ public Boolean getClaimsParameterSupported() {
+ return claimsParameterSupported;
}
- public void setRealm(final String realm) {
- this.realm = realm;
+ public List<String> getScopesSupported() {
+ return scopesSupported;
}
- public String getRealmPublicKey() {
- return this.realmPublicKey;
+ public Boolean getRequestParameterSupported() {
+ return requestParameterSupported;
}
- public void setRealmPublicKey(String realmPublicKey) {
- this.realmPublicKey = realmPublicKey;
+ public Boolean getRequestUriParameterSupported() {
+ return requestUriParameterSupported;
}
- public URI getServerUrl() {
- return this.serverUrl;
+ public String getResourceRegistrationEndpoint() {
+ return resourceRegistrationEndpoint;
}
- public void setServerUrl(URI serverUrl) {
- this.serverUrl = serverUrl;
+ public String getPermissionEndpoint() {
+ return permissionEndpoint;
}
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
index 8fcc6f3..8bd0424 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
@@ -17,12 +17,12 @@
*/
package org.keycloak.authorization.client.representation;
+import java.util.List;
+
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.idm.authorization.Permission;
-import java.util.List;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java
index 0f22ebe..6b30b0d 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java
@@ -18,34 +18,82 @@
package org.keycloak.authorization.client.resource;
-import static org.keycloak.authorization.client.util.Throwables.handleAndWrapException;
+import java.util.concurrent.Callable;
-import org.keycloak.authorization.client.representation.AuthorizationRequest;
-import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
-import org.keycloak.util.JsonSerialization;
+import org.keycloak.authorization.client.util.HttpMethod;
+import org.keycloak.authorization.client.util.Throwables;
+import org.keycloak.authorization.client.util.TokenCallable;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
/**
+ * An entry point for obtaining permissions from the server.
+ *
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class AuthorizationResource {
- private final Http http;
- private final String accessToken;
+ private Configuration configuration;
+ private ServerConfiguration serverConfiguration;
+ private Http http;
+ private TokenCallable token;
- public AuthorizationResource(Http http, String aat) {
+ public AuthorizationResource(Configuration configuration, ServerConfiguration serverConfiguration, Http http, TokenCallable token) {
+ this.configuration = configuration;
+ this.serverConfiguration = serverConfiguration;
this.http = http;
- this.accessToken = aat;
+ this.token = token;
+ }
+
+ /**
+ * Query the server for all permissions.
+ *
+ * @return an {@link AuthorizationResponse} with a RPT holding all granted permissions
+ * @throws AuthorizationDeniedException in case the request was denied by the server
+ */
+ public AuthorizationResponse authorize() throws AuthorizationDeniedException {
+ return authorize(new AuthorizationRequest());
}
- public AuthorizationResponse authorize(AuthorizationRequest request) {
+ /**
+ * Query the server for permissions given an {@link AuthorizationRequest}.
+ *
+ * @param request an {@link AuthorizationRequest} (not {@code null})
+ * @return an {@link AuthorizationResponse} with a RPT holding all granted permissions
+ * @throws AuthorizationDeniedException in case the request was denied by the server
+ */
+ public AuthorizationResponse authorize(final AuthorizationRequest request) throws AuthorizationDeniedException {
+ if (request == null) {
+ throw new IllegalArgumentException("Authorization request must not be null");
+ }
+
+ Callable<AuthorizationResponse> callable = new Callable<AuthorizationResponse>() {
+ @Override
+ public AuthorizationResponse call() throws Exception {
+ request.setAudience(configuration.getResource());
+
+ HttpMethod<AuthorizationResponse> method = http.<AuthorizationResponse>post(serverConfiguration.getTokenEndpoint());
+
+ if (token != null) {
+ method = method.authorizationBearer(token.call());
+ }
+
+ return method
+ .authentication()
+ .uma(request)
+ .response()
+ .json(AuthorizationResponse.class)
+ .execute();
+ }
+ };
try {
- return this.http.<AuthorizationResponse>post("/authz/authorize")
- .authorizationBearer(this.accessToken)
- .json(JsonSerialization.writeValueAsBytes(request))
- .response().json(AuthorizationResponse.class).execute();
+ return callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Failed to obtain authorization data", cause);
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, token, "Failed to obtain authorization data", cause);
}
}
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java
index 785a3a6..b6628a9 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java
@@ -17,36 +17,205 @@
*/
package org.keycloak.authorization.client.resource;
-import static org.keycloak.authorization.client.util.Throwables.handleAndWrapException;
-
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.Callable;
-import org.keycloak.authorization.client.representation.PermissionRequest;
-import org.keycloak.authorization.client.representation.PermissionResponse;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
+import org.keycloak.authorization.client.util.Throwables;
+import org.keycloak.authorization.client.util.TokenCallable;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
+import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
import org.keycloak.util.JsonSerialization;
/**
+ * An entry point for managing permission tickets using the Protection API.
+ *
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PermissionResource {
private final Http http;
- private final Callable<String> pat;
+ private final ServerConfiguration serverConfiguration;
+ private final TokenCallable pat;
- public PermissionResource(Http http, Callable<String> pat) {
+ public PermissionResource(Http http, ServerConfiguration serverConfiguration, TokenCallable pat) {
this.http = http;
+ this.serverConfiguration = serverConfiguration;
this.pat = pat;
}
+ /**
+ * @deprecated use {@link #create(PermissionRequest)}
+ * @param request
+ * @return
+ */
+ @Deprecated
public PermissionResponse forResource(PermissionRequest request) {
+ return create(request);
+ }
+
+ /**
+ * Creates a new permission ticket for a single resource and scope(s).
+ *
+ * @param request the {@link PermissionRequest} representing the resource and scope(s) (not {@code null})
+ * @return a permission response holding a permission ticket with the requested permissions
+ */
+ public PermissionResponse create(PermissionRequest request) {
+ return create(Arrays.asList(request));
+ }
+
+ /**
+ * Creates a new permission ticket for a set of one or more resource and scope(s).
+ *
+ * @param request the {@link PermissionRequest} representing the resource and scope(s) (not {@code null})
+ * @return a permission response holding a permission ticket with the requested permissions
+ */
+ public PermissionResponse create(final List<PermissionRequest> requests) {
+ if (requests == null || requests.isEmpty()) {
+ throw new IllegalArgumentException("Permission request must not be null or empty");
+ }
+ Callable<PermissionResponse> callable = new Callable<PermissionResponse>() {
+ @Override
+ public PermissionResponse call() throws Exception {
+ return http.<PermissionResponse>post(serverConfiguration.getPermissionEndpoint())
+ .authorizationBearer(pat.call())
+ .json(JsonSerialization.writeValueAsBytes(requests))
+ .response().json(PermissionResponse.class).execute();
+ }
+ };
+ try {
+ return callable.call();
+ } catch (Exception cause) {
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error creating permission ticket", cause);
+ }
+ }
+
+ /**
+ * Query the server for any permission ticket associated with the given <code>scopeId</code>.
+ *
+ * @param scopeId the scope id (not {@code null})
+ * @return a list of permission tickets associated with the given <code>scopeId</code>
+ */
+ public List<PermissionTicketRepresentation> findByScope(final String scopeId) {
+ if (scopeId == null) {
+ throw new IllegalArgumentException("Scope id must not be null");
+ }
+ Callable<List<PermissionTicketRepresentation>> callable = new Callable<List<PermissionTicketRepresentation>>() {
+ @Override
+ public List<PermissionTicketRepresentation> call() throws Exception {
+ return http.<List<PermissionTicketRepresentation>>get(serverConfiguration.getPermissionEndpoint())
+ .authorizationBearer(pat.call())
+ .param("scopeId", scopeId)
+ .response().json(new TypeReference<List<PermissionTicketRepresentation>>(){}).execute();
+ }
+ };
+ try {
+ return callable.call();
+ } catch (Exception cause) {
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error querying permission ticket by scope", cause);
+ }
+ }
+
+ /**
+ * Query the server for any permission ticket associated with the given <code>resourceId</code>.
+ *
+ * @param resourceId the resource id (not {@code null})
+ * @return a list of permission tickets associated with the given <code>resourceId</code>
+ */
+ public List<PermissionTicketRepresentation> findByResource(final String resourceId) {
+ if (resourceId == null) {
+ throw new IllegalArgumentException("Resource id must not be null");
+ }
+ Callable<List<PermissionTicketRepresentation>> callable = new Callable<List<PermissionTicketRepresentation>>() {
+ @Override
+ public List<PermissionTicketRepresentation> call() throws Exception {
+ return http.<List<PermissionTicketRepresentation>>get(serverConfiguration.getPermissionEndpoint())
+ .authorizationBearer(pat.call())
+ .param("resourceId", resourceId)
+ .response().json(new TypeReference<List<PermissionTicketRepresentation>>(){}).execute();
+ }
+ };
+ try {
+ return callable.call();
+ } catch (Exception cause) {
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error querying permission ticket by resource", cause);
+ }
+ }
+
+ /**
+ * Query the server for any permission ticket with the matching arguments.
+ *
+ * @param resourceId the resource id or name
+ * @param scopeId the scope id or name
+ * @param owner the owner id or name
+ * @param requester the requester id or name
+ * @param granted if true, only permission tickets marked as granted are returned.
+ * @param returnNames if the response should include names for resource, scope and owner
+ * @param firstResult the position of the first resource to retrieve
+ * @param maxResult the maximum number of resources to retrieve
+ * @return a list of permission tickets with the matching arguments
+ */
+ public List<PermissionTicketRepresentation> find(final String resourceId,
+ final String scopeId,
+ final String owner,
+ final String requester,
+ final Boolean granted,
+ final Boolean returnNames,
+ final Integer firstResult,
+ final Integer maxResult) {
+ Callable<List<PermissionTicketRepresentation>> callable = new Callable<List<PermissionTicketRepresentation>>() {
+ @Override
+ public List<PermissionTicketRepresentation> call() throws Exception {
+ return http.<List<PermissionTicketRepresentation>>get(serverConfiguration.getPermissionEndpoint())
+ .authorizationBearer(pat.call())
+ .param("resourceId", resourceId)
+ .param("scopeId", scopeId)
+ .param("owner", owner)
+ .param("requester", requester)
+ .param("granted", granted == null ? null : granted.toString())
+ .param("returnNames", returnNames == null ? null : returnNames.toString())
+ .param("firstResult", firstResult == null ? null : firstResult.toString())
+ .param("maxResult", maxResult == null ? null : maxResult.toString())
+ .response().json(new TypeReference<List<PermissionTicketRepresentation>>(){}).execute();
+ }
+ };
+ try {
+ return callable.call();
+ } catch (Exception cause) {
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error querying permission ticket", cause);
+ }
+ }
+
+ /**
+ * Updates a permission ticket.
+ *
+ * @param ticket the permission ticket
+ */
+ public void update(final PermissionTicketRepresentation ticket) {
+ if (ticket == null) {
+ throw new IllegalArgumentException("Permission ticket must not be null or empty");
+ }
+ if (ticket.getId() == null) {
+ throw new IllegalArgumentException("Permission ticket must have an id");
+ }
+ Callable callable = new Callable() {
+ @Override
+ public Object call() throws Exception {
+ http.<List>put(serverConfiguration.getPermissionEndpoint())
+ .json(JsonSerialization.writeValueAsBytes(ticket))
+ .authorizationBearer(pat.call())
+ .response().json(List.class).execute();
+ return null;
+ }
+ };
try {
- return this.http.<PermissionResponse>post("/authz/protection/permission")
- .authorizationBearer(this.pat.call())
- .json(JsonSerialization.writeValueAsBytes(request))
- .response().json(PermissionResponse.class).execute();
+ callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Error obtaining permission ticket", cause);
+ Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error updating permission ticket", cause);
}
}
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
index fcf1e43..cc92712 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
@@ -17,88 +17,214 @@
*/
package org.keycloak.authorization.client.resource;
-import static org.keycloak.authorization.client.util.Throwables.handleAndWrapException;
-
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Callable;
-import org.keycloak.authorization.client.representation.RegistrationResponse;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
+import org.keycloak.authorization.client.util.Throwables;
+import org.keycloak.authorization.client.util.TokenCallable;
import org.keycloak.util.JsonSerialization;
/**
+ * An entry point for managing resources using the Protection API.
+ *
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ProtectedResource {
private final Http http;
- private final Callable<String> pat;
+ private ServerConfiguration serverConfiguration;
+ private final TokenCallable pat;
- public ProtectedResource(Http http, Callable<String> pat) {
+ ProtectedResource(Http http, ServerConfiguration serverConfiguration, TokenCallable pat) {
this.http = http;
+ this.serverConfiguration = serverConfiguration;
this.pat = pat;
}
- public RegistrationResponse create(ResourceRepresentation resource) {
+ /**
+ * Creates a new resource.
+ *
+ * @param resource the resource data
+ * @return a {@link RegistrationResponse}
+ */
+ public ResourceRepresentation create(final ResourceRepresentation resource) {
+ Callable<ResourceRepresentation> callable = new Callable<ResourceRepresentation>() {
+ @Override
+ public ResourceRepresentation call() throws Exception {
+ return http.<ResourceRepresentation>post(serverConfiguration.getResourceRegistrationEndpoint())
+ .authorizationBearer(pat.call())
+ .json(JsonSerialization.writeValueAsBytes(resource))
+ .response().json(ResourceRepresentation.class).execute();
+ }
+ };
try {
- return this.http.<RegistrationResponse>post("/authz/protection/resource_set")
- .authorizationBearer(this.pat.call())
- .json(JsonSerialization.writeValueAsBytes(resource))
- .response().json(RegistrationResponse.class).execute();
+ return callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not create resource", cause);
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not create resource", cause);
}
}
- public void update(ResourceRepresentation resource) {
+ /**
+ * Updates a resource.
+ *
+ * @param resource the resource data
+ * @return a {@link RegistrationResponse}
+ */
+ public void update(final ResourceRepresentation resource) {
+ if (resource.getId() == null) {
+ throw new IllegalArgumentException("You must provide the resource id");
+ }
+
+ Callable callable = new Callable() {
+ @Override
+ public Object call() throws Exception {
+ http.<ResourceRepresentation>put(serverConfiguration.getResourceRegistrationEndpoint() + "/" + resource.getId())
+ .authorizationBearer(pat.call())
+ .json(JsonSerialization.writeValueAsBytes(resource)).execute();
+ return null;
+ }
+ };
try {
- this.http.<RegistrationResponse>put("/authz/protection/resource_set/" + resource.getId())
- .authorizationBearer(this.pat.call())
- .json(JsonSerialization.writeValueAsBytes(resource)).execute();
+ callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not update resource", cause);
+ Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not update resource", cause);
}
}
- public RegistrationResponse findById(String id) {
+ /**
+ * Query the server for a resource given its <code>id</code>.
+ *
+ * @param id the resource id
+ * @return a {@link ResourceRepresentation}
+ */
+ public ResourceRepresentation findById(final String id) {
+ Callable<ResourceRepresentation> callable = new Callable<ResourceRepresentation>() {
+ @Override
+ public ResourceRepresentation call() throws Exception {
+ return http.<ResourceRepresentation>get(serverConfiguration.getResourceRegistrationEndpoint() + "/" + id)
+ .authorizationBearer(pat.call())
+ .response().json(ResourceRepresentation.class).execute();
+ }
+ };
try {
- return this.http.<RegistrationResponse>get("/authz/protection/resource_set/" + id)
- .authorizationBearer(this.pat.call())
- .response().json(RegistrationResponse.class).execute();
+ return callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not find resource", cause);
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not find resource", cause);
}
}
- public Set<String> findByFilter(String filter) {
+ /**
+ * Query the server for a resource given its <code>name</code>.
+ *
+ * @param id the resource name
+ * @return a {@link ResourceRepresentation}
+ */
+ public ResourceRepresentation findByName(String name) {
+ String[] representations = find(null, name, null, null, null, null, null, null);
+
+ if (representations.length == 0) {
+ return null;
+ }
+
+ return findById(representations[0]);
+ }
+
+ /**
+ * Query the server for any resource with the matching arguments.
+ *
+ * @param id the resource id
+ * @param name the resource name
+ * @param uri the resource uri
+ * @param owner the resource owner
+ * @param type the resource type
+ * @param scope the resource scope
+ * @param firstResult the position of the first resource to retrieve
+ * @param maxResult the maximum number of resources to retrieve
+ * @return an array of strings with the resource ids
+ */
+ public String[] find(final String id, final String name, final String uri, final String owner, final String type, final String scope, final Integer firstResult, final Integer maxResult) {
+ Callable<String[]> callable = new Callable<String[]>() {
+ @Override
+ public String[] call() throws Exception {
+ return http.<String[]>get(serverConfiguration.getResourceRegistrationEndpoint())
+ .authorizationBearer(pat.call())
+ .param("_id", id)
+ .param("name", name)
+ .param("uri", uri)
+ .param("owner", owner)
+ .param("type", type)
+ .param("scope", scope)
+ .param("deep", Boolean.FALSE.toString())
+ .param("first", firstResult != null ? firstResult.toString() : null)
+ .param("max", maxResult != null ? maxResult.toString() : null)
+ .response().json(String[].class).execute();
+ }
+ };
try {
- return this.http.<Set>get("/authz/protection/resource_set")
- .authorizationBearer(this.pat.call())
- .param("filter", filter)
- .response().json(Set.class).execute();
+ return callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not find resource", cause);
+ return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not find resource", cause);
}
}
- public Set<String> findAll() {
+ /**
+ * Query the server for all resources.
+ *
+ * @return @return an array of strings with the resource ids
+ */
+ public String[] findAll() {
try {
- return this.http.<Set>get("/authz/protection/resource_set")
- .authorizationBearer(this.pat.call())
- .response().json(Set.class).execute();
+ return find(null,null , null, null, null, null, null, null);
} catch (Exception cause) {
- throw handleAndWrapException("Could not find resource", cause);
+ throw Throwables.handleWrapException("Could not find resource", cause);
}
}
- public void delete(String id) {
+ /**
+ * Deletes a resource with the given <code>id</code>.
+ *
+ * @param id the resource id
+ */
+ public void delete(final String id) {
+ Callable callable = new Callable() {
+ @Override
+ public Object call() throws Exception {
+ http.delete(serverConfiguration.getResourceRegistrationEndpoint() + "/" + id)
+ .authorizationBearer(pat.call())
+ .execute();
+ return null;
+ }
+ };
try {
- this.http.delete("/authz/protection/resource_set/" + id)
- .authorizationBearer(this.pat.call())
- .execute();
+ callable.call();
} catch (Exception cause) {
- throw handleAndWrapException("Could not delete resource", cause);
+ Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "", cause);
}
}
+
+ /**
+ * Query the server for all resources with the given uri.
+ *
+ * @param uri the resource uri
+ */
+ public List<ResourceRepresentation> findByUri(String uri) {
+ String[] ids = find(null, null, uri, null, null, null, null, null);
+
+ if (ids.length == 0) {
+ return Collections.emptyList();
+ }
+
+ List<ResourceRepresentation> representations = new ArrayList<>();
+
+ for (String id : ids) {
+ representations.add(findById(id));
+ }
+
+ return representations;
+ }
}
\ No newline at end of file
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java
index 3d2eb2c..7268fe9 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java
@@ -17,38 +17,58 @@
*/
package org.keycloak.authorization.client.resource;
-import java.util.concurrent.Callable;
-
+import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
import org.keycloak.authorization.client.util.Http;
+import org.keycloak.authorization.client.util.TokenCallable;
/**
+ * An entry point to access the Protection API endpoints.
+ *
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ProtectionResource {
- private final Callable<String> pat;
+ private final TokenCallable pat;
private final Http http;
+ private ServerConfiguration serverConfiguration;
- public ProtectionResource(Http http, Callable<String> pat) {
+ public ProtectionResource(Http http, ServerConfiguration serverConfiguration, TokenCallable pat) {
if (pat == null) {
throw new RuntimeException("No access token was provided when creating client for Protection API.");
}
this.http = http;
+ this.serverConfiguration = serverConfiguration;
this.pat = pat;
}
+ /**
+ * Creates a {@link ProtectedResource} which can be used to manage resources.
+ *
+ * @return a {@link ProtectedResource}
+ */
public ProtectedResource resource() {
- return new ProtectedResource(http, pat);
+ return new ProtectedResource(http, serverConfiguration, pat);
}
+ /**
+ * Creates a {@link PermissionResource} which can be used to manage permission tickets.
+ *
+ * @return a {@link PermissionResource}
+ */
public PermissionResource permission() {
- return new PermissionResource(http, pat);
+ return new PermissionResource(http, serverConfiguration, pat);
}
+ /**
+ * Introspects the given <code>rpt</code> using the token introspection endpoint.
+ *
+ * @param rpt the rpt to introspect
+ * @return the {@link TokenIntrospectionResponse}
+ */
public TokenIntrospectionResponse introspectRequestingPartyToken(String rpt) {
- return this.http.<TokenIntrospectionResponse>post("/protocol/openid-connect/token/introspect")
+ return this.http.<TokenIntrospectionResponse>post(serverConfiguration.getTokenIntrospectionEndpoint())
.authentication()
.client()
.form()
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
index f72e6b7..eecb7e3 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
@@ -22,8 +22,6 @@ import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.representation.ServerConfiguration;
-import java.net.URI;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -39,27 +37,19 @@ public class Http {
}
public <R> HttpMethod<R> get(String path) {
- return method(RequestBuilder.get().setUri(this.serverConfiguration.getIssuer() + path));
- }
-
- public <R> HttpMethod<R> get(URI path) {
return method(RequestBuilder.get().setUri(path));
}
- public <R> HttpMethod<R> post(URI path) {
- return method(RequestBuilder.post().setUri(path));
- }
-
public <R> HttpMethod<R> post(String path) {
- return method(RequestBuilder.post().setUri(this.serverConfiguration.getIssuer() + path));
+ return method(RequestBuilder.post().setUri(path));
}
public <R> HttpMethod<R> put(String path) {
- return method(RequestBuilder.put().setUri(this.serverConfiguration.getIssuer() + path));
+ return method(RequestBuilder.put().setUri(path));
}
public <R> HttpMethod<R> delete(String path) {
- return method(RequestBuilder.delete().setUri(this.serverConfiguration.getIssuer() + path));
+ return method(RequestBuilder.delete().setUri(path));
}
private <R> HttpMethod<R> method(RequestBuilder builder) {
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
index 9a7e51a..230b7f8 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
@@ -43,17 +43,17 @@ public class HttpMethod<R> {
private final HttpClient httpClient;
private final ClientAuthenticator authenticator;
- private final RequestBuilder builder;
+ protected final RequestBuilder builder;
protected final Configuration configuration;
- protected final HashMap<String, String> headers;
- protected final HashMap<String, String> params;
+ protected final Map<String, String> headers;
+ protected final Map<String, List<String>> params;
private HttpMethodResponse<R> response;
public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder) {
- this(configuration, authenticator, builder, new HashMap<String, String>(), new HashMap<String, String>());
+ this(configuration, authenticator, builder, new HashMap<String, List<String>>(), new HashMap<String, String>());
}
- public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder, HashMap<String, String> params, HashMap<String, String> headers) {
+ public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder, Map<String, List<String>> params, Map<String, String> headers) {
this.configuration = configuration;
this.httpClient = configuration.getHttpClient();
this.authenticator = authenticator;
@@ -108,8 +108,10 @@ public class HttpMethod<R> {
}
protected void preExecute(RequestBuilder builder) {
- for (Map.Entry<String, String> param : params.entrySet()) {
- builder.addParameter(param.getKey(), param.getValue());
+ for (Map.Entry<String, List<String>> param : params.entrySet()) {
+ for (String value : param.getValue()) {
+ builder.addParameter(param.getKey(), value);
+ }
}
}
@@ -128,7 +130,30 @@ public class HttpMethod<R> {
}
public HttpMethod<R> param(String name, String value) {
- this.params.put(name, value);
+ if (value != null) {
+ List<String> values = params.get(name);
+
+ if (values == null || !values.isEmpty()) {
+ values = new ArrayList<>();
+ params.put(name, values);
+ }
+
+ values.add(value);
+ }
+ return this;
+ }
+
+ public HttpMethod<R> params(String name, String value) {
+ if (value != null) {
+ List<String> values = params.get(name);
+
+ if (values == null) {
+ values = new ArrayList<>();
+ params.put(name, values);
+ }
+
+ values.add(value);
+ }
return this;
}
@@ -145,8 +170,10 @@ public class HttpMethod<R> {
if (params != null) {
List<NameValuePair> formparams = new ArrayList<>();
- for (Map.Entry<String, String> param : params.entrySet()) {
- formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
+ for (Map.Entry<String, List<String>> param : params.entrySet()) {
+ for (String value : param.getValue()) {
+ formparams.add(new BasicNameValuePair(param.getKey(), value));
+ }
}
try {
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
index 8807d39..33674fb 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
@@ -17,8 +17,16 @@
*/
package org.keycloak.authorization.client.util;
+import java.util.Arrays;
+import java.util.Set;
+
+import org.apache.http.Header;
import org.keycloak.OAuth2Constants;
import org.keycloak.authorization.client.ClientAuthenticator;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata;
+import org.keycloak.representations.idm.authorization.PermissionTicketToken;
+import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -34,16 +42,84 @@ public class HttpMethodAuthenticator<R> {
}
public HttpMethod<R> client() {
- this.method.params.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS);
+ this.method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.CLIENT_CREDENTIALS));
authenticator.configureClientCredentials(this.method.params, this.method.headers);
return this.method;
}
public HttpMethod<R> oauth2ResourceOwnerPassword(String userName, String password) {
client();
- this.method.params.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
- this.method.params.put("username", userName);
- this.method.params.put("password", password);
+ this.method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.PASSWORD));
+ this.method.params.put("username", Arrays.asList(userName));
+ this.method.params.put("password", Arrays.asList(password));
return this.method;
}
+
+ public HttpMethod<R> uma() {
+ // if there is an authorization bearer header authenticate using bearer token
+ Header authorizationHeader = method.builder.getFirstHeader("Authorization");
+
+ if (!(authorizationHeader != null && authorizationHeader.getValue().toLowerCase().startsWith("bearer"))) {
+ client();
+ }
+
+ method.params.put(OAuth2Constants.GRANT_TYPE, Arrays.asList(OAuth2Constants.UMA_GRANT_TYPE));
+ return method;
+ }
+
+ public HttpMethod<R> uma(AuthorizationRequest request) {
+ String ticket = request.getTicket();
+ PermissionTicketToken permissions = request.getPermissions();
+
+ if (ticket == null && permissions == null) {
+ throw new IllegalArgumentException("You must either provide a permission ticket or the permissions you want to request.");
+ }
+
+ uma();
+ method.param("ticket", ticket);
+ method.param("claim_token", request.getClaimToken());
+ method.param("claim_token_format", request.getClaimTokenFormat());
+ method.param("pct", request.getPct());
+ method.param("rpt", request.getRpt());
+ method.param("scope", request.getScope());
+ method.param("audience", request.getAudience());
+
+ if (permissions != null) {
+ for (ResourcePermission permission : permissions.getResources()) {
+ String resourceId = permission.getResourceId();
+ Set<String> scopes = permission.getScopes();
+ StringBuilder value = new StringBuilder();
+
+ if (resourceId != null) {
+ value.append(resourceId);
+ }
+
+ if (scopes != null && !scopes.isEmpty()) {
+ value.append("#");
+ for (String scope : scopes) {
+ if (!value.toString().endsWith("#")) {
+ value.append(",");
+ }
+ value.append(scope);
+ }
+ }
+
+ method.params("permission", value.toString());
+ }
+ }
+
+ Metadata metadata = request.getMetadata();
+
+ if (metadata != null) {
+ if (metadata.getIncludeResourceName() != null) {
+ method.param("response_include_resource_name", metadata.getIncludeResourceName().toString());
+ }
+
+ if (metadata.getLimit() != null) {
+ method.param("response_permissions_limit", metadata.getLimit().toString());
+ }
+ }
+
+ return method;
+ }
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
index fceca19..7cfba8e 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
@@ -17,10 +17,12 @@
*/
package org.keycloak.authorization.client.util;
-import org.keycloak.util.JsonSerialization;
-
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.keycloak.util.JsonSerialization;
+
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -58,4 +60,22 @@ public class HttpMethodResponse<R> {
}
};
}
+
+ public HttpMethodResponse<R> json(final TypeReference responseType) {
+ return new HttpMethodResponse<R>(this.method) {
+ @Override
+ public R execute() {
+ return method.execute(new HttpResponseProcessor<R>() {
+ @Override
+ public R process(byte[] entity) {
+ try {
+ return (R) JsonSerialization.readValue(new ByteArrayInputStream(entity), responseType);
+ } catch (IOException e) {
+ throw new RuntimeException("Error parsing JSON response.", e);
+ }
+ }
+ });
+ }
+ };
+ }
}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java
index d51b27e..ae2eaf1 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/Throwables.java
@@ -16,7 +16,10 @@
*/
package org.keycloak.authorization.client.util;
+import java.util.concurrent.Callable;
+
import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -24,19 +27,68 @@ import org.keycloak.authorization.client.AuthorizationDeniedException;
public final class Throwables {
/**
- * Handles an {@code exception} and wraps it into a {@link RuntimeException}. The resulting exception contains
- * more details in case the given {@code exception} is of a {@link HttpResponseException}.
+ * Handles an {@code cause} and wraps it into a {@link RuntimeException}. The resulting cause contains
+ * more details in case the given {@code cause} is of a {@link HttpResponseException}.
+ *
+ *
+ * @param callable
+ * @param pat
+ * @param message the message
+ * @param cause the root cause
+ * @return a {@link RuntimeException} wrapping the given {@code cause}
+ */
+ public static RuntimeException handleWrapException(String message, Throwable cause) {
+ if (cause instanceof HttpResponseException) {
+ throw handleAndWrapHttpResponseException(message, HttpResponseException.class.cast(cause));
+ }
+
+ return new RuntimeException(message, cause);
+ }
+
+ /**
+ * <p>Retries the given {@code callable} after obtaining a fresh {@code token} from the server. If the attempt to retry fails
+ * the exception is handled as defined by {@link #handleWrapException(String, Throwable)}.
*
+ * <p>A retry is only attempted in case the {@code cause} is a {@link HttpResponseException} with a 403 status code. In some cases the
+ * session associated with the token is no longer valid and a new token must be issues.
+ *
+ * @param callable the callable to retry
+ * @param token the token
* @param message the message
- * @param exception the root exception
- * @return a {@link RuntimeException} wrapping the given {@code exception}
+ * @param cause the cause
+ * @param <V> the result of the callable
+ * @return the result of the callable
+ * @throws RuntimeException in case the attempt to retry fails
*/
- public static RuntimeException handleAndWrapException(String message, Exception exception) {
- if (exception instanceof HttpResponseException) {
- throw handleAndWrapHttpResponseException(message, HttpResponseException.class.cast(exception));
+ public static <V> V retryAndWrapExceptionIfNecessary(Callable<V> callable, TokenCallable token, String message, Throwable cause) throws RuntimeException {
+ if (token == null || !token.isRetry()) {
+ throw handleWrapException(message, cause);
+ }
+
+ if (cause instanceof HttpResponseException) {
+ HttpResponseException httpe = HttpResponseException.class.cast(cause);
+
+ if (httpe.getStatusCode() == 403) {
+ TokenIntrospectionResponse response = token.getHttp().<TokenIntrospectionResponse>post(token.getServerConfiguration().getTokenIntrospectionEndpoint())
+ .authentication()
+ .client()
+ .param("token", token.call())
+ .response().json(TokenIntrospectionResponse.class).execute();
+
+ if (!response.getActive()) {
+ token.clearToken();
+ try {
+ return callable.call();
+ } catch (Exception e) {
+ throw handleWrapException(message, e);
+ }
+ }
+
+ throw handleWrapException(message, cause);
+ }
}
- return new RuntimeException(message, exception);
+ throw new RuntimeException(message, cause);
}
private static RuntimeException handleAndWrapHttpResponseException(String message, HttpResponseException exception) {
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java
new file mode 100644
index 0000000..b1c3280
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2018 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.
+ */
+package org.keycloak.authorization.client.util;
+
+import java.util.concurrent.Callable;
+
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.ServerConfiguration;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.util.JsonSerialization;
+
+public class TokenCallable implements Callable<String> {
+
+ private final String userName;
+ private final String password;
+ private final Http http;
+ private final Configuration configuration;
+ private final ServerConfiguration serverConfiguration;
+ private AccessTokenResponse clientToken;
+
+ public TokenCallable(String userName, String password, Http http, Configuration configuration, ServerConfiguration serverConfiguration) {
+ this.userName = userName;
+ this.password = password;
+ this.http = http;
+ this.configuration = configuration;
+ this.serverConfiguration = serverConfiguration;
+ }
+
+ public TokenCallable(Http http, Configuration configuration, ServerConfiguration serverConfiguration) {
+ this(null, null, http, configuration, serverConfiguration);
+ }
+
+ @Override
+ public String call() {
+ if (clientToken == null) {
+ if (userName == null || password == null) {
+ clientToken = obtainAccessToken();
+ } else {
+ clientToken = obtainAccessToken(userName, password);
+ }
+ }
+
+ String token = clientToken.getToken();
+
+ try {
+ AccessToken accessToken = JsonSerialization.readValue(new JWSInput(token).getContent(), AccessToken.class);
+
+ if (accessToken.isActive()) {
+ return token;
+ }
+
+ clientToken = http.<AccessTokenResponse>post(serverConfiguration.getTokenEndpoint())
+ .authentication().client()
+ .form()
+ .param("grant_type", "refresh_token")
+ .param("refresh_token", clientToken.getRefreshToken())
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ } catch (Exception e) {
+ clientToken = null;
+ throw new RuntimeException(e);
+ }
+
+ return clientToken.getToken();
+ }
+
+ /**
+ * Obtains an access token using the client credentials.
+ *
+ * @return an {@link AccessTokenResponse}
+ */
+ AccessTokenResponse obtainAccessToken() {
+ return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint())
+ .authentication()
+ .client()
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ }
+
+ /**
+ * Obtains an access token using the resource owner credentials.
+ *
+ * @return an {@link AccessTokenResponse}
+ */
+ AccessTokenResponse obtainAccessToken(String userName, String password) {
+ return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint())
+ .authentication()
+ .oauth2ResourceOwnerPassword(userName, password)
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ }
+
+ Http getHttp() {
+ return http;
+ }
+
+ protected boolean isRetry() {
+ return true;
+ }
+
+ Configuration getConfiguration() {
+ return configuration;
+ }
+
+ ServerConfiguration getServerConfiguration() {
+ return serverConfiguration;
+ }
+
+ void clearToken() {
+ clientToken = null;
+ }
+}
diff --git a/common/src/main/java/org/keycloak/common/util/Time.java b/common/src/main/java/org/keycloak/common/util/Time.java
index 54809d8..e48f217 100644
--- a/common/src/main/java/org/keycloak/common/util/Time.java
+++ b/common/src/main/java/org/keycloak/common/util/Time.java
@@ -52,6 +52,15 @@ public class Time {
}
/**
+ * Returns {@link Date} object, its value set to time
+ * @param time Time in milliseconds since the epoch
+ * @return see description
+ */
+ public static Date toDate(long time) {
+ return new Date(time);
+ }
+
+ /**
* Returns time in milliseconds for a time in seconds. No adjustment is made to the parameter.
* @param time Time in seconds since the epoch
* @return Time in milliseconds
diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java
index e096e7e..0a9b332 100644
--- a/core/src/main/java/org/keycloak/AuthorizationContext.java
+++ b/core/src/main/java/org/keycloak/AuthorizationContext.java
@@ -68,7 +68,7 @@ public class AuthorizationContext {
if (hasResourcePermission(resourceName)) {
for (Permission permission : authorization.getPermissions()) {
for (PathConfig pathHolder : paths.values()) {
- if (pathHolder.getId().equals(permission.getResourceSetId())) {
+ if (pathHolder.getId().equals(permission.getResourceId())) {
if (permission.getScopes().contains(scopeName)) {
return true;
}
@@ -98,7 +98,7 @@ public class AuthorizationContext {
}
for (Permission permission : authorization.getPermissions()) {
- if (permission.getResourceSetName().equals(resourceName) || permission.getResourceSetId().equals(resourceName)) {
+ if (permission.getResourceName().equals(resourceName) || permission.getResourceId().equals(resourceName)) {
return true;
}
}
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 59e0eee..df54112 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -111,6 +111,8 @@ public interface OAuth2Constants {
String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt";
String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token";
+ String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
+
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
index 26dc220..89dadbf 100644
--- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -17,14 +17,13 @@
*/
package org.keycloak.representations.adapters.config;
+import java.util.ArrayList;
+import java.util.List;
+
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -37,26 +36,18 @@ public class PolicyEnforcerConfig {
@JsonProperty("enforcement-mode")
private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
- @JsonProperty("user-managed-access")
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private UmaProtocolConfig userManagedAccess;
-
- @JsonProperty("entitlement")
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private EntitlementProtocolConfig entitlement;
-
@JsonProperty("paths")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<PathConfig> paths = new ArrayList<>();
- @JsonProperty("online-introspection")
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private Boolean onlineIntrospection = Boolean.FALSE;
-
@JsonProperty("on-deny-redirect-to")
@JsonInclude(JsonInclude.Include.NON_NULL)
private String onDenyRedirectTo;
+ @JsonProperty("user-managed-access")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private UserManagedAccessConfig userManagedAccess;
+
public Boolean isCreateResources() {
return this.createResources;
}
@@ -73,26 +64,14 @@ public class PolicyEnforcerConfig {
this.enforcementMode = enforcementMode;
}
- public UmaProtocolConfig getUserManagedAccess() {
+ public UserManagedAccessConfig getUserManagedAccess() {
return this.userManagedAccess;
}
- public EntitlementProtocolConfig getEntitlement() {
- return this.entitlement;
- }
-
- public Boolean isOnlineIntrospection() {
- return onlineIntrospection;
- }
-
public void setCreateResources(Boolean createResources) {
this.createResources = createResources;
}
- public void setOnlineIntrospection(Boolean onlineIntrospection) {
- this.onlineIntrospection = onlineIntrospection;
- }
-
public void setPaths(List<PathConfig> paths) {
this.paths = paths;
}
@@ -101,14 +80,10 @@ public class PolicyEnforcerConfig {
return onDenyRedirectTo;
}
- public void setUserManagedAccess(UmaProtocolConfig userManagedAccess) {
+ public void setUserManagedAccess(UserManagedAccessConfig userManagedAccess) {
this.userManagedAccess = userManagedAccess;
}
- public void setEntitlement(EntitlementProtocolConfig entitlement) {
- this.entitlement = entitlement;
- }
-
public void setOnDenyRedirectTo(String onDenyRedirectTo) {
this.onDenyRedirectTo = onDenyRedirectTo;
}
@@ -259,11 +234,7 @@ public class PolicyEnforcerConfig {
ANY
}
- public static class UmaProtocolConfig {
-
- }
-
- public static class EntitlementProtocolConfig {
+ public static class UserManagedAccessConfig {
}
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
new file mode 100644
index 0000000..764ae02
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2018 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.
+ */
+
+package org.keycloak.representations.idm.authorization;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+import org.keycloak.representations.idm.authorization.PermissionTicketToken.ResourcePermission;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationRequest {
+
+ private String ticket;
+ private String rpt;
+ private String claimToken;
+ private String claimTokenFormat;
+ private String pct;
+ private String scope;
+ private PermissionTicketToken permissions = new PermissionTicketToken();
+ private Metadata metadata;
+ private String audience;
+ private String accessToken;
+ private boolean submitRequest;
+
+ public AuthorizationRequest(String ticket) {
+ this.ticket = ticket;
+ }
+
+ public AuthorizationRequest() {
+ this(null);
+ }
+
+ public String getTicket() {
+ return this.ticket;
+ }
+
+ public void setTicket(String ticket) {
+ this.ticket = ticket;
+ }
+
+ public String getRpt() {
+ return this.rpt;
+ }
+
+ public void setRpt(String rpt) {
+ this.rpt = rpt;
+ }
+
+ public void setClaimToken(String claimToken) {
+ this.claimToken = claimToken;
+ }
+
+ public String getClaimToken() {
+ return claimToken;
+ }
+
+ public void setClaimTokenFormat(String claimTokenFormat) {
+ this.claimTokenFormat = claimTokenFormat;
+ }
+
+ public String getClaimTokenFormat() {
+ return claimTokenFormat;
+ }
+
+ public void setPct(String pct) {
+ this.pct = pct;
+ }
+
+ public String getPct() {
+ return pct;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setPermissions(PermissionTicketToken permissions) {
+ this.permissions = permissions;
+ }
+
+ public PermissionTicketToken getPermissions() {
+ return permissions;
+ }
+
+ public Metadata getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Metadata metadata) {
+ this.metadata = metadata;
+ }
+
+ public void setAudience(String audience) {
+ this.audience = audience;
+ }
+
+ public String getAudience() {
+ return audience;
+ }
+
+ public void setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public void addPermission(String resourceId, List<String> scopes) {
+ addPermission(resourceId, scopes.toArray(new String[scopes.size()]));
+ }
+
+ public void addPermission(String resourceId, String... scopes) {
+ if (permissions == null) {
+ permissions = new PermissionTicketToken(new ArrayList<ResourcePermission>());
+ }
+
+ ResourcePermission permission = null;
+
+ for (ResourcePermission resourcePermission : permissions.getResources()) {
+ if (resourcePermission.getResourceId().equals(resourceId)) {
+ permission = resourcePermission;
+ break;
+ }
+ }
+
+ if (permission == null) {
+ permission = new ResourcePermission(resourceId, new HashSet<String>());
+ permissions.getResources().add(permission);
+ }
+
+ permission.getScopes().addAll(Arrays.asList(scopes));
+ }
+
+ public void setSubmitRequest(boolean submitRequest) {
+ this.submitRequest = submitRequest;
+ }
+
+ public boolean isSubmitRequest() {
+ return submitRequest && ticket != null;
+ }
+
+ public static class Metadata {
+
+ private Boolean includeResourceName;
+ private Integer limit;
+
+ public Boolean getIncludeResourceName() {
+ if (includeResourceName == null) {
+ includeResourceName = Boolean.TRUE;
+ }
+ return includeResourceName;
+ }
+
+ public void setIncludeResourceName(Boolean includeResourceName) {
+ this.includeResourceName = includeResourceName;
+ }
+
+ public Integer getLimit() {
+ return limit;
+ }
+
+ public void setLimit(Integer limit) {
+ this.limit = limit;
+ }
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java
index 74df64f..ed392f0 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java
@@ -28,11 +28,11 @@ import java.util.Set;
*/
public class Permission {
- @JsonProperty("resource_set_id")
- private String resourceSetId;
+ @JsonProperty("rsid")
+ private String resourceId;
- @JsonProperty("resource_set_name")
- private final String resourceSetName;
+ @JsonProperty("rsname")
+ private final String resourceName;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Set<String> scopes;
@@ -44,19 +44,19 @@ public class Permission {
this(null, null, null, null);
}
- public Permission(final String resourceSetId, String resourceSetName, final Set<String> scopes, Map<String, Set<String>> claims) {
- this.resourceSetId = resourceSetId;
- this.resourceSetName = resourceSetName;
+ public Permission(final String resourceId, String resourceName, final Set<String> scopes, Map<String, Set<String>> claims) {
+ this.resourceId = resourceId;
+ this.resourceName = resourceName;
this.scopes = scopes;
this.claims = claims;
}
- public String getResourceSetId() {
- return this.resourceSetId;
+ public String getResourceId() {
+ return this.resourceId;
}
- public String getResourceSetName() {
- return this.resourceSetName;
+ public String getResourceName() {
+ return this.resourceName;
}
public Set<String> getScopes() {
@@ -75,7 +75,7 @@ public class Permission {
public String toString() {
StringBuilder builder = new StringBuilder();
- builder.append("Permission {").append("id=").append(resourceSetId).append(", name=").append(resourceSetName)
+ builder.append("Permission {").append("id=").append(resourceId).append(", name=").append(resourceName)
.append(", scopes=").append(scopes).append("}");
return builder.toString();
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketRepresentation.java
new file mode 100644
index 0000000..2a3e020
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketRepresentation.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.representations.idm.authorization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionTicketRepresentation {
+
+ private String id;
+ private String owner;
+ private String resource;
+ private String scope;
+ private boolean granted;
+ private String scopeName;
+ private String resourceName;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ public boolean isGranted() {
+ return granted;
+ }
+
+ public void setGranted(boolean granted) {
+ this.granted = granted;
+ }
+
+ public void setScopeName(String scopeName) {
+ this.scopeName = scopeName;
+ }
+
+ public String getScopeName() {
+ return scopeName;
+ }
+
+ public void setResourceName(String resourceName) {
+ this.resourceName = resourceName;
+ }
+
+ public String getResourceName() {
+ return resourceName;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java
index c058b9d..4188ab3 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java
@@ -24,6 +24,14 @@ public class ResourceOwnerRepresentation {
private String id;
private String name;
+ public ResourceOwnerRepresentation() {
+
+ }
+
+ public ResourceOwnerRepresentation(String id) {
+ this.id = id;
+ }
+
public String getId() {
return this.id;
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
index acbd2f2..ae876f0 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
@@ -47,10 +47,12 @@ public class ResourceRepresentation {
@JsonProperty("icon_uri")
private String iconUri;
private ResourceOwnerRepresentation owner;
+ private Boolean ownerManagedAccess;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<PolicyRepresentation> policies;
private List<ScopeRepresentation> typedScopes;
+ private String displayName;
/**
* Creates a new instance.
@@ -121,6 +123,10 @@ public class ResourceRepresentation {
return this.name;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
public String getUri() {
return this.uri;
}
@@ -145,6 +151,10 @@ public class ResourceRepresentation {
this.name = name;
}
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
public void setUri(String uri) {
this.uri = uri;
}
@@ -169,6 +179,14 @@ public class ResourceRepresentation {
this.owner = owner;
}
+ public Boolean getOwnerManagedAccess() {
+ return ownerManagedAccess;
+ }
+
+ public void setOwnerManagedAccess(Boolean ownerManagedAccess) {
+ this.ownerManagedAccess = ownerManagedAccess;
+ }
+
public void setTypedScopes(List<ScopeRepresentation> typedScopes) {
this.typedScopes = typedScopes;
}
@@ -177,6 +195,15 @@ public class ResourceRepresentation {
return typedScopes;
}
+ public void addScope(String... scopeNames) {
+ if (scopes == null) {
+ scopes = new HashSet<>();
+ }
+ for (String scopeName : scopeNames) {
+ scopes.add(new ScopeRepresentation(scopeName));
+ }
+ }
+
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ScopeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ScopeRepresentation.java
index 3a1f252..e6445e4 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/ScopeRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ScopeRepresentation.java
@@ -35,6 +35,7 @@ public class ScopeRepresentation {
private String iconUri;
private List<PolicyRepresentation> policies;
private List<ResourceRepresentation> resources;
+ private String displayName;
/**
* Creates an instance.
@@ -67,6 +68,10 @@ public class ScopeRepresentation {
return this.name;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
public String getIconUri() {
return this.iconUri;
}
@@ -83,6 +88,10 @@ public class ScopeRepresentation {
this.name = name;
}
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
public void setIconUri(String iconUri) {
this.iconUri = iconUri;
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index cd52d7c..1c039d3 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -145,6 +145,8 @@ public class RealmRepresentation {
protected String keycloakVersion;
+ protected Boolean userManagedAccessAllowed;
+
@Deprecated
protected Boolean social;
@Deprecated
@@ -964,4 +966,12 @@ public class RealmRepresentation {
public void setFederatedUsers(List<UserRepresentation> federatedUsers) {
this.federatedUsers = federatedUsers;
}
+
+ public void setUserManagedAccessAllowed(Boolean userManagedAccessAllowed) {
+ this.userManagedAccessAllowed = userManagedAccessAllowed;
+ }
+
+ public Boolean isUserManagedAccessAllowed() {
+ return userManagedAccessAllowed;
+ }
}
diff --git a/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java b/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
index ea37d60..4c4573b 100644
--- a/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
+++ b/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
@@ -18,18 +18,14 @@
package org.keycloak.authz.helloworld;
import org.keycloak.authorization.client.AuthzClient;
-import org.keycloak.authorization.client.representation.EntitlementRequest;
-import org.keycloak.authorization.client.representation.EntitlementResponse;
-import org.keycloak.authorization.client.representation.PermissionRequest;
-import org.keycloak.authorization.client.representation.RegistrationResponse;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.representation.ScopeRepresentation;
import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
import org.keycloak.authorization.client.resource.ProtectedResource;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
-import java.util.Set;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -47,28 +43,10 @@ public class AuthorizationClientExample {
// create a new instance based on the configuration defined in keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();
- // query the server for a resource with a given name
- Set<String> resourceId = authzClient.protection()
- .resource()
- .findByFilter("name=Default Resource");
-
- // obtain an Entitlement API Token in order to get access to the Entitlement API.
- // this token is just an access token issued to a client on behalf of an user
- // with a scope = kc_entitlement
- String eat = getEntitlementAPIToken(authzClient);
-
- // create an entitlement request
- EntitlementRequest request = new EntitlementRequest();
- PermissionRequest permission = new PermissionRequest();
-
- permission.setResourceSetId(resourceId.iterator().next());
-
- request.addPermission(permission);
-
- // send the entitlement request to the server in order to
+ // send the authorization request to the server in order to
// obtain an RPT with all permissions granted to the user
- EntitlementResponse response = authzClient.entitlement(eat).get("hello-world-authz-service", request);
- String rpt = response.getRpt();
+ AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize();
+ String rpt = response.getToken();
TokenIntrospectionResponse requestingPartyToken = authzClient.protection().introspectRequestingPartyToken(rpt);
@@ -78,7 +56,6 @@ public class AuthorizationClientExample {
for (Permission granted : requestingPartyToken.getPermissions()) {
System.out.println(granted);
}
-
}
private static void createResource() {
@@ -94,18 +71,18 @@ public class AuthorizationClientExample {
newResource.addScope(new ScopeRepresentation("urn:hello-world-authz:scopes:view"));
ProtectedResource resourceClient = authzClient.protection().resource();
- Set<String> existingResource = resourceClient.findByFilter("name=" + newResource.getName());
+ ResourceRepresentation existingResource = resourceClient.findByName(newResource.getName());
- if (!existingResource.isEmpty()) {
- resourceClient.delete(existingResource.iterator().next());
+ if (existingResource != null) {
+ resourceClient.delete(existingResource.getId());
}
// create the resource on the server
- RegistrationResponse response = resourceClient.create(newResource);
+ ResourceRepresentation response = resourceClient.create(newResource);
String resourceId = response.getId();
// query the resource using its newly generated id
- ResourceRepresentation resource = resourceClient.findById(resourceId).getResourceDescription();
+ ResourceRepresentation resource = resourceClient.findById(resourceId);
System.out.println(resource);
}
@@ -120,20 +97,20 @@ public class AuthorizationClientExample {
resource.setName("New Resource");
ProtectedResource resourceClient = authzClient.protection().resource();
- Set<String> existingResource = resourceClient.findByFilter("name=" + resource.getName());
+ ResourceRepresentation existingResource = resourceClient.findByName(resource.getName());
- if (existingResource.isEmpty()) {
+ if (existingResource == null) {
createResource();
}
- resource.setId(existingResource.iterator().next());
+ resource.setId(existingResource.getId());
resource.setUri("Changed URI");
// update the resource on the server
resourceClient.update(resource);
// query the resource using its newly generated id
- ResourceRepresentation existing = resourceClient.findById(resource.getId()).getResourceDescription();
+ ResourceRepresentation existing = resourceClient.findById(resource.getId());
System.out.println(existing);
}
@@ -142,23 +119,16 @@ public class AuthorizationClientExample {
// create a new instance based on the configuration define at keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();
- // obtain an Entitlement API Token in order to get access to the Entitlement API.
- // this token is just an access token issued to a client on behalf of an user
- // with a scope = kc_entitlement
- String eat = getEntitlementAPIToken(authzClient);
+ // create an authorization request
+ AuthorizationRequest request = new AuthorizationRequest();
- // create an entitlement request
- EntitlementRequest request = new EntitlementRequest();
- PermissionRequest permission = new PermissionRequest();
+ // add permissions to the request based on the resources and scopes you want to check access
+ request.addPermission("Default Resource");
- permission.setResourceSetName("Default Resource");
-
- request.addPermission(permission);
-
- // send the entitlement request to the server in order to obtain a RPT
- // with all permissions granted to the user
- EntitlementResponse response = authzClient.entitlement(eat).get("hello-world-authz-service", request);
- String rpt = response.getRpt();
+ // send the entitlement request to the server in order to
+ // obtain an RPT with permissions for a single resource
+ AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize(request);
+ String rpt = response.getToken();
System.out.println("You got a RPT: " + rpt);
@@ -169,27 +139,16 @@ public class AuthorizationClientExample {
// create a new instance based on the configuration defined in keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();
- // obtian a Entitlement API Token in order to get access to the Entitlement API.
- // this token is just an access token issued to a client on behalf of an user with a scope kc_entitlement
- String eat = getEntitlementAPIToken(authzClient);
+ // create an authorization request
+ AuthorizationRequest request = new AuthorizationRequest();
- // send the entitlement request to the server in order to obtain a RPT with all permissions granted to the user
- EntitlementResponse response = authzClient.entitlement(eat).getAll("hello-world-authz-service");
- String rpt = response.getRpt();
+ // send the entitlement request to the server in order to
+ // obtain an RPT with all permissions granted to the user
+ AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize(request);
+ String rpt = response.getToken();
System.out.println("You got a RPT: " + rpt);
// now you can use the RPT to access protected resources on the resource server
}
-
- /**
- * Obtain an Entitlement API Token or EAT from the server. Usually, EATs are going to be obtained by clients using a
- * authorization_code grant type. Here we are using resource owner credentials for demonstration purposes.
- *
- * @param authzClient the authorization client instance
- * @return a string representing a EAT
- */
- private static String getEntitlementAPIToken(AuthzClient authzClient) {
- return authzClient.obtainAccessToken("alice", "alice").getToken();
- }
}
diff --git a/examples/authz/hello-world/src/main/resources/keycloak.json b/examples/authz/hello-world/src/main/resources/keycloak.json
index b337389..4f9b0e5 100644
--- a/examples/authz/hello-world/src/main/resources/keycloak.json
+++ b/examples/authz/hello-world/src/main/resources/keycloak.json
@@ -1,6 +1,6 @@
{
"realm": "hello-world-authz",
- "auth-server-url" : "http://localhost:8080/auth",
+ "auth-server-url" : "http://localhost:8180/auth",
"resource" : "hello-world-authz-service",
"credentials": {
"secret": "secret"
diff --git a/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp b/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
index 0aea6b0..c511b2d 100644
--- a/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
+++ b/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
@@ -38,8 +38,8 @@
for (Permission permission : authzContext.getPermissions()) {
%>
<li>
- <p>Resource: <%= permission.getResourceSetName() %></p>
- <p>ID: <%= permission.getResourceSetId() %></p>
+ <p>Resource: <%= permission.getResourceName() %></p>
+ <p>ID: <%= permission.getResourceId() %></p>
</li>
<%
}
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html
index 203b6e2..158d89f 100755
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html
@@ -11,15 +11,15 @@
<script src="lib/angular/angular-route.min.js"></script>
<script src="lib/jwt-decode.min.js"></script>
- <script src="http://localhost:8080/auth/js/keycloak.js"></script>
- <script src="http://localhost:8080/auth/js/keycloak-authz.js"></script>
+ <script src="http://localhost:8180/auth/js/keycloak.js"></script>
+ <script src="http://localhost:8180/auth/js/keycloak-authz.js"></script>
<script src="js/identity.js" type="text/javascript"></script>
<script src="js/app.js" type="text/javascript"></script>
</head>
<body data-ng-controller="TokenCtrl">
-<a href data-ng-click="showRpt()">Show Requesting Party Token </a> | <a href data-ng-click="showAccessToken()">Show Access Token </a> | <a href data-ng-click="requestEntitlements()">Request Entitlements</a> | <a href="" ng-click="Identity.logout()">Sign Out</a>
+<a href data-ng-click="showRpt()">Show Requesting Party Token </a> | <a href data-ng-click="showAccessToken()">Show Access Token </a> | <a href data-ng-click="requestEntitlements()">Request Entitlements</a> | <a href="" ng-click="Identity.account()">My Account</a> | <a href="" ng-click="Identity.logout()">Sign Out</a>
<div id="content-area" class="col-md-9" role="main">
<div id="content" ng-view/>
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js b/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js
index e58c5f5..b552391 100755
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js
@@ -42,6 +42,9 @@ module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Albu
Album.query(function (albums) {
$scope.albums = albums;
});
+ Album.shares(function (albums) {
+ $scope.shares = albums;
+ });
$scope.Identity = Identity;
@@ -50,6 +53,23 @@ module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Albu
$route.reload();
});
}
+
+ $scope.requestDeleteAccess = function (album) {
+ new Album(album).$delete({id: album.id}, function () {
+ // no-op
+ }, function () {
+ document.getElementById("output").innerHTML = 'Sent authorization request to resource owner, please, wait for approval.';
+ });
+ }
+
+ $scope.hasAccess = function (share, scope) {
+ for (i = 0; i < share.scopes.length; i++) {
+ if (share.scopes[i] == scope) {
+ return true;
+ }
+ }
+ return false;
+ }
});
module.controller('TokenCtrl', function ($scope, Identity) {
@@ -98,7 +118,9 @@ module.controller('AdminAlbumCtrl', function ($scope, $http, $route, $location,
});
module.factory('Album', ['$resource', function ($resource) {
- return $resource(apiUrl + '/album/:id');
+ return $resource(apiUrl + '/album/:id', {id: '@id'}, {
+ shares: {url: apiUrl + '/album/shares', method: 'GET', isArray: true}
+ });
}]);
module.factory('Profile', ['$resource', function ($resource) {
@@ -133,11 +155,46 @@ module.factory('authInterceptor', function ($q, $injector, $timeout, Identity) {
}
if (rejection.config.url.indexOf('/authorize') == -1 && retry) {
- var deferred = $q.defer();
-
// here is the authorization logic, which tries to obtain an authorization token from the server in case the resource server
// returns a 403 or 401.
- Identity.authorization.authorize(rejection.headers('WWW-Authenticate')).then(function (rpt) {
+ var wwwAuthenticateHeader = rejection.headers('WWW-Authenticate');
+
+ // when using UMA, a WWW-Authenticate header should be returned by the resource server
+ if (!wwwAuthenticateHeader) {
+ return $q.reject(rejection);
+ }
+
+ // when using UMA, a WWW-Authenticate header should contain UMA data
+ if (wwwAuthenticateHeader.indexOf('UMA') == -1) {
+ return $q.reject(rejection);
+ }
+
+ var deferred = $q.defer();
+
+ var params = wwwAuthenticateHeader.split(',');
+ var ticket;
+
+ // try to extract the permission ticket from the WWW-Authenticate header
+ for (i = 0; i < params.length; i++) {
+ var param = params[i].split('=');
+
+ if (param[0] == 'ticket') {
+ ticket = param[1].substring(1, param[1].length - 1).trim();
+ break;
+ }
+ }
+
+ // a permission ticket must exist in order to send an authorization request
+ if (!ticket) {
+ return $q.reject(rejection);
+ }
+
+ // prepare a authorization request with the permission ticket
+ var authorizationRequest = {};
+ authorizationRequest.ticket = ticket;
+
+ // send the authorization request, if successful retry the request
+ Identity.authorization.authorize(authorizationRequest).then(function (rpt) {
deferred.resolve(rejection);
}, function () {
document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/identity.js b/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/identity.js
index 9a018e4..4088f80 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/identity.js
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/js/identity.js
@@ -34,6 +34,10 @@
keycloak.logout();
};
+ this.account = function () {
+ keycloak.accountManagement();
+ }
+
this.hasRole = function (name) {
if (keycloak && keycloak.hasRealmRole(name)) {
return true;
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json b/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json
index affafdd..d9354e3 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json
@@ -1,6 +1,6 @@
{
"realm": "photoz",
- "auth-server-url" : "http://localhost:8080/auth",
+ "auth-server-url" : "http://localhost:8180/auth",
"ssl-required" : "external",
"resource" : "photoz-html5-client",
"public-client" : true
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html
index 78c252a..fffcdea 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html
@@ -5,18 +5,18 @@
<div data-ng-show="!Identity.isAdmin()">
<a href="#/album/create" id="create-album">Create Album</a> | <a href="#/profile">My Profile</a>
<br/>
-<br/>
+<h3>Your Albums</h3>
<span data-ng-show="albums.length == 0" id="resource-list-empty">You don't have any albums, yet.</span>
<table class="table" data-ng-show="albums.length > 0">
- <thead>
- <tr>
- <th>Your Albums</th>
- </tr>
- </thead>
- <tbody>
- <tr data-ng-repeat="p in albums">
- <td><a id="view-{{p.name}}" href="#/album/{{p.id}}">{{p.name}}</a> - [<a href="#" id="delete-{{p.name}}" ng-click="deleteAlbum(p)">X</a>]</td>
- </tr>
- </tbody>
+ <tr data-ng-repeat="p in albums">
+ <td><a id="view-{{p.name}}" href="#/album/{{p.id}}">{{p.name}}</a> - [<a href="#" id="delete-{{p.name}}" ng-click="deleteAlbum(p)">X</a>]</td>
+ </tr>
+</table>
+<h3>Shared With Me</h3>
+<span data-ng-show="shares.length == 0" id="share-list-empty">You don't have any shares, yet.</span>
+<table class="table" data-ng-show="shares.length > 0">
+ <tr data-ng-repeat="p in shares">
+ <td><a id="view-share-{{p.album.name}}" href="#/album/{{p.album.id}}">{{p.album.name}}</a> - <a href="#" id="delete-share-{{p.album.name}}" data-ng-show="hasAccess(p, 'album:delete')" ng-click="deleteAlbum(p.album)">[X]</a><a href="#" id="request-delete-share-{{p.album.name}}" data-ng-hide="hasAccess(p, 'album:delete')" ng-click="requestDeleteAccess(p.album)">Request Delete Access</a></td>
+ </tr>
</table>
</div>
\ No newline at end of file
examples/authz/photoz/photoz-realm.json 17(+12 -5)
diff --git a/examples/authz/photoz/photoz-realm.json b/examples/authz/photoz/photoz-realm.json
index 118b982..4a15c38 100644
--- a/examples/authz/photoz/photoz-realm.json
+++ b/examples/authz/photoz/photoz-realm.json
@@ -1,6 +1,7 @@
{
"realm": "photoz",
"enabled": true,
+ "userManagedAccessAllowed": "true",
"sslRequired": "external",
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
@@ -26,6 +27,9 @@
"clientRoles": {
"photoz-restful-api": [
"manage-albums"
+ ],
+ "account": [
+ "manage-account"
]
}
},
@@ -47,6 +51,9 @@
"clientRoles": {
"photoz-restful-api": [
"manage-albums"
+ ],
+ "account": [
+ "manage-account"
]
}
},
@@ -100,13 +107,13 @@
{
"clientId": "photoz-html5-client",
"enabled": true,
- "adminUrl": "/photoz-html5-client",
- "baseUrl": "/photoz-html5-client",
+ "adminUrl": "http://localhost:8080/photoz-html5-client",
+ "baseUrl": "http://localhost:8080/photoz-html5-client",
"publicClient": true,
"consentRequired" : true,
"fullScopeAllowed" : true,
"redirectUris": [
- "/photoz-html5-client/*"
+ "http://localhost:8080/photoz-html5-client/*"
],
"webOrigins": ["http://localhost:8080"]
},
@@ -114,10 +121,10 @@
"clientId": "photoz-restful-api",
"secret": "secret",
"enabled": true,
- "baseUrl": "/photoz-restful-api",
+ "baseUrl": "http://localhost:8080/photoz-restful-api",
"authorizationServicesEnabled" : true,
"redirectUris": [
- "/photoz-restful-api/*"
+ "http://localhost:8080/photoz-html5-client"
],
"webOrigins" : ["http://localhost:8080"]
}
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
index 056ff05..b49ba90 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -1,14 +1,11 @@
package org.keycloak.example.photoz.album;
-import org.keycloak.KeycloakSecurityContext;
-import org.keycloak.authorization.client.AuthzClient;
-import org.keycloak.authorization.client.ClientAuthorizationContext;
-import org.keycloak.authorization.client.representation.ResourceRepresentation;
-import org.keycloak.authorization.client.representation.ScopeRepresentation;
-import org.keycloak.authorization.client.resource.ProtectionResource;
-import org.keycloak.example.photoz.ErrorResponse;
-import org.keycloak.example.photoz.entity.Album;
-import org.keycloak.example.photoz.util.Transaction;
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
import javax.inject.Inject;
import javax.persistence.EntityManager;
@@ -24,18 +21,24 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
-import java.security.Principal;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
+
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.ClientAuthorizationContext;
+import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.representation.ScopeRepresentation;
+import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.example.photoz.ErrorResponse;
+import org.keycloak.example.photoz.entity.Album;
+import org.keycloak.example.photoz.util.Transaction;
+import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
@Path("/album")
@Transaction
public class AlbumService {
- public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view";
- public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete";
+ public static final String SCOPE_ALBUM_VIEW = "album:view";
+ public static final String SCOPE_ALBUM_DELETE = "album:delete";
@Inject
private EntityManager entityManager;
@@ -60,9 +63,12 @@ public class AlbumService {
throw new ErrorResponse("Name [" + newAlbum.getName() + "] already taken. Choose another one.", Status.CONFLICT);
}
- this.entityManager.persist(newAlbum);
-
- createProtectedResource(newAlbum);
+ try {
+ this.entityManager.persist(newAlbum);
+ createProtectedResource(newAlbum);
+ } catch (Exception e) {
+ getAuthzClient().protection().resource().delete(newAlbum.getExternalId());
+ }
return Response.ok(newAlbum).build();
}
@@ -89,6 +95,29 @@ public class AlbumService {
}
@GET
+ @Path("/shares")
+ @Produces("application/json")
+ public Response findShares() {
+ List<PermissionTicketRepresentation> permissions = getAuthzClient().protection().permission().find(null, null, null, getKeycloakSecurityContext().getToken().getSubject(), true, true, null, null);
+ Map<String, SharedAlbum> shares = new HashMap<>();
+
+ for (PermissionTicketRepresentation permission : permissions) {
+ SharedAlbum share = shares.get(permission.getResource());
+
+ if (share == null) {
+ share = new SharedAlbum(Album.class.cast(entityManager.createQuery("from Album where externalId = :externalId").setParameter("externalId", permission.getResource()).getSingleResult()));
+ shares.put(permission.getResource(), share);
+ }
+
+ if (permission.getScope() != null) {
+ share.addScope(permission.getScopeName());
+ }
+ }
+
+ return Response.ok(shares.values()).build();
+ }
+
+ @GET
@Path("{id}")
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
@@ -111,8 +140,11 @@ public class AlbumService {
ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album");
albumResource.setOwner(album.getUserId());
+ albumResource.setOwnerManagedAccess(true);
+
+ ResourceRepresentation response = getAuthzClient().protection().resource().create(albumResource);
- getAuthzClient().protection().resource().create(albumResource);
+ album.setExternalId(response.getId());
} catch (Exception e) {
throw new RuntimeException("Could not register protected resource.", e);
}
@@ -123,13 +155,13 @@ public class AlbumService {
try {
ProtectionResource protection = getAuthzClient().protection();
- Set<String> search = protection.resource().findByFilter("uri=" + uri);
+ List<ResourceRepresentation> search = protection.resource().findByUri(uri);
if (search.isEmpty()) {
throw new RuntimeException("Could not find protected resource with URI [" + uri + "]");
}
- protection.resource().delete(search.iterator().next());
+ protection.resource().delete(search.get(0).getId());
} catch (Exception e) {
throw new RuntimeException("Could not search protected resource.", e);
}
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/SharedAlbum.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/SharedAlbum.java
new file mode 100644
index 0000000..dfc5fb1
--- /dev/null
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/SharedAlbum.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.example.photoz.album;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.keycloak.example.photoz.entity.Album;
+
+public class SharedAlbum {
+
+ private Album album;
+ private List<String> scopes;
+
+ public SharedAlbum(Album album) {
+ this.album = album;
+ }
+
+ public Album getAlbum() {
+ return album;
+ }
+
+ public List<String> getScopes() {
+ return scopes;
+ }
+
+ public void addScope(String scope) {
+ if (scopes == null) {
+ scopes = new ArrayList<>();
+ }
+ scopes.add(scope);
+ }
+}
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
index 990595e..d8dda5f 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
@@ -17,6 +17,9 @@
*/
package org.keycloak.example.photoz.entity;
+import java.util.ArrayList;
+import java.util.List;
+
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@@ -43,6 +46,9 @@ public class Album {
@Column(nullable = false)
private String userId;
+ @Column
+ private String externalId;
+
public String getId() {
return this.id;
}
@@ -74,4 +80,12 @@ public class Album {
public String getUserId() {
return this.userId;
}
+
+ public void setExternalId(String externalId) {
+ this.externalId = externalId;
+ }
+
+ public String getExternalId() {
+ return externalId;
+ }
}
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/resources/photoz-restful-api-authz-service.json b/examples/authz/photoz/photoz-restful-api/src/main/resources/photoz-restful-api-authz-service.json
index 28b87bc..d94ce40 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/resources/photoz-restful-api-authz-service.json
+++ b/examples/authz/photoz/photoz-restful-api/src/main/resources/photoz-restful-api-authz-service.json
@@ -3,12 +3,22 @@
"policyEnforcementMode": "ENFORCING",
"resources": [
{
+ "name": "Admin Resources",
+ "uri": "/admin/*",
+ "type": "http://photoz.com/admin",
+ "scopes": [
+ {
+ "name": "admin:manage"
+ }
+ ]
+ },
+ {
"name": "User Profile Resource",
"uri": "/profile",
"type": "http://photoz.com/profile",
"scopes": [
{
- "name": "urn:photoz.com:scopes:profile:view"
+ "name": "profile:view"
}
]
},
@@ -18,29 +28,46 @@
"type": "http://photoz.com/album",
"scopes": [
{
- "name": "urn:photoz.com:scopes:album:view"
- },
- {
- "name": "urn:photoz.com:scopes:album:delete"
+ "name": "album:delete"
},
{
- "name": "urn:photoz.com:scopes:album:create"
- }
- ]
- },
- {
- "name": "Admin Resources",
- "uri": "/admin/*",
- "type": "http://photoz.com/admin",
- "scopes": [
- {
- "name": "urn:photoz.com:scopes:album:admin:manage"
+ "name": "album:view"
}
]
}
],
"policies": [
{
+ "name": "Only Owner and Administrators Policy",
+ "description": "Defines that only the resource owner and administrators can do something",
+ "type": "aggregate",
+ "logic": "POSITIVE",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "applyPolicies": "[\"Administration Policy\",\"Only Owner Policy\"]"
+ }
+ },
+ {
+ "name": "Administration Policy",
+ "description": "Defines that only administrators from a specific network address can do something.",
+ "type": "aggregate",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "applyPolicies": "[\"Any Admin Policy\",\"Only From a Specific Client Address\"]"
+ }
+ },
+ {
+ "name": "Only From @keycloak.org or Admin",
+ "description": "Defines that only users from @keycloak.org",
+ "type": "js",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRealmRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}"
+ }
+ },
+ {
"name": "Only Owner Policy",
"description": "Defines that only the resource owner is allowed to do something",
"type": "rules",
@@ -67,16 +94,6 @@
}
},
{
- "name": "Any User Policy",
- "description": "Defines that only users from well known clients are allowed to access",
- "type": "role",
- "logic": "POSITIVE",
- "decisionStrategy": "UNANIMOUS",
- "config": {
- "roles": "[{\"id\":\"user\"},{\"id\":\"manage-albums\",\"required\":true}]"
- }
- },
- {
"name": "Only From a Specific Client Address",
"description": "Defines that only clients from a specific address can do something",
"type": "js",
@@ -87,45 +104,13 @@
}
},
{
- "name": "Administration Policy",
- "description": "Defines that only administrators from a specific network address can do something.",
- "type": "aggregate",
- "logic": "POSITIVE",
- "decisionStrategy": "UNANIMOUS",
- "config": {
- "applyPolicies": "[\"Only From a Specific Client Address\",\"Any Admin Policy\"]"
- }
- },
- {
- "name": "Only Owner and Administrators Policy",
- "description": "Defines that only the resource owner and administrators can do something",
- "type": "aggregate",
- "logic": "POSITIVE",
- "decisionStrategy": "AFFIRMATIVE",
- "config": {
- "applyPolicies": "[\"Administration Policy\",\"Only Owner Policy\"]"
- }
- },
- {
- "name": "Only From @keycloak.org or Admin",
- "description": "Defines that only users from @keycloak.org",
- "type": "js",
+ "name": "Any User Policy",
+ "description": "Defines that only users from well known clients are allowed to access",
+ "type": "role",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRealmRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}"
- }
- },
- {
- "name": "Album Resource Permission",
- "description": "General policies that apply to all album resources.",
- "type": "resource",
- "logic": "POSITIVE",
- "decisionStrategy": "AFFIRMATIVE",
- "config": {
- "defaultResourceType": "http://photoz.com/album",
- "default": "true",
- "applyPolicies": "[\"Any User Policy\",\"Administration Policy\"]"
+ "roles": "[{\"id\":\"user\",\"required\":false},{\"id\":\"photoz-restful-api/manage-albums\",\"required\":true}]"
}
},
{
@@ -136,48 +121,32 @@
"decisionStrategy": "UNANIMOUS",
"config": {
"defaultResourceType": "http://photoz.com/admin",
- "default": "true",
- "applyPolicies": "[\"Administration Policy\"]"
+ "applyPolicies": "[\"Administration Policy\"]",
+ "default": "true"
}
},
{
- "name": "View User Permission",
- "description": "Defines who is allowed to view an user profile",
+ "name": "Album Resource Permission",
+ "description": "A default permission that defines access for any album resource",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "applyPolicies": "[\"Only From @keycloak.org or Admin\"]",
- "scopes": "[\"urn:photoz.com:scopes:profile:view\"]"
+ "resources": "[\"Album Resource\"]",
+ "scopes": "[\"album:view\",\"album:delete\"]",
+ "applyPolicies": "[\"Only Owner and Administrators Policy\"]"
}
},
{
- "name": "Delete Album Permission",
- "description": "A policy that only allows the owner to delete his albums.",
+ "name": "View User Permission",
+ "description": "Defines who is allowed to view an user profile",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
- "applyPolicies": "[\"Only Owner and Administrators Policy\"]",
- "scopes": "[\"urn:photoz.com:scopes:album:delete\"]"
+ "scopes": "[\"profile:view\"]",
+ "applyPolicies": "[\"Only From @keycloak.org or Admin\"]"
}
}
- ],
- "scopes": [
- {
- "name": "urn:photoz.com:scopes:profile:view"
- },
- {
- "name": "urn:photoz.com:scopes:album:view"
- },
- {
- "name": "urn:photoz.com:scopes:album:create"
- },
- {
- "name": "urn:photoz.com:scopes:album:delete"
- },
- {
- "name": "urn:photoz.com:scopes:album:admin:manage"
- }
]
}
\ No newline at end of file
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
index 9e06730..7748450 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
@@ -1,6 +1,6 @@
{
"realm": "photoz",
- "auth-server-url": "http://localhost:8080/auth",
+ "auth-server-url": "http://localhost:8180/auth",
"ssl-required": "external",
"resource": "photoz-restful-api",
"bearer-only" : true,
@@ -8,36 +8,29 @@
"secret": "secret"
},
"policy-enforcer": {
- "user-managed-access" : {},
+ "enforcement-mode": "PERMISSIVE",
+ "user-managed-access": {},
"paths": [
{
- "path" : "/album/*",
- "methods" : [
- {
- "method": "POST",
- "scopes" : ["urn:photoz.com:scopes:album:create"]
- },
- {
- "method": "GET",
- "scopes" : ["urn:photoz.com:scopes:album:view"]
- }
- ]
- },
- {
"name" : "Album Resource",
"path" : "/album/{id}",
"methods" : [
{
"method": "DELETE",
- "scopes" : ["urn:photoz.com:scopes:album:delete"]
+ "scopes" : ["album:delete"]
},
{
"method": "GET",
- "scopes" : ["urn:photoz.com:scopes:album:view"]
+ "scopes" : ["album:view"]
}
]
},
{
+ "name" : "Album Resource",
+ "path" : "/album/shares",
+ "enforcement-mode": "DISABLED"
+ },
+ {
"path" : "/profile"
},
{
diff --git a/examples/authz/servlet-authz/src/main/webapp/index.jsp b/examples/authz/servlet-authz/src/main/webapp/index.jsp
index 3fbfca2..345a69d 100755
--- a/examples/authz/servlet-authz/src/main/webapp/index.jsp
+++ b/examples/authz/servlet-authz/src/main/webapp/index.jsp
@@ -23,8 +23,8 @@
for (Permission permission : authzContext.getPermissions()) {
%>
<li>
- <p>Resource: <%= permission.getResourceSetName() %></p>
- <p>ID: <%= permission.getResourceSetId() %></p>
+ <p>Resource: <%= permission.getResourceName() %></p>
+ <p>ID: <%= permission.getResourceId() %></p>
<p>Scopes: <%= permission.getScopes() %></p>
</li>
<%
diff --git a/examples/authz/servlet-authz/src/main/webapp/logout-include.jsp b/examples/authz/servlet-authz/src/main/webapp/logout-include.jsp
index 364d887..21ef2ed 100644
--- a/examples/authz/servlet-authz/src/main/webapp/logout-include.jsp
+++ b/examples/authz/servlet-authz/src/main/webapp/logout-include.jsp
@@ -7,5 +7,5 @@
String contextPath = request.getContextPath();
String redirectUri = scheme + "://" + host + ":" + port + contextPath;
%>
-<h2>Click here <a href="<%= KeycloakUriBuilder.fromUri("http://localhost:8080/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+<h2>Click here <a href="<%= KeycloakUriBuilder.fromUri("http://localhost:8180/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
.queryParam("redirect_uri", redirectUri).build("servlet-authz").toString()%>">Sign Out</a></h2>
\ No newline at end of file
diff --git a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json
index 7983fa3..d2834c3 100644
--- a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json
@@ -1,6 +1,6 @@
{
"realm": "servlet-authz",
- "auth-server-url": "http://localhost:8080/auth",
+ "auth-server-url": "http://localhost:8180/auth",
"ssl-required": "external",
"resource": "servlet-authz-app",
"credentials": {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java
new file mode 100644
index 0000000..a906a7d
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package org.keycloak.models.cache.infinispan.authorization.entities;
+
+import org.keycloak.authorization.model.PermissionTicket;
+import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedPermissionTicket extends AbstractRevisioned implements InResourceServer {
+
+ private final String requester;
+ private String owner;
+ private String resourceServerId;
+ private String resourceId;
+ private String scopeId;
+ private boolean granted;
+ private Long createdTimestamp;
+ private Long grantedTimestamp;
+
+ public CachedPermissionTicket(Long revision, PermissionTicket permissionTicket) {
+ super(revision, permissionTicket.getId());
+ this.owner = permissionTicket.getOwner();
+ requester = permissionTicket.getRequester();
+ this.resourceServerId = permissionTicket.getResourceServer().getId();
+ this.resourceId = permissionTicket.getResource().getId();
+ if (permissionTicket.getScope() != null) {
+ this.scopeId = permissionTicket.getScope().getId();
+ }
+ this.granted = permissionTicket.isGranted();
+ createdTimestamp = permissionTicket.getCreatedTimestamp();
+ grantedTimestamp = permissionTicket.getGrantedTimestamp();
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public String getRequester() {
+ return requester;
+ }
+
+ public String getResourceId() {
+ return resourceId;
+ }
+
+ public String getScopeId() {
+ return scopeId;
+ }
+
+ public boolean isGranted() {
+ return granted;
+ }
+
+ public long getCreatedTimestamp() {
+ return createdTimestamp;
+ }
+
+ public Long getGrantedTimestamp() {
+ return grantedTimestamp;
+ }
+
+ public String getResourceServerId() {
+ return this.resourceServerId;
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java
index f153883..383ab1c 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java
@@ -35,18 +35,22 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
private String owner;
private String type;
private String name;
+ private String displayName;
private String uri;
private Set<String> scopesIds;
+ private boolean ownerManagedAccess;
public CachedResource(Long revision, Resource resource) {
super(revision, resource.getId());
this.name = resource.getName();
+ this.displayName = resource.getDisplayName();
this.uri = resource.getUri();
this.type = resource.getType();
this.owner = resource.getOwner();
this.iconUri = resource.getIconUri();
this.resourceServerId = resource.getResourceServer().getId();
this.scopesIds = resource.getScopes().stream().map(Scope::getId).collect(Collectors.toSet());
+ ownerManagedAccess = resource.isOwnerManagedAccess();
}
@@ -54,6 +58,10 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
return this.name;
}
+ public String getDisplayName() {
+ return this.displayName;
+ }
+
public String getUri() {
return this.uri;
}
@@ -70,6 +78,10 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
return this.owner;
}
+ public boolean isOwnerManagedAccess() {
+ return ownerManagedAccess;
+ }
+
public String getResourceServerId() {
return this.resourceServerId;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedScope.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedScope.java
index 7bc31ed..e879134 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedScope.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedScope.java
@@ -28,11 +28,13 @@ public class CachedScope extends AbstractRevisioned implements InResourceServer
private String resourceServerId;
private String name;
+ private String displayName;
private String iconUri;
public CachedScope(Long revision, Scope scope) {
super(revision, scope.getId());
this.name = scope.getName();
+ this.displayName = scope.getDisplayName();
this.iconUri = scope.getIconUri();
this.resourceServerId = scope.getResourceServer().getId();
}
@@ -41,6 +43,10 @@ public class CachedScope extends AbstractRevisioned implements InResourceServer
return this.name;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
public String getIconUri() {
return this.iconUri;
}
@@ -49,5 +55,4 @@ public class CachedScope extends AbstractRevisioned implements InResourceServer
public String getResourceServerId() {
return this.resourceServerId;
}
-
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketListQuery.java
new file mode 100755
index 0000000..d8c397c
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketListQuery.java
@@ -0,0 +1,42 @@
+package org.keycloak.models.cache.infinispan.authorization.entities;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class PermissionTicketListQuery extends AbstractRevisioned implements PermissionTicketQuery {
+
+ private final Set<String> permissions;
+ private final String serverId;
+
+ public PermissionTicketListQuery(Long revision, String id, String permissionId, String serverId) {
+ super(revision, id);
+ this.serverId = serverId;
+ permissions = new HashSet<>();
+ permissions.add(permissionId);
+ }
+ public PermissionTicketListQuery(Long revision, String id, Set<String> permissions, String serverId) {
+ super(revision, id);
+ this.serverId = serverId;
+ this.permissions = permissions;
+ }
+
+ @Override
+ public String getResourceServerId() {
+ return serverId;
+ }
+
+ public Set<String> getPermissions() {
+ return permissions;
+ }
+
+ @Override
+ public boolean isInvalid(Set<String> invalidations) {
+ return invalidations.contains(getId()) || invalidations.contains(getResourceServerId());
+ }
+}
\ No newline at end of file
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java
new file mode 100644
index 0000000..bbef979
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package org.keycloak.models.cache.infinispan.authorization.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.authorization.StoreFactoryCacheManager;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PermissionTicketRemovedEvent extends InvalidationEvent implements AuthorizationCacheInvalidationEvent {
+
+ private String id;
+ private String owner;
+ private String resource;
+ private String scope;
+ private String serverId;
+
+ public static PermissionTicketRemovedEvent create(String id, String owner, String resource, String scope, String serverId) {
+ PermissionTicketRemovedEvent event = new PermissionTicketRemovedEvent();
+ event.id = id;
+ event.owner = owner;
+ event.resource = resource;
+ event.scope = scope;
+ event.serverId = serverId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PermissionTicketRemovedEvent [ id=%s, name=%s]", id, resource);
+ }
+
+ @Override
+ public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
+ cache.permissionTicketRemoval(id, owner, resource, scope, serverId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java
new file mode 100644
index 0000000..1d830ed
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package org.keycloak.models.cache.infinispan.authorization.events;
+
+import java.util.Set;
+
+import org.keycloak.models.cache.infinispan.authorization.StoreFactoryCacheManager;
+import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PermissionTicketUpdatedEvent extends InvalidationEvent implements AuthorizationCacheInvalidationEvent {
+
+ private String id;
+ private String owner;
+ private String resource;
+ private String scope;
+ private String serverId;
+
+ public static PermissionTicketUpdatedEvent create(String id, String owner, String resource, String scope, String serverId) {
+ PermissionTicketUpdatedEvent event = new PermissionTicketUpdatedEvent();
+ event.id = id;
+ event.owner = owner;
+ event.resource = resource;
+ event.scope = scope;
+ event.serverId = serverId;
+ return event;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PermissionTicketUpdatedEvent [ id=%s, name=%s]", id, resource);
+ }
+
+ @Override
+ public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
+ cache.permissionTicketUpdated(id, owner, resource, scope, serverId, invalidations);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java
new file mode 100644
index 0000000..d6a7e07
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.models.cache.infinispan.authorization;
+
+import org.keycloak.authorization.model.CachedModel;
+import org.keycloak.authorization.model.PermissionTicket;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.models.cache.infinispan.authorization.entities.CachedPermissionTicket;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class PermissionTicketAdapter implements PermissionTicket, CachedModel<PermissionTicket> {
+
+ protected CachedPermissionTicket cached;
+ protected StoreFactoryCacheSession cacheSession;
+ protected PermissionTicket updated;
+
+ public PermissionTicketAdapter(CachedPermissionTicket cached, StoreFactoryCacheSession cacheSession) {
+ this.cached = cached;
+ this.cacheSession = cacheSession;
+ }
+
+ @Override
+ public PermissionTicket getDelegateForUpdate() {
+ if (updated == null) {
+ cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getResourceId(), cached.getScopeId(), cached.getResourceServerId());
+ updated = cacheSession.getPermissionTicketStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
+ if (updated == null) throw new IllegalStateException("Not found in database");
+ }
+ return updated;
+ }
+
+ protected boolean invalidated;
+
+ protected void invalidateFlag() {
+ invalidated = true;
+ }
+
+ @Override
+ public void invalidate() {
+ invalidated = true;
+ getDelegateForUpdate();
+ }
+
+ @Override
+ public long getCacheTimestamp() {
+ return cached.getCacheTimestamp();
+ }
+
+ protected boolean isUpdated() {
+ if (updated != null) return true;
+ if (!invalidated) return false;
+ updated = cacheSession.getPermissionTicketStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
+ if (updated == null) throw new IllegalStateException("Not found in database");
+ return true;
+ }
+
+
+ @Override
+ public String getId() {
+ if (isUpdated()) return updated.getId();
+ return cached.getId();
+ }
+
+ @Override
+ public String getOwner() {
+ if (isUpdated()) return updated.getOwner();
+ return cached.getOwner();
+ }
+
+ @Override
+ public String getRequester() {
+ if (isUpdated()) return updated.getRequester();
+ return cached.getRequester();
+ }
+
+ @Override
+ public boolean isGranted() {
+ if (isUpdated()) return updated.isGranted();
+ return cached.isGranted();
+ }
+
+ @Override
+ public Long getCreatedTimestamp() {
+ if (isUpdated()) return updated.getCreatedTimestamp();
+ return cached.getCreatedTimestamp();
+ }
+
+ @Override
+ public Long getGrantedTimestamp() {
+ if (isUpdated()) return updated.getGrantedTimestamp();
+ return cached.getGrantedTimestamp();
+ }
+
+ @Override
+ public void setGrantedTimestamp(Long millis) {
+ getDelegateForUpdate();
+ cacheSession.registerPermissionTicketInvalidation(cached.getId(), cached.getOwner(), cached.getResourceId(), cached.getScopeId(), cached.getResourceServerId());
+ updated.setGrantedTimestamp(millis);
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ return cacheSession.getResourceServerStore().findById(cached.getResourceServerId());
+ }
+
+ @Override
+ public Resource getResource() {
+ return cacheSession.getResourceStore().findById(cached.getResourceId(), getResourceServer().getId());
+ }
+
+ @Override
+ public Scope getScope() {
+ return cacheSession.getScopeStore().findById(cached.getScopeId(), getResourceServer().getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return getId().hashCode();
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java
index 7660c96..ae96113 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PolicyAdapter.java
@@ -25,6 +25,7 @@ import org.keycloak.models.cache.infinispan.authorization.entities.CachedPolicy;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.Logic;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
@@ -206,14 +207,15 @@ public class PolicyAdapter implements Policy, CachedModel<Policy> {
@Override
public void addScope(Scope scope) {
getDelegateForUpdate();
+ cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), new HashSet<>(Arrays.asList(scope.getId())), cached.getResourceServerId());
updated.addScope(scope);
}
@Override
public void removeScope(Scope scope) {
getDelegateForUpdate();
+ cacheSession.registerPolicyInvalidation(cached.getId(), cached.getName(), cached.getResourcesIds(), new HashSet<>(Arrays.asList(scope.getId())), cached.getResourceServerId());
updated.removeScope(scope);
-
}
@Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java
index 38b6860..d310fca 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java
@@ -17,9 +17,11 @@
package org.keycloak.models.cache.infinispan.authorization;
import org.keycloak.authorization.model.CachedModel;
+import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.PermissionTicketStore;
import org.keycloak.models.cache.infinispan.authorization.entities.CachedResource;
import java.util.Collections;
@@ -96,7 +98,19 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
getDelegateForUpdate();
cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
updated.setName(name);
+ }
+ @Override
+ public String getDisplayName() {
+ if (isUpdated()) return updated.getDisplayName();
+ return cached.getDisplayName();
+ }
+
+ @Override
+ public void setDisplayName(String name) {
+ getDelegateForUpdate();
+ cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+ updated.setDisplayName(name);
}
@Override
@@ -165,8 +179,33 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
}
@Override
- public void updateScopes(Set<Scope> scopes) {
+ public boolean isOwnerManagedAccess() {
+ if (isUpdated()) return updated.isOwnerManagedAccess();
+ return cached.isOwnerManagedAccess();
+ }
+
+ @Override
+ public void setOwnerManagedAccess(boolean ownerManagedAccess) {
getDelegateForUpdate();
+ cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+ updated.setOwnerManagedAccess(ownerManagedAccess);
+ }
+
+ @Override
+ public void updateScopes(Set<Scope> scopes) {
+ Resource updated = getDelegateForUpdate();
+
+ for (Scope scope : updated.getScopes()) {
+ if (!scopes.contains(scope)) {
+ PermissionTicketStore permissionStore = cacheSession.getPermissionTicketStoreDelegate();
+ List<PermissionTicket> permissions = permissionStore.findByScope(scope.getId(), getResourceServer().getId());
+
+ for (PermissionTicket permission : permissions) {
+ permissionStore.delete(permission.getId());
+ }
+ }
+ }
+
cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUri(), scopes.stream().map(scope1 -> scope1.getId()).collect(Collectors.toSet()), cached.getResourceServerId(), cached.getOwner());
updated.updateScopes(scopes);
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ScopeAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ScopeAdapter.java
index d90b27a..a4492a7 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ScopeAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ScopeAdapter.java
@@ -92,6 +92,18 @@ public class ScopeAdapter implements Scope, CachedModel<Scope> {
}
@Override
+ public String getDisplayName() {
+ if (isUpdated()) return updated.getDisplayName();
+ return cached.getDisplayName();
+ }
+
+ @Override
+ public void setDisplayName(String name) {
+ getDelegateForUpdate();
+ updated.setDisplayName(name);
+ }
+
+ @Override
public String getIconUri() {
if (isUpdated()) return updated.getIconUri();
return cached.getIconUri();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
index 3f189a5..7b463be 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
@@ -68,6 +68,7 @@ public class StoreFactoryCacheManager extends CacheManager {
invalidations.add(id);
invalidations.add(StoreFactoryCacheSession.getScopeByNameCacheKey(name, serverId));
invalidations.add(StoreFactoryCacheSession.getResourceByScopeCacheKey(id, serverId));
+ invalidations.add(StoreFactoryCacheSession.getPermissionTicketByScope(id, serverId));
}
public void scopeRemoval(String id, String name, String serverId, Set<String> invalidations) {
@@ -79,6 +80,8 @@ public class StoreFactoryCacheManager extends CacheManager {
invalidations.add(id);
invalidations.add(StoreFactoryCacheSession.getResourceByNameCacheKey(name, serverId));
invalidations.add(StoreFactoryCacheSession.getResourceByOwnerCacheKey(owner, serverId));
+ invalidations.add(StoreFactoryCacheSession.getResourceByOwnerCacheKey(owner, null));
+ invalidations.add(StoreFactoryCacheSession.getPermissionTicketByResource(id, serverId));
if (type != null) {
invalidations.add(StoreFactoryCacheSession.getResourceByTypeCacheKey(type, serverId));
@@ -125,9 +128,21 @@ public class StoreFactoryCacheManager extends CacheManager {
}
}
+ public void permissionTicketUpdated(String id, String owner, String resource, String scope, String serverId, Set<String> invalidations) {
+ invalidations.add(id);
+ invalidations.add(StoreFactoryCacheSession.getPermissionTicketByOwner(owner, serverId));
+ invalidations.add(StoreFactoryCacheSession.getPermissionTicketByResource(resource, serverId));
+ if (scope != null) {
+ invalidations.add(StoreFactoryCacheSession.getPermissionTicketByScope(scope, serverId));
+ }
+ }
+
public void policyRemoval(String id, String name, Set<String> resources, Set<String> resourceTypes, Set<String> scopes, String serverId, Set<String> invalidations) {
policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations);
}
+ public void permissionTicketRemoval(String id, String owner, String resource, String scope, String serverId, Set<String> invalidations) {
+ permissionTicketUpdated(id, owner, resource, scope, serverId, invalidations);
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
index cb5d060..33c1ddc 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
@@ -30,10 +30,12 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
+import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.PermissionTicketStore;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore;
@@ -43,10 +45,15 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.ModelException;
import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
+import org.keycloak.models.cache.infinispan.authorization.entities.CachedPermissionTicket;
import org.keycloak.models.cache.infinispan.authorization.entities.CachedPolicy;
import org.keycloak.models.cache.infinispan.authorization.entities.CachedResource;
import org.keycloak.models.cache.infinispan.authorization.entities.CachedResourceServer;
import org.keycloak.models.cache.infinispan.authorization.entities.CachedScope;
+import org.keycloak.models.cache.infinispan.authorization.entities.PermissionTicketListQuery;
+import org.keycloak.models.cache.infinispan.authorization.entities.PermissionTicketQuery;
+import org.keycloak.models.cache.infinispan.authorization.entities.PermissionTicketResourceListQuery;
+import org.keycloak.models.cache.infinispan.authorization.entities.PermissionTicketScopeListQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.PolicyListQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.PolicyQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.PolicyResourceListQuery;
@@ -55,6 +62,8 @@ import org.keycloak.models.cache.infinispan.authorization.entities.ResourceListQ
import org.keycloak.models.cache.infinispan.authorization.entities.ResourceQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.ResourceScopeListQuery;
import org.keycloak.models.cache.infinispan.authorization.entities.ScopeListQuery;
+import org.keycloak.models.cache.infinispan.authorization.events.PermissionTicketRemovedEvent;
+import org.keycloak.models.cache.infinispan.authorization.events.PermissionTicketUpdatedEvent;
import org.keycloak.models.cache.infinispan.authorization.events.PolicyRemovedEvent;
import org.keycloak.models.cache.infinispan.authorization.events.PolicyUpdatedEvent;
import org.keycloak.models.cache.infinispan.authorization.events.ResourceRemovedEvent;
@@ -82,6 +91,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
protected Map<String, ScopeAdapter> managedScopes = new HashMap<>();
protected Map<String, ResourceAdapter> managedResources = new HashMap<>();
protected Map<String, PolicyAdapter> managedPolicies = new HashMap<>();
+ protected Map<String, PermissionTicketAdapter> managedPermissionTickets = new HashMap<>();
protected Set<String> invalidations = new HashSet<>();
protected Set<InvalidationEvent> invalidationEvents = new HashSet<>(); // Events to be sent across cluster
@@ -93,6 +103,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
protected ScopeCache scopeCache;
protected ResourceCache resourceCache;
protected PolicyCache policyCache;
+ protected PermissionTicketCache permissionTicketCache;
public StoreFactoryCacheSession(StoreFactoryCacheManager cache, KeycloakSession session) {
this.cache = cache;
@@ -102,6 +113,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
this.scopeCache = new ScopeCache();
this.resourceCache = new ResourceCache();
this.policyCache = new PolicyCache();
+ this.permissionTicketCache = new PermissionTicketCache();
session.getTransactionManager().enlistPrepare(getPrepareTransaction());
session.getTransactionManager().enlistAfterCompletion(getAfterTransaction());
}
@@ -126,6 +138,11 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
return policyCache;
}
+ @Override
+ public PermissionTicketStore getPermissionTicketStore() {
+ return permissionTicketCache;
+ }
+
public void close() {
}
@@ -263,6 +280,14 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
invalidationEvents.add(PolicyUpdatedEvent.create(id, name, resources, resourceTypes, scopes, serverId));
}
+ public void registerPermissionTicketInvalidation(String id, String owner, String resource, String scope, String serverId) {
+ cache.permissionTicketUpdated(id, owner, resource, scope, serverId, invalidations);
+ PermissionTicketAdapter adapter = managedPermissionTickets.get(id);
+ if (adapter != null) adapter.invalidateFlag();
+
+ invalidationEvents.add(PermissionTicketUpdatedEvent.create(id, owner, resource, scope, serverId));
+ }
+
private Set<String> getResourceTypes(Set<String> resources, String serverId) {
if (resources == null) {
return Collections.emptySet();
@@ -296,6 +321,10 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
return getDelegate().getPolicyStore();
}
+ public PermissionTicketStore getPermissionTicketStoreDelegate() {
+ return getDelegate().getPermissionTicketStore();
+ }
+
public static String getResourceServerByClientCacheKey(String clientId) {
return "resource.server.client.id." + clientId;
}
@@ -340,6 +369,18 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
return "policy.scope." + scope + "." + serverId;
}
+ public static String getPermissionTicketByResource(String resourceId, String serverId) {
+ return "permission.ticket.resource." + resourceId + "." + serverId;
+ }
+
+ public static String getPermissionTicketByScope(String scopeId, String serverId) {
+ return "permission.ticket.scope." + scopeId + "." + serverId;
+ }
+
+ public static String getPermissionTicketByOwner(String owner, String serverId) {
+ return "permission.ticket.owner." + owner + "." + serverId;
+ }
+
public StoreFactory getDelegate() {
if (delegate != null) return delegate;
delegate = session.getProvider(StoreFactory.class);
@@ -592,7 +633,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
@Override
public List<Resource> findByType(String type, String resourceServerId) {
- if (type == null) return null;
+ if (type == null) return Collections.emptyList();
String cacheKey = getResourceByTypeCacheKey(type, resourceServerId);
return cacheQuery(cacheKey, ResourceListQuery.class, () -> getResourceStoreDelegate().findByType(type, resourceServerId),
(revision, resources) -> new ResourceListQuery(revision, cacheKey, resources.stream().map(resource -> resource.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
@@ -761,5 +802,108 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
}
}
+ protected class PermissionTicketCache implements PermissionTicketStore {
+ @Override
+ public PermissionTicket create(String resourceId, String scopeId, String requester, ResourceServer resourceServer) {
+ PermissionTicket created = getPermissionTicketStoreDelegate().create(resourceId, scopeId, requester, resourceServer);
+ registerPermissionTicketInvalidation(created.getId(), created.getOwner(), created.getResource().getId(), scopeId, created.getResourceServer().getId());
+ return created;
+ }
+
+ @Override
+ public void delete(String id) {
+ if (id == null) return;
+ PermissionTicket permission = findById(id, null);
+ if (permission == null) return;
+
+ cache.invalidateObject(id);
+ String scopeId = null;
+ if (permission.getScope() != null) {
+ scopeId = permission.getScope().getId();
+ }
+ invalidationEvents.add(PermissionTicketRemovedEvent.create(id, permission.getOwner(), permission.getResource().getId(), scopeId, permission.getResourceServer().getId()));
+ cache.permissionTicketRemoval(id, permission.getOwner(), permission.getResource().getId(), scopeId, permission.getResourceServer().getId(), invalidations);
+ getPermissionTicketStoreDelegate().delete(id);
+
+ }
+
+ @Override
+ public PermissionTicket findById(String id, String resourceServerId) {
+ if (id == null) return null;
+
+ CachedPermissionTicket cached = cache.get(id, CachedPermissionTicket.class);
+ if (cached != null) {
+ logger.tracev("by id cache hit: {0}", cached.getId());
+ }
+ boolean wasCached = false;
+ if (cached == null) {
+ Long loaded = cache.getCurrentRevision(id);
+ PermissionTicket model = getPermissionTicketStoreDelegate().findById(id, resourceServerId);
+ if (model == null) return null;
+ if (invalidations.contains(id)) return model;
+ cached = new CachedPermissionTicket(loaded, model);
+ cache.addRevisioned(cached, startupRevision);
+ wasCached =true;
+ } else if (invalidations.contains(id)) {
+ return getPermissionTicketStoreDelegate().findById(id, resourceServerId);
+ } else if (managedPermissionTickets.containsKey(id)) {
+ return managedPermissionTickets.get(id);
+ }
+ PermissionTicketAdapter adapter = new PermissionTicketAdapter(cached, StoreFactoryCacheSession.this);
+ managedPermissionTickets.put(id, adapter);
+ return adapter;
+ }
+
+ @Override
+ public List<PermissionTicket> findByResourceServer(String resourceServerId) {
+ return getPermissionTicketStoreDelegate().findByResourceServer(resourceServerId);
+ }
+
+ @Override
+ public List<PermissionTicket> findByResource(String resourceId, String resourceServerId) {
+ String cacheKey = getPermissionTicketByResource(resourceId, resourceServerId);
+ return cacheQuery(cacheKey, PermissionTicketResourceListQuery.class, () -> getPermissionTicketStoreDelegate().findByResource(resourceId, resourceServerId),
+ (revision, permissions) -> new PermissionTicketResourceListQuery(revision, cacheKey, resourceId, permissions.stream().map(permission -> permission.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
+ }
+
+ @Override
+ public List<PermissionTicket> findByScope(String scopeId, String resourceServerId) {
+ String cacheKey = getPermissionTicketByScope(scopeId, resourceServerId);
+ return cacheQuery(cacheKey, PermissionTicketScopeListQuery.class, () -> getPermissionTicketStoreDelegate().findByScope(scopeId, resourceServerId),
+ (revision, permissions) -> new PermissionTicketScopeListQuery(revision, cacheKey, scopeId, permissions.stream().map(permission -> permission.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
+ }
+
+ @Override
+ public List<PermissionTicket> find(Map<String, String> attributes, String resourceServerId, int firstResult, int maxResult) {
+ return getPermissionTicketStoreDelegate().find(attributes, resourceServerId, firstResult, maxResult);
+ }
+
+ @Override
+ public List<PermissionTicket> findByOwner(String owner, String resourceServerId) {
+ String cacheKey = getPermissionTicketByOwner(owner, resourceServerId);
+ return cacheQuery(cacheKey, PermissionTicketListQuery.class, () -> getPermissionTicketStoreDelegate().findByOwner(owner, resourceServerId),
+ (revision, permissions) -> new PermissionTicketListQuery(revision, cacheKey, permissions.stream().map(permission -> permission.getId()).collect(Collectors.toSet()), resourceServerId), resourceServerId);
+ }
+
+ private <R, Q extends PermissionTicketQuery> List<R> cacheQuery(String cacheKey, Class<Q> queryType, Supplier<List<R>> resultSupplier, BiFunction<Long, List<R>, Q> querySupplier, String resourceServerId) {
+ Q query = cache.get(cacheKey, queryType);
+ if (query != null) {
+ logger.tracev("cache hit for key: {0}", cacheKey);
+ }
+ if (query == null) {
+ Long loaded = cache.getCurrentRevision(cacheKey);
+ List<R> model = resultSupplier.get();
+ if (model == null) return null;
+ if (invalidations.contains(cacheKey)) return model;
+ query = querySupplier.apply(loaded, model);
+ cache.addRevisioned(query, startupRevision);
+ return model;
+ } else if (query.isInvalid(invalidations)) {
+ return resultSupplier.get();
+ } else {
+ return query.getPermissions().stream().map(resourceId -> (R) findById(resourceId, resourceServerId)).collect(Collectors.toList());
+ }
+ }
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index e854002..f187e9c 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -128,6 +128,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected Set<String> adminEnabledEventOperations = new HashSet<String>();
protected boolean adminEventsDetailsEnabled;
protected List<String> defaultRoles;
+ private boolean allowUserManagedAccess;
public Set<IdentityProviderMapperModel> getIdentityProviderMapperSet() {
return identityProviderMapperSet;
@@ -151,6 +152,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
displayName = model.getDisplayName();
displayNameHtml = model.getDisplayNameHtml();
enabled = model.isEnabled();
+ allowUserManagedAccess = model.isUserManagedAccessAllowed();
sslRequired = model.getSslRequired();
registrationAllowed = model.isRegistrationAllowed();
registrationEmailAsUsername = model.isRegistrationEmailAsUsername();
@@ -629,4 +631,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
public Map<String, String> getAttributes() {
return attributes;
}
+
+ public boolean isAllowUserManagedAccess() {
+ return allowUserManagedAccess;
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index dd62377..7e6c3c7 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -142,6 +142,18 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
+ public boolean isUserManagedAccessAllowed() {
+ if (isUpdated()) return updated.isEnabled();
+ return cached.isAllowUserManagedAccess();
+ }
+
+ @Override
+ public void setUserManagedAccessAllowed(boolean userManagedAccessAllowed) {
+ getDelegateForUpdate();
+ updated.setUserManagedAccessAllowed(userManagedAccessAllowed);
+ }
+
+ @Override
public SslRequired getSslRequired() {
if (isUpdated()) return updated.getSslRequired();
return cached.getSslRequired();
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java
new file mode 100644
index 0000000..c607bec
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package org.keycloak.authorization.jpa.entities;
+
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@Entity
+@Table(name = "RESOURCE_SERVER_PERMISSION_TICKET", uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"OWNER", "RESOURCE_SERVER_ID", "RESOURCE_ID", "SCOPE_ID"})
+})
+@NamedQueries(
+ {
+ @NamedQuery(name="findPermissionIdByResource", query="select p.id from PermissionTicketEntity p inner join p.resource r where p.resourceServer.id = :serverId and (r.resourceServer.id = :serverId and r.id = :resourceId)"),
+ @NamedQuery(name="findPermissionIdByScope", query="select p.id from PermissionTicketEntity p inner join p.scope s where p.resourceServer.id = :serverId and (s.resourceServer.id = :serverId and s.id = :scopeId)"),
+ @NamedQuery(name="findPermissionTicketIdByServerId", query="select p.id from PermissionTicketEntity p where p.resourceServer.id = :serverId ")
+ }
+)
+public class PermissionTicketEntity {
+
+ @Id
+ @Column(name = "ID", length = 36)
+ @Access(AccessType.PROPERTY)
+ // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
+ private String id;
+
+ @Column(name = "OWNER")
+ private String owner;
+
+ @Column(name = "REQUESTER")
+ private String requester;
+
+ @Column(name = "CREATED_TIMESTAMP")
+ private Long createdTimestamp;
+
+ @Column(name = "GRANTED_TIMESTAMP")
+ private Long grantedTimestamp;
+
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
+ @JoinColumn(name = "RESOURCE_ID")
+ private ResourceEntity resource;
+
+ @ManyToOne(optional = true, fetch = FetchType.LAZY)
+ @JoinColumn(name = "SCOPE_ID")
+ private ScopeEntity scope;
+
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
+ @JoinColumn(name = "RESOURCE_SERVER_ID")
+ private ResourceServerEntity resourceServer;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public ResourceEntity getResource() {
+ return resource;
+ }
+
+ public void setResource(ResourceEntity resource) {
+ this.resource = resource;
+ }
+
+ public ScopeEntity getScope() {
+ return scope;
+ }
+
+ public void setScope(ScopeEntity scope) {
+ this.scope = scope;
+ }
+
+ public ResourceServerEntity getResourceServer() {
+ return resourceServer;
+ }
+
+ public void setResourceServer(ResourceServerEntity resourceServer) {
+ this.resourceServer = resourceServer;
+ }
+
+ public void setRequester(String requester) {
+ this.requester = requester;
+ }
+
+ public String getRequester() {
+ return requester;
+ }
+
+ public Long getCreatedTimestamp() {
+ return createdTimestamp;
+ }
+
+ public void setCreatedTimestamp(Long createdTimestamp) {
+ this.createdTimestamp = createdTimestamp;
+ }
+
+ public Long getGrantedTimestamp() {
+ return grantedTimestamp;
+ }
+
+ public void setGrantedTimestamp(long grantedTimestamp) {
+ this.grantedTimestamp = grantedTimestamp;
+ }
+
+ public boolean isGranted() {
+ return grantedTimestamp != null;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PermissionTicketEntity that = (PermissionTicketEntity) o;
+
+ return getId().equals(that.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return getId().hashCode();
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
index d91be73..6031a6e 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
@@ -45,6 +45,7 @@ import java.util.List;
@NamedQueries(
{
@NamedQuery(name="findResourceIdByOwner", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.owner = :owner"),
+ @NamedQuery(name="findAnyResourceIdByOwner", query="select r.id from ResourceEntity r where r.owner = :owner"),
@NamedQuery(name="findResourceIdByUri", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.uri = :uri"),
@NamedQuery(name="findResourceIdByName", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.name = :name"),
@NamedQuery(name="findResourceIdByType", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.type = :type"),
@@ -63,6 +64,9 @@ public class ResourceEntity {
@Column(name = "NAME")
private String name;
+ @Column(name = "DISPLAY_NAME")
+ private String displayName;
+
@Column(name = "URI")
private String uri;
@@ -75,6 +79,9 @@ public class ResourceEntity {
@Column(name = "OWNER")
private String owner;
+ @Column(name = "OWNER_MANAGED_ACCESS")
+ private boolean ownerManagedAccess;
+
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "RESOURCE_SERVER_ID")
private ResourceServerEntity resourceServer;
@@ -103,6 +110,14 @@ public class ResourceEntity {
this.name = name;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
public String getUri() {
return uri;
}
@@ -147,6 +162,14 @@ public class ResourceEntity {
this.owner = owner;
}
+ public void setOwnerManagedAccess(boolean ownerManagedAccess) {
+ this.ownerManagedAccess = ownerManagedAccess;
+ }
+
+ public boolean isOwnerManagedAccess() {
+ return ownerManagedAccess;
+ }
+
public List<PolicyEntity> getPolicies() {
return this.policies;
}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java
index 5d86d7b..9d9d7d8 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java
@@ -59,6 +59,9 @@ public class ScopeEntity {
@Column(name = "NAME")
private String name;
+ @Column(name = "DISPLAY_NAME")
+ private String displayName;
+
@Column(name = "ICON_URI")
private String iconUri;
@@ -86,6 +89,18 @@ public class ScopeEntity {
this.name = name;
}
+ public void setDisplayName(String displayName) {
+ if (displayName != null && !"".equals(displayName.trim())) {
+ this.displayName = displayName;
+ } else {
+ this.displayName = null;
+ }
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
public String getIconUri() {
return iconUri;
}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java
new file mode 100644
index 0000000..ee51d5e
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.authorization.jpa.store;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.persistence.EntityManager;
+import javax.persistence.FlushModeType;
+import javax.persistence.Query;
+import javax.persistence.TypedQuery;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.jpa.entities.PermissionTicketEntity;
+import org.keycloak.authorization.model.PermissionTicket;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.store.PermissionTicketStore;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JPAPermissionTicketStore implements PermissionTicketStore {
+
+ private final EntityManager entityManager;
+ private final AuthorizationProvider provider;
+
+ public JPAPermissionTicketStore(EntityManager entityManager, AuthorizationProvider provider) {
+ this.entityManager = entityManager;
+ this.provider = provider;
+ }
+
+ @Override
+ public PermissionTicket create(String resourceId, String scopeId, String requester, ResourceServer resourceServer) {
+ PermissionTicketEntity entity = new PermissionTicketEntity();
+
+ entity.setId(KeycloakModelUtils.generateId());
+ entity.setResource(ResourceAdapter.toEntity(entityManager, provider.getStoreFactory().getResourceStore().findById(resourceId, resourceServer.getId())));
+ entity.setRequester(requester);
+ entity.setCreatedTimestamp(System.currentTimeMillis());
+
+ if (scopeId != null) {
+ entity.setScope(ScopeAdapter.toEntity(entityManager, provider.getStoreFactory().getScopeStore().findById(scopeId, resourceServer.getId())));
+ }
+
+ entity.setOwner(entity.getResource().getOwner());
+ entity.setResourceServer(ResourceServerAdapter.toEntity(entityManager, resourceServer));
+
+ this.entityManager.persist(entity);
+ this.entityManager.flush();
+ PermissionTicket model = new PermissionTicketAdapter(entity, entityManager, provider.getStoreFactory());
+ return model;
+ }
+
+ @Override
+ public void delete(String id) {
+ PermissionTicketEntity policy = entityManager.find(PermissionTicketEntity.class, id);
+ if (policy != null) {
+ this.entityManager.remove(policy);
+ }
+ }
+
+
+ @Override
+ public PermissionTicket findById(String id, String resourceServerId) {
+ if (id == null) {
+ return null;
+ }
+
+ PermissionTicketEntity entity = entityManager.find(PermissionTicketEntity.class, id);
+ if (entity == null) return null;
+
+ return new PermissionTicketAdapter(entity, entityManager, provider.getStoreFactory());
+ }
+
+ @Override
+ public List<PermissionTicket> findByResourceServer(final String resourceServerId) {
+ TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByServerId", String.class);
+
+ query.setParameter("serverId", resourceServerId);
+
+ List<String> result = query.getResultList();
+ List<PermissionTicket> list = new LinkedList<>();
+ for (String id : result) {
+ list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId));
+ }
+ return list;
+ }
+
+ @Override
+ public List<PermissionTicket> findByResource(final String resourceId, String resourceServerId) {
+ TypedQuery<String> query = entityManager.createNamedQuery("findPermissionIdByResource", String.class);
+
+ query.setFlushMode(FlushModeType.COMMIT);
+ query.setParameter("resourceId", resourceId);
+ query.setParameter("serverId", resourceServerId);
+
+ List<String> result = query.getResultList();
+ List<PermissionTicket> list = new LinkedList<>();
+ for (String id : result) {
+ list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId));
+ }
+ return list;
+ }
+
+ @Override
+ public List<PermissionTicket> findByScope(String scopeId, String resourceServerId) {
+ if (scopeId==null) {
+ return Collections.emptyList();
+ }
+
+ // Use separate subquery to handle DB2 and MSSSQL
+ TypedQuery<String> query = entityManager.createNamedQuery("findPermissionIdByScope", String.class);
+
+ query.setFlushMode(FlushModeType.COMMIT);
+ query.setParameter("scopeId", scopeId);
+ query.setParameter("serverId", resourceServerId);
+
+ List<String> result = query.getResultList();
+ List<PermissionTicket> list = new LinkedList<>();
+ for (String id : result) {
+ list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId));
+ }
+ return list;
+ }
+
+ @Override
+ public List<PermissionTicket> find(Map<String, String> attributes, String resourceServerId, int firstResult, int maxResult) {
+ CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+ CriteriaQuery<PermissionTicketEntity> querybuilder = builder.createQuery(PermissionTicketEntity.class);
+ Root<PermissionTicketEntity> root = querybuilder.from(PermissionTicketEntity.class);
+
+ querybuilder.select(root.get("id"));
+
+ List<Predicate> predicates = new ArrayList();
+
+ if (resourceServerId != null) {
+ predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId));
+ }
+
+ attributes.forEach((name, value) -> {
+ if (PermissionTicket.ID.equals(name)) {
+ predicates.add(root.get(name).in(value));
+ } else if (PermissionTicket.SCOPE.equals(name)) {
+ predicates.add(root.join("scope").get("id").in(value));
+ } else if (PermissionTicket.SCOPE_IS_NULL.equals(name)) {
+ if (Boolean.valueOf(value)) {
+ predicates.add(builder.isNull(root.get("scope")));
+ } else {
+ predicates.add(builder.isNotNull(root.get("scope")));
+ }
+ } else if (PermissionTicket.RESOURCE.equals(name)) {
+ predicates.add(root.join("resource").get("id").in(value));
+ } else if (PermissionTicket.OWNER.equals(name)) {
+ predicates.add(builder.equal(root.get("owner"), value));
+ } else if (PermissionTicket.REQUESTER.equals(name)) {
+ predicates.add(builder.equal(root.get("requester"), value));
+ } else if (PermissionTicket.GRANTED.equals(name)) {
+ if (Boolean.valueOf(value)) {
+ predicates.add(builder.isNotNull(root.get("grantedTimestamp")));
+ } else {
+ predicates.add(builder.isNull(root.get("grantedTimestamp")));
+ }
+ } else if (PermissionTicket.REQUESTER_IS_NULL.equals(name)) {
+ predicates.add(builder.isNull(root.get("requester")));
+ } else {
+ throw new RuntimeException("Unsupported filter [" + name + "]");
+ }
+ });
+
+ querybuilder.where(predicates.toArray(new Predicate[predicates.size()])).orderBy(builder.asc(root.get("resource").get("id")));
+
+ Query query = entityManager.createQuery(querybuilder);
+
+ if (firstResult != -1) {
+ query.setFirstResult(firstResult);
+ }
+ if (maxResult != -1) {
+ query.setMaxResults(maxResult);
+ }
+
+ List<String> result = query.getResultList();
+ List<PermissionTicket> list = new LinkedList<>();
+ PermissionTicketStore ticket = provider.getStoreFactory().getPermissionTicketStore();
+
+ for (String id : result) {
+ list.add(ticket.findById(id, resourceServerId));
+ }
+
+ return list;
+ }
+
+ @Override
+ public List<PermissionTicket> findByOwner(String owner, String resourceServerId) {
+ TypedQuery<String> query = entityManager.createNamedQuery("findPolicyIdByType", String.class);
+
+ query.setFlushMode(FlushModeType.COMMIT);
+ query.setParameter("serverId", resourceServerId);
+ query.setParameter("owner", owner);
+
+ List<String> result = query.getResultList();
+ List<PermissionTicket> list = new LinkedList<>();
+ for (String id : result) {
+ list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId));
+ }
+ return list;
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
index 5e79bad..308b073 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
@@ -18,11 +18,11 @@
package org.keycloak.authorization.jpa.store;
import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.jpa.entities.PermissionTicketEntity;
import org.keycloak.authorization.jpa.entities.PolicyEntity;
import org.keycloak.authorization.jpa.entities.ResourceEntity;
import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
import org.keycloak.authorization.jpa.entities.ScopeEntity;
-import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.models.ModelException;
@@ -30,7 +30,6 @@ import org.keycloak.storage.StorageId;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
-import java.util.LinkedList;
import java.util.List;
/**
@@ -77,6 +76,17 @@ public class JPAResourceServerStore implements ResourceServerStore {
}
}
+ {
+ TypedQuery<String> query = entityManager.createNamedQuery("findPermissionTicketIdByServerId", String.class);
+
+ query.setParameter("serverId", id);
+
+ List<String> result = query.getResultList();
+ for (String permissionId : result) {
+ entityManager.remove(entityManager.getReference(PermissionTicketEntity.class, permissionId));
+ }
+ }
+
//entityManager.createNamedQuery("deleteResourceByResourceServer")
// .setParameter("serverId", id).executeUpdate();
{
@@ -85,7 +95,6 @@ public class JPAResourceServerStore implements ResourceServerStore {
query.setParameter("serverId", id);
List<String> result = query.getResultList();
- List<Resource> list = new LinkedList<>();
for (String resourceId : result) {
entityManager.remove(entityManager.getReference(ResourceEntity.class, resourceId));
}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
index de83c42..c938afe 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
@@ -88,11 +88,20 @@ public class JPAResourceStore implements ResourceStore {
@Override
public List<Resource> findByOwner(String ownerId, String resourceServerId) {
- TypedQuery<String> query = entityManager.createNamedQuery("findResourceIdByOwner", String.class);
+ String queryName = "findResourceIdByOwner";
+
+ if (resourceServerId == null) {
+ queryName = "findAnyResourceIdByOwner";
+ }
+
+ TypedQuery<String> query = entityManager.createNamedQuery(queryName, String.class);
query.setFlushMode(FlushModeType.COMMIT);
query.setParameter("owner", ownerId);
- query.setParameter("serverId", resourceServerId);
+
+ if (resourceServerId != null) {
+ query.setParameter("serverId", resourceServerId);
+ }
List<String> result = query.getResultList();
List<Resource> list = new LinkedList<>();
@@ -161,13 +170,17 @@ public class JPAResourceStore implements ResourceStore {
querybuilder.select(root.get("id"));
List<Predicate> predicates = new ArrayList();
- predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId));
+ if (resourceServerId != null) {
+ predicates.add(builder.equal(root.get("resourceServer").get("id"), resourceServerId));
+ }
attributes.forEach((name, value) -> {
if ("id".equals(name)) {
predicates.add(root.get(name).in(value));
} else if ("scope".equals(name)) {
predicates.add(root.join("scopes").get("id").in(value));
+ } else if ("ownerManagedAccess".equals(name)) {
+ predicates.add(builder.equal(root.get(name), Boolean.valueOf(value[0])));
} else {
predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%"));
}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java
index 855f66a..cd08f0c 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java
@@ -21,6 +21,7 @@ package org.keycloak.authorization.jpa.store;
import javax.persistence.EntityManager;
import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.store.PermissionTicketStore;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore;
@@ -36,12 +37,14 @@ public class JPAStoreFactory implements StoreFactory {
private final ResourceServerStore resourceServerStore;
private final ResourceStore resourceStore;
private final ScopeStore scopeStore;
+ private final JPAPermissionTicketStore permissionTicketStore;
public JPAStoreFactory(EntityManager entityManager, AuthorizationProvider provider) {
policyStore = new JPAPolicyStore(entityManager, provider);
resourceServerStore = new JPAResourceServerStore(entityManager, provider);
resourceStore = new JPAResourceStore(entityManager, provider);
scopeStore = new JPAScopeStore(entityManager, provider);
+ permissionTicketStore = new JPAPermissionTicketStore(entityManager, provider);
}
@Override
@@ -65,6 +68,11 @@ public class JPAStoreFactory implements StoreFactory {
}
@Override
+ public PermissionTicketStore getPermissionTicketStore() {
+ return permissionTicketStore;
+ }
+
+ @Override
public void close() {
}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PermissionTicketAdapter.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PermissionTicketAdapter.java
new file mode 100644
index 0000000..e1c56a2
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PermissionTicketAdapter.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.authorization.jpa.store;
+
+import javax.persistence.EntityManager;
+
+import org.keycloak.authorization.jpa.entities.PermissionTicketEntity;
+import org.keycloak.authorization.jpa.entities.ScopeEntity;
+import org.keycloak.authorization.model.PermissionTicket;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.jpa.JpaModel;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class PermissionTicketAdapter implements PermissionTicket, JpaModel<PermissionTicketEntity> {
+
+ private PermissionTicketEntity entity;
+ private EntityManager em;
+ private StoreFactory storeFactory;
+
+ public PermissionTicketAdapter(PermissionTicketEntity entity, EntityManager em, StoreFactory storeFactory) {
+ this.entity = entity;
+ this.em = em;
+ this.storeFactory = storeFactory;
+ }
+
+ @Override
+ public PermissionTicketEntity getEntity() {
+ return entity;
+ }
+
+ @Override
+ public String getId() {
+ return entity.getId();
+ }
+
+ @Override
+ public String getOwner() {
+ return entity.getOwner();
+ }
+
+ @Override
+ public String getRequester() {
+ return entity.getRequester();
+ }
+
+ @Override
+ public boolean isGranted() {
+ return entity.isGranted();
+ }
+
+ @Override
+ public Long getCreatedTimestamp() {
+ return entity.getCreatedTimestamp();
+ }
+
+ @Override
+ public Long getGrantedTimestamp() {
+ return entity.getGrantedTimestamp();
+ }
+
+ @Override
+ public void setGrantedTimestamp(Long millis) {
+ entity.setGrantedTimestamp(millis);
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ return storeFactory.getResourceServerStore().findById(entity.getResourceServer().getId());
+ }
+
+ @Override
+ public Resource getResource() {
+ return storeFactory.getResourceStore().findById(entity.getResource().getId(), getResourceServer().getId());
+ }
+
+ @Override
+ public Scope getScope() {
+ ScopeEntity scope = entity.getScope();
+
+ if (scope == null) {
+ return null;
+ }
+
+ return storeFactory.getScopeStore().findById(scope.getId(), getResourceServer().getId());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || !(o instanceof Policy)) return false;
+
+ PermissionTicket that = (PermissionTicket) o;
+ return that.getId().equals(getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return getId().hashCode();
+ }
+
+ public static PermissionTicketEntity toEntity(EntityManager em, PermissionTicket permission) {
+ if (permission instanceof PermissionTicketAdapter) {
+ return ((PermissionTicketAdapter)permission).getEntity();
+ } else {
+ return em.getReference(PermissionTicketEntity.class, permission.getId());
+ }
+ }
+
+
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java
index 9ce0de2..782f084 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java
@@ -63,6 +63,16 @@ public class ResourceAdapter implements Resource, JpaModel<ResourceEntity> {
}
@Override
+ public String getDisplayName() {
+ return entity.getDisplayName();
+ }
+
+ @Override
+ public void setDisplayName(String name) {
+ entity.setDisplayName(name);
+ }
+
+ @Override
public void setName(String name) {
entity.setName(name);
@@ -122,6 +132,16 @@ public class ResourceAdapter implements Resource, JpaModel<ResourceEntity> {
}
@Override
+ public boolean isOwnerManagedAccess() {
+ return entity.isOwnerManagedAccess();
+ }
+
+ @Override
+ public void setOwnerManagedAccess(boolean ownerManagedAccess) {
+ entity.setOwnerManagedAccess(ownerManagedAccess);
+ }
+
+ @Override
public void updateScopes(Set<Scope> toUpdate) {
Set<String> ids = new HashSet<>();
for (Scope scope : toUpdate) {
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ScopeAdapter.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ScopeAdapter.java
index f77310e..1a83d27 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ScopeAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ScopeAdapter.java
@@ -61,6 +61,16 @@ public class ScopeAdapter implements Scope, JpaModel<ScopeEntity> {
}
@Override
+ public String getDisplayName() {
+ return entity.getDisplayName();
+ }
+
+ @Override
+ public void setDisplayName(String name) {
+ entity.setDisplayName(name);
+ }
+
+ @Override
public String getIconUri() {
return entity.getIconUri();
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index fe6ee14..4f6244f 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -237,6 +237,9 @@ public class RealmEntity {
@Column(name="DEFAULT_LOCALE")
protected String defaultLocale;
+ @Column(name="ALLOW_USER_MANAGED_ACCESS")
+ private boolean allowUserManagedAccess;
+
public String getId() {
return id;
@@ -762,6 +765,14 @@ public class RealmEntity {
this.clientTemplates = clientTemplates;
}
+ public void setAllowUserManagedAccess(boolean allowUserManagedAccess) {
+ this.allowUserManagedAccess = allowUserManagedAccess;
+ }
+
+ public boolean isAllowUserManagedAccess() {
+ return allowUserManagedAccess;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -779,6 +790,5 @@ public class RealmEntity {
public int hashCode() {
return id.hashCode();
}
-
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 26bfc24..e737a25 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -115,6 +115,17 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
}
@Override
+ public boolean isUserManagedAccessAllowed() {
+ return realm.isAllowUserManagedAccess();
+ }
+
+ @Override
+ public void setUserManagedAccessAllowed(boolean userManagedAccessAllowed) {
+ realm.setAllowUserManagedAccess(userManagedAccessAllowed);
+ em.flush();
+ }
+
+ @Override
public boolean isRegistrationAllowed() {
return realm.isRegistrationAllowed();
}
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.CR1.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.CR1.xml
new file mode 100755
index 0000000..aa3d17e
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.CR1.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ * Copyright 2017 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-3.3.0.CR1">
+ <createTable tableName="RESOURCE_SERVER_PERMISSION_TICKET">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="OWNER" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="REQUESTER" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="CREATED_TIMESTAMP" type="BIGINT">
+ <constraints nullable="false"/>
+ </column>
+ <column name="GRANTED_TIMESTAMP" type="BIGINT">
+ <constraints nullable="true"/>
+ </column>
+ <column name="RESOURCE_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="SCOPE_ID" type="VARCHAR(36)">
+ <constraints nullable="true"/>
+ </column>
+ <column name="RESOURCE_SERVER_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_FAPMT" tableName="RESOURCE_SERVER_PERMISSION_TICKET"/>
+ <addForeignKeyConstraint baseColumnNames="RESOURCE_SERVER_ID" baseTableName="RESOURCE_SERVER_PERMISSION_TICKET" constraintName="FK_FRSRHO213XCX4WNKOG82SSPMT" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER"/>
+ <addForeignKeyConstraint baseColumnNames="RESOURCE_ID" baseTableName="RESOURCE_SERVER_PERMISSION_TICKET" constraintName="FK_FRSRHO213XCX4WNKOG83SSPMT" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_RESOURCE"/>
+ <addForeignKeyConstraint baseColumnNames="SCOPE_ID" baseTableName="RESOURCE_SERVER_PERMISSION_TICKET" constraintName="FK_FRSRHO213XCX4WNKOG84SSPMT" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_SCOPE"/>
+ <addUniqueConstraint columnNames="OWNER, REQUESTER, RESOURCE_SERVER_ID, RESOURCE_ID, SCOPE_ID" constraintName="UK_FRSR6T700S9V50BU18WS5PMT" tableName="RESOURCE_SERVER_PERMISSION_TICKET"/>
+
+ <addColumn tableName="RESOURCE_SERVER_RESOURCE">
+ <column name="OWNER_MANAGED_ACCESS" type="BOOLEAN" defaultValueBoolean="false">
+ <constraints nullable="false" />
+ </column>
+ </addColumn>
+
+ <addColumn tableName="RESOURCE_SERVER_RESOURCE">
+ <column name="DISPLAY_NAME" type="VARCHAR(255)" >
+ <constraints nullable="true" />
+ </column>
+ </addColumn>
+
+ <addColumn tableName="RESOURCE_SERVER_SCOPE">
+ <column name="DISPLAY_NAME" type="VARCHAR(255)" >
+ <constraints nullable="true" />
+ </column>
+ </addColumn>
+
+ <addColumn tableName="REALM">
+ <column name="ALLOW_USER_MANAGED_ACCESS" type="BOOLEAN" defaultValueBoolean="false">
+ <constraints nullable="false"/>
+ </column>
+ </addColumn>
+ </changeSet>
+</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
index fa824e2..c9d0e16 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
@@ -54,4 +54,5 @@
<include file="META-INF/jpa-changelog-3.4.1.xml"/>
<include file="META-INF/jpa-changelog-3.4.2.xml"/>
<include file="META-INF/jpa-changelog-4.0.0.xml"/>
+ <include file="META-INF/jpa-changelog-authz-4.0.0.CR1.xml"/>
</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml
index 36e3fb4..86eda78 100755
--- a/model/jpa/src/main/resources/META-INF/persistence.xml
+++ b/model/jpa/src/main/resources/META-INF/persistence.xml
@@ -66,6 +66,7 @@
<class>org.keycloak.authorization.jpa.entities.ResourceEntity</class>
<class>org.keycloak.authorization.jpa.entities.ScopeEntity</class>
<class>org.keycloak.authorization.jpa.entities.PolicyEntity</class>
+ <class>org.keycloak.authorization.jpa.entities.PermissionTicketEntity</class>
<!-- User Federation Storage -->
<class>org.keycloak.storage.jpa.entity.BrokerLinkEntity</class>
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index 5eb18db..c316c47 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -111,6 +111,10 @@ public interface RealmModel extends RoleContainerModel {
void setEditUsernameAllowed(boolean editUsernameAllowed);
+ boolean isUserManagedAccessAllowed();
+
+ void setUserManagedAccessAllowed(boolean userManagedAccessAllowed);
+
void setAttribute(String name, String value);
void setAttribute(String name, Boolean value);
void setAttribute(String name, Integer value);
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
index 644b90a..81da078 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
@@ -24,6 +24,7 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
@@ -32,6 +33,7 @@ import org.keycloak.authorization.permission.evaluator.Evaluators;
import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.authorization.store.PermissionTicketStore;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore;
@@ -122,11 +124,69 @@ public final class AuthorizationProvider implements Provider {
return storeFactoryDelegate;
}
+ /**
+ * Returns the registered {@link PolicyProviderFactory}.
+ *
+ * @return a {@link List} containing all registered {@link PolicyProviderFactory}
+ */
+ public Collection<PolicyProviderFactory> getProviderFactories() {
+ return this.policyProviderFactories.values();
+ }
+
+ /**
+ * Returns a {@link PolicyProviderFactory} given a <code>type</code>.
+ *
+ * @param type the type of the policy provider
+ * @param <F> the expected type of the provider
+ * @return a {@link PolicyProviderFactory} with the given <code>type</code>
+ */
+ public <F extends PolicyProviderFactory> F getProviderFactory(String type) {
+ return (F) policyProviderFactories.get(type);
+ }
+
+ /**
+ * Returns a {@link PolicyProviderFactory} given a <code>type</code>.
+ *
+ * @param type the type of the policy provider
+ * @param <P> the expected type of the provider
+ * @return a {@link PolicyProvider} with the given <code>type</code>
+ */
+ public <P extends PolicyProvider> P getProvider(String type) {
+ PolicyProviderFactory policyProviderFactory = policyProviderFactories.get(type);
+
+ if (policyProviderFactory == null) {
+ return null;
+ }
+
+ return (P) policyProviderFactory.create(this);
+ }
+
+ public KeycloakSession getKeycloakSession() {
+ return this.keycloakSession;
+ }
+
+ public RealmModel getRealm() {
+ return realm;
+ }
+
+ @Override
+ public void close() {
+
+ }
+
private StoreFactory createStoreFactory(StoreFactory storeFactory) {
return new StoreFactory() {
+
+ ResourceStore resourceStore;
+ ScopeStore scopeStore;
+ PolicyStore policyStore;
+
@Override
public ResourceStore getResourceStore() {
- return storeFactory.getResourceStore();
+ if (resourceStore == null) {
+ resourceStore = createResourceStoreWrapper(storeFactory);
+ }
+ return resourceStore;
}
@Override
@@ -136,192 +196,268 @@ public final class AuthorizationProvider implements Provider {
@Override
public ScopeStore getScopeStore() {
- return storeFactory.getScopeStore();
+ if (scopeStore == null) {
+ scopeStore = createScopeWrapper(storeFactory);
+ }
+ return scopeStore;
}
@Override
public PolicyStore getPolicyStore() {
- PolicyStore policyStore = storeFactory.getPolicyStore();
- return new PolicyStore() {
- @Override
- public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) {
- Set<String> resources = representation.getResources();
-
- if (resources != null) {
- representation.setResources(resources.stream().map(id -> {
- Resource resource = getResourceStore().findById(id, resourceServer.getId());
-
- if (resource == null) {
- resource = getResourceStore().findByName(id, resourceServer.getId());
- }
-
- if (resource == null) {
- throw new RuntimeException("Resource [" + id + "] does not exist");
- }
-
- return resource.getId();
- }).collect(Collectors.toSet()));
+ if (policyStore == null) {
+ policyStore = createPolicyWrapper(storeFactory);
+ }
+ return policyStore;
+ }
+
+ @Override
+ public PermissionTicketStore getPermissionTicketStore() {
+ return storeFactory.getPermissionTicketStore();
+ }
+
+ @Override
+ public void close() {
+ storeFactory.close();
+ }
+ };
+ }
+
+ private ScopeStore createScopeWrapper(StoreFactory storeFactory) {
+ return new ScopeStore() {
+
+ ScopeStore delegate = storeFactory.getScopeStore();
+
+ @Override
+ public Scope create(String name, ResourceServer resourceServer) {
+ return delegate.create(name, resourceServer);
+ }
+
+ @Override
+ public void delete(String id) {
+ Scope scope = findById(id, null);
+ PermissionTicketStore ticketStore = storeFactory.getPermissionTicketStore();
+ List<PermissionTicket> permissions = ticketStore.findByScope(id, scope.getResourceServer().getId());
+
+ for (PermissionTicket permission : permissions) {
+ ticketStore.delete(permission.getId());
+ }
+
+ delegate.delete(id);
+ }
+
+ @Override
+ public Scope findById(String id, String resourceServerId) {
+ return delegate.findById(id, resourceServerId);
+ }
+
+ @Override
+ public Scope findByName(String name, String resourceServerId) {
+ return delegate.findByName(name, resourceServerId);
+ }
+
+ @Override
+ public List<Scope> findByResourceServer(String id) {
+ return delegate.findByResourceServer(id);
+ }
+
+ @Override
+ public List<Scope> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
+ return delegate.findByResourceServer(attributes, resourceServerId, firstResult, maxResult);
+ }
+ };
+ }
+
+ private PolicyStore createPolicyWrapper(StoreFactory storeFactory) {
+ return new PolicyStore() {
+
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+
+ @Override
+ public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) {
+ Set<String> resources = representation.getResources();
+
+ if (resources != null) {
+ representation.setResources(resources.stream().map(id -> {
+ Resource resource = storeFactory.getResourceStore().findById(id, resourceServer.getId());
+
+ if (resource == null) {
+ resource = storeFactory.getResourceStore().findByName(id, resourceServer.getId());
+ }
+
+ if (resource == null) {
+ throw new RuntimeException("Resource [" + id + "] does not exist");
}
- Set<String> scopes = representation.getScopes();
+ return resource.getId();
+ }).collect(Collectors.toSet()));
+ }
- if (scopes != null) {
- representation.setScopes(scopes.stream().map(id -> {
- Scope scope = getScopeStore().findById(id, resourceServer.getId());
+ Set<String> scopes = representation.getScopes();
- if (scope == null) {
- scope = getScopeStore().findByName(id, resourceServer.getId());
- }
+ if (scopes != null) {
+ representation.setScopes(scopes.stream().map(id -> {
+ Scope scope = storeFactory.getScopeStore().findById(id, resourceServer.getId());
- if (scope == null) {
- throw new RuntimeException("Scope [" + id + "] does not exist");
- }
+ if (scope == null) {
+ scope = storeFactory.getScopeStore().findByName(id, resourceServer.getId());
+ }
- return scope.getId();
- }).collect(Collectors.toSet()));
+ if (scope == null) {
+ throw new RuntimeException("Scope [" + id + "] does not exist");
}
+ return scope.getId();
+ }).collect(Collectors.toSet()));
+ }
- Set<String> policies = representation.getPolicies();
- if (policies != null) {
- representation.setPolicies(policies.stream().map(id -> {
- Policy policy = getPolicyStore().findById(id, resourceServer.getId());
+ Set<String> policies = representation.getPolicies();
- if (policy == null) {
- policy = getPolicyStore().findByName(id, resourceServer.getId());
- }
+ if (policies != null) {
+ representation.setPolicies(policies.stream().map(id -> {
+ Policy policy = storeFactory.getPolicyStore().findById(id, resourceServer.getId());
- if (policy == null) {
- throw new RuntimeException("Policy [" + id + "] does not exist");
- }
+ if (policy == null) {
+ policy = storeFactory.getPolicyStore().findByName(id, resourceServer.getId());
+ }
- return policy.getId();
- }).collect(Collectors.toSet()));
+ if (policy == null) {
+ throw new RuntimeException("Policy [" + id + "] does not exist");
}
- return RepresentationToModel.toModel(representation, AuthorizationProvider.this, policyStore.create(representation, resourceServer));
- }
+ return policy.getId();
+ }).collect(Collectors.toSet()));
+ }
- @Override
- public void delete(String id) {
- Policy policy = findById(id, null);
+ return RepresentationToModel.toModel(representation, AuthorizationProvider.this, policyStore.create(representation, resourceServer));
+ }
- if (policy != null) {
- ResourceServer resourceServer = policy.getResourceServer();
+ @Override
+ public void delete(String id) {
+ Policy policy = findById(id, null);
- findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> {
- dependentPolicy.removeAssociatedPolicy(policy);
- if (dependentPolicy.getAssociatedPolicies().isEmpty()) {
- delete(dependentPolicy.getId());
- }
- });
+ if (policy != null) {
+ ResourceServer resourceServer = policy.getResourceServer();
- policyStore.delete(id);
+ findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> {
+ dependentPolicy.removeAssociatedPolicy(policy);
+ if (dependentPolicy.getAssociatedPolicies().isEmpty()) {
+ delete(dependentPolicy.getId());
}
- }
+ });
- @Override
- public Policy findById(String id, String resourceServerId) {
- return policyStore.findById(id, resourceServerId);
- }
+ policyStore.delete(id);
+ }
+ }
- @Override
- public Policy findByName(String name, String resourceServerId) {
- return policyStore.findByName(name, resourceServerId);
- }
+ @Override
+ public Policy findById(String id, String resourceServerId) {
+ return policyStore.findById(id, resourceServerId);
+ }
- @Override
- public List<Policy> findByResourceServer(String resourceServerId) {
- return policyStore.findByResourceServer(resourceServerId);
- }
+ @Override
+ public Policy findByName(String name, String resourceServerId) {
+ return policyStore.findByName(name, resourceServerId);
+ }
- @Override
- public List<Policy> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
- return policyStore.findByResourceServer(attributes, resourceServerId, firstResult, maxResult);
- }
+ @Override
+ public List<Policy> findByResourceServer(String resourceServerId) {
+ return policyStore.findByResourceServer(resourceServerId);
+ }
- @Override
- public List<Policy> findByResource(String resourceId, String resourceServerId) {
- return policyStore.findByResource(resourceId, resourceServerId);
- }
+ @Override
+ public List<Policy> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
+ return policyStore.findByResourceServer(attributes, resourceServerId, firstResult, maxResult);
+ }
- @Override
- public List<Policy> findByResourceType(String resourceType, String resourceServerId) {
- return policyStore.findByResourceType(resourceType, resourceServerId);
- }
+ @Override
+ public List<Policy> findByResource(String resourceId, String resourceServerId) {
+ return policyStore.findByResource(resourceId, resourceServerId);
+ }
- @Override
- public List<Policy> findByScopeIds(List<String> scopeIds, String resourceServerId) {
- return policyStore.findByScopeIds(scopeIds, resourceServerId);
- }
+ @Override
+ public List<Policy> findByResourceType(String resourceType, String resourceServerId) {
+ return policyStore.findByResourceType(resourceType, resourceServerId);
+ }
- @Override
- public List<Policy> findByType(String type, String resourceServerId) {
- return policyStore.findByType(type, resourceServerId);
- }
+ @Override
+ public List<Policy> findByScopeIds(List<String> scopeIds, String resourceServerId) {
+ return policyStore.findByScopeIds(scopeIds, resourceServerId);
+ }
- @Override
- public List<Policy> findDependentPolicies(String id, String resourceServerId) {
- return policyStore.findDependentPolicies(id, resourceServerId);
- }
- };
+ @Override
+ public List<Policy> findByType(String type, String resourceServerId) {
+ return policyStore.findByType(type, resourceServerId);
}
@Override
- public void close() {
- storeFactory.close();
+ public List<Policy> findDependentPolicies(String id, String resourceServerId) {
+ return policyStore.findDependentPolicies(id, resourceServerId);
}
};
}
- /**
- * Returns the registered {@link PolicyProviderFactory}.
- *
- * @return a {@link List} containing all registered {@link PolicyProviderFactory}
- */
- public Collection<PolicyProviderFactory> getProviderFactories() {
- return this.policyProviderFactories.values();
- }
+ private ResourceStore createResourceStoreWrapper(StoreFactory storeFactory) {
+ return new ResourceStore() {
+ ResourceStore delegate = storeFactory.getResourceStore();
- /**
- * Returns a {@link PolicyProviderFactory} given a <code>type</code>.
- *
- * @param type the type of the policy provider
- * @param <F> the expected type of the provider
- * @return a {@link PolicyProviderFactory} with the given <code>type</code>
- */
- public <F extends PolicyProviderFactory> F getProviderFactory(String type) {
- return (F) policyProviderFactories.get(type);
- }
+ @Override
+ public Resource create(String name, ResourceServer resourceServer, String owner) {
+ return delegate.create(name, resourceServer, owner);
+ }
- /**
- * Returns a {@link PolicyProviderFactory} given a <code>type</code>.
- *
- * @param type the type of the policy provider
- * @param <P> the expected type of the provider
- * @return a {@link PolicyProvider} with the given <code>type</code>
- */
- public <P extends PolicyProvider> P getProvider(String type) {
- PolicyProviderFactory policyProviderFactory = policyProviderFactories.get(type);
+ @Override
+ public void delete(String id) {
+ Resource resource = findById(id, null);
+ PermissionTicketStore ticketStore = storeFactory.getPermissionTicketStore();
+ List<PermissionTicket> permissions = ticketStore.findByResource(id, resource.getResourceServer().getId());
- if (policyProviderFactory == null) {
- return null;
- }
+ for (PermissionTicket permission : permissions) {
+ ticketStore.delete(permission.getId());
+ }
- return (P) policyProviderFactory.create(this);
- }
+ delegate.delete(id);
+ }
- public KeycloakSession getKeycloakSession() {
- return this.keycloakSession;
- }
+ @Override
+ public Resource findById(String id, String resourceServerId) {
+ return delegate.findById(id, resourceServerId);
+ }
- public RealmModel getRealm() {
- return realm;
- }
+ @Override
+ public List<Resource> findByOwner(String ownerId, String resourceServerId) {
+ return delegate.findByOwner(ownerId, resourceServerId);
+ }
- @Override
- public void close() {
+ @Override
+ public List<Resource> findByUri(String uri, String resourceServerId) {
+ return delegate.findByUri(uri, resourceServerId);
+ }
+
+ @Override
+ public List<Resource> findByResourceServer(String resourceServerId) {
+ return delegate.findByResourceServer(resourceServerId);
+ }
+
+ @Override
+ public List<Resource> findByResourceServer(Map<String, String[]> attributes, String resourceServerId, int firstResult, int maxResult) {
+ return delegate.findByResourceServer(attributes, resourceServerId, firstResult, maxResult);
+ }
+
+ @Override
+ public List<Resource> findByScope(List<String> id, String resourceServerId) {
+ return delegate.findByScope(id, resourceServerId);
+ }
+ @Override
+ public Resource findByName(String name, String resourceServerId) {
+ return delegate.findByName(name, resourceServerId);
+ }
+
+ @Override
+ public List<Resource> findByType(String type, String resourceServerId) {
+ return delegate.findByType(type, resourceServerId);
+ }
+ };
}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java
new file mode 100644
index 0000000..39366d6
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.authorization.model;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PermissionTicket {
+
+ String ID = "id";
+ String RESOURCE = "resource.id";
+ String SCOPE = "scope.id";
+ String SCOPE_IS_NULL = "scope_is_null";
+ String OWNER = "owner";
+ String GRANTED = "granted";
+ String REQUESTER = "requester";
+ String REQUESTER_IS_NULL = "requester_is_null";
+
+ /**
+ * Returns the unique identifier for this instance.
+ *
+ * @return the unique identifier for this instance
+ */
+ String getId();
+
+ /**
+ * Returns the resource's owner, which is usually an identifier that uniquely identifies the resource's owner.
+ *
+ * @return the owner of this resource
+ */
+ String getOwner();
+
+ String getRequester();
+
+ /**
+ * Returns the {@link Resource} associated with this instance
+ *
+ * @return the {@link Resource} associated with this instance
+ */
+ Resource getResource();
+
+ /**
+ * Returns the {@link Scope} associated with this instance
+ *
+ * @return the {@link Scope} associated with this instance
+ */
+ Scope getScope();
+
+ boolean isGranted();
+
+ Long getCreatedTimestamp();
+
+ Long getGrantedTimestamp();
+ void setGrantedTimestamp(Long millis);
+
+ /**
+ * Returns the {@link ResourceServer} where this policy belongs to.
+ *
+ * @return a resource server
+ */
+ ResourceServer getResourceServer();
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java
index 4c2521c..cdfc0b6 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java
@@ -50,6 +50,20 @@ public interface Resource {
void setName(String name);
/**
+ * Returns the end user friendly name for this resource. If not defined, value for {@link #getName()} is returned.
+ *
+ * @return the friendly name for this resource
+ */
+ String getDisplayName();
+
+ /**
+ * Sets an end user friendly name for this resource.
+ *
+ * @param name the name of this resource
+ */
+ void setDisplayName(String name);
+
+ /**
* Returns a {@link java.net.URI} that uniquely identify this resource.
*
* @return an {@link java.net.URI} for this resource or null if not defined.
@@ -112,5 +126,8 @@ public interface Resource {
*/
String getOwner();
+ boolean isOwnerManagedAccess();
+ void setOwnerManagedAccess(boolean ownerManagedAccess);
+
void updateScopes(Set<Scope> scopes);
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/Scope.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/Scope.java
index e13a789..fd90eca 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/model/Scope.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/Scope.java
@@ -48,6 +48,20 @@ public interface Scope {
void setName(String name);
/**
+ * Returns the end user friendly name for this scope. If not defined, value for {@link #getName()} is returned.
+ *
+ * @return the friendly name for this scope
+ */
+ String getDisplayName();
+
+ /**
+ * Sets an end user friendly name for this scope.
+ *
+ * @param name the name of this scope
+ */
+ void setDisplayName(String name);
+
+ /**
* Returns an icon {@link java.net.URI} for this scope.
*
* @return a uri for an icon
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java
index f2da3a5..c43acac 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java
@@ -44,7 +44,7 @@ class IterablePermissionEvaluator implements PermissionEvaluator {
}
@Override
- public void evaluate(Decision decision) {
+ public Decision evaluate(Decision decision) {
try {
while (this.permissions.hasNext()) {
this.policyEvaluator.evaluate(this.permissions.next(), this.executionContext, decision);
@@ -53,6 +53,7 @@ class IterablePermissionEvaluator implements PermissionEvaluator {
} catch (Throwable cause) {
decision.onError(cause);
}
+ return decision;
}
@Override
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java
index 587856f..ae0d7fd 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java
@@ -30,6 +30,6 @@ import org.keycloak.authorization.policy.evaluation.Result;
*/
public interface PermissionEvaluator {
- void evaluate(Decision decision);
+ <D extends Decision> D evaluate(D decision);
List<Result> evaluate();
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
index 2ffc049..bfba2c6 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
@@ -62,17 +62,25 @@ public abstract class DecisionResultCollector implements Decision<DefaultEvaluat
}
if (deniedCount == 0) {
- result.setStatus(Effect.PERMIT);
+ onGrant(result);
} else {
- result.setStatus(Effect.DENY);
+ onDeny(result);
}
}
onComplete(results.values().stream().collect(Collectors.toList()));
}
+ protected void onGrant(Result result) {
+ result.setStatus(Effect.PERMIT);
+ }
+
protected abstract void onComplete(List<Result> results);
+ protected void onDeny(Result result) {
+ result.setStatus(Effect.DENY);
+ }
+
private boolean isGranted(Result.PolicyResult policyResult) {
List<Result.PolicyResult> values = policyResult.getAssociatedPolicies();
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
index c720504..9b708c5 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
@@ -19,6 +19,7 @@
package org.keycloak.authorization.policy.evaluation;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -75,7 +76,15 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator {
evaluatePolicies(() -> policyStore.findByResource(resource.getId(), resourceServer.getId()), consumer);
if (resource.getType() != null) {
- evaluatePolicies(() -> policyStore.findByResourceType(resource.getType(), resourceServer.getId()), consumer);
+ evaluatePolicies(() -> {
+ List<Policy> policies = policyStore.findByResourceType(resource.getType(), resourceServer.getId());
+
+ for (Resource typedResource : resourceStore.findByType(resource.getType(), resourceServer.getId())) {
+ policies.addAll(policyStore.findByResource(typedResource.getId(), resourceServer.getId()));
+ }
+
+ return policies;
+ }, consumer);
}
if (scopes.isEmpty() && !resource.getScopes().isEmpty()) {
@@ -137,7 +146,11 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator {
Set<Resource> policyResources = policy.getResources();
if (resourcePermission != null && !policyResources.isEmpty()) {
- if (!policyResources.stream().filter(resource -> resource.getId().equals(resourcePermission.getId())).findFirst().isPresent()) {
+ if (!policyResources.stream().filter(resource -> {
+ Iterator<Resource> policyResourceType = policy.getResources().iterator();
+ Resource policyResource = policyResourceType.hasNext() ? policyResourceType.next() : null;
+ return resource.getId().equals(resourcePermission.getId()) || (policyResourceType != null && policyResource.getType() != null && policyResource.getType().equals(resourcePermission.getType()));
+ }).findFirst().isPresent()) {
return false;
}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java
new file mode 100644
index 0000000..708f7e1
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.authorization.policy.evaluation;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.identity.Identity;
+import org.keycloak.authorization.model.PermissionTicket;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.Result.PolicyResult;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.PermissionTicketToken;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionTicketAwareDecisionResultCollector extends DecisionResultCollector {
+
+ private final AuthorizationRequest request;
+ private PermissionTicketToken ticket;
+ private final Identity identity;
+ private ResourceServer resourceServer;
+ private final AuthorizationProvider authorization;
+ private List<Result> results;
+
+ public PermissionTicketAwareDecisionResultCollector(AuthorizationRequest request, PermissionTicketToken ticket, Identity identity, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ this.request = request;
+ this.ticket = ticket;
+ this.identity = identity;
+ this.resourceServer = resourceServer;
+ this.authorization = authorization;
+ }
+
+ @Override
+ protected void onDeny(Result result) {
+ ResourcePermission permission = result.getPermission();
+ Resource resource = permission.getResource();
+
+ if (resource != null && resource.isOwnerManagedAccess()) {
+ if (!resource.getOwner().equals(identity.getId())) {
+ Map<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.RESOURCE, resource.getId());
+ filters.put(PermissionTicket.REQUESTER, identity.getId());
+ filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString());
+
+ List<PermissionTicket> permissions = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer().getId(), -1, -1);
+
+ if (!permissions.isEmpty()) {
+ List<Scope> grantedScopes = new ArrayList<>();
+
+ for (PolicyResult policyResult : result.getResults()) {
+ for (PermissionTicket ticket : permissions) {
+ Scope grantedScope = ticket.getScope();
+
+ if ("resource".equals(policyResult.getPolicy().getType())) {
+ policyResult.setStatus(Effect.PERMIT);
+ }
+
+ if (grantedScope != null) {
+ grantedScopes.add(grantedScope);
+
+ for (Scope policyScope : policyResult.getPolicy().getScopes()) {
+ if (policyScope.equals(grantedScope)) {
+ policyResult.setStatus(Effect.PERMIT);
+ }
+ }
+ }
+ }
+ }
+
+ permission.getScopes().clear();
+ permission.getScopes().addAll(grantedScopes);
+ }
+ }
+ }
+
+ super.onDeny(result);
+ }
+
+ @Override
+ public void onComplete() {
+ super.onComplete();
+
+ if (request.isSubmitRequest()) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ ResourceStore resourceStore = storeFactory.getResourceStore();
+
+ if (ticket.getResources() != null) {
+ for (PermissionTicketToken.ResourcePermission permission : ticket.getResources()) {
+ Resource resource = resourceStore.findById(permission.getResourceId(), resourceServer.getId());
+
+ if (resource == null) {
+ resource = resourceStore.findByName(permission.getResourceId(), resourceServer.getId());
+ }
+
+ if (!resource.isOwnerManagedAccess() || resource.getOwner().equals(identity.getId()) || resource.getOwner().equals(resourceServer.getId())) {
+ continue;
+ }
+
+ Set<String> scopes = permission.getScopes();
+
+ if (scopes.isEmpty()) {
+ scopes = resource.getScopes().stream().map(Scope::getName).collect(Collectors.toSet());
+ }
+
+ if (scopes.isEmpty()) {
+ Map<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.RESOURCE, resource.getId());
+ filters.put(PermissionTicket.REQUESTER, identity.getId());
+ filters.put(PermissionTicket.SCOPE_IS_NULL, Boolean.TRUE.toString());
+
+ List<PermissionTicket> permissions = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer().getId(), -1, -1);
+
+ if (permissions.isEmpty()) {
+ authorization.getStoreFactory().getPermissionTicketStore().create(resource.getId(), null, identity.getId(), resource.getResourceServer());
+ }
+ } else {
+ ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore();
+
+ for (String scopeId : scopes) {
+ Scope scope = scopeStore.findByName(scopeId, resourceServer.getId());
+
+ if (scope == null) {
+ scope = scopeStore.findById(scopeId, resourceServer.getId());
+ }
+
+ Map<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.RESOURCE, resource.getId());
+ filters.put(PermissionTicket.REQUESTER, identity.getId());
+ filters.put(PermissionTicket.SCOPE, scope.getId());
+
+ List<PermissionTicket> permissions = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer().getId(), -1, -1);
+
+ if (permissions.isEmpty()) {
+ authorization.getStoreFactory().getPermissionTicketStore().create(resource.getId(), scope.getId(), identity.getId(), resource.getResourceServer());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onComplete(List<Result> results) {
+ this.results = results;
+ }
+
+ public List<Result> results() {
+ return results;
+ }
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java
new file mode 100644
index 0000000..654d68b
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.authorization.store;
+
+
+import java.util.List;
+import java.util.Map;
+
+import org.keycloak.authorization.model.PermissionTicket;
+import org.keycloak.authorization.model.ResourceServer;
+
+/**
+ * A {@link PermissionTicketStore} is responsible to manage the persistence of {@link org.keycloak.authorization.model.PermissionTicket} instances.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PermissionTicketStore {
+
+ /**
+ * Creates a new {@link PermissionTicket} instance.
+ *
+ * @param permission the policy representation
+ * @param resourceServer the resource server to which this policy belongs
+ * @return a new instance of {@link PermissionTicket}
+ */
+ PermissionTicket create(String resourceId, String scopeId, String requester, ResourceServer resourceServer);
+
+ /**
+ * Deletes a permission from the underlying persistence mechanism.
+ *
+ * @param id the id of the policy to delete
+ */
+ void delete(String id);
+
+ /**
+ * Returns a {@link PermissionTicket} with the given <code>id</code>
+ *
+ * @param id the identifier of the permission
+ * @param resourceServerId the resource server id
+ * @return a permission with the given identifier.
+ */
+ PermissionTicket findById(String id, String resourceServerId);
+
+ /**
+ * Returns a list of {@link PermissionTicket} associated with a {@link ResourceServer} with the given <code>resourceServerId</code>.
+ *
+ * @param resourceServerId the identifier of a resource server
+ * @return a list of permissions belonging to the given resource server
+ */
+ List<PermissionTicket> findByResourceServer(String resourceServerId);
+
+ /**
+ * Returns a list of {@link PermissionTicket} associated with the given <code>owner</code>.
+ *
+ * @param owner the identifier of a resource server
+ * @return a list of permissions belonging to the given owner
+ */
+ List<PermissionTicket> findByOwner(String owner, String resourceServerId);
+
+ /**
+ * Returns a list of {@link PermissionTicket} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given <code>resourceId</code>.
+ *
+ * @param resourceId the identifier of a resource
+ * @param resourceServerId the resource server id
+ * @return a list of permissions associated with the given resource
+ */
+ List<PermissionTicket> findByResource(String resourceId, String resourceServerId);
+
+ /**
+ * Returns a list of {@link PermissionTicket} associated with a {@link org.keycloak.authorization.core.model.Scope} with the given <code>scopeId</code>.
+ *
+ * @param scopeId the id of the scopes
+ * @param resourceServerId the resource server id
+ * @return a list of permissions associated with the given scopes
+ */
+ List<PermissionTicket> findByScope(String scopeId, String resourceServerId);
+
+ List<PermissionTicket> find(Map<String, String> attributes, String resourceServerId, int firstResult, int maxResult);
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/store/StoreFactory.java b/server-spi-private/src/main/java/org/keycloak/authorization/store/StoreFactory.java
index 4f50c11..a1123a9 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/store/StoreFactory.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/store/StoreFactory.java
@@ -58,4 +58,10 @@ public interface StoreFactory extends Provider {
*/
PolicyStore getPolicyStore();
+ /**
+ * Returns a {@link PermissionTicketStore}.
+ *
+ * @return the permission ticket store
+ */
+ PermissionTicketStore getPermissionTicketStore();
}
diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
index b48e243..29b38bc 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
@@ -125,7 +125,10 @@ public enum EventType {
CLIENT_INITIATED_ACCOUNT_LINKING(true),
CLIENT_INITIATED_ACCOUNT_LINKING_ERROR(true),
TOKEN_EXCHANGE(true),
- TOKEN_EXCHANGE_ERROR(true);
+ TOKEN_EXCHANGE_ERROR(true),
+
+ PERMISSION_TOKEN(true),
+ PERMISSION_TOKEN_ERROR(false);
private boolean saveByDefault;
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountPages.java b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountPages.java
index f9b4835..6c01f79 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountPages.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountPages.java
@@ -22,6 +22,6 @@ package org.keycloak.forms.account;
*/
public enum AccountPages {
- ACCOUNT, PASSWORD, TOTP, FEDERATED_IDENTITY, LOG, SESSIONS, APPLICATIONS;
+ ACCOUNT, PASSWORD, TOTP, FEDERATED_IDENTITY, LOG, SESSIONS, APPLICATIONS, RESOURCES, RESOURCE_DETAIL;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java
index a61c0a9..1568f18 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/account/AccountProvider.java
@@ -65,7 +65,7 @@ public interface AccountProvider extends Provider {
AccountProvider setStateChecker(String stateChecker);
- AccountProvider setFeatures(boolean social, boolean events, boolean passwordUpdateSupported);
+ AccountProvider setFeatures(boolean social, boolean events, boolean passwordUpdateSupported, boolean authorizationSupported);
AccountProvider setAttribute(String key, String value);
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 2e7d3ef..56c95e5 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -18,6 +18,7 @@
package org.keycloak.models.utils;
import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
@@ -234,6 +235,7 @@ public class ModelToRepresentation {
rep.setQuickLoginCheckMilliSeconds(realm.getQuickLoginCheckMilliSeconds());
rep.setMaxDeltaTimeSeconds(realm.getMaxDeltaTimeSeconds());
rep.setFailureFactor(realm.getFailureFactor());
+ rep.setUserManagedAccessAllowed(realm.isUserManagedAccessAllowed());
rep.setEventsEnabled(realm.isEventsEnabled());
if (realm.getEventsExpiration() != 0) {
@@ -741,6 +743,7 @@ public class ModelToRepresentation {
scope.setId(model.getId());
scope.setName(model.getName());
+ scope.setDisplayName(model.getDisplayName());
scope.setIconUri(model.getIconUri());
return scope;
@@ -800,8 +803,10 @@ public class ModelToRepresentation {
resource.setId(model.getId());
resource.setType(model.getType());
resource.setName(model.getName());
+ resource.setDisplayName(model.getDisplayName());
resource.setUri(model.getUri());
resource.setIconUri(model.getIconUri());
+ resource.setOwnerManagedAccess(model.isOwnerManagedAccess());
ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation();
@@ -858,4 +863,35 @@ public class ModelToRepresentation {
return resource;
}
+
+ public static PermissionTicketRepresentation toRepresentation(PermissionTicket ticket) {
+ return toRepresentation(ticket, false);
+ }
+
+ public static PermissionTicketRepresentation toRepresentation(PermissionTicket ticket, boolean returnNames) {
+ PermissionTicketRepresentation representation = new PermissionTicketRepresentation();
+
+ representation.setId(ticket.getId());
+ representation.setGranted(ticket.isGranted());
+ representation.setOwner(ticket.getOwner());
+
+ Resource resource = ticket.getResource();
+
+ representation.setResource(resource.getId());
+
+ if (returnNames) {
+ representation.setResourceName(resource.getName());
+ }
+
+ Scope scope = ticket.getScope();
+
+ if (scope != null) {
+ representation.setScope(scope.getId());
+ if (returnNames) {
+ representation.setScopeName(scope.getName());
+ }
+ }
+
+ return representation;
+ }
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 98ccf38..c738b81 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -33,11 +33,13 @@ import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.AuthorizationProviderFactory;
+import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.authorization.store.PermissionTicketStore;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore;
@@ -109,6 +111,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.PermissionTicketRepresentation;
import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
@@ -145,6 +148,7 @@ public class RepresentationToModel {
if (rep.getDisplayName() != null) newRealm.setDisplayName(rep.getDisplayName());
if (rep.getDisplayNameHtml() != null) newRealm.setDisplayNameHtml(rep.getDisplayNameHtml());
if (rep.isEnabled() != null) newRealm.setEnabled(rep.isEnabled());
+ if (rep.isUserManagedAccessAllowed() != null) newRealm.setUserManagedAccessAllowed(rep.isUserManagedAccessAllowed());
if (rep.isBruteForceProtected() != null) newRealm.setBruteForceProtected(rep.isBruteForceProtected());
if (rep.isPermanentLockout() != null) newRealm.setPermanentLockout(rep.isPermanentLockout());
if (rep.getMaxFailureWaitSeconds() != null) newRealm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds());
@@ -817,6 +821,7 @@ public class RepresentationToModel {
if (rep.getDisplayName() != null) realm.setDisplayName(rep.getDisplayName());
if (rep.getDisplayNameHtml() != null) realm.setDisplayNameHtml(rep.getDisplayNameHtml());
if (rep.isEnabled() != null) realm.setEnabled(rep.isEnabled());
+ if (rep.isUserManagedAccessAllowed() != null) realm.setUserManagedAccessAllowed(rep.isUserManagedAccessAllowed());
if (rep.isBruteForceProtected() != null) realm.setBruteForceProtected(rep.isBruteForceProtected());
if (rep.isPermanentLockout() != null) realm.setPermanentLockout(rep.isPermanentLockout());
if (rep.getMaxFailureWaitSeconds() != null) realm.setMaxFailureWaitSeconds(rep.getMaxFailureWaitSeconds());
@@ -2308,9 +2313,11 @@ public class RepresentationToModel {
if (existing != null) {
existing.setName(resource.getName());
+ existing.setDisplayName(resource.getDisplayName());
existing.setType(resource.getType());
existing.setUri(resource.getUri());
existing.setIconUri(resource.getIconUri());
+ existing.setOwnerManagedAccess(Boolean.TRUE.equals(resource.getOwnerManagedAccess()));
existing.updateScopes(resource.getScopes().stream()
.map((ScopeRepresentation scope) -> toModel(scope, resourceServer, authorization))
.collect(Collectors.toSet()));
@@ -2356,9 +2363,11 @@ public class RepresentationToModel {
Resource model = resourceStore.create(resource.getName(), resourceServer, ownerId);
+ model.setDisplayName(resource.getDisplayName());
model.setType(resource.getType());
model.setUri(resource.getUri());
model.setIconUri(resource.getIconUri());
+ model.setOwnerManagedAccess(Boolean.TRUE.equals(resource.getOwnerManagedAccess()));
Set<ScopeRepresentation> scopes = resource.getScopes();
@@ -2384,17 +2393,35 @@ public class RepresentationToModel {
if (existing != null) {
existing.setName(scope.getName());
+ existing.setDisplayName(scope.getDisplayName());
existing.setIconUri(scope.getIconUri());
return existing;
}
Scope model = scopeStore.create(scope.getName(), resourceServer);
+
+ model.setDisplayName(scope.getDisplayName());
model.setIconUri(scope.getIconUri());
+
scope.setId(model.getId());
return model;
}
+ public static PermissionTicket toModel(PermissionTicketRepresentation representation, String resourceServerId, AuthorizationProvider authorization) {
+ PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore();
+ PermissionTicket ticket = ticketStore.findById(representation.getId(), resourceServerId);
+ boolean granted = representation.isGranted();
+
+ if (granted && !ticket.isGranted()) {
+ ticket.setGrantedTimestamp(System.currentTimeMillis());
+ } else if (!granted) {
+ ticket.setGrantedTimestamp(null);
+ }
+
+ return ticket;
+ }
+
public static void importFederatedUser(KeycloakSession session, RealmModel newRealm, UserRepresentation userRep) {
UserFederatedStorageProvider federatedStorage = session.userFederatedStorage();
if (userRep.getAttributes() != null) {
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
index d1acce0..3ba29dd 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
@@ -20,6 +20,7 @@ package org.keycloak.authorization.admin;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@@ -81,9 +82,6 @@ public class PolicyEvaluationService {
private final AuthorizationProvider authorization;
private final AdminPermissionEvaluator auth;
- @Context
- private HttpRequest httpRequest;
-
private final ResourceServer resourceServer;
PolicyEvaluationService(ResourceServer resourceServer, AuthorizationProvider authorization, AdminPermissionEvaluator auth) {
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java
index 17002ab..b5023b5 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java
@@ -24,9 +24,11 @@ import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.util.Permissions;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.DecisionEffect;
import org.keycloak.representations.idm.authorization.PolicyEvaluationResponse;
+import org.keycloak.representations.idm.authorization.PolicyEvaluationResponse.PolicyResultRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
@@ -108,7 +110,13 @@ public class PolicyEvaluationResponseBuilder {
List<PolicyEvaluationResponse.PolicyResultRepresentation> policies = new ArrayList<>();
for (Result.PolicyResult policy : result.getResults()) {
- policies.add(toRepresentation(policy, authorization));
+ PolicyResultRepresentation policyRep = toRepresentation(policy, authorization);
+
+ if ("resource".equals(policy.getPolicy().getType())) {
+ policyRep.getPolicy().setScopes(result.getPermission().getResource().getScopes().stream().map(Scope::getName).collect(Collectors.toSet()));
+ }
+
+ policies.add(policyRep);
}
rep.setPolicies(policies);
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index f4d685c..ff294c9 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -17,6 +17,34 @@
*/
package org.keycloak.authorization.admin;
+import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
+import static org.keycloak.models.utils.RepresentationToModel.toModel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
+
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@@ -40,32 +68,6 @@ import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriInfo;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
-import static org.keycloak.models.utils.RepresentationToModel.toModel;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -88,12 +90,32 @@ public class ResourceSetService {
@Consumes("application/json")
@Produces("application/json")
public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource) {
- Response response = create(resource);
+ return create(uriInfo, resource, (Function<Resource, ResourceRepresentation>) resource1 -> {
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setId(resource1.getId());
+
+ return representation;
+ });
+ }
+
+ public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource, Function<Resource, ?> toRepresentation) {
+ Response response = create(resource, toRepresentation);
audit(uriInfo, resource, resource.getId(), OperationType.CREATE);
return response;
}
public Response create(ResourceRepresentation resource) {
+ return create(resource, (Function<Resource, ResourceRepresentation>) resource1 -> {
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setId(resource1.getId());
+
+ return representation;
+ });
+ }
+
+ public Response create(ResourceRepresentation resource, Function<Resource, ?> toRepresentation) {
requireManage();
StoreFactory storeFactory = this.authorization.getStoreFactory();
Resource existingResource = storeFactory.getResourceStore().findByName(resource.getName(), this.resourceServer.getId());
@@ -114,11 +136,7 @@ public class ResourceSetService {
return ErrorResponse.exists("Resource with name [" + resource.getName() + "] already exists.");
}
- ResourceRepresentation representation = new ResourceRepresentation();
-
- representation.setId(toModel(resource, this.resourceServer, authorization).getId());
-
- return Response.status(Status.CREATED).entity(representation).build();
+ return Response.status(Status.CREATED).entity(toRepresentation.apply(toModel(resource, this.resourceServer, authorization))).build();
}
@Path("{id}")
@@ -179,6 +197,10 @@ public class ResourceSetService {
@NoCache
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
+ return findById(id, (Function<Resource, ResourceRepresentation>) resource -> toRepresentation(resource, resourceServer, authorization, true));
+ }
+
+ public Response findById(@PathParam("id") String id, Function<Resource, ?> toRepresentation) {
requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
Resource model = storeFactory.getResourceStore().findById(id, resourceServer.getId());
@@ -187,7 +209,7 @@ public class ResourceSetService {
return Response.status(Status.NOT_FOUND).build();
}
- return Response.ok(toRepresentation(model, this.resourceServer, authorization, true)).build();
+ return Response.ok(toRepresentation.apply(model)).build();
}
@Path("{id}/scopes")
@@ -295,14 +317,27 @@ public class ResourceSetService {
@NoCache
@Produces("application/json")
public Response find(@QueryParam("_id") String id,
- @QueryParam("name") String name,
- @QueryParam("uri") String uri,
- @QueryParam("owner") String owner,
- @QueryParam("type") String type,
- @QueryParam("scope") String scope,
- @QueryParam("deep") Boolean deep,
- @QueryParam("first") Integer firstResult,
- @QueryParam("max") Integer maxResult) {
+ @QueryParam("name") String name,
+ @QueryParam("uri") String uri,
+ @QueryParam("owner") String owner,
+ @QueryParam("type") String type,
+ @QueryParam("scope") String scope,
+ @QueryParam("deep") Boolean deep,
+ @QueryParam("first") Integer firstResult,
+ @QueryParam("max") Integer maxResult) {
+ return find(id, name, uri, owner, type, scope, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, ResourceRepresentation>) (resource, deep1) -> toRepresentation(resource, resourceServer, authorization, deep1));
+ }
+
+ public Response find(@QueryParam("_id") String id,
+ @QueryParam("name") String name,
+ @QueryParam("uri") String uri,
+ @QueryParam("owner") String owner,
+ @QueryParam("type") String type,
+ @QueryParam("scope") String scope,
+ @QueryParam("deep") Boolean deep,
+ @QueryParam("first") Integer firstResult,
+ @QueryParam("max") Integer maxResult,
+ BiFunction<Resource, Boolean, ?> toRepresentation) {
requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
@@ -363,7 +398,7 @@ public class ResourceSetService {
Boolean finalDeep = deep;
return Response.ok(
storeFactory.getResourceStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
- .map(resource -> toRepresentation(resource, resourceServer, authorization, finalDeep))
+ .map(resource -> toRepresentation.apply(resource, finalDeep))
.collect(Collectors.toList()))
.build();
}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
index 1ab3546..294951c 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
@@ -120,7 +120,7 @@ public class ScopeService {
List<Resource> resources = storeFactory.getResourceStore().findByScope(Arrays.asList(id), resourceServer.getId());
if (!resources.isEmpty()) {
- return ErrorResponse.exists("Scopes can not be removed while associated with resources.");
+ return ErrorResponse.error("Scopes can not be removed while associated with resources.", Status.BAD_REQUEST);
}
Scope scope = storeFactory.getScopeStore().findById(id, resourceServer.getId());
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
index c86dd45..a8075a0 100644
--- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
+++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
@@ -16,185 +16,335 @@
*/
package org.keycloak.authorization.authorization;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.authorization.representation.AuthorizationRequest;
-import org.keycloak.authorization.authorization.representation.AuthorizationResponse;
import org.keycloak.authorization.common.KeycloakEvaluationContext;
import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.PermissionTicketAwareDecisionResultCollector;
import org.keycloak.authorization.policy.evaluation.Result;
-import org.keycloak.authorization.protection.permission.PermissionTicket;
+import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.authorization.util.Permissions;
import org.keycloak.authorization.util.Tokens;
+import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.protocol.oidc.TokenManager.AccessTokenResponseBuilder;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessToken.Authorization;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.RefreshToken;
+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.ScopeRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionTicketToken;
+import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.Cors;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.OPTIONS;
-import javax.ws.rs.POST;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Objects;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class AuthorizationTokenService {
- protected static final Logger logger = Logger.getLogger(AuthorizationTokenService.class);
-
- private final AuthorizationProvider authorization;
- @Context
- private HttpRequest httpRequest;
+ private static final Logger logger = Logger.getLogger(AuthorizationTokenService.class);
+ private static Map<String, BiFunction<AuthorizationRequest, AuthorizationProvider, KeycloakEvaluationContext>> SUPPORTED_CLAIM_TOKEN_FORMATS;
- @Context
- private KeycloakSession session;
+ static {
+ SUPPORTED_CLAIM_TOKEN_FORMATS = new HashMap<>();
+ SUPPORTED_CLAIM_TOKEN_FORMATS.put("urn:ietf:params:oauth:token-type:jwt", (authorizationRequest, authorization) -> {
+ String claimToken = authorizationRequest.getClaimToken();
- public AuthorizationTokenService(AuthorizationProvider authorization) {
- this.authorization = authorization;
- }
+ if (claimToken == null) {
+ claimToken = authorizationRequest.getAccessToken();
+ }
- @OPTIONS
- public Response authorizepPreFlight() {
- return Cors.add(this.httpRequest, Response.ok()).auth().preflight().build();
+ return new KeycloakEvaluationContext(new KeycloakIdentity(authorization.getKeycloakSession(), Tokens.getAccessToken(claimToken, authorization.getKeycloakSession())), authorization.getKeycloakSession());
+ });
+ SUPPORTED_CLAIM_TOKEN_FORMATS.put("http://openid.net/specs/openid-connect-core-1_0.html#IDToken", (authorizationRequest, authorization) -> {
+ try {
+ KeycloakSession keycloakSession = authorization.getKeycloakSession();
+ IDToken idToken = new TokenManager().verifyIDTokenSignature(keycloakSession, authorization.getRealm(), authorizationRequest.getClaimToken());
+ return new KeycloakEvaluationContext(new KeycloakIdentity(keycloakSession, idToken), keycloakSession);
+ } catch (OAuthErrorException cause) {
+ throw new RuntimeException("Failed to verify ID token", cause);
+ }
+ });
}
- @POST
- @Consumes("application/json")
- @Produces("application/json")
- public Response authorize(AuthorizationRequest authorizationRequest) {
- KeycloakEvaluationContext evaluationContext = new KeycloakEvaluationContext(this.authorization.getKeycloakSession());
- KeycloakIdentity identity = (KeycloakIdentity) evaluationContext.getIdentity();
+ private final TokenManager tokenManager;
+ private final EventBuilder event;
+ private final HttpRequest httpRequest;
+ private final AuthorizationProvider authorization;
+ private final Cors cors;
- if (!identity.hasRealmRole("uma_authorization")) {
- throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_authorization scope.", Status.FORBIDDEN);
- }
+ public AuthorizationTokenService(AuthorizationProvider authorization, TokenManager tokenManager, EventBuilder event, HttpRequest httpRequest, Cors cors) {
+ this.tokenManager = tokenManager;
+ this.event = event;
+ this.httpRequest = httpRequest;
+ this.authorization = authorization;
+ this.cors = cors;
+ }
- if (authorizationRequest == null) {
- throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid authorization request.", Status.BAD_REQUEST);
+ public Response authorize(AuthorizationRequest request) {
+ if (request == null) {
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Invalid authorization request.", Status.BAD_REQUEST);
}
try {
- PermissionTicket ticket = verifyPermissionTicket(authorizationRequest);
- ResourceServer resourceServer = authorization.getStoreFactory().getResourceServerStore().findById(ticket.getResourceServerId());
-
- if (resourceServer == null) {
- throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN);
+ PermissionTicketToken ticket = getPermissionTicket(request);
+ ResourceServer resourceServer = getResourceServer(ticket);
+ KeycloakEvaluationContext evaluationContext = createEvaluationContext(request);
+ KeycloakIdentity identity = KeycloakIdentity.class.cast(evaluationContext.getIdentity());
+ List<Result> evaluation;
+
+ if (ticket.getResources().isEmpty() && request.getRpt() == null) {
+ evaluation = evaluateAllPermissions(resourceServer, evaluationContext, identity);
+ } else if(!request.getPermissions().getResources().isEmpty()) {
+ evaluation = evaluatePermissions(request, ticket, resourceServer, evaluationContext, identity);
+ } else {
+ evaluation = evaluateUserManagedPermissions(request, ticket, resourceServer, evaluationContext, identity);
}
- List<Result> result = authorization.evaluators().from(createPermissions(ticket, authorizationRequest, authorization), evaluationContext).evaluate();
+ List<Permission> permissions = Permissions.permits(evaluation, request.getMetadata(), authorization, resourceServer);
+
+ if (permissions.isEmpty()) {
+ if (request.isSubmitRequest()) {
+ throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "request_submitted", Status.FORBIDDEN);
+ } else {
+ throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "not_authorized", Status.FORBIDDEN);
+ }
+ }
- List<Permission> entitlements = Permissions.permits(result, authorizationRequest.getMetadata(), authorization, resourceServer);
+ ClientModel targetClient = this.authorization.getRealm().getClientById(resourceServer.getId());
+ AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(identity, permissions, targetClient), request.getRpt() != null);
- if (!entitlements.isEmpty()) {
- AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(entitlements, identity.getAccessToken(), resourceServer));
- return Cors.add(httpRequest, Response.status(Status.CREATED).entity(response)).allowedOrigins(identity.getAccessToken())
- .allowedMethods("POST")
- .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+ return Cors.add(httpRequest, Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response))
+ .allowedOrigins(getKeycloakSession().getContext().getUri(), targetClient)
+ .allowedMethods(HttpMethod.POST)
+ .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+ } catch (ErrorResponseException | CorsErrorResponseException cause) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Error while evaluating permissions", cause);
}
+ throw cause;
} catch (Exception cause) {
- logger.error("Failed to evaluate permissions", cause);
- throw new ErrorResponseException(OAuthErrorException.SERVER_ERROR, "Error while evaluating permissions.", Status.INTERNAL_SERVER_ERROR);
+ logger.error("Unexpected error while evaluating permissions", cause);
+ throw new CorsErrorResponseException(cors, OAuthErrorException.SERVER_ERROR, "Unexpected error while evaluating permissions", Status.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ private List<Result> evaluatePermissions(AuthorizationRequest authorizationRequest, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
+ return authorization.evaluators()
+ .from(createPermissions(ticket, authorizationRequest, resourceServer, authorization), evaluationContext)
+ .evaluate();
+ }
+
+ private List<Result> evaluateUserManagedPermissions(AuthorizationRequest request, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
+ return authorization.evaluators()
+ .from(createPermissions(ticket, request, resourceServer, authorization), evaluationContext)
+ .evaluate(new PermissionTicketAwareDecisionResultCollector(request, ticket, identity, resourceServer, authorization)).results();
+ }
+
+ private List<Result> evaluateAllPermissions(ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) {
+ return authorization.evaluators()
+ .from(Permissions.all(resourceServer, identity, authorization), evaluationContext)
+ .evaluate();
+ }
+
+ private AccessTokenResponse createRequestingPartyToken(KeycloakIdentity identity, List<Permission> entitlements, ClientModel targetClient) {
+ KeycloakSession keycloakSession = getKeycloakSession();
+ AccessToken accessToken = identity.getAccessToken();
+ UserSessionModel userSessionModel = keycloakSession.sessions().getUserSession(getRealm(), accessToken.getSessionState());
+ ClientModel client = getRealm().getClientByClientId(accessToken.getIssuedFor());
+ AuthenticatedClientSessionModel clientSession = userSessionModel.getAuthenticatedClientSessionByClient(client.getId());
+ AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(getRealm(), clientSession.getClient(), event, keycloakSession, userSessionModel, clientSession)
+ .generateAccessToken()
+ .generateRefreshToken();
+
+ AccessToken rpt = responseBuilder.getAccessToken();
+
+ rpt.issuedFor(client.getClientId());
+
+ Authorization authorization = new Authorization();
+
+ authorization.setPermissions(entitlements);
+
+ rpt.setAuthorization(authorization);
+
+ RefreshToken refreshToken = responseBuilder.getRefreshToken();
+
+ refreshToken.issuedFor(client.getClientId());
+ refreshToken.setAuthorization(authorization);
+
+ if (!rpt.hasAudience(targetClient.getClientId())) {
+ rpt.audience(targetClient.getClientId());
}
- HashMap<Object, Object> error = new HashMap<>();
+ return responseBuilder.build();
+ }
- error.put(OAuth2Constants.ERROR, "not_authorized");
+ private PermissionTicketToken getPermissionTicket(AuthorizationRequest request) {
+ // if there is a ticket is because it is a UMA flow and the ticket was sent by the client after obtaining it from the target resource server
+ if (request.getTicket() != null) {
+ return verifyPermissionTicket(request);
+ }
- return Cors.add(httpRequest, Response.status(Status.FORBIDDEN)
- .entity(error))
- .allowedOrigins(identity.getAccessToken())
- .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
+ // if there is no ticket, we use the permissions the client is asking for.
+ // This is a Keycloak extension to UMA flow where clients are capable of obtaining a RPT without a ticket
+ PermissionTicketToken permissions = request.getPermissions();
+
+ // an audience must be set by the client when doing this method of obtaining RPT, that is how we know the target resource server
+ permissions.audience(request.getAudience());
+
+ return permissions;
}
- private List<ResourcePermission> createPermissions(PermissionTicket ticket, AuthorizationRequest request, AuthorizationProvider authorization) {
+ private ResourceServer getResourceServer(PermissionTicketToken ticket) {
StoreFactory storeFactory = authorization.getStoreFactory();
- ResourceServer resourceServer = authorization.getStoreFactory().getResourceServerStore().findById(ticket.getResourceServerId());
+ ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
+ String[] audience = ticket.getAudience();
+
+ if (audience == null || audience.length == 0) {
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "You must provide the audience", Status.BAD_REQUEST);
+ }
+
+ ClientModel clientModel = getRealm().getClientByClientId(audience[0]);
+
+ if (clientModel == null) {
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Unknown resource server id.", Status.BAD_REQUEST);
+ }
+
+ ResourceServer resourceServer = resourceServerStore.findById(clientModel.getId());
if (resourceServer == null) {
- throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN);
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.BAD_REQUEST);
}
- Map<String, Set<String>> permissionsToEvaluate = new HashMap<>();
+ return resourceServer;
+ }
- ticket.getResources().forEach(requestedResource -> {
- Resource resource;
+ private KeycloakEvaluationContext createEvaluationContext(AuthorizationRequest authorizationRequest) {
+ String claimTokenFormat = authorizationRequest.getClaimTokenFormat();
- if (requestedResource.getId() != null) {
- resource = storeFactory.getResourceStore().findById(requestedResource.getId(), ticket.getResourceServerId());
- } else {
- resource = storeFactory.getResourceStore().findByName(requestedResource.getName(), ticket.getResourceServerId());
+ if (claimTokenFormat == null) {
+ claimTokenFormat = "urn:ietf:params:oauth:token-type:jwt";
+ }
+
+ BiFunction<AuthorizationRequest, AuthorizationProvider, KeycloakEvaluationContext> evaluationContextProvider = SUPPORTED_CLAIM_TOKEN_FORMATS.get(claimTokenFormat);
+
+ if (evaluationContextProvider == null) {
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Claim token format [" + claimTokenFormat + "] not supported", Status.BAD_REQUEST);
+ }
+
+ return evaluationContextProvider.apply(authorizationRequest, authorization);
+ }
+
+ private List<ResourcePermission> createPermissions(PermissionTicketToken ticket, AuthorizationRequest request, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Map<String, Set<String>> permissionsToEvaluate = new LinkedHashMap<>();
+ ResourceStore resourceStore = storeFactory.getResourceStore();
+ Metadata metadata = request.getMetadata();
+ Integer limit = metadata != null ? metadata.getLimit() : null;
+
+ for (PermissionTicketToken.ResourcePermission requestedResource : ticket.getResources()) {
+ if (limit != null && limit <= 0) {
+ break;
}
- if (resource == null && (requestedResource.getScopes() == null || requestedResource.getScopes().isEmpty())) {
- throw new ErrorResponseException("invalid_resource", "Resource with id [" + requestedResource.getId() + "] or name [" + requestedResource.getName() + "] does not exist.", Status.FORBIDDEN);
+ Set<String> requestedScopes = requestedResource.getScopes();
+
+ if (requestedResource.getScopes() == null) {
+ requestedScopes = new HashSet<>();
}
- Set<ScopeRepresentation> requestedScopes = requestedResource.getScopes();
- Set<String> collect = requestedScopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet());
+ Resource existingResource = null;
- if (resource != null) {
- permissionsToEvaluate.put(resource.getId(), collect);
- } else {
- ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
- ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore();
- List<Resource> resources = new ArrayList<Resource>();
+ if (requestedResource.getResourceId() != null) {
+ existingResource = resourceStore.findById(requestedResource.getResourceId(), resourceServer.getId());
+
+ if (existingResource == null) {
+ existingResource = resourceStore.findByName(requestedResource.getResourceId(), resourceServer.getId());
+ }
+ }
+
+ if (existingResource == null && (requestedScopes == null || requestedScopes.isEmpty())) {
+ throw new CorsErrorResponseException(cors, "invalid_resource", "Resource with id [" + requestedResource.getResourceId() + "] does not exist.", Status.FORBIDDEN);
+ }
+
+ String clientAdditionalScopes = request.getScope();
- resources.addAll(resourceStore.findByScope(requestedScopes.stream().map(scopeRepresentation -> {
- Scope scope = scopeStore.findByName(scopeRepresentation.getName(), ticket.getResourceServerId());
+ if (clientAdditionalScopes != null) {
+ requestedScopes.addAll(Arrays.asList(clientAdditionalScopes.split(" ")));
+ }
+
+ if (existingResource != null) {
+ Set<String> scopes = permissionsToEvaluate.get(existingResource.getId());
- if (scope == null) {
- return null;
+ if (scopes == null) {
+ scopes = new HashSet<>();
+ permissionsToEvaluate.put(existingResource.getId(), scopes);
+ if (limit != null) {
+ limit--;
}
+ }
- return scope.getId();
- }).filter(s -> s != null).collect(Collectors.toList()), ticket.getResourceServerId()));
+ scopes.addAll(requestedScopes);
+ } else {
+ List<Resource> resources = resourceStore.findByScope(new ArrayList<>(requestedScopes), ticket.getAudience()[0]);
- for (Resource resource1 : resources) {
- permissionsToEvaluate.put(resource1.getId(), collect);
+ for (Resource resource : resources) {
+ permissionsToEvaluate.put(resource.getId(), requestedScopes);
+ if (limit != null) {
+ limit--;
+ }
}
- permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", collect);
+ permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", requestedScopes);
}
- });
+ }
String rpt = request.getRpt();
- if (rpt != null && !"".equals(rpt)) {
- if (!Tokens.verifySignature(session, getRealm(), rpt)) {
- throw new ErrorResponseException("invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN);
+ if (rpt != null) {
+ if (!Tokens.verifySignature(getKeycloakSession(), getRealm(), rpt)) {
+ throw new CorsErrorResponseException(cors, "invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN);
}
AccessToken requestingPartyToken;
@@ -202,7 +352,7 @@ public class AuthorizationTokenService {
try {
requestingPartyToken = new JWSInput(rpt).readJsonContent(AccessToken.class);
} catch (JWSInputException e) {
- throw new ErrorResponseException("invalid_rpt", "Invalid RPT", Status.FORBIDDEN);
+ throw new CorsErrorResponseException(cors, "invalid_rpt", "Invalid RPT", Status.FORBIDDEN);
}
if (requestingPartyToken.isActive()) {
@@ -212,8 +362,12 @@ public class AuthorizationTokenService {
List<Permission> permissions = authorizationData.getPermissions();
if (permissions != null) {
- permissions.forEach(permission -> {
- Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId(), ticket.getResourceServerId());
+ for (Permission permission : permissions) {
+ if (limit != null && limit <= 0) {
+ break;
+ }
+
+ Resource resourcePermission = resourceStore.findById(permission.getResourceId(), ticket.getAudience()[0]);
if (resourcePermission != null) {
Set<String> scopes = permissionsToEvaluate.get(resourcePermission.getId());
@@ -221,6 +375,9 @@ public class AuthorizationTokenService {
if (scopes == null) {
scopes = new HashSet<>();
permissionsToEvaluate.put(resourcePermission.getId(), scopes);
+ if (limit != null) {
+ limit--;
+ }
}
Set<String> scopePermission = permission.getScopes();
@@ -229,63 +386,52 @@ public class AuthorizationTokenService {
scopes.addAll(scopePermission);
}
}
- });
+ }
}
}
}
}
+ ScopeStore scopeStore = storeFactory.getScopeStore();
+
return permissionsToEvaluate.entrySet().stream()
.flatMap((Function<Entry<String, Set<String>>, Stream<ResourcePermission>>) entry -> {
String key = entry.getKey();
-
if ("$KC_SCOPE_PERMISSION".equals(key)) {
- ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore();
List<Scope> scopes = entry.getValue().stream().map(scopeName -> scopeStore.findByName(scopeName, resourceServer.getId())).filter(scope -> Objects.nonNull(scope)).collect(Collectors.toList());
return Arrays.asList(new ResourcePermission(null, scopes, resourceServer)).stream();
} else {
- Resource entryResource = storeFactory.getResourceStore().findById(key, resourceServer.getId());
+ Resource entryResource = resourceStore.findById(key, resourceServer.getId());
return Permissions.createResourcePermissions(entryResource, entry.getValue(), authorization).stream();
}
}).collect(Collectors.toList());
}
- private RealmModel getRealm() {
- return this.authorization.getKeycloakSession().getContext().getRealm();
- }
-
- private String createRequestingPartyToken(List<Permission> permissions, AccessToken accessToken, ResourceServer resourceServer) {
- AccessToken.Authorization authorization = new AccessToken.Authorization();
-
- authorization.setPermissions(permissions);
- accessToken.setAuthorization(authorization);
-
- ClientModel clientModel = this.authorization.getRealm().getClientById(resourceServer.getId());
-
- if (!accessToken.hasAudience(clientModel.getClientId())) {
- accessToken.audience(clientModel.getClientId());
- }
-
- return new TokenManager().encodeToken(session, getRealm(), accessToken);
- }
-
- private PermissionTicket verifyPermissionTicket(AuthorizationRequest request) {
+ private PermissionTicketToken verifyPermissionTicket(AuthorizationRequest request) {
String ticketString = request.getTicket();
- if (ticketString == null || !Tokens.verifySignature(session, getRealm(), ticketString)) {
- throw new ErrorResponseException("invalid_ticket", "Ticket verification failed", Status.FORBIDDEN);
+ if (ticketString == null || !Tokens.verifySignature(getKeycloakSession(), getRealm(), ticketString)) {
+ throw new CorsErrorResponseException(cors, "invalid_ticket", "Ticket verification failed", Status.FORBIDDEN);
}
try {
- PermissionTicket ticket = new JWSInput(ticketString).readJsonContent(PermissionTicket.class);
+ PermissionTicketToken ticket = new JWSInput(ticketString).readJsonContent(PermissionTicketToken.class);
if (!ticket.isActive()) {
- throw new ErrorResponseException("invalid_ticket", "Invalid permission ticket.", Status.FORBIDDEN);
+ throw new CorsErrorResponseException(cors, "invalid_ticket", "Invalid permission ticket.", Status.FORBIDDEN);
}
return ticket;
} catch (JWSInputException e) {
- throw new ErrorResponseException("invalid_ticket", "Could not parse permission ticket.", Status.FORBIDDEN);
+ throw new CorsErrorResponseException(cors, "invalid_ticket", "Could not parse permission ticket.", Status.FORBIDDEN);
}
}
+
+ private KeycloakSession getKeycloakSession() {
+ return this.authorization.getKeycloakSession();
+ }
+
+ private RealmModel getRealm() {
+ return getKeycloakSession().getContext().getRealm();
+ }
}
diff --git a/services/src/main/java/org/keycloak/authorization/AuthorizationService.java b/services/src/main/java/org/keycloak/authorization/AuthorizationService.java
index f519b40..2d0d7d7 100644
--- a/services/src/main/java/org/keycloak/authorization/AuthorizationService.java
+++ b/services/src/main/java/org/keycloak/authorization/AuthorizationService.java
@@ -18,13 +18,11 @@
package org.keycloak.authorization;
+import javax.ws.rs.Path;
+
import org.jboss.resteasy.spi.ResteasyProviderFactory;
-import org.keycloak.authorization.authorization.AuthorizationTokenService;
-import org.keycloak.authorization.entitlement.EntitlementService;
import org.keycloak.authorization.protection.ProtectionService;
-import javax.ws.rs.Path;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -36,30 +34,12 @@ public class AuthorizationService {
this.authorization = authorization;
}
- @Path("/entitlement")
- public Object getEntitlementService() {
- EntitlementService service = new EntitlementService(this.authorization);
-
- ResteasyProviderFactory.getInstance().injectProperties(service);
-
- return service;
- }
-
@Path("/protection")
public Object getProtectionService() {
- ProtectionService service = new ProtectionService(this.authorization);
+ ProtectionService service = new ProtectionService(authorization);
ResteasyProviderFactory.getInstance().injectProperties(service);
return service;
}
-
- @Path("/authorize")
- public Object authorize() {
- AuthorizationTokenService resource = new AuthorizationTokenService(this.authorization);
-
- ResteasyProviderFactory.getInstance().injectProperties(resource);
-
- return resource;
- }
}
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
index da3cb0f..047ff5a 100644
--- a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
@@ -18,14 +18,14 @@
package org.keycloak.authorization.common;
-import org.keycloak.authorization.identity.Identity;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.representations.AccessToken;
-
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
+import org.keycloak.authorization.identity.Identity;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.representations.AccessToken;
+
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -33,10 +33,6 @@ public class KeycloakEvaluationContext extends DefaultEvaluationContext {
private final KeycloakIdentity identity;
- public KeycloakEvaluationContext(KeycloakSession keycloakSession) {
- this(new KeycloakIdentity(keycloakSession), keycloakSession);
- }
-
public KeycloakEvaluationContext(KeycloakIdentity identity, KeycloakSession keycloakSession) {
super(identity, keycloakSession);
this.identity = identity;
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
index 59963ba..654193d 100644
--- a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
@@ -22,11 +22,16 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.keycloak.authorization.attribute.Attributes;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.util.Tokens;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.IDToken;
import org.keycloak.saml.common.util.StringUtil;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.util.JsonSerialization;
@@ -35,9 +40,11 @@ import javax.ws.rs.core.Response.Status;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -53,12 +60,12 @@ public class KeycloakIdentity implements Identity {
this(Tokens.getAccessToken(keycloakSession), keycloakSession);
}
- public KeycloakIdentity(KeycloakSession keycloakSession, AccessToken accessToken) {
- this(accessToken, keycloakSession, keycloakSession.getContext().getRealm());
+ public KeycloakIdentity(KeycloakSession keycloakSession, IDToken token) {
+ this(token, keycloakSession, keycloakSession.getContext().getRealm());
}
- public KeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession, RealmModel realm) {
- if (accessToken == null) {
+ public KeycloakIdentity(IDToken token, KeycloakSession keycloakSession, RealmModel realm) {
+ if (token == null) {
throw new ErrorResponseException("invalid_bearer_token", "Could not obtain bearer access_token from request.", Status.FORBIDDEN);
}
if (keycloakSession == null) {
@@ -67,14 +74,13 @@ public class KeycloakIdentity implements Identity {
if (realm == null) {
throw new ErrorResponseException("no_keycloak_session", "No realm set", Status.FORBIDDEN);
}
- this.accessToken = accessToken;
this.keycloakSession = keycloakSession;
this.realm = realm;
Map<String, Collection<String>> attributes = new HashMap<>();
try {
- ObjectNode objectNode = JsonSerialization.createObjectNode(this.accessToken);
+ ObjectNode objectNode = JsonSerialization.createObjectNode(token);
Iterator<String> iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
@@ -103,13 +109,30 @@ public class KeycloakIdentity implements Identity {
}
}
- AccessToken.Access realmAccess = accessToken.getRealmAccess();
+ if (token instanceof AccessToken) {
+ this.accessToken = AccessToken.class.cast(token);
+ } else {
+ UserModel userById = keycloakSession.users().getUserById(token.getSubject(), realm);
+ UserSessionModel userSession = keycloakSession.sessions().getUserSession(realm, token.getSessionState());
+ ClientModel client = realm.getClientByClientId(token.getIssuedFor());
+ AuthenticatedClientSessionModel clientSessionModel = userSession.getAuthenticatedClientSessions().get(client.getId());
+ Set<RoleModel> requestedRoles = new HashSet<>();
+ for (String roleId : clientSessionModel.getRoles()) {
+ RoleModel role = realm.getRoleById(roleId);
+ if (role != null) {
+ requestedRoles.add(role);
+ }
+ }
+ this.accessToken = new TokenManager().createClientAccessToken(keycloakSession, requestedRoles, realm, client, userById, userSession, clientSessionModel);
+ }
+
+ AccessToken.Access realmAccess = this.accessToken.getRealmAccess();
if (realmAccess != null) {
attributes.put("kc.realm.roles", realmAccess.getRoles());
}
- Map<String, AccessToken.Access> resourceAccess = accessToken.getResourceAccess();
+ Map<String, AccessToken.Access> resourceAccess = this.accessToken.getResourceAccess();
if (resourceAccess != null) {
resourceAccess.forEach((clientId, access) -> attributes.put("kc.client." + clientId + ".roles", access.getRoles()));
diff --git a/services/src/main/java/org/keycloak/authorization/config/UmaConfiguration.java b/services/src/main/java/org/keycloak/authorization/config/UmaConfiguration.java
new file mode 100644
index 0000000..67fb296
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/config/UmaConfiguration.java
@@ -0,0 +1,89 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual 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.
+ */
+package org.keycloak.authorization.config;
+
+import java.net.URI;
+
+import javax.ws.rs.core.UriBuilder;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.authorization.AuthorizationService;
+import org.keycloak.authorization.protection.ProtectionService;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory;
+import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
+import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.wellknown.WellKnownProvider;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaConfiguration extends OIDCConfigurationRepresentation {
+
+ public static final UmaConfiguration create(KeycloakSession session) {
+ WellKnownProvider oidcProvider = session.getProvider(WellKnownProvider.class, OIDCWellKnownProviderFactory.PROVIDER_ID);
+ OIDCConfigurationRepresentation oidcConfig = OIDCConfigurationRepresentation.class.cast(oidcProvider.getConfig());
+ UmaConfiguration configuration = new UmaConfiguration();
+
+ configuration.setIssuer(oidcConfig.getIssuer());
+ configuration.setAuthorizationEndpoint(oidcConfig.getAuthorizationEndpoint());
+ configuration.setTokenEndpoint(oidcConfig.getTokenEndpoint());
+ configuration.setJwksUri(oidcConfig.getJwksUri());
+ configuration.setRegistrationEndpoint(oidcConfig.getRegistrationEndpoint());
+ configuration.setScopesSupported(oidcConfig.getScopesSupported());
+ configuration.setResponseTypesSupported(oidcConfig.getResponseTypesSupported());
+ configuration.setResponseModesSupported(oidcConfig.getResponseModesSupported());
+ configuration.setGrantTypesSupported(oidcConfig.getGrantTypesSupported());
+ configuration.setTokenEndpointAuthMethodsSupported(oidcConfig.getTokenEndpointAuthMethodsSupported());
+ configuration.setTokenEndpointAuthSigningAlgValuesSupported(oidcConfig.getTokenEndpointAuthSigningAlgValuesSupported());
+ configuration.setTokenIntrospectionEndpoint(oidcConfig.getTokenIntrospectionEndpoint());
+ configuration.setLogoutEndpoint(oidcConfig.getLogoutEndpoint());
+
+ UriBuilder uriBuilder = session.getContext().getUri().getBaseUriBuilder();
+
+ RealmModel realm = session.getContext().getRealm();
+
+ configuration.setPermissionEndpoint(uriBuilder.clone().path(RealmsResource.class).path(RealmsResource.class, "getAuthorizationService").path(AuthorizationService.class, "getProtectionService").path(ProtectionService.class, "permission").build(realm.getName()).toString());
+ configuration.setResourceRegistrationEndpoint(uriBuilder.clone().path(RealmsResource.class).path(RealmsResource.class, "getAuthorizationService").path(AuthorizationService.class, "getProtectionService").path(ProtectionService.class, "resource").build(realm.getName()).toString());
+
+ return configuration;
+ }
+
+ @JsonProperty("resource_registration_endpoint")
+ private String resourceRegistrationEndpoint;
+
+ @JsonProperty("permission_endpoint")
+ private String permissionEndpoint;
+
+ public String getResourceRegistrationEndpoint() {
+ return this.resourceRegistrationEndpoint;
+ }
+
+ void setResourceRegistrationEndpoint(String resourceRegistrationEndpoint) {
+ this.resourceRegistrationEndpoint = resourceRegistrationEndpoint;
+ }
+
+ public String getPermissionEndpoint() {
+ return this.permissionEndpoint;
+ }
+
+ void setPermissionEndpoint(String permissionEndpoint) {
+ this.permissionEndpoint = permissionEndpoint;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java
index 222e754..7410f45 100644
--- a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java
@@ -17,17 +17,9 @@
*/
package org.keycloak.authorization.config;
-import org.keycloak.common.util.PemUtils;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
-import org.keycloak.protocol.oidc.OIDCLoginProtocol;
-import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
-import org.keycloak.services.resources.RealmsResource;
import org.keycloak.wellknown.WellKnownProvider;
-import javax.ws.rs.core.UriInfo;
-import java.net.URI;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -41,13 +33,7 @@ public class UmaWellKnownProvider implements WellKnownProvider {
@Override
public Object getConfig() {
- RealmModel realm = this.session.getContext().getRealm();
- UriInfo uriInfo = this.session.getContext().getUri();
-
- return Configuration.fromDefault(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(), realm.getName(),
- URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "auth").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()),
- URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "token").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()),
- PemUtils.encodeKey(session.keys().getActiveRsaKey(realm).getPublicKey()));
+ return UmaConfiguration.create(session);
}
@Override
diff --git a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java
index 7776720..a5acd63 100644
--- a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java
+++ b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java
@@ -27,6 +27,9 @@ import org.keycloak.wellknown.WellKnownProviderFactory;
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class UmaWellKnownProviderFactory implements WellKnownProviderFactory {
+
+ public static final String PROVIDER_ID = "uma2-configuration";
+
@Override
public WellKnownProvider create(KeycloakSession session) {
return new UmaWellKnownProvider(session);
@@ -49,6 +52,6 @@ public class UmaWellKnownProviderFactory implements WellKnownProviderFactory {
@Override
public String getId() {
- return "uma-configuration";
+ return PROVIDER_ID;
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java
index a31e834..989dbe1 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java
@@ -17,6 +17,11 @@
*/
package org.keycloak.authorization.protection.introspect;
+import java.io.IOException;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
@@ -25,9 +30,6 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.util.JsonSerialization;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-
/**
* Introspects token accordingly with UMA Bearer Token Profile.
*
@@ -45,28 +47,30 @@ public class RPTIntrospectionProvider extends AccessTokenIntrospectionProvider {
public Response introspect(String token) {
LOGGER.debug("Introspecting requesting party token");
try {
- AccessToken requestingPartyToken = toAccessToken(token);
- boolean active = isActive(requestingPartyToken);
+ AccessToken accessToken = verifyAccessToken(token);
+
ObjectNode tokenMetadata;
- if (active) {
- LOGGER.debug("Token is active");
- AccessToken introspect = new AccessToken();
- introspect.type(requestingPartyToken.getType());
- introspect.expiration(requestingPartyToken.getExpiration());
- introspect.issuedAt(requestingPartyToken.getIssuedAt());
- introspect.audience(requestingPartyToken.getAudience());
- introspect.notBefore(requestingPartyToken.getNotBefore());
- introspect.setRealmAccess(null);
- introspect.setResourceAccess(null);
- tokenMetadata = JsonSerialization.createObjectNode(introspect);
- tokenMetadata.putPOJO("permissions", requestingPartyToken.getAuthorization().getPermissions());
+ if (accessToken != null) {
+ AccessToken metadata = new AccessToken();
+
+ metadata.id(accessToken.getId());
+ metadata.setAcr(accessToken.getAcr());
+ metadata.type(accessToken.getType());
+ metadata.expiration(accessToken.getExpiration());
+ metadata.issuedAt(accessToken.getIssuedAt());
+ metadata.audience(accessToken.getAudience());
+ metadata.notBefore(accessToken.getNotBefore());
+ metadata.setRealmAccess(null);
+ metadata.setResourceAccess(null);
+
+ tokenMetadata = JsonSerialization.createObjectNode(metadata);
+ tokenMetadata.putPOJO("permissions", accessToken.getAuthorization().getPermissions());
} else {
- LOGGER.debug("Token is not active");
tokenMetadata = JsonSerialization.createObjectNode();
}
- tokenMetadata.put("active", active);
+ tokenMetadata.put("active", accessToken != null);
return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).type(MediaType.APPLICATION_JSON_TYPE).build();
} catch (Exception e) {
@@ -74,11 +78,6 @@ public class RPTIntrospectionProvider extends AccessTokenIntrospectionProvider {
}
}
- private boolean isActive(AccessToken requestingPartyToken) {
- Authorization authorization = requestingPartyToken.getAuthorization();
- return requestingPartyToken.isActive() && authorization != null && authorization.getPermissions() != null && !authorization.getPermissions().isEmpty();
- }
-
@Override
public void close() {
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
index 1e669cf..7a8e05e 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
@@ -21,16 +21,23 @@ import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
-import org.keycloak.authorization.protection.permission.representation.PermissionRequest;
-import org.keycloak.authorization.protection.permission.representation.PermissionResponse;
-import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.ClientModel;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
+import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.KeyManager;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionTicketToken;
+import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
import javax.ws.rs.core.Response;
+
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@@ -51,53 +58,46 @@ public class AbstractPermissionService {
}
public Response create(List<PermissionRequest> request) {
- if (request == null) {
+ if (request == null || request.isEmpty()) {
throw new ErrorResponseException("invalid_permission_request", "Invalid permission request.", Response.Status.BAD_REQUEST);
}
- List<ResourceRepresentation> resource = verifyRequestedResource(request);
-
- return Response.status(Response.Status.CREATED).entity(new PermissionResponse(createPermissionTicket(resource))).build();
+ return Response.status(Response.Status.CREATED).entity(new PermissionResponse(createPermissionTicket(request))).build();
}
private List<ResourceRepresentation> verifyRequestedResource(List<PermissionRequest> request) {
- StoreFactory storeFactory = authorization.getStoreFactory();
- return request.stream().map(request1 -> {
- String resourceSetId = request1.getResourceSetId();
- String resourceSetName = request1.getResourceSetName();
- boolean resourceNotProvided = resourceSetId == null && resourceSetName == null;
-
- if (resourceNotProvided) {
- if ((request1.getScopes() == null || request1.getScopes().isEmpty())) {
- throw new ErrorResponseException("invalid_resource_set_id", "Resource id or name not provided.", Response.Status.BAD_REQUEST);
- }
- }
+ ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
+ return request.stream().map(permissionRequest -> {
+ String resourceSetId = permissionRequest.getResourceId();
Resource resource = null;
- if (!resourceNotProvided) {
- if (resourceSetId != null) {
- resource = storeFactory.getResourceStore().findById(resourceSetId, resourceServer.getId());
- } else {
- resource = storeFactory.getResourceStore().findByName(resourceSetName, this.resourceServer.getId());
+ if (resourceSetId == null) {
+ if (permissionRequest.getScopes() == null || permissionRequest.getScopes().isEmpty()) {
+ throw new ErrorResponseException("invalid_resource_id", "Resource id or name not provided.", Response.Status.BAD_REQUEST);
}
+ } else {
+ resource = resourceStore.findById(resourceSetId, resourceServer.getId());
if (resource == null) {
- if (resourceSetId != null) {
- throw new ErrorResponseException("nonexistent_resource_set_id", "Resource set with id[" + resourceSetId + "] does not exists in this server.", Response.Status.BAD_REQUEST);
- } else {
- throw new ErrorResponseException("nonexistent_resource_set_name", "Resource set with name[" + resourceSetName + "] does not exists in this server.", Response.Status.BAD_REQUEST);
- }
+ resource = resourceStore.findByName(resourceSetId, this.resourceServer.getId());
+ }
+
+ if (resource == null) {
+ throw new ErrorResponseException("invalid_resource_id", "Resource set with id [" + resourceSetId + "] does not exists in this server.", Response.Status.BAD_REQUEST);
}
}
- Set<ScopeRepresentation> scopes = verifyRequestedScopes(request1, resource);
+ Set<ScopeRepresentation> scopes = verifyRequestedScopes(permissionRequest, resource);
if (resource != null) {
- if (scopes.isEmpty() && !request1.getScopes().isEmpty()) {
- return new ResourceRepresentation(null, request1.getScopes().stream().map(ScopeRepresentation::new).collect(Collectors.toSet()));
- }
- return new ResourceRepresentation(resource.getName(), scopes);
+ ResourceRepresentation representation = new ResourceRepresentation(resource.getName(), scopes);
+
+ representation.setId(resource.getId());
+ representation.setOwnerManagedAccess(resource.isOwnerManagedAccess());
+ representation.setOwner(new ResourceOwnerRepresentation(resource.getOwner()));
+
+ return representation;
}
return new ResourceRepresentation(null, scopes);
@@ -105,34 +105,52 @@ public class AbstractPermissionService {
}
private Set<ScopeRepresentation> verifyRequestedScopes(PermissionRequest request, Resource resource) {
- return request.getScopes().stream().map(scopeName -> {
+ Set<String> requestScopes = request.getScopes();
+
+ if (requestScopes == null) {
+ return Collections.emptySet();
+ }
+
+ ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
+
+ return requestScopes.stream().map(scopeName -> {
+ Scope scope = null;
+
if (resource != null) {
- for (Scope scope : resource.getScopes()) {
- if (scope.getName().equals(scopeName)) {
- return new ScopeRepresentation(scopeName);
- }
- }
+ scope = resource.getScopes().stream().filter(scope1 -> scope1.getName().equals(scopeName)).findFirst().orElse(null);
- for (Resource baseResource : authorization.getStoreFactory().getResourceStore().findByType(resource.getType(), resourceServer.getId())) {
- if (baseResource.getOwner().equals(resource.getResourceServer().getId())) {
- for (Scope baseScope : baseResource.getScopes()) {
- if (baseScope.getName().equals(scopeName)) {
- return new ScopeRepresentation(scopeName);
- }
- }
- }
+ if (scope == null && resource.getType() != null) {
+ scope = resourceStore.findByType(resource.getType(), resourceServer.getId()).stream()
+ .filter(baseResource -> baseResource.getOwner().equals(resource.getResourceServer().getId()))
+ .flatMap(resource1 -> resource1.getScopes().stream())
+ .filter(baseScope -> baseScope.getName().equals(scopeName)).findFirst().orElse(null);
}
-
- return null;
} else {
- return new ScopeRepresentation(scopeName);
+ scope = authorization.getStoreFactory().getScopeStore().findByName(scopeName, resourceServer.getId());
+ }
+
+ if (scope == null) {
+ throw new ErrorResponseException("invalid_scope", "Scope [" + scopeName + "] is invalid", Response.Status.BAD_REQUEST);
}
- }).filter(scopeRepresentation -> scopeRepresentation != null).collect(Collectors.toSet());
+
+ return ModelToRepresentation.toRepresentation(scope);
+ }).collect(Collectors.toSet());
}
- private String createPermissionTicket(List<ResourceRepresentation> resources) {
+ private String createPermissionTicket(List<PermissionRequest> request) {
+ List<PermissionTicketToken.ResourcePermission> permissions = verifyRequestedResource(request).stream().flatMap(resource -> {
+ List<PermissionTicketToken.ResourcePermission> perms = new ArrayList<>();
+ Set<ScopeRepresentation> scopes = resource.getScopes();
+
+ perms.add(new PermissionTicketToken.ResourcePermission(resource.getId(), scopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet())));
+
+ return perms.stream();
+ }).collect(Collectors.toList());
+
KeyManager.ActiveRsaKey keys = this.authorization.getKeycloakSession().keys().getActiveRsaKey(this.authorization.getRealm());
- return new JWSBuilder().kid(keys.getKid()).jsonContent(new PermissionTicket(resources, this.resourceServer.getId(), this.identity.getAccessToken()))
+ ClientModel targetClient = authorization.getRealm().getClientById(resourceServer.getId());
+
+ return new JWSBuilder().kid(keys.getKid()).jsonContent(new PermissionTicketToken(permissions, targetClient.getClientId(), this.identity.getAccessToken()))
.rsa256(keys.getPrivateKey());
}
-}
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java
index 4f2181f..e30f7d0 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java
@@ -17,31 +17,129 @@
*/
package org.keycloak.authorization.protection.permission;
+import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.common.KeycloakIdentity;
+import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.ResourceServer;
-import org.keycloak.authorization.protection.permission.representation.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.authorization.store.PermissionTicketStore;
+import org.keycloak.models.Constants;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
+import org.keycloak.services.ErrorResponseException;
import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
-import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PermissionService extends AbstractPermissionService {
+ private final AuthorizationProvider authorization;
+ private final ResourceServer resourceServer;
+
public PermissionService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization) {
super(identity, resourceServer, authorization);
+ this.resourceServer = resourceServer;
+ this.authorization = authorization;
}
@POST
@Consumes("application/json")
@Produces("application/json")
- public Response create(PermissionRequest request) {
- return create(Arrays.asList(request));
+ public Response create(List<PermissionRequest> request) {
+ return super.create(request);
+ }
+
+ @PUT
+ @Consumes("application/json")
+ public Response update(PermissionTicketRepresentation representation) {
+ if (representation == null || representation.getId() == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST);
+ }
+
+ PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore();
+ PermissionTicket ticket = ticketStore.findById(representation.getId(), resourceServer.getId());
+
+ if (ticket == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST);
+ }
+
+ RepresentationToModel.toModel(representation, resourceServer.getId(), authorization);
+
+ return Response.noContent().build();
+ }
+
+ @DELETE
+ @Consumes("application/json")
+ public Response delete(String id) {
+ if (id == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST);
+ }
+
+ PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore();
+ PermissionTicket ticket = ticketStore.findById(id, resourceServer.getId());
+
+ if (ticket == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "invalid_ticket", Response.Status.BAD_REQUEST);
+ }
+
+ ticketStore.delete(id);
+
+ return Response.noContent().build();
}
+ @GET
+ @Produces("application/json")
+ public Response find(@QueryParam("scopeId") String scopeId,
+ @QueryParam("resourceId") String resourceId,
+ @QueryParam("owner") String owner,
+ @QueryParam("requester") String requester,
+ @QueryParam("granted") Boolean granted,
+ @QueryParam("returnNames") Boolean returnNames,
+ @QueryParam("first") Integer firstResult,
+ @QueryParam("max") Integer maxResult) {
+ PermissionTicketStore permissionTicketStore = authorization.getStoreFactory().getPermissionTicketStore();
+
+ Map<String, String> filters = new HashMap<>();
+
+ if (resourceId != null) {
+ filters.put(PermissionTicket.RESOURCE, resourceId);
+ }
+
+ if (scopeId != null) {
+ filters.put(PermissionTicket.SCOPE, scopeId);
+ }
+
+ if (owner != null) {
+ filters.put(PermissionTicket.OWNER, owner);
+ }
+
+ if (requester != null) {
+ filters.put(PermissionTicket.REQUESTER, requester);
+ }
+
+ if (granted != null) {
+ filters.put(PermissionTicket.GRANTED, granted.toString());
+ }
+
+ return Response.ok().entity(permissionTicketStore.find(filters, resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS)
+ .stream()
+ .map(permissionTicket -> ModelToRepresentation.toRepresentation(permissionTicket, returnNames == null ? false : returnNames))
+ .collect(Collectors.toList()))
+ .build();
+ }
}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java
index eea2108..37944d2 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java
@@ -20,7 +20,7 @@ package org.keycloak.authorization.protection.permission;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.model.ResourceServer;
-import org.keycloak.authorization.protection.permission.representation.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
diff --git a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
index 30afbc7..8a811f1 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
@@ -22,10 +22,8 @@ import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.admin.ResourceSetService;
import org.keycloak.authorization.common.KeycloakIdentity;
-import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.protection.permission.PermissionService;
-import org.keycloak.authorization.protection.permission.PermissionsService;
import org.keycloak.authorization.protection.resource.ResourceService;
import org.keycloak.common.ClientConnection;
import org.keycloak.models.ClientModel;
@@ -46,6 +44,7 @@ import javax.ws.rs.core.Response.Status;
public class ProtectionService {
private final AuthorizationProvider authorization;
+
@Context
protected ClientConnection clientConnection;
@@ -55,7 +54,7 @@ public class ProtectionService {
@Path("/resource_set")
public Object resource() {
- KeycloakIdentity identity = createIdentity();
+ KeycloakIdentity identity = createIdentity(true);
ResourceServer resourceServer = getResourceServer(identity);
RealmModel realm = authorization.getRealm();
ClientModel client = realm.getClientById(identity.getId());
@@ -75,7 +74,7 @@ public class ProtectionService {
@Path("/permission")
public Object permission() {
- KeycloakIdentity identity = createIdentity();
+ KeycloakIdentity identity = createIdentity(false);
PermissionService resource = new PermissionService(identity, getResourceServer(identity), this.authorization);
@@ -84,43 +83,39 @@ public class ProtectionService {
return resource;
}
- @Path("/permissions")
- public Object permissions() {
- KeycloakIdentity identity = createIdentity();
-
- PermissionsService resource = new PermissionsService(identity, getResourceServer(identity), this.authorization);
-
- ResteasyProviderFactory.getInstance().injectProperties(resource);
-
- return resource;
- }
-
- private KeycloakIdentity createIdentity() {
+ private KeycloakIdentity createIdentity(boolean checkProtectionScope) {
KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession());
ResourceServer resourceServer = getResourceServer(identity);
KeycloakSession keycloakSession = authorization.getKeycloakSession();
RealmModel realm = keycloakSession.getContext().getRealm();
ClientModel client = realm.getClientById(resourceServer.getId());
- if (!identity.hasClientRole(client.getClientId(), "uma_protection")) {
- throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN);
+ if (checkProtectionScope) {
+ if (!identity.hasClientRole(client.getClientId(), "uma_protection")) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN);
+ }
}
return identity;
}
- private ResourceServer getResourceServer(Identity identity) {
- RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm();
- ClientModel clientApplication = realm.getClientById(identity.getId());
+ private ResourceServer getResourceServer(KeycloakIdentity identity) {
+ String clientId = identity.getAccessToken().getIssuedFor();
+ RealmModel realm = authorization.getKeycloakSession().getContext().getRealm();
+ ClientModel clientModel = realm.getClientByClientId(clientId);
+
+ if (clientModel == null) {
+ clientModel = realm.getClientById(clientId);
- if (clientApplication == null) {
- throw new ErrorResponseException("invalid_clientId", "Client application with id [" + identity.getId() + "] does not exist in realm [" + realm.getName() + "]", Status.BAD_REQUEST);
+ if (clientModel == null) {
+ throw new ErrorResponseException("invalid_clientId", "Client application with id [" + clientId + "] does not exist in realm [" + realm.getName() + "]", Status.BAD_REQUEST);
+ }
}
- ResourceServer resourceServer = this.authorization.getStoreFactory().getResourceServerStore().findById(identity.getId());
+ ResourceServer resourceServer = this.authorization.getStoreFactory().getResourceServerStore().findById(clientModel.getId());
if (resourceServer == null) {
- throw new ErrorResponseException("invalid_clientId", "Client application [" + clientApplication.getClientId() + "] is not registered as resource server.", Status.FORBIDDEN);
+ throw new ErrorResponseException("invalid_clientId", "Client application [" + clientModel.getClientId() + "] is not registered as a resource server.", Status.FORBIDDEN);
}
return resourceServer;
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java
index 2997498..fbbe08e 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java
@@ -41,11 +41,14 @@ public class UmaResourceRepresentation {
private String type;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @JsonProperty("resource_scopes")
private Set<UmaScopeRepresentation> scopes;
@JsonProperty("icon_uri")
private String iconUri;
private String owner;
+ private Boolean ownerManagedAccess;
+
/**
* Creates a new instance.
@@ -150,4 +153,12 @@ public class UmaResourceRepresentation {
public void setOwner(String owner) {
this.owner = owner;
}
+
+ public void setOwnerManagedAccess(Boolean ownerManagedAccess) {
+ this.ownerManagedAccess = ownerManagedAccess;
+ }
+
+ public Boolean getOwnerManagedAccess() {
+ return ownerManagedAccess;
+ }
}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
index c2e11dc..0cead7d 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
@@ -17,9 +17,8 @@
*/
package org.keycloak.authorization.protection.resource;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
import java.util.stream.Collectors;
import javax.ws.rs.Consumes;
@@ -36,6 +35,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
+import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.admin.ResourceSetService;
import org.keycloak.authorization.identity.Identity;
@@ -43,8 +43,6 @@ import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.protection.resource.representation.UmaResourceRepresentation;
import org.keycloak.authorization.protection.resource.representation.UmaScopeRepresentation;
-import org.keycloak.authorization.store.StoreFactory;
-import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
@@ -72,14 +70,10 @@ public class ResourceService {
@Produces("application/json")
public Response create(@Context UriInfo uriInfo, UmaResourceRepresentation umaResource) {
checkResourceServerSettings();
- ResourceRepresentation resource = toResourceRepresentation(umaResource);
- Response response = this.resourceManager.create(uriInfo, resource);
-
- if (response.getEntity() instanceof ResourceRepresentation) {
- return Response.status(Status.CREATED).entity(toUmaRepresentation((ResourceRepresentation) response.getEntity())).build();
+ if (umaResource == null) {
+ return Response.status(Status.BAD_REQUEST).build();
}
-
- return response;
+ return this.resourceManager.create(uriInfo, toResourceRepresentation(umaResource), (Function<Resource, UmaResourceRepresentation>) this::toUmaRepresentation);
}
@Path("{id}")
@@ -107,79 +101,23 @@ public class ResourceService {
@Path("/{id}")
@GET
@Produces("application/json")
- public RegistrationResponse findById(@PathParam("id") String id) {
- Response response = this.resourceManager.findById(id);
- UmaResourceRepresentation resource = toUmaRepresentation((ResourceRepresentation) response.getEntity());
-
- if (resource == null) {
- throw new ErrorResponseException("not_found", "Resource with id [" + id + "] not found.", Status.NOT_FOUND);
- }
-
- return new RegistrationResponse(resource);
+ public Response findById(@PathParam("id") String id) {
+ return this.resourceManager.findById(id, (Function<Resource, UmaResourceRepresentation>) resource -> toUmaRepresentation(resource));
}
@GET
+ @NoCache
@Produces("application/json")
- public Set<String> find(@QueryParam("filter") String filter) {
- if (filter == null) {
- return findAll();
- } else {
- return findByFilter(filter);
- }
- }
-
- private Set<String> findAll() {
- Response response = this.resourceManager.find(null, null, null, null, null, null, true, -1, -1);
- List<ResourceRepresentation> resources = (List<ResourceRepresentation>) response.getEntity();
- return resources.stream().map(ResourceRepresentation::getId).collect(Collectors.toSet());
- }
-
- private Set<String> findByFilter(String filter) {
- Set<ResourceRepresentation> resources = new HashSet<>();
- StoreFactory storeFactory = authorization.getStoreFactory();
-
- if (filter != null) {
- for (String currentFilter : filter.split("&")) {
- String[] parts = currentFilter.split("=");
- String filterType = parts[0];
- final String filterValue;
-
- if (parts.length > 1) {
- filterValue = parts[1];
- } else {
- filterValue = null;
- }
-
-
- if ("name".equals(filterType)) {
- Resource resource = storeFactory.getResourceStore().findByName(filterValue, this.resourceServer.getId());
-
- if (resource != null) {
- resources.add(ModelToRepresentation.toRepresentation(resource, resourceServer, authorization));
- }
- } else if ("type".equals(filterType)) {
- resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getType())).collect(Collectors.toSet()).stream()
- .map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization))
- .collect(Collectors.toList()));
- } else if ("uri".equals(filterType)) {
- resources.addAll(storeFactory.getResourceStore().findByUri(filterValue, this.resourceServer.getId()).stream()
- .map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization))
- .collect(Collectors.toList()));
- } else if ("owner".equals(filterType)) {
- resources.addAll(storeFactory.getResourceStore().findByOwner(filterValue, this.resourceServer.getId()).stream()
- .map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization))
- .collect(Collectors.toList()));
- }
- }
- } else {
- resources = storeFactory.getResourceStore().findByOwner(identity.getId(), resourceServer.getId()).stream()
- .map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization))
- .collect(Collectors.toSet());
- }
-
- return resources.stream()
- .map(ResourceRepresentation::getId)
- .collect(Collectors.toSet());
+ public Response find(@QueryParam("_id") String id,
+ @QueryParam("name") String name,
+ @QueryParam("uri") String uri,
+ @QueryParam("owner") String owner,
+ @QueryParam("type") String type,
+ @QueryParam("scope") String scope,
+ @QueryParam("deep") Boolean deep,
+ @QueryParam("first") Integer firstResult,
+ @QueryParam("max") Integer maxResult) {
+ return resourceManager.find(id, name, uri, owner, type, scope, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, String>) (resource, deep1) -> resource.getId());
}
private ResourceRepresentation toResourceRepresentation(UmaResourceRepresentation umaResource) {
@@ -190,6 +128,7 @@ public class ResourceService {
resource.setName(umaResource.getName());
resource.setUri(umaResource.getUri());
resource.setType(umaResource.getType());
+ resource.setOwnerManagedAccess(umaResource.getOwnerManagedAccess());
ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation();
String ownerId = umaResource.getOwner();
@@ -214,24 +153,24 @@ public class ResourceService {
return resource;
}
- private UmaResourceRepresentation toUmaRepresentation(ResourceRepresentation representation) {
- if (representation == null) {
+ private UmaResourceRepresentation toUmaRepresentation(Resource model) {
+ if (model == null) {
return null;
}
UmaResourceRepresentation resource = new UmaResourceRepresentation();
- resource.setId(representation.getId());
- resource.setIconUri(representation.getIconUri());
- resource.setName(representation.getName());
- resource.setUri(representation.getUri());
- resource.setType(representation.getType());
+ resource.setId(model.getId());
+ resource.setIconUri(model.getIconUri());
+ resource.setName(model.getName());
+ resource.setUri(model.getUri());
+ resource.setType(model.getType());
- if (representation.getOwner() != null) {
- resource.setOwner(representation.getOwner().getId());
+ if (model.getOwner() != null) {
+ resource.setOwner(model.getOwner());
}
- resource.setScopes(representation.getScopes().stream().map(scopeRepresentation -> {
+ resource.setScopes(model.getScopes().stream().map(scopeRepresentation -> {
UmaScopeRepresentation umaScopeRep = new UmaScopeRepresentation();
umaScopeRep.setId(scopeRepresentation.getId());
umaScopeRep.setName(scopeRepresentation.getName());
diff --git a/services/src/main/java/org/keycloak/authorization/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
index 0c285bc..38b65b8 100644
--- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java
+++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
@@ -27,11 +27,11 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+
import javax.ws.rs.core.Response.Status;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision.Effect;
-import org.keycloak.authorization.authorization.representation.AuthorizationRequestMetadata;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
@@ -42,6 +42,7 @@ import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.services.ErrorResponseException;
@@ -142,7 +143,11 @@ public final class Permissions {
return permissions;
}
- public static List<Permission> permits(List<Result> evaluation, AuthorizationRequestMetadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) {
+ public static List<Permission> permits(List<Result> evaluation, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) {
+ return permits(evaluation, null, authorizationProvider, resourceServer);
+ }
+
+ public static List<Permission> permits(List<Result> evaluation, Metadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) {
Map<String, Permission> permissions = new LinkedHashMap<>();
for (Result result : evaluation) {
@@ -159,8 +164,12 @@ public final class Permissions {
if (Effect.PERMIT.equals(policyResult.getStatus())) {
if (isScopePermission(policy)) {
- // try to grant any scope from a scope-based permission
- grantedScopes.addAll(policyScopes);
+ for (Scope scope : permission.getScopes()) {
+ if (policyScopes.contains(scope)) {
+ // try to grant any scope from a scope-based permission
+ grantedScopes.add(scope);
+ }
+ }
} else if (isResourcePermission(policy)) {
// we assume that all requested scopes should be granted given that we are processing a resource-based permission.
// Later they will be filtered based on any denied scope, if any.
@@ -173,38 +182,35 @@ public final class Permissions {
// store all scopes associated with the scope-based permission
deniedScopes.addAll(policyScopes);
} else if (isResourcePermission(policy)) {
- // we should not grant anything
resourceDenied = true;
- break;
+ deniedScopes.addAll(permission.getResource().getScopes());
}
}
}
- if (!resourceDenied) {
- // remove any scope denied from the list of granted scopes
- if (!deniedScopes.isEmpty()) {
- grantedScopes.removeAll(deniedScopes);
- }
+ // remove any scope denied from the list of granted scopes
+ if (!deniedScopes.isEmpty()) {
+ grantedScopes.removeAll(deniedScopes);
+ }
- // if there are no policy results is because the permission didn't match any policy.
- // In this case, if results is empty is because we are in permissive mode.
- if (!results.isEmpty()) {
- // update the current permission with the granted scopes
- permission.getScopes().clear();
- permission.getScopes().addAll(grantedScopes);
- }
+ // if there are no policy results is because the permission didn't match any policy.
+ // In this case, if results is empty is because we are in permissive mode.
+ if (!results.isEmpty()) {
+ // update the current permission with the granted scopes
+ permission.getScopes().clear();
+ permission.getScopes().addAll(grantedScopes);
+ }
- if (deniedCount == 0) {
+ if (deniedCount == 0) {
+ result.setStatus(Effect.PERMIT);
+ grantPermission(authorizationProvider, permissions, permission, resourceServer, metadata);
+ } else {
+ // if a full deny or resource denied or the requested scopes were denied
+ if (deniedCount == results.size() || resourceDenied || (!deniedScopes.isEmpty() && grantedScopes.isEmpty())) {
+ result.setStatus(Effect.DENY);
+ } else {
result.setStatus(Effect.PERMIT);
grantPermission(authorizationProvider, permissions, permission, resourceServer, metadata);
- } else {
- // if a full deny or resource denied or the requested scopes were denied
- if (deniedCount == results.size() || resourceDenied || (!deniedScopes.isEmpty() && grantedScopes.isEmpty())) {
- result.setStatus(Effect.DENY);
- } else {
- result.setStatus(Effect.PERMIT);
- grantPermission(authorizationProvider, permissions, permission, resourceServer, metadata);
- }
}
}
}
@@ -220,7 +226,7 @@ public final class Permissions {
return "scope".equals(policy.getType());
}
- private static void grantPermission(AuthorizationProvider authorizationProvider, Map<String, Permission> permissions, ResourcePermission permission, ResourceServer resourceServer, AuthorizationRequestMetadata metadata) {
+ private static void grantPermission(AuthorizationProvider authorizationProvider, Map<String, Permission> permissions, ResourcePermission permission, ResourceServer resourceServer, Metadata metadata) {
List<Resource> resources = new ArrayList<>();
Resource resource = permission.getResource();
Set<String> scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet());
@@ -239,7 +245,7 @@ public final class Permissions {
if (!resources.isEmpty()) {
for (Resource allowedResource : resources) {
String resourceId = allowedResource.getId();
- String resourceName = metadata == null || metadata.isIncludeResourceName() ? allowedResource.getName() : null;
+ String resourceName = metadata == null || metadata.getIncludeResourceName() ? allowedResource.getName() : null;
Permission evalPermission = permissions.get(allowedResource.getId());
if (evalPermission == null) {
diff --git a/services/src/main/java/org/keycloak/authorization/util/Tokens.java b/services/src/main/java/org/keycloak/authorization/util/Tokens.java
index d693a4c..745d7c7 100644
--- a/services/src/main/java/org/keycloak/authorization/util/Tokens.java
+++ b/services/src/main/java/org/keycloak/authorization/util/Tokens.java
@@ -48,10 +48,16 @@ public class Tokens {
return null;
}
- public static String getAccessTokenAsString(KeycloakSession keycloakSession) {
+ public static AccessToken getAccessToken(String accessToken, KeycloakSession keycloakSession) {
AppAuthManager authManager = new AppAuthManager();
+ KeycloakContext context = keycloakSession.getContext();
+ AuthResult authResult = authManager.authenticateBearerToken(accessToken, keycloakSession, context.getRealm(), context.getUri(), context.getConnection(), context.getRequestHeaders());
+
+ if (authResult != null) {
+ return authResult.getToken();
+ }
- return authManager.extractAuthorizationHeaderToken(keycloakSession.getContext().getRequestHeaders());
+ return null;
}
public static boolean verifySignature(KeycloakSession keycloakSession, RealmModel realm, String token) {
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java
index 64fc15b..2d68f49 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java
@@ -23,6 +23,7 @@ import org.keycloak.forms.account.AccountProvider;
import org.keycloak.forms.account.freemarker.model.AccountBean;
import org.keycloak.forms.account.freemarker.model.AccountFederatedIdentityBean;
import org.keycloak.forms.account.freemarker.model.ApplicationsBean;
+import org.keycloak.forms.account.freemarker.model.AuthorizationBean;
import org.keycloak.forms.account.freemarker.model.FeaturesBean;
import org.keycloak.forms.account.freemarker.model.LogBean;
import org.keycloak.forms.account.freemarker.model.PasswordBean;
@@ -52,6 +53,7 @@ import org.keycloak.utils.MediaType;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
@@ -92,6 +94,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
protected List<FormMessage> messages = null;
protected MessageType messageType = MessageType.ERROR;
+ private boolean authorizationSupported;
public FreeMarkerAccountProvider(KeycloakSession session, FreeMarkerUtil freeMarker) {
this.session = session;
@@ -156,7 +159,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
}
- attributes.put("features", new FeaturesBean(identityProviderEnabled, eventsEnabled, passwordUpdateSupported));
+ attributes.put("features", new FeaturesBean(identityProviderEnabled, eventsEnabled, passwordUpdateSupported, authorizationSupported));
attributes.put("account", new AccountBean(user, profileFormData));
switch (page) {
@@ -179,7 +182,16 @@ public class FreeMarkerAccountProvider implements AccountProvider {
case PASSWORD:
attributes.put("password", new PasswordBean(passwordSet));
break;
- default:
+ case RESOURCES:
+ if (!realm.isUserManagedAccessAllowed()) {
+ return Response.status(Status.FORBIDDEN).build();
+ }
+ attributes.put("authorization", new AuthorizationBean(session, user, uriInfo));
+ case RESOURCE_DETAIL:
+ if (!realm.isUserManagedAccessAllowed()) {
+ return Response.status(Status.FORBIDDEN).build();
+ }
+ attributes.put("authorization", new AuthorizationBean(session, user, uriInfo));
}
return processTemplate(theme, page, attributes, locale);
@@ -187,7 +199,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
/**
* Get Theme used for page rendering.
- *
+ *
* @return theme for page rendering, never null
* @throws IOException in case of Theme loading problem
*/
@@ -197,7 +209,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
/**
* Load message bundle and place it into <code>msg</code> template attribute. Also load Theme properties and place them into <code>properties</code> template attribute.
- *
+ *
* @param theme actual Theme to load bundle from
* @param locale to load bundle for
* @param attributes template attributes to add resources to
@@ -222,7 +234,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
/**
* Handle messages to be shown on the page - set them to template attributes
- *
+ *
* @param locale to be used for message text loading
* @param messagesBundle to be used for message text loading
* @param attributes template attributes to messages related info to
@@ -247,7 +259,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
/**
* Process FreeMarker template and prepare Response. Some fields are used for rendering also.
- *
+ *
* @param theme to be used (provided by <code>getTheme()</code>)
* @param page to be rendered
* @param attributes pushed to the template
@@ -358,10 +370,11 @@ public class FreeMarkerAccountProvider implements AccountProvider {
}
@Override
- public AccountProvider setFeatures(boolean identityProviderEnabled, boolean eventsEnabled, boolean passwordUpdateSupported) {
+ public AccountProvider setFeatures(boolean identityProviderEnabled, boolean eventsEnabled, boolean passwordUpdateSupported, boolean authorizationSupported) {
this.identityProviderEnabled = identityProviderEnabled;
this.eventsEnabled = eventsEnabled;
this.passwordUpdateSupported = passwordUpdateSupported;
+ this.authorizationSupported = authorizationSupported;
return this;
}
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java
new file mode 100755
index 0000000..837a843
--- /dev/null
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package org.keycloak.forms.account.freemarker.model;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.core.UriInfo;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.PermissionTicket;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.PermissionTicketStore;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AuthorizationBean {
+
+ private final UserModel user;
+ private final AuthorizationProvider authorization;
+ private final UriInfo uriInfo;
+ private ResourceBean resource;
+ private List<ResourceBean> resources;
+ private Collection<ResourceBean> userSharedResources;
+ private Collection<ResourceBean> requestsWaitingPermission;
+ private Collection<ResourceBean> resourcesWaitingOthersApproval;
+
+ public AuthorizationBean(KeycloakSession session, UserModel user, UriInfo uriInfo) {
+ this.user = user;
+ this.uriInfo = uriInfo;
+ authorization = session.getProvider(AuthorizationProvider.class);
+ List<String> pathParameters = uriInfo.getPathParameters().get("resource_id");
+
+ if (pathParameters != null && !pathParameters.isEmpty()) {
+ Resource resource = authorization.getStoreFactory().getResourceStore().findById(pathParameters.get(0), null);
+
+ if (resource != null && !resource.getOwner().equals(user.getId())) {
+ throw new RuntimeException("User [" + user.getUsername() + "] can not access resource [" + resource.getId() + "]");
+ }
+ }
+ }
+
+ public Collection<ResourceBean> getResourcesWaitingOthersApproval() {
+ if (resourcesWaitingOthersApproval == null) {
+ HashMap<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.REQUESTER, user.getId());
+ filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString());
+
+ resourcesWaitingOthersApproval = toResourceRepresentation(findPermissions(filters));
+ }
+
+ return resourcesWaitingOthersApproval;
+ }
+
+ public Collection<ResourceBean> getResourcesWaitingApproval() {
+ if (requestsWaitingPermission == null) {
+ HashMap<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.OWNER, user.getId());
+ filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString());
+
+ requestsWaitingPermission = toResourceRepresentation(findPermissions(filters));
+ }
+
+ return requestsWaitingPermission;
+ }
+
+ public List<ResourceBean> getResources() {
+ if (resources == null) {
+ resources = authorization.getStoreFactory().getResourceStore().findByOwner(user.getId(), null).stream()
+ .filter(Resource::isOwnerManagedAccess)
+ .map(ResourceBean::new)
+ .collect(Collectors.toList());
+ }
+ return resources;
+ }
+
+ public Collection<ResourceBean> getSharedResources() {
+ if (userSharedResources == null) {
+ HashMap<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.REQUESTER, user.getId());
+ filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString());
+
+ PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore();
+
+ userSharedResources = toResourceRepresentation(ticketStore.find(filters, null, -1, -1));
+ }
+ return userSharedResources;
+ }
+
+ public ResourceBean getResource() {
+ if (resource == null) {
+ String resourceId = uriInfo.getPathParameters().getFirst("resource_id");
+
+ if (resourceId != null) {
+ resource = getResource(resourceId);
+ }
+ }
+
+ return resource;
+ }
+
+ private ResourceBean getResource(String id) {
+ return new ResourceBean(authorization.getStoreFactory().getResourceStore().findById(id, null));
+ }
+
+ public static class RequesterBean {
+
+ private final Long createdTimestamp;
+ private final Long grantedTimestamp;
+ private UserModel requester;
+ private List<PermissionScopeBean> scopes = new ArrayList<>();
+ private boolean granted;
+
+ public RequesterBean(PermissionTicket ticket, AuthorizationProvider authorization) {
+ this.requester = authorization.getKeycloakSession().users().getUserById(ticket.getRequester(), authorization.getRealm());
+ granted = ticket.isGranted();
+ createdTimestamp = ticket.getCreatedTimestamp();
+ grantedTimestamp = ticket.getGrantedTimestamp();
+ }
+
+ public UserModel getRequester() {
+ return requester;
+ }
+
+ public List<PermissionScopeBean> getScopes() {
+ return scopes;
+ }
+
+ private void addScope(PermissionTicket ticket) {
+ if (ticket != null) {
+ scopes.add(new PermissionScopeBean(ticket));
+ }
+ }
+
+ public boolean isGranted() {
+ return (granted && scopes.isEmpty()) || scopes.stream().filter(permissionScopeBean -> permissionScopeBean.isGranted()).count() > 0;
+ }
+
+ public Date getCreatedDate() {
+ return Time.toDate(createdTimestamp);
+ }
+
+ public Date getGrantedDate() {
+ if (grantedTimestamp == null) {
+ PermissionScopeBean permission = scopes.stream().filter(permissionScopeBean -> permissionScopeBean.isGranted()).findFirst().orElse(null);
+
+ if (permission == null) {
+ return null;
+ }
+
+ return permission.getGrantedDate();
+ }
+ return Time.toDate(grantedTimestamp);
+ }
+ }
+
+ public static class PermissionScopeBean {
+
+ private final Scope scope;
+ private final PermissionTicket ticket;
+
+ public PermissionScopeBean(PermissionTicket ticket) {
+ this.ticket = ticket;
+ scope = ticket.getScope();
+ }
+
+ public String getId() {
+ return ticket.getId();
+ }
+
+ public Scope getScope() {
+ return scope;
+ }
+
+ public boolean isGranted() {
+ return ticket.isGranted();
+ }
+
+ private Date getGrantedDate() {
+ if (isGranted()) {
+ return Time.toDate(ticket.getGrantedTimestamp());
+ }
+ return null;
+ }
+ }
+
+ public class ResourceBean {
+
+ private final ResourceServerBean resourceServer;
+ private final UserModel owner;
+ private Resource resource;
+ private Map<String, RequesterBean> permissions = new HashMap<>();
+ private Collection<RequesterBean> shares;
+
+ public ResourceBean(Resource resource) {
+ RealmModel realm = authorization.getRealm();
+ resourceServer = new ResourceServerBean(realm.getClientById(resource.getResourceServer().getId()));
+ this.resource = resource;
+ owner = authorization.getKeycloakSession().users().getUserById(resource.getOwner(), realm);
+ }
+
+ public String getId() {
+ return resource.getId();
+ }
+
+ public String getName() {
+ return resource.getName();
+ }
+
+ public String getDisplayName() {
+ return resource.getDisplayName();
+ }
+
+ public String getIconUri() {
+ return resource.getIconUri();
+ }
+
+ public UserModel getOwner() {
+ return owner;
+ }
+
+ public List<ScopeRepresentation> getScopes() {
+ return resource.getScopes().stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList());
+ }
+
+ public Collection<RequesterBean> getShares() {
+ if (shares == null) {
+ Map<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.RESOURCE, resource.getId());
+ filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString());
+
+ shares = toPermissionRepresentation(findPermissions(filters));
+ }
+
+ return shares;
+ }
+
+ public ResourceServerBean getResourceServer() {
+ return resourceServer;
+ }
+
+ public Collection<RequesterBean> getPermissions() {
+ return permissions.values();
+ }
+
+ private void addPermission(PermissionTicket ticket, AuthorizationProvider authorization) {
+ permissions.computeIfAbsent(ticket.getRequester(), key -> new RequesterBean(ticket, authorization)).addScope(ticket);
+ }
+ }
+
+ private Collection<RequesterBean> toPermissionRepresentation(List<PermissionTicket> permissionRequests) {
+ Map<String, RequesterBean> requests = new HashMap<>();
+
+ for (PermissionTicket ticket : permissionRequests) {
+ Resource resource = ticket.getResource();
+
+ if (!resource.isOwnerManagedAccess()) {
+ continue;
+ }
+
+ requests.computeIfAbsent(ticket.getRequester(), resourceId -> new RequesterBean(ticket, authorization)).addScope(ticket);
+ }
+
+ return requests.values();
+ }
+
+ private Collection<ResourceBean> toResourceRepresentation(List<PermissionTicket> tickets) {
+ Map<String, ResourceBean> requests = new HashMap<>();
+
+ for (PermissionTicket ticket : tickets) {
+ Resource resource = ticket.getResource();
+
+ if (!resource.isOwnerManagedAccess()) {
+ continue;
+ }
+
+ requests.computeIfAbsent(resource.getId(), resourceId -> getResource(resourceId)).addPermission(ticket, authorization);
+ }
+
+ return requests.values();
+ }
+
+ private List<PermissionTicket> findPermissions(Map<String, String> filters) {
+ return authorization.getStoreFactory().getPermissionTicketStore().find(filters, null, -1, -1);
+ }
+
+ public class ResourceServerBean {
+
+ private ClientModel clientModel;
+
+ public ResourceServerBean(ClientModel clientModel) {
+ this.clientModel = clientModel;
+ }
+
+ public String getName() {
+ String name = clientModel.getName();
+
+ if (name != null) {
+ return name;
+ }
+
+ return clientModel.getClientId();
+ }
+
+ public String getRedirectUri() {
+ Set<String> redirectUris = clientModel.getRedirectUris();
+
+ if (redirectUris.isEmpty()) {
+ return null;
+ }
+
+ return redirectUris.iterator().next();
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/FeaturesBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/FeaturesBean.java
index fa41dda..262063d 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/FeaturesBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/FeaturesBean.java
@@ -25,11 +25,13 @@ public class FeaturesBean {
private final boolean identityFederation;
private final boolean log;
private final boolean passwordUpdateSupported;
+ private boolean authorization;
- public FeaturesBean(boolean identityFederation, boolean log, boolean passwordUpdateSupported) {
+ public FeaturesBean(boolean identityFederation, boolean log, boolean passwordUpdateSupported, boolean authorization) {
this.identityFederation = identityFederation;
this.log = log;
this.passwordUpdateSupported = passwordUpdateSupported;
+ this.authorization = authorization;
}
public boolean isIdentityFederation() {
@@ -43,4 +45,8 @@ public class FeaturesBean {
public boolean isPasswordUpdateSupported() {
return passwordUpdateSupported;
}
+
+ public boolean isAuthorization() {
+ return authorization;
+ }
}
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/RealmBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/RealmBean.java
index d818876..a0407aa 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/RealmBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/RealmBean.java
@@ -68,4 +68,8 @@ public class RealmBean {
public boolean isRegistrationEmailAsUsername() {
return realm.isRegistrationEmailAsUsername();
}
+
+ public boolean isUserManagedAccessAllowed() {
+ return realm.isUserManagedAccessAllowed();
+ }
}
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java
index a67db6e..0de20be 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/UrlBean.java
@@ -74,6 +74,22 @@ public class UrlBean {
return Urls.accountLogout(baseQueryURI, currentURI, realm).toString();
}
+ public String getResourceUrl() {
+ return Urls.accountResourcesPage(baseQueryURI, realm).toString();
+ }
+
+ public String getResourceDetailUrl(String id) {
+ return Urls.accountResourceDetailPage(id, baseQueryURI, realm).toString();
+ }
+
+ public String getResourceGrant(String id) {
+ return Urls.accountResourceGrant(id, baseQueryURI, realm).toString();
+ }
+
+ public String getResourceShare(String id) {
+ return Urls.accountResourceShare(id, baseQueryURI, realm).toString();
+ }
+
public String getResourcesPath() {
URI uri = Urls.themeRoot(baseURI);
return uri.getPath() + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName();
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/Templates.java b/services/src/main/java/org/keycloak/forms/account/freemarker/Templates.java
index 5f98987..4882293 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/Templates.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/Templates.java
@@ -40,6 +40,10 @@ public class Templates {
return "sessions.ftl";
case APPLICATIONS:
return "applications.ftl";
+ case RESOURCES:
+ return "resources.ftl";
+ case RESOURCE_DETAIL:
+ return "resource-detail.ftl";
default:
throw new IllegalArgumentException();
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
index e89db0f..6166071 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
@@ -17,7 +17,14 @@
*/
package org.keycloak.protocol.oidc;
+import java.io.IOException;
+import java.security.PublicKey;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.keycloak.OAuthErrorException;
import org.keycloak.RSATokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.models.KeycloakSession;
@@ -27,10 +34,6 @@ import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;
import org.keycloak.util.JsonSerialization;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import java.security.PublicKey;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -48,42 +51,18 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
public Response introspect(String token) {
try {
- boolean valid = true;
-
- AccessToken toIntrospect = null;
-
- try {
- RSATokenVerifier verifier = RSATokenVerifier.create(token)
- .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
-
- PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
- if (publicKey == null) {
- valid = false;
- } else {
- verifier.publicKey(publicKey);
- verifier.verify();
- toIntrospect = verifier.getToken();
- }
- } catch (VerificationException e) {
- valid = false;
- }
-
- RealmModel realm = this.session.getContext().getRealm();
+ AccessToken accessToken = verifyAccessToken(token);
ObjectNode tokenMetadata;
- if (valid && toIntrospect != null) {
- valid = tokenManager.isTokenValid(session, realm, toIntrospect);
- }
-
- if (valid) {
- tokenMetadata = JsonSerialization.createObjectNode(toIntrospect);
- tokenMetadata.put("client_id", toIntrospect.getIssuedFor());
- tokenMetadata.put("username", toIntrospect.getPreferredUsername());
+ if (accessToken != null) {
+ tokenMetadata = JsonSerialization.createObjectNode(accessToken);
+ tokenMetadata.put("client_id", accessToken.getIssuedFor());
+ tokenMetadata.put("username", accessToken.getPreferredUsername());
} else {
tokenMetadata = JsonSerialization.createObjectNode();
}
- tokenMetadata.put("active", valid);
+ tokenMetadata.put("active", accessToken != null);
return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).type(MediaType.APPLICATION_JSON_TYPE).build();
} catch (Exception e) {
@@ -91,6 +70,28 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
}
}
+ protected AccessToken verifyAccessToken(String token) throws OAuthErrorException, IOException {
+ AccessToken accessToken;
+
+ try {
+ RSATokenVerifier verifier = RSATokenVerifier.create(token)
+ .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
+ PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
+
+ if (publicKey == null) {
+ return null;
+ }
+
+ accessToken = verifier.publicKey(publicKey).verify().getToken();
+ } catch (VerificationException e) {
+ return null;
+ }
+
+ RealmModel realm = this.session.getContext().getRealm();
+
+ return tokenManager.isTokenValid(session, realm, accessToken) ? accessToken : null;
+ }
+
protected AccessToken toAccessToken(String token) {
try {
RSATokenVerifier verifier = RSATokenVerifier.create(token)
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index cbf93bb..05f9f4c 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -24,8 +24,10 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
-import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
-import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.authorization.AuthorizationTokenService;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.authorization.util.Tokens;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ExchangeExternalToken;
import org.keycloak.broker.provider.IdentityProvider;
@@ -47,7 +49,6 @@ import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.Constants;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel;
@@ -57,33 +58,31 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.AuthenticationFlowResolver;
-import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.JsonWebToken;
-import org.keycloak.services.ErrorPage;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest.Metadata;
import org.keycloak.services.CorsErrorResponseException;
-import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
+import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
-import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.IdentityBrokerService;
-import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.admin.AdminAuth;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.services.validation.Validation;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
+import org.keycloak.util.JsonSerialization;
import org.keycloak.util.TokenUtil;
import org.keycloak.utils.ProfileHelper;
@@ -95,8 +94,9 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
-import java.net.URI;
+
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -116,7 +116,7 @@ public class TokenEndpoint {
private Map<String, String> clientAuthAttributes;
private enum Action {
- AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS, TOKEN_EXCHANGE
+ AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS, TOKEN_EXCHANGE, PERMISSION
}
// https://tools.ietf.org/html/rfc7636#section-4.2
@@ -166,7 +166,10 @@ public class TokenEndpoint {
checkSsl();
checkRealm();
checkGrantType();
- checkClient();
+
+ if (!action.equals(Action.PERMISSION)) {
+ checkClient();
+ }
switch (action) {
case AUTHORIZATION_CODE:
@@ -179,6 +182,8 @@ public class TokenEndpoint {
return clientCredentialsGrant();
case TOKEN_EXCHANGE:
return tokenExchange();
+ case PERMISSION:
+ return permissionGrant();
}
throw new RuntimeException("Unknown action " + action);
@@ -247,7 +252,9 @@ public class TokenEndpoint {
} else if (grantType.equals(OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)) {
event.event(EventType.TOKEN_EXCHANGE);
action = Action.TOKEN_EXCHANGE;
-
+ } else if (grantType.equals(OAuth2Constants.UMA_GRANT_TYPE)) {
+ event.event(EventType.PERMISSION_TOKEN);
+ action = Action.PERMISSION;
} else {
throw new CorsErrorResponseException(cors, Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
}
@@ -973,6 +980,90 @@ public class TokenEndpoint {
return user;
}
+ public Response permissionGrant() {
+ event.detail(Details.AUTH_METHOD, "oauth_credentials");
+
+ String accessTokenString = null;
+ String authorizationHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+
+ if (authorizationHeader != null && authorizationHeader.toLowerCase().startsWith("bearer")) {
+ accessTokenString = new AppAuthManager().extractAuthorizationHeaderToken(headers);
+ }
+
+ if (accessTokenString != null) {
+ AccessToken accessToken = Tokens.getAccessToken(session);
+
+ if (accessToken == null) {
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Invalid bearer token", Status.UNAUTHORIZED);
+ }
+
+ cors.allowedOrigins(uriInfo, realm.getClientByClientId(accessToken.getIssuedFor()));
+ }
+
+ String claimToken = null;
+
+ // claim_token is optional, if provided we just grab it from the request
+ if (formParams.containsKey("claim_token")) {
+ claimToken = formParams.get("claim_token").get(0);
+ }
+
+ if (accessTokenString == null) {
+ // in case no bearer token is provided, we force client authentication
+ checkClient();
+ // Clients need to authenticate in order to obtain a RPT from the server.
+ // In order to support cases where the client is obtaining permissions on its on behalf, we issue a temporary access token
+ accessTokenString = AccessTokenResponse.class.cast(clientCredentialsGrant().getEntity()).getToken();
+ }
+
+ AuthorizationRequest authorizationRequest = new AuthorizationRequest(formParams.getFirst("ticket"));
+
+ authorizationRequest.setClaimToken(claimToken);
+ authorizationRequest.setClaimTokenFormat(formParams.getFirst("claim_token_format"));
+ authorizationRequest.setPct(formParams.getFirst("pct"));
+ authorizationRequest.setRpt(formParams.getFirst("rpt"));
+ authorizationRequest.setScope(formParams.getFirst("scope"));
+ authorizationRequest.setAudience(formParams.getFirst("audience"));
+ authorizationRequest.setAccessToken(accessTokenString);
+
+ String submitRequest = formParams.getFirst("submit_request");
+
+ authorizationRequest.setSubmitRequest(submitRequest == null ? true : Boolean.valueOf(submitRequest));
+
+ // permissions have a format like RESOURCE#SCOPE1,SCOPE2
+ List<String> permissions = formParams.get("permission");
+
+ if (permissions != null) {
+ 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);
+ }
+ }
+ }
+
+ Metadata metadata = new Metadata();
+
+ String responseIncludeResourceName = formParams.getFirst("response_include_resource_name");
+
+ if (responseIncludeResourceName != null) {
+ metadata.setIncludeResourceName(Boolean.parseBoolean(responseIncludeResourceName));
+ }
+
+ String responsePermissionsLimit = formParams.getFirst("response_permissions_limit");
+
+ if (responsePermissionsLimit != null) {
+ metadata.setLimit(Integer.parseInt(responsePermissionsLimit));
+ }
+
+ authorizationRequest.setMetadata(metadata);
+
+ return new AuthorizationTokenService(session.getProvider(AuthorizationProvider.class), tokenManager, event, request, cors).authorize(authorizationRequest);
+ }
// https://tools.ietf.org/html/rfc7636#section-4.1
private boolean isValidPkceCodeVerifier(String codeVerifier) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
index 4fc73f7..60970d8 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
@@ -156,7 +156,6 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide
enforcerConfig.setEnforcementMode(null);
enforcerConfig.setCreateResources(null);
- enforcerConfig.setOnlineIntrospection(null);
rep.setEnforcerConfig(enforcerConfig);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index dd94094..e21f3ad 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -257,6 +257,10 @@ public class TokenManager {
validation.clientSession.setTimestamp(currentTime);
validation.userSession.setLastSessionRefresh(currentTime);
+ if (refreshToken.getAuthorization() != null) {
+ validation.newToken.setAuthorization(refreshToken.getAuthorization());
+ }
+
AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
.accessToken(validation.newToken)
.generateRefreshToken();
diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
index bebcb2d..68aca41 100755
--- a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java
@@ -63,7 +63,10 @@ public class AppAuthManager extends AuthenticationManager {
}
public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
- String tokenString = extractAuthorizationHeaderToken(headers);
+ return authenticateBearerToken(extractAuthorizationHeaderToken(headers), session, realm, uriInfo, connection, headers);
+ }
+
+ public AuthResult authenticateBearerToken(String tokenString, KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
if (tokenString == null) return null;
AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, true, true, false, tokenString, headers);
return authResult;
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
index d3c0260..d316e34 100755
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
@@ -18,6 +18,13 @@ package org.keycloak.services.resources.account;
import org.jboss.logging.Logger;
import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.PermissionTicket;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.PermissionTicketStore;
+import org.keycloak.common.Profile;
+import org.keycloak.common.Profile.Feature;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.Time;
import org.keycloak.common.util.UriUtils;
@@ -47,6 +54,7 @@ import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
@@ -70,6 +78,7 @@ import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
@@ -78,6 +87,9 @@ import java.lang.reflect.Method;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -155,7 +167,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
account.setUser(auth.getUser());
}
- account.setFeatures(realm.isIdentityFederationEnabled(), eventStore != null && realm.isEventsEnabled(), true);
+ account.setFeatures(realm.isIdentityFederationEnabled(), eventStore != null && realm.isEventsEnabled(), true, Profile.isFeatureEnabled(Feature.AUTHORIZATION));
}
public static UriBuilder accountServiceBaseUrl(UriInfo uriInfo) {
@@ -684,6 +696,193 @@ public class AccountFormService extends AbstractSecuredLocalService {
}
}
+ @Path("resource")
+ @GET
+ public Response resourcesPage(@QueryParam("resource_id") String resourceId) {
+ return forwardToPage("resources", AccountPages.RESOURCES);
+ }
+
+ @Path("resource/{resource_id}")
+ @GET
+ public Response resourceDetailPage(@PathParam("resource_id") String resourceId) {
+ return forwardToPage("resource-detail", AccountPages.RESOURCE_DETAIL);
+ }
+
+ @Path("resource/{resource_id}/grant")
+ @POST
+ public Response grantPermission(@PathParam("resource_id") String resourceId, @FormParam("action") String action, @FormParam("permission_id") String[] permissionId, @FormParam("requester") String requester) {
+ AuthorizationProvider authorization = session.getProvider(AuthorizationProvider.class);
+ PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore();
+ Resource resource = authorization.getStoreFactory().getResourceStore().findById(resourceId, null);
+
+ if (resource == null) {
+ return ErrorResponse.error("Invalid resource", Response.Status.BAD_REQUEST);
+ }
+
+ if (action == null) {
+ return ErrorResponse.error("Invalid action", Response.Status.BAD_REQUEST);
+ }
+
+ boolean isGrant = "grant".equals(action);
+ boolean isDeny = "deny".equals(action);
+ boolean isRevoke = "revoke".equals(action);
+
+ Map<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.RESOURCE, resource.getId());
+ filters.put(PermissionTicket.REQUESTER, session.users().getUserByUsername(requester, realm).getId());
+
+ if (isRevoke) {
+ filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString());
+ } else {
+ filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString());
+ }
+
+ List<PermissionTicket> tickets = ticketStore.find(filters, resource.getResourceServer().getId(), -1, -1);
+ Iterator<PermissionTicket> iterator = tickets.iterator();
+
+ while (iterator.hasNext()) {
+ PermissionTicket ticket = iterator.next();
+
+ if (isGrant) {
+ if (permissionId != null && permissionId.length > 0 && !Arrays.asList(permissionId).contains(ticket.getId())) {
+ continue;
+ }
+ }
+
+ if (isGrant && !ticket.isGranted()) {
+ ticket.setGrantedTimestamp(System.currentTimeMillis());
+ iterator.remove();
+ } else if (isDeny || isRevoke) {
+ if (permissionId != null && permissionId.length > 0 && Arrays.asList(permissionId).contains(ticket.getId())) {
+ iterator.remove();
+ }
+ }
+ }
+
+ for (PermissionTicket ticket : tickets) {
+ ticketStore.delete(ticket.getId());
+ }
+
+ if (isRevoke) {
+ return forwardToPage("resource-detail", AccountPages.RESOURCE_DETAIL);
+ }
+
+ return forwardToPage("resources", AccountPages.RESOURCES);
+ }
+
+ @Path("resource/{resource_id}/share")
+ @POST
+ public Response shareResource(@PathParam("resource_id") String resourceId, @FormParam("user_id") String[] userIds, @FormParam("scope_id") String[] scopes) {
+ AuthorizationProvider authorization = session.getProvider(AuthorizationProvider.class);
+ PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore();
+ Resource resource = authorization.getStoreFactory().getResourceStore().findById(resourceId, null);
+
+ if (resource == null) {
+ return ErrorResponse.error("Invalid resource", Response.Status.BAD_REQUEST);
+ }
+
+ if (userIds == null || userIds.length == 0) {
+ return account.setError(Status.BAD_REQUEST, Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD);
+ }
+
+ for (String id : userIds) {
+ UserModel user = session.users().getUserById(id, realm);
+
+ if (user == null) {
+ user = session.users().getUserByUsername(id, realm);
+ }
+
+ if (user == null) {
+ user = session.users().getUserByEmail(id, realm);
+ }
+
+ if (user == null) {
+ return account.setError(Status.BAD_REQUEST, Messages.INVALID_USER).createResponse(AccountPages.RESOURCE_DETAIL);
+ }
+
+ Map<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.RESOURCE, resource.getId());
+ filters.put(PermissionTicket.OWNER, auth.getUser().getId());
+ filters.put(PermissionTicket.REQUESTER, user.getId());
+
+ List<PermissionTicket> tickets = ticketStore.find(filters, resource.getResourceServer().getId(), -1, -1);
+
+ if (tickets.isEmpty()) {
+ if (scopes != null && scopes.length > 0) {
+ for (String scope : scopes) {
+ PermissionTicket ticket = ticketStore.create(resourceId, scope, user.getId(), resource.getResourceServer());
+ ticket.setGrantedTimestamp(System.currentTimeMillis());
+ }
+ } else {
+ if (resource.getScopes().isEmpty()) {
+ PermissionTicket ticket = ticketStore.create(resourceId, null, user.getId(), resource.getResourceServer());
+ ticket.setGrantedTimestamp(System.currentTimeMillis());
+ } else {
+ for (Scope scope : resource.getScopes()) {
+ PermissionTicket ticket = ticketStore.create(resourceId, scope.getId(), user.getId(), resource.getResourceServer());
+ ticket.setGrantedTimestamp(System.currentTimeMillis());
+ }
+ }
+ }
+ } else if (scopes != null && scopes.length > 0) {
+ List<String> grantScopes = new ArrayList<>(Arrays.asList(scopes));
+
+ for (PermissionTicket ticket : tickets) {
+ Scope scope = ticket.getScope();
+
+ if (scope != null) {
+ grantScopes.remove(scope.getId());
+ }
+ }
+
+ for (String grantScope : grantScopes) {
+ PermissionTicket ticket = ticketStore.create(resourceId, grantScope, user.getId(), resource.getResourceServer());
+ ticket.setGrantedTimestamp(System.currentTimeMillis());
+ }
+ }
+ }
+
+ return forwardToPage("resource-detail", AccountPages.RESOURCE_DETAIL);
+ }
+
+ @Path("resource")
+ @POST
+ public Response processResourceActions(@FormParam("resource_id") String[] resourceIds, @FormParam("action") String action) {
+ AuthorizationProvider authorization = session.getProvider(AuthorizationProvider.class);
+ PermissionTicketStore ticketStore = authorization.getStoreFactory().getPermissionTicketStore();
+
+ if (action == null) {
+ return ErrorResponse.error("Invalid action", Response.Status.BAD_REQUEST);
+ }
+
+ for (String resourceId : resourceIds) {
+ Resource resource = authorization.getStoreFactory().getResourceStore().findById(resourceId, null);
+
+ if (resource == null) {
+ return ErrorResponse.error("Invalid resource", Response.Status.BAD_REQUEST);
+ }
+
+ HashMap<String, String> filters = new HashMap<>();
+
+ filters.put(PermissionTicket.REQUESTER, auth.getUser().getId());
+ filters.put(PermissionTicket.RESOURCE, resource.getId());
+
+ if ("cancel".equals(action)) {
+ filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString());
+ } else if ("cancelRequest".equals(action)) {
+ filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString());
+ }
+
+ for (PermissionTicket ticket : ticketStore.find(filters, resource.getResourceServer().getId(), -1, -1)) {
+ ticketStore.delete(ticket.getId());
+ }
+ }
+
+ return forwardToPage("authorization", AccountPages.RESOURCES);
+ }
+
public static UriBuilder loginRedirectUrl(UriBuilder base) {
return RealmsResource.accountUrl(base).path(AccountFormService.class, "loginRedirect");
}
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index c2d4929..ac45614 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -143,6 +143,22 @@ public class Urls {
return realmLogout(baseUri).queryParam("redirect_uri", redirectUri).build(realmName);
}
+ public static URI accountResourcesPage(URI baseUri, String realmName) {
+ return accountBase(baseUri).path(AccountFormService.class, "resourcesPage").build(realmName);
+ }
+
+ public static URI accountResourceDetailPage(String resourceId, URI baseUri, String realmName) {
+ return accountBase(baseUri).path(AccountFormService.class, "resourceDetailPage").build(realmName, resourceId);
+ }
+
+ public static URI accountResourceGrant(String resourceId, URI baseUri, String realmName) {
+ return accountBase(baseUri).path(AccountFormService.class, "grantPermission").build(realmName, resourceId);
+ }
+
+ public static URI accountResourceShare(String resourceId, URI baseUri, String realmName) {
+ return accountBase(baseUri).path(AccountFormService.class, "shareResource").build(realmName, resourceId);
+ }
+
public static URI loginActionUpdatePassword(URI baseUri, String realmName) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "updatePassword").build(realmName);
}
diff --git a/services/src/main/java/org/keycloak/theme/FreeMarkerUtil.java b/services/src/main/java/org/keycloak/theme/FreeMarkerUtil.java
index a8b1784..7415bf1 100755
--- a/services/src/main/java/org/keycloak/theme/FreeMarkerUtil.java
+++ b/services/src/main/java/org/keycloak/theme/FreeMarkerUtil.java
@@ -45,6 +45,7 @@ public class FreeMarkerUtil {
public String processTemplate(Object data, String templateName, Theme theme) throws FreeMarkerException {
try {
Template template;
+ cache = null;
if (cache != null) {
String key = theme.getName() + "/" + templateName;
template = cache.get(key);
diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/index.jsp b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/index.jsp
index 0aea6b0..c511b2d 100644
--- a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/index.jsp
+++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/index.jsp
@@ -38,8 +38,8 @@
for (Permission permission : authzContext.getPermissions()) {
%>
<li>
- <p>Resource: <%= permission.getResourceSetName() %></p>
- <p>ID: <%= permission.getResourceSetId() %></p>
+ <p>Resource: <%= permission.getResourceName() %></p>
+ <p>ID: <%= permission.getResourceId() %></p>
</li>
<%
}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html
index 077f9dc..692e05d 100755
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html
@@ -23,7 +23,8 @@
<a href data-ng-click="showRpt()">Show Requesting Party Token </a> |
<a href data-ng-click="showAccessToken()">Show Access Token </a> |
<a id="entitlements" href data-ng-click="requestEntitlements()">Request Entitlements</a> |
-<a id="entitlement" href data-ng-click="requestEntitlement()">Request Entitlement</a> |
+<a id="entitlement" href data-ng-click="requestEntitlement()">Request Entitlement</a> |
+<a id="my-account" href ng-click="Identity.account()">My Account</a> |
<a href="" ng-click="Identity.logout()">Sign Out</a>
<div id="content-area" class="col-md-9" role="main">
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
index ecdbf0b..3d2ed43 100755
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
@@ -60,6 +60,12 @@ module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Albu
$http.get(apiUrl + '/scope-all').success(function (data) {
});
}
+
+ $scope.getAllResources = function () {
+ Album.getAll(function (albums) {
+ $scope.albums = albums;
+ });
+ }
});
module.controller('TokenCtrl', function ($scope, Identity) {
@@ -78,8 +84,13 @@ module.controller('TokenCtrl', function ($scope, Identity) {
}
$scope.requestEntitlement = function () {
- var param={"permissions" : [{"resource_set_name" : "Album Resource"}]};
- Identity.authorization.entitlement('photoz-restful-api', param).then(function (rpt) {
+ Identity.authorization.entitlement('photoz-restful-api', {
+ "permissions": [
+ {
+ "id" : "Album Resource"
+ }
+ ]
+ }).then(function (rpt) {
document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(rpt), null, ' ');
});
}
@@ -99,6 +110,14 @@ module.controller('AlbumCtrl', function ($scope, $http, $routeParams, $location,
});
};
+ $scope.createManaged = function () {
+ $scope.album.userManaged = true;
+ var newAlbum = new Album($scope.album);
+ newAlbum.$save({}, function (data) {
+ $location.path('/');
+ });
+ };
+
$scope.createWithInvalidUser = function () {
var newAlbum = new Album($scope.album);
newAlbum.$save({user: 'invalidUser'}, function (data) {
@@ -127,7 +146,9 @@ module.controller('AdminAlbumCtrl', function ($scope, $http, $route, $location,
});
module.factory('Album', ['$resource', function ($resource) {
- return $resource(apiUrl + '/album/:id');
+ return $resource(apiUrl + '/album/:id', {id: '@id'}, {
+ getAll: {method: 'GET', params: {getAll: true}, isArray: true}
+ });
}]);
module.factory('Profile', ['$resource', function ($resource) {
@@ -162,11 +183,46 @@ module.factory('authInterceptor', function ($q, $injector, $timeout, Identity) {
}
if (rejection.config.url.indexOf('/authorize') == -1 && retry) {
- var deferred = $q.defer();
-
// here is the authorization logic, which tries to obtain an authorization token from the server in case the resource server
// returns a 403 or 401.
- Identity.authorization.authorize(rejection.headers('WWW-Authenticate')).then(function (rpt) {
+ var wwwAuthenticateHeader = rejection.headers('WWW-Authenticate');
+
+ // when using UMA, a WWW-Authenticate header should be returned by the resource server
+ if (!wwwAuthenticateHeader) {
+ return $q.reject(rejection);
+ }
+
+ // when using UMA, a WWW-Authenticate header should contain UMA data
+ if (wwwAuthenticateHeader.indexOf('UMA') == -1) {
+ return $q.reject(rejection);
+ }
+
+ var deferred = $q.defer();
+
+ var params = wwwAuthenticateHeader.split(',');
+ var ticket;
+
+ // try to extract the permission ticket from the WWW-Authenticate header
+ for (i = 0; i < params.length; i++) {
+ var param = params[i].split('=');
+
+ if (param[0] == 'ticket') {
+ ticket = param[1].substring(1, param[1].length - 1).trim();
+ break;
+ }
+ }
+
+ // a permission ticket must exist in order to send an authorization request
+ if (!ticket) {
+ return $q.reject(rejection);
+ }
+
+ // prepare a authorization request with the permission ticket
+ var authorizationRequest = {};
+ authorizationRequest.ticket = ticket;
+
+ // send the authorization request, if successful retry the request
+ Identity.authorization.authorize(authorizationRequest).then(function (rpt) {
deferred.resolve(rejection);
}, function () {
document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js
index 9a018e4..3a892b5 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js
@@ -26,6 +26,7 @@
this.claims = {};
this.claims.name = keycloak.idTokenParsed.name;
+ this.claims.sub = keycloak.idTokenParsed.sub;
this.authc = {};
this.authc.token = keycloak.token;
@@ -45,6 +46,10 @@
return this.hasRole("admin");
};
+ this.account = function () {
+ keycloak.accountManagement();
+ }
+
this.authorization = new KeycloakAuthorization(keycloak);
}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html
index 403adfa..ab65313 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html
@@ -4,5 +4,6 @@
Name: <input type="text" id="album.name" ng-model="album.name"/>
<button ng-click="create()" id="save-album">Save</button>
+ <button ng-click="createManaged()" id="save-managed-album">Save Managed</button>
<button ng-click="createWithInvalidUser()" id="save-album-invalid">Save with invalid user</button>
</form>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html
index 788763b..b4208bb 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html
@@ -3,7 +3,7 @@
<hr/>
<br/>
<div data-ng-show="!Identity.isAdmin()">
-<a href="#/album/create" id="create-album">Create Album</a> | <a href="#/profile">My Profile</a> | <a href="#" id="requestPathWithAnyProtectedScope" ng-click="requestPathWithAnyProtectedScope()">Any Scope Access</a> | <a href="#" id="requestPathWithAllProtectedScope" ng-click="requestPathWithAllProtectedScope()">All Scope Access</a>
+<a href="#/album/create" id="create-album">Create Album</a> | <a href="#/profile">My Profile</a> | <a href="#" id="requestPathWithAnyProtectedScope" ng-click="requestPathWithAnyProtectedScope()">Any Scope Access</a> | <a href="#" id="requestPathWithAllProtectedScope" ng-click="requestPathWithAllProtectedScope()">All Scope Access</a> | <a href id="get-all-resources" ng-click="getAllResources()">Get All Resources</a>
<br/>
<br/>
<span data-ng-show="albums.length == 0" id="resource-list-empty">You don't have any albums, yet.</span>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json
index 7ec7e02..e44fec3 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json
@@ -1,9 +1,11 @@
{
"realm": "photoz",
"enabled": true,
+ "userManagedAccessAllowed": true,
"sslRequired": "external",
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "accessTokenLifespan": 100000,
"requiredCredentials": [
"password"
],
@@ -26,6 +28,9 @@
"clientRoles": {
"photoz-restful-api": [
"manage-albums"
+ ],
+ "account": [
+ "manage-account"
]
}
},
@@ -47,6 +52,32 @@
"clientRoles": {
"photoz-restful-api": [
"manage-albums"
+ ],
+ "account": [
+ "manage-account"
+ ]
+ }
+ },
+ {
+ "username": "pedroigor",
+ "enabled": true,
+ "email": "pedroigor@keycloak.org",
+ "firstName": "Pedro Igor",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "pedroigor"
+ }
+ ],
+ "realmRoles": [
+ "user", "uma_authorization"
+ ],
+ "clientRoles": {
+ "photoz-restful-api": [
+ "manage-albums"
+ ],
+ "account": [
+ "manage-account"
]
}
},
@@ -71,6 +102,9 @@
],
"photoz-restful-api": [
"manage-albums"
+ ],
+ "account": [
+ "manage-account"
]
}
},
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
index 77bb3b6..22b5388 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
@@ -37,7 +37,7 @@ import java.util.List;
@Path("/admin/album")
public class AdminAlbumService {
- public static final String SCOPE_ADMIN_ALBUM_MANAGE = "urn:photoz.com:scopes:album:admin:manage";
+ public static final String SCOPE_ADMIN_ALBUM_MANAGE = "admin:manage";
@Inject
private EntityManager entityManager;
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
index 7969492..9070416 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -39,8 +39,8 @@ public class AlbumService {
private static volatile long nextId = 0;
- public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view";
- public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete";
+ public static final String SCOPE_ALBUM_VIEW = "album:view";
+ public static final String SCOPE_ALBUM_DELETE = "album:delete";
@Inject
private EntityManager entityManager;
@@ -91,8 +91,12 @@ public class AlbumService {
@GET
@Produces("application/json")
- public Response findAll() {
- return Response.ok(this.entityManager.createQuery("from Album where userId = '" + request.getUserPrincipal().getName() + "'").getResultList()).build();
+ public Response findAll(@QueryParam("getAll") Boolean getAll) {
+ if (getAll != null && getAll) {
+ return Response.ok(this.entityManager.createQuery("from Album").getResultList()).build();
+ } else {
+ return Response.ok(this.entityManager.createQuery("from Album where userId = '" + request.getUserPrincipal().getName() + "'").getResultList()).build();
+ }
}
@GET
@@ -119,6 +123,10 @@ public class AlbumService {
albumResource.setOwner(album.getUserId());
+ if (album.isUserManaged()) {
+ albumResource.setOwnerManagedAccess(true);
+ }
+
getAuthzClient().protection().resource().create(albumResource);
} catch (Exception e) {
throw new RuntimeException("Could not register protected resource.", e);
@@ -130,13 +138,13 @@ public class AlbumService {
try {
ProtectionResource protection = getAuthzClient().protection();
- Set<String> search = protection.resource().findByFilter("uri=" + uri);
+ List<ResourceRepresentation> search = protection.resource().findByUri(uri);
if (search.isEmpty()) {
throw new RuntimeException("Could not find protected resource with URI [" + uri + "]");
}
- protection.resource().delete(search.iterator().next());
+ protection.resource().delete(search.get(0).getId());
} catch (Exception e) {
throw new RuntimeException("Could not search protected resource.", e);
}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
index 92e300d..6e3e3b0 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
@@ -34,7 +34,7 @@ import java.util.List;
@Path("/profile")
public class ProfileService {
- private static final String PROFILE_VIEW = "urn:photoz.com:scopes:profile:view";
+ private static final String PROFILE_VIEW = "profile:view";
@Inject
private EntityManager entityManager;
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
index cc8bea2..f887e2a 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
@@ -24,6 +24,7 @@ import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.GenerationType;
+import javax.persistence.Transient;
import java.util.ArrayList;
import java.util.List;
@@ -45,6 +46,9 @@ public class Album {
@Column(nullable = false)
private String userId;
+ @Transient
+ private boolean userManaged = false;
+
public Long getId() {
return this.id;
}
@@ -76,4 +80,12 @@ public class Album {
public String getUserId() {
return this.userId;
}
+
+ public boolean isUserManaged() {
+ return userManaged;
+ }
+
+ public void setUserManaged(boolean userManaged) {
+ this.userManaged = userManaged;
+ }
}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
index f3db78d..a0f8711 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
@@ -16,32 +16,20 @@
}
},
"policy-enforcer": {
- "user-managed-access" : {},
+ "enforcement-mode": "PERMISSIVE",
+ "user-managed-access": {},
"paths": [
{
- "path" : "/album/*",
- "methods" : [
- {
- "method": "POST",
- "scopes" : ["urn:photoz.com:scopes:album:create"]
- },
- {
- "method": "GET",
- "scopes" : ["urn:photoz.com:scopes:album:view"]
- }
- ]
- },
- {
"name" : "Album Resource",
"path" : "/album/{id}",
"methods" : [
{
"method": "DELETE",
- "scopes" : ["urn:photoz.com:scopes:album:delete"]
+ "scopes" : ["album:delete"]
},
{
"method": "GET",
- "scopes" : ["urn:photoz.com:scopes:album:view"]
+ "scopes" : ["album:view"]
}
]
},
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json
index 0b621f5..7327cba 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json
@@ -8,7 +8,7 @@
"type": "http://photoz.com/profile",
"scopes": [
{
- "name": "urn:photoz.com:scopes:profile:view"
+ "name": "profile:view"
}
]
},
@@ -18,13 +18,13 @@
"type": "http://photoz.com/album",
"scopes": [
{
- "name": "urn:photoz.com:scopes:album:view"
+ "name": "album:view"
},
{
- "name": "urn:photoz.com:scopes:album:delete"
+ "name": "album:delete"
},
{
- "name": "urn:photoz.com:scopes:album:create"
+ "name": "album:create"
}
]
},
@@ -34,7 +34,7 @@
"type": "http://photoz.com/admin",
"scopes": [
{
- "name": "urn:photoz.com:scopes:album:admin:manage"
+ "name": "admin:manage"
}
]
},
@@ -165,7 +165,7 @@
"decisionStrategy": "UNANIMOUS",
"config": {
"applyPolicies": "[\"Only From @keycloak.org or Admin\"]",
- "scopes": "[\"urn:photoz.com:scopes:profile:view\"]"
+ "scopes": "[\"profile:view\"]"
}
},
{
@@ -176,7 +176,18 @@
"decisionStrategy": "UNANIMOUS",
"config": {
"applyPolicies": "[\"Only Owner and Administrators Policy\"]",
- "scopes": "[\"urn:photoz.com:scopes:album:delete\"]"
+ "scopes": "[\"album:delete\"]"
+ }
+ },
+ {
+ "name": "View Album Permission",
+ "description": "A policy that only allows the owner to view his albums.",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "applyPolicies": "[\"Only Owner and Administrators Policy\"]",
+ "scopes": "[\"album:view\"]"
}
},
{
@@ -213,19 +224,19 @@
],
"scopes": [
{
- "name": "urn:photoz.com:scopes:profile:view"
+ "name": "profile:view"
},
{
- "name": "urn:photoz.com:scopes:album:view"
+ "name": "album:view"
},
{
- "name": "urn:photoz.com:scopes:album:create"
+ "name": "album:create"
},
{
- "name": "urn:photoz.com:scopes:album:delete"
+ "name": "album:delete"
},
{
- "name": "urn:photoz.com:scopes:album:admin:manage"
+ "name": "admin:manage"
}
]
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/index.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/index.jsp
index 3fbfca2..345a69d 100755
--- a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/index.jsp
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/index.jsp
@@ -23,8 +23,8 @@
for (Permission permission : authzContext.getPermissions()) {
%>
<li>
- <p>Resource: <%= permission.getResourceSetName() %></p>
- <p>ID: <%= permission.getResourceSetId() %></p>
+ <p>Resource: <%= permission.getResourceName() %></p>
+ <p>ID: <%= permission.getResourceId() %></p>
<p>Scopes: <%= permission.getScopes() %></p>
</li>
<%
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
index 27df535..1f61268 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
@@ -27,6 +27,7 @@ import org.keycloak.testsuite.util.URLUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.Select;
import java.net.URL;
@@ -41,7 +42,7 @@ import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl {
public static final String DEPLOYMENT_NAME = "photoz-html5-client";
- public static final int WAIT_AFTER_OPERATION = 2000;
+ public static final int WAIT_AFTER_OPERATION = 1000;
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
@@ -62,11 +63,22 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl {
@FindBy(id = "entitlements")
private WebElement entitlements;
+ @FindBy(id = "get-all-resources")
+ private WebElement viewAllAlbums;
+
@FindBy(id = "output")
private WebElement output;
-
+
public void createAlbum(String name) {
- createAlbum(name, "save-album");
+ createAlbum(name, false);
+ }
+
+ public void createAlbum(String name, boolean managed) {
+ if (managed) {
+ createAlbum(name, "save-managed-album");
+ } else {
+ createAlbum(name, "save-album");
+ }
}
public void createAlbum(String name, String buttonId) {
@@ -118,13 +130,15 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl {
}
public void login(String username, String password, String... scopes) throws InterruptedException {
- if (this.driver.getCurrentUrl().startsWith(getInjectedUrl().toString())) {
- Thread.sleep(2000);
+ String currentUrl = this.driver.getCurrentUrl();
+
+ if (currentUrl.startsWith(getInjectedUrl().toString())) {
+ Thread.sleep(1000);
logOut();
navigateTo();
}
- Thread.sleep(2000);
+ Thread.sleep(1000);
if (scopes.length > 0) {
StringBuilder scopesValue = new StringBuilder();
@@ -136,7 +150,21 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl {
scopesValue.append(scope);
}
- URLUtils.navigateToUri(this.driver.getCurrentUrl() + " " + scopesValue, true);
+ scopesValue.append(" openid");
+
+ int scopeIndex = currentUrl.indexOf("scope");
+
+ if (scopeIndex != -1) {
+ StringBuilder url = new StringBuilder(currentUrl);
+
+ url.delete(scopeIndex, currentUrl.indexOf('&', scopeIndex));
+
+ url.append("&").append("scope=").append(scopesValue);
+
+ currentUrl = url.toString();
+ }
+
+ URLUtils.navigateToUri(currentUrl + " " + scopesValue, true);
}
this.loginPage.form().login(username, password);
@@ -154,12 +182,82 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl {
}
public void viewAlbum(String name) throws InterruptedException {
+ viewAlbum(name, true);
+ }
+
+ public void viewAllAlbums() {
+ viewAllAlbums.click();
+ pause(WAIT_AFTER_OPERATION);
+ }
+
+ public void viewAlbum(String name, boolean refresh) throws InterruptedException {
this.driver.findElement(By.xpath("//a[text() = '" + name + "']")).click();
waitForPageToLoad();
- driver.navigate().refresh(); // This is sometimes necessary for loading the new policy settings
+ if (refresh) {
+ driver.navigate().refresh(); // This is sometimes necessary for loading the new policy settings
+ }
+ pause(WAIT_AFTER_OPERATION);
+ }
+
+ public void accountPage() throws InterruptedException {
+ navigateTo();
+ this.driver.findElement(By.id("my-account")).click();
pause(WAIT_AFTER_OPERATION);
}
+ public void accountMyResources() throws InterruptedException {
+ accountPage();
+ this.driver.findElement(By.xpath("//a[text() = 'My Resources']")).click();
+ waitForPageToLoad();
+ pause(WAIT_AFTER_OPERATION);
+ }
+
+ public void accountMyResource(String name) throws InterruptedException {
+ accountMyResources();
+ this.driver.findElement(By.id("detail-" + name)).click();
+ waitForPageToLoad();
+ pause(WAIT_AFTER_OPERATION);
+ }
+
+ public void accountGrantResource(String name, String requester) throws InterruptedException {
+ accountMyResources();
+ this.driver.findElement(By.id("grant-" + name + "-" + requester)).click();
+ waitForPageToLoad();
+ }
+
+ public void accountGrantRemoveScope(String name, String requester, String scope) throws InterruptedException {
+ accountMyResources();
+ this.driver.findElement(By.id("grant-remove-scope-" + name + "-" + requester + "-" + scope)).click();
+ waitForPageToLoad();
+ }
+
+ public void accountRevokeResource(String name, String requester) throws InterruptedException {
+ accountMyResource(name);
+ this.driver.findElement(By.id("revoke-" + name + "-" + requester)).click();
+ waitForPageToLoad();
+ }
+
+ public void accountShareResource(String name, String user) throws InterruptedException {
+ accountMyResource(name);
+ this.driver.findElement(By.id("user_id")).sendKeys(user);
+ this.driver.findElement(By.id("share-button")).click();
+ waitForPageToLoad();
+ }
+
+ public void accountShareRemoveScope(String name, String user, String scope) throws InterruptedException {
+ accountMyResource(name);
+ this.driver.findElement(By.id("user_id")).sendKeys(user);
+ this.driver.findElement(By.id("share-remove-scope-" + name + "-" + scope)).click();
+ this.driver.findElement(By.id("share-button")).click();
+ waitForPageToLoad();
+ }
+
+ public void accountDenyResource(String name) throws InterruptedException {
+ accountMyResource(name);
+ this.driver.findElement(By.xpath("//a[text() = 'Deny']")).click();
+ waitForPageToLoad();
+ }
+
public void requestResourceProtectedAnyScope() throws InterruptedException {
navigateTo();
this.driver.findElement(By.id("requestPathWithAnyProtectedScope")).click();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java
index ba670e3..e1ad409 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java
@@ -33,7 +33,7 @@ public abstract class AbstractPermissiveModeAdapterTest extends AbstractServletA
@Deployment(name = RESOURCE_SERVER_ID, managed = false)
public static WebArchive deployment() throws IOException {
return exampleDeployment(RESOURCE_SERVER_ID)
- .addAsWebInfResource(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/servlet-authz-realm.json"), "keycloak.-permissive-authz-service.json");
+ .addAsWebInfResource(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/servlet-authz-realm.json"), "keycloak-permissive-authz-service.json");
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
index edd264c..a7cded4 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
@@ -16,6 +16,24 @@
*/
package org.keycloak.testsuite.adapter.example.authorization;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.util.IOUtil.loadJson;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
import org.jboss.arquillian.container.test.api.Deployer;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.graphene.page.Page;
@@ -43,24 +61,6 @@ import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
import org.keycloak.testsuite.adapter.page.PhotozClientAuthzTestApp;
import org.keycloak.util.JsonSerialization;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.keycloak.testsuite.util.IOUtil.loadJson;
-import static org.keycloak.testsuite.util.IOUtil.loadRealm;
-import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -358,6 +358,9 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
loginToClientPage("alice", "alice");
assertFalse(this.clientPage.wasDenied());
+ this.clientPage.createAlbum("Alice Family Album");
+ this.clientPage.viewAlbum("Alice Family Album");
+ assertFalse(this.clientPage.wasDenied());
UsersResource usersResource = realmsResouce().realm(REALM_NAME).users();
List<UserRepresentation> users = usersResource.search("alice", null, null, null, null, null);
@@ -380,9 +383,11 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
roleResource.update(roleRepresentation);
loginToClientPage("alice", "alice");
+ this.clientPage.viewAlbum("Alice Family Album");
assertTrue(this.clientPage.wasDenied());
loginToClientPage("alice", "alice", RESOURCE_SERVER_ID + "/manage-albums");
+ this.clientPage.viewAlbum("Alice Family Album", false);
assertFalse(this.clientPage.wasDenied());
} finally {
this.deployer.undeploy(RESOURCE_SERVER_ID);
@@ -398,6 +403,10 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
assertFalse(this.clientPage.wasDenied());
+ this.clientPage.createAlbum("Alice Family Album");
+ this.clientPage.viewAlbum("Alice Family Album");
+ assertFalse(this.clientPage.wasDenied());
+
UsersResource usersResource = realmsResouce().realm(REALM_NAME).users();
List<UserRepresentation> users = usersResource.search("alice", null, null, null, null, null);
@@ -419,6 +428,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
manageAlbumRole.update(roleRepresentation);
loginToClientPage("alice", "alice");
+ this.clientPage.viewAlbum("Alice Family Album");
assertTrue(this.clientPage.wasDenied());
for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) {
@@ -438,6 +448,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
}
loginToClientPage("alice", "alice");
+ this.clientPage.viewAlbum("Alice Family Album");
assertFalse(this.clientPage.wasDenied());
} finally {
this.deployer.undeploy(RESOURCE_SERVER_ID);
@@ -589,7 +600,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
resourcesResource.resources().forEach(resource -> {
if (resource.getName().equals(resourceName)) {
- resource.setScopes(resource.getScopes().stream().filter(scope -> !scope.getName().equals("urn:photoz.com:scopes:album:view")).collect(Collectors.toSet()));
+ resource.setScopes(resource.getScopes().stream().filter(scope -> !scope.getName().equals("album:view")).collect(Collectors.toSet()));
resourcesResource.resource(resource.getId()).update(resource);
}
});
@@ -631,12 +642,12 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
loginToClientPage("admin", "admin");
clientPage.requestEntitlements();
- assertTrue(driver.getPageSource().contains("urn:photoz.com:scopes:album:admin:manage"));
+ assertTrue(driver.getPageSource().contains("admin:manage"));
clientPage.requestEntitlement();
String pageSource = driver.getPageSource();
- assertTrue(pageSource.contains("urn:photoz.com:scopes:album:view"));
- assertFalse(pageSource.contains("urn:photoz.com:scopes:album:admin:manage"));
+ assertTrue(pageSource.contains("album:view"));
+ assertTrue(pageSource.contains("album:delete"));
} finally {
this.deployer.undeploy(RESOURCE_SERVER_ID);
}
@@ -656,6 +667,105 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
}
}
+ @Test
+ public void testRequestResourceToOwner() throws Exception {
+ try {
+ this.deployer.deploy(RESOURCE_SERVER_ID);
+ loginToClientPage("alice", "alice");
+ this.clientPage.createAlbum("Alice-Family-Album", true);
+
+ loginToClientPage("jdoe", "jdoe");
+ this.clientPage.viewAllAlbums();
+ this.clientPage.viewAlbum("Alice-Family-Album");
+ assertTrue(this.clientPage.wasDenied());
+ this.clientPage.navigateTo();
+ this.clientPage.viewAllAlbums();
+ this.clientPage.deleteAlbum("Alice-Family-Album");
+ assertTrue(this.clientPage.wasDenied());
+
+ loginToClientPage("alice", "alice");
+ this.clientPage.accountGrantResource("Alice-Family-Album", "jdoe");
+
+ loginToClientPage("jdoe", "jdoe");
+ this.clientPage.viewAllAlbums();
+ this.clientPage.viewAlbum("Alice-Family-Album");
+ assertFalse(this.clientPage.wasDenied());
+ this.clientPage.navigateTo();
+ this.clientPage.viewAllAlbums();
+ this.clientPage.deleteAlbum("Alice-Family-Album");
+ assertFalse(this.clientPage.wasDenied());
+
+ loginToClientPage("alice", "alice");
+ this.clientPage.createAlbum("Alice-Family-Album", true);
+
+ loginToClientPage("jdoe", "jdoe");
+ this.clientPage.viewAllAlbums();
+ this.clientPage.viewAlbum("Alice-Family-Album");
+ assertTrue(this.clientPage.wasDenied());
+ this.clientPage.navigateTo();
+ this.clientPage.viewAllAlbums();
+ this.clientPage.deleteAlbum("Alice-Family-Album");
+ assertTrue(this.clientPage.wasDenied());
+
+ loginToClientPage("alice", "alice");
+ this.clientPage.accountGrantRemoveScope("Alice-Family-Album", "jdoe", "album:delete");
+ this.clientPage.accountGrantResource("Alice-Family-Album", "jdoe");
+
+ loginToClientPage("jdoe", "jdoe");
+ this.clientPage.viewAllAlbums();
+ this.clientPage.viewAlbum("Alice-Family-Album");
+ assertFalse(this.clientPage.wasDenied());
+ this.clientPage.navigateTo();
+ this.clientPage.viewAllAlbums();
+ this.clientPage.deleteAlbum("Alice-Family-Album");
+ assertTrue(this.clientPage.wasDenied());
+ } finally {
+ this.deployer.undeploy(RESOURCE_SERVER_ID);
+ }
+ }
+
+ @Test
+ public void testOwnerSharingResource() throws Exception {
+ try {
+ this.deployer.deploy(RESOURCE_SERVER_ID);
+ loginToClientPage("alice", "alice");
+ this.clientPage.createAlbum("Alice-Family-Album", true);
+ this.clientPage.accountShareResource("Alice-Family-Album", "jdoe");
+
+ loginToClientPage("jdoe", "jdoe");
+ this.clientPage.viewAllAlbums();
+ this.clientPage.viewAlbum("Alice-Family-Album");
+ assertFalse(this.clientPage.wasDenied());
+ this.clientPage.navigateTo();
+ this.clientPage.viewAllAlbums();
+ this.clientPage.deleteAlbum("Alice-Family-Album");
+ assertFalse(this.clientPage.wasDenied());
+
+ loginToClientPage("alice", "alice");
+ this.clientPage.createAlbum("Alice-Family-Album", true);
+ this.clientPage.accountShareRemoveScope("Alice-Family-Album", "jdoe", "album:delete");
+
+ loginToClientPage("jdoe", "jdoe");
+ this.clientPage.viewAllAlbums();
+ this.clientPage.viewAlbum("Alice-Family-Album");
+ assertFalse(this.clientPage.wasDenied());
+ this.clientPage.navigateTo();
+ this.clientPage.viewAllAlbums();
+ this.clientPage.deleteAlbum("Alice-Family-Album");
+ assertTrue(this.clientPage.wasDenied());
+
+ loginToClientPage("alice", "alice");
+ this.clientPage.accountRevokeResource("Alice-Family-Album", "jdoe");
+
+ loginToClientPage("jdoe", "jdoe");
+ this.clientPage.viewAllAlbums();
+ this.clientPage.viewAlbum("Alice-Family-Album");
+ assertTrue(this.clientPage.wasDenied());
+ } finally {
+ this.deployer.undeploy(RESOURCE_SERVER_ID);
+ }
+ }
+
private void importResourceServerSettings() throws FileNotFoundException {
ResourceServerRepresentation authSettings = loadJson(new FileInputStream(new File(TEST_APPS_HOME_DIR + "/photoz/photoz-restful-api-authz-service.json")), ResourceServerRepresentation.class);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
index 5f07b2f..536d122 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
@@ -20,10 +20,8 @@ package org.keycloak.testsuite.admin.client.authorization;
import java.io.IOException;
import java.util.stream.Collectors;
-import org.jetbrains.annotations.NotNull;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.RegistrationResponse;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
@@ -42,7 +40,7 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
org.keycloak.authorization.client.representation.ResourceRepresentation resource = toResourceRepresentation(newResource);
AuthzClient authzClient = getAuthzClient();
- RegistrationResponse response = authzClient.protection().resource().create(resource);
+ org.keycloak.authorization.client.representation.ResourceRepresentation response = authzClient.protection().resource().create(resource);
return toResourceRepresentation(authzClient, response.getId());
}
@@ -62,7 +60,7 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
}
private ResourceRepresentation toResourceRepresentation(AuthzClient authzClient, String id) {
- org.keycloak.authorization.client.representation.ResourceRepresentation created = authzClient.protection().resource().findById(id).getResourceDescription();
+ org.keycloak.authorization.client.representation.ResourceRepresentation created = authzClient.protection().resource().findById(id);
ResourceRepresentation resourceRepresentation = new ResourceRepresentation();
resourceRepresentation.setId(created.getId());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
index b4b493e..56d2bee 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
@@ -294,6 +294,7 @@ public class RealmTest extends AbstractAdminTest {
rep.setRegistrationAllowed(true);
rep.setRegistrationEmailAsUsername(true);
rep.setEditUsernameAllowed(true);
+ rep.setUserManagedAccessAllowed(true);
realm.update(rep);
assertAdminEvents.assertEvent(realmId, OperationType.UPDATE, Matchers.nullValue(String.class), rep, ResourceType.REALM);
@@ -308,11 +309,13 @@ public class RealmTest extends AbstractAdminTest {
assertEquals(Boolean.TRUE, rep.isRegistrationAllowed());
assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername());
assertEquals(Boolean.TRUE, rep.isEditUsernameAllowed());
+ assertEquals(Boolean.TRUE, rep.isUserManagedAccessAllowed());
// second change
rep.setRegistrationAllowed(false);
rep.setRegistrationEmailAsUsername(false);
rep.setEditUsernameAllowed(false);
+ rep.setUserManagedAccessAllowed(false);
realm.update(rep);
assertAdminEvents.assertEvent(realmId, OperationType.UPDATE, Matchers.nullValue(String.class), rep, ResourceType.REALM);
@@ -321,7 +324,7 @@ public class RealmTest extends AbstractAdminTest {
assertEquals(Boolean.FALSE, rep.isRegistrationAllowed());
assertEquals(Boolean.FALSE, rep.isRegistrationEmailAsUsername());
assertEquals(Boolean.FALSE, rep.isEditUsernameAllowed());
-
+ assertEquals(Boolean.FALSE, rep.isUserManagedAccessAllowed());
}
@Test
@@ -527,6 +530,7 @@ public class RealmTest extends AbstractAdminTest {
assertEquals(realm.getAttributes(), attributes);
}
+ if (realm.isUserManagedAccessAllowed() != null) assertEquals(realm.isUserManagedAccessAllowed(), storedRealm.isUserManagedAccessAllowed());
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractAuthzTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractAuthzTest.java
index 02d1865..77952be 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractAuthzTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractAuthzTest.java
@@ -1,7 +1,6 @@
package org.keycloak.testsuite.authz;
import org.junit.BeforeClass;
-import org.keycloak.authorization.client.representation.EntitlementResponse;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessToken;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java
new file mode 100644
index 0000000..237d5e3
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import javax.ws.rs.core.Response;
+
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
+import org.keycloak.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractResourceServerTest extends AbstractKeycloakTest {
+
+ protected static final String REALM_NAME = "authz-test";
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ testRealms.add(RealmBuilder.create().name(REALM_NAME)
+ .roles(RolesBuilder.create()
+ .realmRole(RoleBuilder.create().name("uma_authorization").build())
+ .realmRole(RoleBuilder.create().name("uma_protection").build())
+ )
+ .user(UserBuilder.create().username("marta").password("password")
+ .addRoles("uma_authorization", "uma_protection")
+ .role("resource-server-test", "uma_protection"))
+ .user(UserBuilder.create().username("kolo").password("password"))
+ .client(ClientBuilder.create().clientId("resource-server-test")
+ .secret("secret")
+ .authorizationServicesEnabled(true)
+ .redirectUris("http://localhost/resource-server-test")
+ .defaultRoles("uma_protection")
+ .directAccessGrants())
+ .client(ClientBuilder.create().clientId("test-app")
+ .redirectUris("http://localhost:8180/auth/realms/master/app/auth")
+ .publicClient())
+ .build());
+ }
+
+ protected AuthorizationResponse authorize(String resourceName, String[] scopeNames, String claimToken) {
+ return authorize(null, null, resourceName, scopeNames, null, null, claimToken);
+ }
+
+ protected AuthorizationResponse authorize(String resourceName, String[] scopeNames, String claimToken, String tokenFormat) {
+ return authorize(null, null, null, null, null, claimToken, tokenFormat, new PermissionRequest(resourceName, scopeNames));
+ }
+
+ protected AuthorizationResponse authorize(String resourceName, String[] scopeNames) {
+ return authorize(null, null, resourceName, scopeNames, null, null, null);
+ }
+
+ protected AuthorizationResponse authorize(String userName, String password, String resourceName, String[] scopeNames) {
+ return authorize(userName, password, resourceName, scopeNames, null, null, null);
+ }
+
+ protected AuthorizationResponse authorize(String userName, String password, PermissionRequest... permissions) {
+ return authorize(userName, password, null, null, null, null, null, permissions);
+ }
+
+ protected AuthorizationResponse authorize(String userName, String password, String resourceName, String[] scopeNames, String rpt) {
+ return authorize(userName, password, resourceName, scopeNames, null, rpt, null);
+ }
+
+ protected AuthorizationResponse authorize(String userName, String password, String resourceName, String[] scopeNames, String[] additionalScopes) {
+ return authorize(userName, password, resourceName, scopeNames, additionalScopes, null, null);
+ }
+
+ protected AuthorizationResponse authorize(String userName, String password, String resourceName, String[] scopeNames, String[] additionalScopes, String rpt, String claimToken) {
+ return authorize(userName, password, additionalScopes, rpt, null, claimToken, null, new PermissionRequest(resourceName, scopeNames));
+ }
+
+ protected AuthorizationResponse authorize(String userName, String password, String[] additionalScopes, String rpt, String accessToken, String claimToken, String tokenFormat, PermissionRequest... permissions) {
+ ProtectionResource protection;
+
+ if (userName != null) {
+ protection = getAuthzClient().protection(userName, password);
+ } else {
+ protection = getAuthzClient().protection();
+ }
+
+ String ticket = protection.permission().create(Arrays.asList(permissions)).getTicket();
+
+ AuthorizationRequest authorizationRequest = new AuthorizationRequest(ticket);
+
+ if (additionalScopes != null) {
+ StringBuilder builder = new StringBuilder();
+
+ for (String scope : additionalScopes) {
+ if (builder.length() > 0) {
+ builder.append(" ");
+ }
+ builder.append(scope);
+ }
+
+ authorizationRequest.setScope(builder.toString());
+ }
+
+ authorizationRequest.setRpt(rpt);
+ authorizationRequest.setClaimTokenFormat(tokenFormat);
+ authorizationRequest.setClaimToken(claimToken);
+
+ org.keycloak.authorization.client.resource.AuthorizationResource authorization;
+
+ if (userName != null) {
+ authorization = getAuthzClient().authorization(userName, password);
+ } else if (accessToken != null) {
+ authorization = getAuthzClient().authorization(accessToken);
+ } else {
+ authorization = getAuthzClient().authorization();
+ }
+
+ return authorization.authorize(authorizationRequest);
+ }
+
+ protected RealmResource getRealm() throws Exception {
+ return adminClient.realm("authz-test");
+ }
+
+ protected ClientResource getClient(RealmResource realm) {
+ ClientsResource clients = realm.clients();
+ return clients.findByClientId("resource-server-test").stream().map(representation -> clients.get(representation.getId())).findFirst().orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
+ }
+
+ protected AuthzClient getAuthzClient() {
+ try {
+ return AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak-uma2.json"), Configuration.class));
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to create authz client", cause);
+ }
+ }
+
+ protected AccessToken toAccessToken(String rpt) throws Exception {
+ return JsonSerialization.readValue(new JWSInput(rpt).getContent(), AccessToken.class);
+ }
+
+ protected void assertPermissions(List<Permission> permissions, String expectedResource, String... expectedScopes) {
+ Iterator<Permission> iterator = permissions.iterator();
+
+ while (iterator.hasNext()) {
+ Permission permission = iterator.next();
+
+ if (permission.getResourceName().equalsIgnoreCase(expectedResource)) {
+ Set<String> scopes = permission.getScopes();
+
+ assertEquals(expectedScopes.length, scopes.size());
+
+ if (scopes.containsAll(Arrays.asList(expectedScopes))) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ protected ResourceRepresentation addResource(String resourceName, String... scopeNames) throws Exception {
+ return addResource(resourceName, null, false, scopeNames);
+ }
+
+ protected ResourceRepresentation addResource(String resourceName, boolean ownerManagedAccess, String... scopeNames) throws Exception {
+ return addResource(resourceName, null, ownerManagedAccess, scopeNames);
+ }
+
+ protected ResourceRepresentation addResource(String resourceName, String owner, boolean ownerManagedAccess, String... scopeNames) throws Exception {
+ ClientResource client = getClient(getRealm());
+ AuthorizationResource authorization = client.authorization();
+ ResourceRepresentation resource = new ResourceRepresentation(resourceName);
+
+ if (owner != null) {
+ resource.setOwner(new ResourceOwnerRepresentation(owner));
+ }
+
+ resource.setOwnerManagedAccess(ownerManagedAccess);
+ resource.addScope(scopeNames);
+
+ Response response = authorization.resources().create(resource);
+ ResourceRepresentation temp = response.readEntity(ResourceRepresentation.class);
+ resource.setId(temp.getId());
+ response.close();
+
+ return resource;
+ }
+
+ @Override
+ protected boolean isImportAfterEachMethod() {
+ return true;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthorizationAPITest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthorizationAPITest.java
index 5529cfd..61d2773 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthorizationAPITest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthorizationAPITest.java
@@ -18,10 +18,8 @@ package org.keycloak.testsuite.authz;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.fail;
import java.io.IOException;
-import java.util.Arrays;
import java.util.List;
import javax.ws.rs.core.Response;
@@ -32,17 +30,14 @@ import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
-import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.AuthorizationRequest;
-import org.keycloak.authorization.client.representation.AuthorizationResponse;
-import org.keycloak.authorization.client.representation.PermissionRequest;
-import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.util.ClientBuilder;
@@ -108,45 +103,12 @@ public class AuthorizationAPITest extends AbstractAuthzTest {
@Test
public void testAccessTokenWithUmaAuthorization() {
AuthzClient authzClient = getAuthzClient();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Resource A");
+ PermissionRequest request = new PermissionRequest("Resource A");
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ String ticket = authzClient.protection().permission().create(request).getTicket();
AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
- }
-
- @Test
- public void failAccessTokenWithoutUmaAuthorization() {
- AuthzClient authzClient = getAuthzClient();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Resource A");
-
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
-
- try {
- authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
- fail("Should fail because user does not have uma_authorization");
- } catch (AuthorizationDeniedException cause) {
- assertEquals(403, ((HttpResponseException) cause.getCause()).getStatusCode());
- }
- }
-
- @Test
- public void failClientMockingUmaAuthorization() throws Exception {
- RealmResource realm = getRealm();
- ClientResource client = getClient(realm);
- RoleRepresentation umaAuthorizationRole = new RoleRepresentation("uma_authorization", "", false);
-
- client.roles().create(umaAuthorizationRole);
- umaAuthorizationRole = client.roles().get(umaAuthorizationRole.getName()).toRepresentation();
-
- realm.users().get(realm.users().search("kolo").get(0).getId()).roles().clientLevel(client.toRepresentation().getId()).add(Arrays.asList(umaAuthorizationRole));
-
- failAccessTokenWithoutUmaAuthorization();
+ assertNotNull(response.getToken());
}
@Test
@@ -154,14 +116,14 @@ public class AuthorizationAPITest extends AbstractAuthzTest {
AuthzClient authzClient = getAuthzClient();
PermissionRequest request = new PermissionRequest();
- request.setResourceSetName("Resource A");
+ request.setResourceId("Resource A");
String accessToken = new OAuthClient().realm("authz-test").clientId("test-client").doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken();
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ String ticket = authzClient.protection().permission().create(request).getTicket();
AuthorizationResponse response = authzClient.authorization(accessToken).authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
- AccessToken rpt = toAccessToken(response.getRpt());
+ assertNotNull(response.getToken());
+ AccessToken rpt = toAccessToken(response.getToken());
assertEquals("resource-server-test", rpt.getAudience()[0]);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java
index cfeb153..7fece47 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java
@@ -19,12 +19,16 @@ package org.keycloak.testsuite.authz;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.InputStream;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import org.junit.Before;
import org.junit.Test;
@@ -35,15 +39,9 @@ import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
-import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.AuthorizationRequest;
-import org.keycloak.authorization.client.representation.AuthorizationResponse;
-import org.keycloak.authorization.client.representation.PermissionRequest;
-import org.keycloak.authorization.client.representation.PermissionResponse;
-import org.keycloak.authorization.client.representation.RegistrationResponse;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.authorization.client.util.HttpResponseException;
@@ -53,9 +51,12 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
-import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RolesBuilder;
@@ -111,15 +112,12 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
public void testSuccessfulAuthorizationRequest() throws Exception {
AuthzClient authzClient = getAuthzClient("keycloak-with-jwt-authentication.json");
ProtectionResource protection = authzClient.protection();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Default Resource");
-
- PermissionResponse ticketResponse = protection.permission().forResource(request);
+ PermissionRequest request = new PermissionRequest("Default Resource");
+ PermissionResponse ticketResponse = protection.permission().create(request);
String ticket = ticketResponse.getTicket();
AuthorizationResponse authorizationResponse = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
- String rpt = authorizationResponse.getRpt();
+ String rpt = authorizationResponse.getToken();
assertNotNull(rpt);
@@ -132,35 +130,17 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
List<Permission> permissions = authorization.getPermissions();
assertFalse(permissions.isEmpty());
- assertEquals("Default Resource", permissions.get(0).getResourceSetName());
- }
-
- @Test
- public void failUserWithoutUmaAuthorizationScope() throws Exception {
- AuthzClient authzClient = getAuthzClient("keycloak-with-jwt-authentication.json");
- ProtectionResource protection = authzClient.protection();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Default Resource");
-
- PermissionResponse ticketResponse = protection.permission().forResource(request);
- String ticket = ticketResponse.getTicket();
-
- try {
- authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
- fail("Should fail because user does not have uma_authorization");
- } catch (AuthorizationDeniedException cause) {
- assertEquals(403, ((HttpResponseException) cause.getCause()).getStatusCode());
- }
+ assertEquals("Default Resource", permissions.get(0).getResourceName());
}
@Test
public void failJWTAuthentication() {
try {
- getAuthzClient("keycloak-with-invalid-keys-jwt-authentication.json").protection();
+ getAuthzClient("keycloak-with-invalid-keys-jwt-authentication.json").protection().resource().findAll();
fail("Should fail due to invalid signature");
- } catch (HttpResponseException cause) {
- assertEquals(400, cause.getStatusCode());
+ } catch (Exception cause) {
+ assertTrue(HttpResponseException.class.isInstance(cause.getCause().getCause()));
+ assertEquals(400, HttpResponseException.class.cast(cause.getCause().getCause()).getStatusCode());
}
}
@@ -181,14 +161,14 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
AuthzClient authzClient = getAuthzClient("default-session-keycloak.json");
ProtectionResource protection = authzClient.protection();
- protection.resource().findByFilter("name=Default Resource");
+ protection.resource().findByName("Default Resource");
userSessions = clients.get(clientRepresentation.getId()).getUserSessions(null, null);
assertEquals(1, userSessions.size());
Thread.sleep(2000);
protection = authzClient.protection();
- protection.resource().findByFilter("name=Default Resource");
+ protection.resource().findByName("Default Resource");
userSessions = clients.get(clientRepresentation.getId()).getUserSessions(null, null);
@@ -211,8 +191,7 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
ResourceRepresentation expected = new ResourceRepresentation("Resource A", Collections.emptySet());
String id = protection.resource().create(expected).getId();
- RegistrationResponse response = protection.resource().findById(id);
- ResourceRepresentation actual = response.getResourceDescription();
+ ResourceRepresentation actual = protection.resource().findById(id);
assertNotNull(actual);
assertEquals(expected.getName(), actual.getName());
@@ -224,8 +203,12 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest {
return AuthzClient.create(new Configuration(deployment.getAuthServerBaseUrl(), deployment.getRealm(), deployment.getResourceName(), deployment.getResourceCredentials(), deployment.getClient()), new ClientAuthenticator() {
@Override
- public void configureClientCredentials(HashMap<String, String> requestParams, HashMap<String, String> requestHeaders) {
- ClientCredentialsProviderUtils.setClientCredentials(deployment, requestHeaders, requestParams);
+ public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
+ Map<String, String> formparams = new HashMap<>();
+ ClientCredentialsProviderUtils.setClientCredentials(deployment, requestHeaders, formparams);
+ for (Entry<String, String> param : formparams.entrySet()) {
+ requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
+ }
}
});
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java
index d7f8c6b..b55f348 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java
@@ -38,18 +38,17 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.EntitlementResponse;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.representation.ScopeRepresentation;
import org.keycloak.jose.jws.JWSInput;
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.Permission;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
-import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
@@ -96,7 +95,7 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
List<Permission> permissions = getEntitlements("marta", "password");
for (Permission permission : new ArrayList<>(permissions)) {
- String resourceSetName = permission.getResourceSetName();
+ String resourceSetName = permission.getResourceName();
switch (resourceSetName) {
case "Resource A":
@@ -122,11 +121,11 @@ public class ConflictingScopePermissionTest extends AbstractAuthzTest {
private List<Permission> getEntitlements(String username, String password) {
AuthzClient authzClient = getAuthzClient();
- EntitlementResponse response = authzClient.entitlement(authzClient.obtainAccessToken(username, password).getToken()).getAll("resource-server-test");
+ AuthorizationResponse response = authzClient.authorization(username, password).authorize();
AccessToken accessToken;
try {
- accessToken = new JWSInput(response.getRpt()).readJsonContent(AccessToken.class);
+ accessToken = new JWSInput(response.getToken()).readJsonContent(AccessToken.class);
} catch (JWSInputException cause) {
throw new RuntimeException("Failed to deserialize RPT", cause);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java
index c0a8868..54bcd2e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java
@@ -25,8 +25,6 @@ import java.io.IOException;
import java.util.List;
import java.util.function.Supplier;
-import javax.ws.rs.core.Response;
-
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.AuthorizationResource;
@@ -35,20 +33,15 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.AuthorizationRequestMetadata;
-import org.keycloak.authorization.client.representation.EntitlementRequest;
-import org.keycloak.authorization.client.representation.EntitlementResponse;
-import org.keycloak.authorization.client.representation.PermissionRequest;
-import org.keycloak.jose.jws.JWSInput;
-import org.keycloak.jose.jws.JWSInputException;
-import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
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.JSPolicyRepresentation;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
-import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
@@ -113,76 +106,76 @@ public class EntitlementAPITest extends AbstractAuthzTest {
@Test
public void testRptRequestWithoutResourceName() {
- AuthorizationRequestMetadata metadata = new AuthorizationRequestMetadata();
+ Metadata metadata = new Metadata();
metadata.setIncludeResourceName(false);
assertResponse(metadata, () -> {
- EntitlementRequest request = new EntitlementRequest();
+ AuthorizationRequest request = new AuthorizationRequest();
request.setMetadata(metadata);
- request.addPermission(new PermissionRequest("Resource 1"));
+ request.addPermission("Resource 1");
- return getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
+ return getAuthzClient().authorization("marta", "password").authorize(request);
});
}
@Test
public void testRptRequestWithResourceName() {
- AuthorizationRequestMetadata metadata = new AuthorizationRequestMetadata();
+ Metadata metadata = new Metadata();
metadata.setIncludeResourceName(true);
- assertResponse(metadata, () -> getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).getAll("resource-server-test"));
+ assertResponse(metadata, () -> getAuthzClient().authorization("marta", "password").authorize());
- EntitlementRequest request = new EntitlementRequest();
+ AuthorizationRequest request = new AuthorizationRequest();
request.setMetadata(metadata);
- request.addPermission(new PermissionRequest("Resource 13"));
+ request.addPermission("Resource 13");
- assertResponse(metadata, () -> getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request));
+ assertResponse(metadata, () -> getAuthzClient().authorization("marta", "password").authorize(request));
request.setMetadata(null);
- assertResponse(metadata, () -> getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request));
+ assertResponse(metadata, () -> getAuthzClient().authorization("marta", "password").authorize(request));
}
@Test
public void testPermissionLimit() {
- EntitlementRequest request = new EntitlementRequest();
+ AuthorizationRequest request = new AuthorizationRequest();
for (int i = 1; i <= 10; i++) {
- request.addPermission(new PermissionRequest("Resource " + i));
+ request.addPermission("Resource " + i);
}
- AuthorizationRequestMetadata metadata = new AuthorizationRequestMetadata();
+ Metadata metadata = new Metadata();
metadata.setLimit(10);
request.setMetadata(metadata);
- EntitlementResponse response = getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
- AccessToken rpt = toAccessToken(response.getRpt());
+ AuthorizationResponse response = getAuthzClient().authorization("marta", "password").authorize(request);
+ AccessToken rpt = toAccessToken(response.getToken());
List<Permission> permissions = rpt.getAuthorization().getPermissions();
assertEquals(10, permissions.size());
for (int i = 0; i < 10; i++) {
- assertEquals("Resource " + (i + 1), permissions.get(i).getResourceSetName());
+ assertEquals("Resource " + (i + 1), permissions.get(i).getResourceName());
}
- request = new EntitlementRequest();
+ request = new AuthorizationRequest();
for (int i = 11; i <= 15; i++) {
- request.addPermission(new PermissionRequest("Resource " + i));
+ request.addPermission("Resource " + i);
}
request.setMetadata(metadata);
- request.setRpt(response.getRpt());
+ request.setRpt(response.getToken());
- response = getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
- rpt = toAccessToken(response.getRpt());
+ response = getAuthzClient().authorization("marta", "password").authorize(request);
+ rpt = toAccessToken(response.getToken());
permissions = rpt.getAuthorization().getPermissions();
@@ -190,72 +183,72 @@ public class EntitlementAPITest extends AbstractAuthzTest {
for (int i = 0; i < 10; i++) {
if (i < 5) {
- assertEquals("Resource " + (i + 11), permissions.get(i).getResourceSetName());
+ assertEquals("Resource " + (i + 11), permissions.get(i).getResourceName());
} else {
- assertEquals("Resource " + (i - 4), permissions.get(i).getResourceSetName());
+ assertEquals("Resource " + (i - 4), permissions.get(i).getResourceName());
}
}
- request = new EntitlementRequest();
+ request = new AuthorizationRequest();
for (int i = 16; i <= 18; i++) {
- request.addPermission(new PermissionRequest("Resource " + i));
+ request.addPermission("Resource " + i);
}
request.setMetadata(metadata);
- request.setRpt(response.getRpt());
+ request.setRpt(response.getToken());
- response = getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
- rpt = toAccessToken(response.getRpt());
+ response = getAuthzClient().authorization("marta", "password").authorize(request);
+ rpt = toAccessToken(response.getToken());
permissions = rpt.getAuthorization().getPermissions();
assertEquals(10, permissions.size());
- assertEquals("Resource 16", permissions.get(0).getResourceSetName());
- assertEquals("Resource 17", permissions.get(1).getResourceSetName());
- assertEquals("Resource 18", permissions.get(2).getResourceSetName());
- assertEquals("Resource 11", permissions.get(3).getResourceSetName());
- assertEquals("Resource 12", permissions.get(4).getResourceSetName());
- assertEquals("Resource 13", permissions.get(5).getResourceSetName());
- assertEquals("Resource 14", permissions.get(6).getResourceSetName());
- assertEquals("Resource 15", permissions.get(7).getResourceSetName());
- assertEquals("Resource 1", permissions.get(8).getResourceSetName());
- assertEquals("Resource 2", permissions.get(9).getResourceSetName());
-
- request = new EntitlementRequest();
+ assertEquals("Resource 16", permissions.get(0).getResourceName());
+ assertEquals("Resource 17", permissions.get(1).getResourceName());
+ assertEquals("Resource 18", permissions.get(2).getResourceName());
+ assertEquals("Resource 11", permissions.get(3).getResourceName());
+ assertEquals("Resource 12", permissions.get(4).getResourceName());
+ assertEquals("Resource 13", permissions.get(5).getResourceName());
+ assertEquals("Resource 14", permissions.get(6).getResourceName());
+ assertEquals("Resource 15", permissions.get(7).getResourceName());
+ assertEquals("Resource 1", permissions.get(8).getResourceName());
+ assertEquals("Resource 2", permissions.get(9).getResourceName());
+
+ request = new AuthorizationRequest();
metadata.setLimit(5);
request.setMetadata(metadata);
- request.setRpt(response.getRpt());
+ request.setRpt(response.getToken());
- response = getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
- rpt = toAccessToken(response.getRpt());
+ response = getAuthzClient().authorization("marta", "password").authorize(request);
+ rpt = toAccessToken(response.getToken());
permissions = rpt.getAuthorization().getPermissions();
assertEquals(5, permissions.size());
- assertEquals("Resource 16", permissions.get(0).getResourceSetName());
- assertEquals("Resource 17", permissions.get(1).getResourceSetName());
- assertEquals("Resource 18", permissions.get(2).getResourceSetName());
- assertEquals("Resource 11", permissions.get(3).getResourceSetName());
- assertEquals("Resource 12", permissions.get(4).getResourceSetName());
+ assertEquals("Resource 16", permissions.get(0).getResourceName());
+ assertEquals("Resource 17", permissions.get(1).getResourceName());
+ assertEquals("Resource 18", permissions.get(2).getResourceName());
+ assertEquals("Resource 11", permissions.get(3).getResourceName());
+ assertEquals("Resource 12", permissions.get(4).getResourceName());
}
@Test
public void testResourceServerAsAudience() throws Exception {
- EntitlementRequest request = new EntitlementRequest();
+ AuthorizationRequest request = new AuthorizationRequest();
- request.addPermission(new PermissionRequest("Resource 1"));
+ request.addPermission("Resource 1");
String accessToken = new OAuthClient().realm("authz-test").clientId("test-client").doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken();
- EntitlementResponse response = getAuthzClient().entitlement(accessToken).get("resource-server-test", request);
- AccessToken rpt = toAccessToken(response.getRpt());
+ AuthorizationResponse response = getAuthzClient().authorization(accessToken).authorize(request);
+ AccessToken rpt = toAccessToken(response.getToken());
assertEquals("resource-server-test", rpt.getAudience()[0]);
}
- private void assertResponse(AuthorizationRequestMetadata metadata, Supplier<EntitlementResponse> responseSupplier) {
- AccessToken.Authorization authorization = toAccessToken(responseSupplier.get().getRpt()).getAuthorization();
+ private void assertResponse(Metadata metadata, Supplier<AuthorizationResponse> responseSupplier) {
+ AccessToken.Authorization authorization = toAccessToken(responseSupplier.get().getToken()).getAuthorization();
List<Permission> permissions = authorization.getPermissions();
@@ -263,10 +256,10 @@ public class EntitlementAPITest extends AbstractAuthzTest {
assertFalse(permissions.isEmpty());
for (Permission permission : permissions) {
- if (metadata.isIncludeResourceName()) {
- assertNotNull(permission.getResourceSetName());
+ if (metadata.getIncludeResourceName()) {
+ assertNotNull(permission.getResourceName());
} else {
- assertNull(permission.getResourceSetName());
+ assertNull(permission.getResourceName());
}
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
index 256c24c..f4fcce5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
@@ -37,9 +37,6 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.AuthorizationRequest;
-import org.keycloak.authorization.client.representation.AuthorizationResponse;
-import org.keycloak.authorization.client.representation.PermissionRequest;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
@@ -47,10 +44,12 @@ import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
-import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.GroupBuilder;
@@ -138,14 +137,11 @@ public class GroupNamePolicyTest extends AbstractAuthzTest {
@Test
public void testExactNameMatch() {
AuthzClient authzClient = getAuthzClient();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Resource A");
-
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ PermissionRequest request = new PermissionRequest("Resource A");
+ String ticket = authzClient.protection().permission().create(request).getTicket();
AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
+ assertNotNull(response.getToken());
try {
authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
@@ -166,11 +162,8 @@ public class GroupNamePolicyTest extends AbstractAuthzTest {
public void testOnlyChildrenPolicy() throws Exception {
RealmResource realm = getRealm();
AuthzClient authzClient = getAuthzClient();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Resource B");
-
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ PermissionRequest request = new PermissionRequest("Resource B");
+ String ticket = authzClient.protection().permission().create(request).getTicket();
try {
authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
@@ -181,7 +174,7 @@ public class GroupNamePolicyTest extends AbstractAuthzTest {
AuthorizationResponse response = authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
+ assertNotNull(response.getToken());
try {
authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
@@ -190,15 +183,10 @@ public class GroupNamePolicyTest extends AbstractAuthzTest {
}
- request = new PermissionRequest();
-
- request.setResourceSetName("Resource C");
-
- ticket = authzClient.protection().permission().forResource(request).getTicket();
-
+ request = new PermissionRequest("Resource C");
+ ticket = authzClient.protection().permission().create(request).getTicket();
response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
-
- assertNotNull(response.getRpt());
+ assertNotNull(response.getToken());
}
private void createGroupPolicy(String name, String groupPath, boolean extendChildren) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
index 9b3b728..ab0bfe9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
@@ -22,7 +22,6 @@ import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -38,22 +37,19 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.AuthorizationRequest;
-import org.keycloak.authorization.client.representation.AuthorizationResponse;
-import org.keycloak.authorization.client.representation.PermissionRequest;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
-import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
-import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.GroupBuilder;
@@ -128,14 +124,11 @@ public class GroupPathPolicyTest extends AbstractAuthzTest {
@Test
public void testAllowParentAndChildren() {
AuthzClient authzClient = getAuthzClient();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Resource A");
-
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ PermissionRequest request = new PermissionRequest("Resource A");
+ String ticket = authzClient.protection().permission().create(request).getTicket();
AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
+ assertNotNull(response.getToken());
RealmResource realm = getRealm();
GroupRepresentation group = getGroup("/Group A/Group B/Group C");
@@ -143,21 +136,18 @@ public class GroupPathPolicyTest extends AbstractAuthzTest {
realm.users().get(user.getId()).joinGroup(group.getId());
- ticket = authzClient.protection().permission().forResource(request).getTicket();
+ ticket = authzClient.protection().permission().create(request).getTicket();
response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
+ assertNotNull(response.getToken());
}
@Test
public void testOnlyChildrenPolicy() throws Exception {
RealmResource realm = getRealm();
AuthzClient authzClient = getAuthzClient();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Resource B");
-
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ PermissionRequest request = new PermissionRequest("Resource B");
+ String ticket = authzClient.protection().permission().create(request).getTicket();
try {
authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
@@ -173,7 +163,7 @@ public class GroupPathPolicyTest extends AbstractAuthzTest {
AuthorizationResponse response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
+ assertNotNull(response.getToken());
try {
authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java
index cde7fc2..7a9600f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java
@@ -32,19 +32,16 @@ import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
-import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.AuthorizationRequest;
-import org.keycloak.authorization.client.representation.AuthorizationResponse;
-import org.keycloak.authorization.client.representation.PermissionRequest;
-import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
import org.keycloak.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.util.ClientBuilder;
@@ -121,15 +118,15 @@ public class PermissionClaimTest extends AbstractAuthzTest {
PermissionRequest request = new PermissionRequest();
- request.setResourceSetName(resource.getName());
+ request.setResourceId(resource.getName());
String accessToken = new OAuthClient().realm("authz-test").clientId("test-client").doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken();
AuthzClient authzClient = getAuthzClient();
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ String ticket = authzClient.protection().permission().create(request).getTicket();
AuthorizationResponse response = authzClient.authorization(accessToken).authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
- AccessToken rpt = toAccessToken(response.getRpt());
+ assertNotNull(response.getToken());
+ AccessToken rpt = toAccessToken(response.getToken());
Authorization authorizationClaim = rpt.getAuthorization();
List<Permission> permissions = authorizationClaim.getPermissions();
@@ -157,15 +154,15 @@ public class PermissionClaimTest extends AbstractAuthzTest {
PermissionRequest request = new PermissionRequest();
- request.setResourceSetName(resource.getName());
+ request.setResourceId(resource.getName());
String accessToken = new OAuthClient().realm("authz-test").clientId("test-client").doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken();
AuthzClient authzClient = getAuthzClient();
String ticket = authzClient.protection().permission().forResource(request).getTicket();
AuthorizationResponse response = authzClient.authorization(accessToken).authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
- AccessToken rpt = toAccessToken(response.getRpt());
+ assertNotNull(response.getToken());
+ AccessToken rpt = toAccessToken(response.getToken());
Authorization authorizationClaim = rpt.getAuthorization();
List<Permission> permissions = authorizationClaim.getPermissions();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java
new file mode 100644
index 0000000..21bb9ee
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ResourceScopesResource;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.util.HttpResponseException;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.PermissionResponse;
+import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
+import org.keycloak.representations.idm.authorization.PermissionTicketToken;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionManagementTest extends AbstractResourceServerTest {
+
+ @Test
+ public void testCreatePermissionTicketWithResourceName() throws Exception {
+ ResourceRepresentation resource = addResource("Resource A", "kolo", true);
+ AuthzClient authzClient = getAuthzClient();
+ PermissionResponse response = authzClient.protection("marta", "password").permission().create(new PermissionRequest(resource.getName()));
+ AuthorizationRequest request = new AuthorizationRequest();
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+ try {
+ authzClient.authorization().authorize(request);
+ } catch (Exception e) {
+
+ }
+ assertPersistence(response, resource);
+ }
+
+ @Test
+ public void testCreatePermissionTicketWithResourceId() throws Exception {
+ ResourceRepresentation resource = addResource("Resource A", "kolo", true);
+ AuthzClient authzClient = getAuthzClient();
+ PermissionResponse response = authzClient.protection("marta", "password").permission().create(new PermissionRequest(resource.getId()));
+ AuthorizationRequest request = new AuthorizationRequest();
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+ try {
+ authzClient.authorization().authorize(request);
+ } catch (Exception e) {
+
+ }
+ assertNotNull(response.getTicket());
+ assertFalse(authzClient.protection().permission().findByResource(resource.getId()).isEmpty());
+ }
+
+ @Test
+ public void testCreatePermissionTicketWithScopes() throws Exception {
+ ResourceRepresentation resource = addResource("Resource A", "kolo", true, "ScopeA", "ScopeB", "ScopeC");
+ AuthzClient authzClient = getAuthzClient();
+ PermissionResponse response = authzClient.protection("marta", "password").permission().create(new PermissionRequest(resource.getId(), "ScopeA", "ScopeB", "ScopeC"));
+ AuthorizationRequest request = new AuthorizationRequest();
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+ try {
+ authzClient.authorization().authorize(request);
+ } catch (Exception e) {
+
+ }
+ assertPersistence(response, resource, "ScopeA", "ScopeB", "ScopeC");
+ }
+
+ @Test
+ public void testDeleteResourceAndPermissionTicket() throws Exception {
+ ResourceRepresentation resource = addResource("Resource A", true);
+ PermissionResponse response = getAuthzClient().protection().permission().create(new PermissionRequest(resource.getName()));
+ assertNotNull(response.getTicket());
+
+ getAuthzClient().protection().resource().delete(resource.getId());
+ assertTrue(getAuthzClient().protection().permission().findByResource(resource.getId()).isEmpty());
+ }
+
+ @Test
+ public void testMultiplePermissionRequest() throws Exception {
+ List<PermissionRequest> permissions = new ArrayList<>();
+
+ permissions.add(new PermissionRequest(addResource("Resource A", true).getName()));
+ permissions.add(new PermissionRequest(addResource("Resource B", true).getName()));
+ permissions.add(new PermissionRequest(addResource("Resource C", true).getName()));
+ permissions.add(new PermissionRequest(addResource("Resource D", true).getName()));
+
+ PermissionResponse response = getAuthzClient().protection().permission().create(permissions);
+ assertNotNull(response.getTicket());
+ }
+
+ @Test
+ public void testDeleteScopeAndPermissionTicket() throws Exception {
+ ResourceRepresentation resource = addResource("Resource A", "kolo", true, "ScopeA", "ScopeB", "ScopeC");
+ PermissionRequest permissionRequest = new PermissionRequest(resource.getName());
+
+ permissionRequest.setScopes(new HashSet<>(Arrays.asList("ScopeA", "ScopeB", "ScopeC")));
+
+ AuthzClient authzClient = getAuthzClient();
+ PermissionResponse response = authzClient.protection("marta", "password").permission().create(permissionRequest);
+ assertNotNull(response.getTicket());
+
+ AuthorizationRequest request = new AuthorizationRequest();
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+ try {
+ authzClient.authorization().authorize(request);
+ } catch (Exception e) {
+
+ }
+
+ assertEquals(3, authzClient.protection().permission().findByResource(resource.getId()).size());
+
+ AuthorizationResource authorization = getClient(getRealm()).authorization();
+ ResourceScopesResource scopes = authorization.scopes();
+ ScopeRepresentation scope = scopes.findByName("ScopeA");
+
+ List permissions = authzClient.protection().permission().findByScope(scope.getId());
+ assertFalse(permissions.isEmpty());
+ assertEquals(1, permissions.size());
+
+ resource.setScopes(Collections.emptySet());
+ authorization.resources().resource(resource.getId()).update(resource);
+ scopes.scope(scope.getId()).remove();
+
+ assertTrue(authzClient.protection().permission().findByScope(scope.getId()).isEmpty());
+ assertEquals(0, authzClient.protection().permission().findByResource(resource.getId()).size());
+ }
+
+ @Test
+ public void testRemoveScopeFromResource() throws Exception {
+ ResourceRepresentation resource = addResource("Resource A", "kolo", true, "ScopeA", "ScopeB");
+ PermissionRequest permissionRequest = new PermissionRequest(resource.getName(), "ScopeA", "ScopeB");
+ AuthzClient authzClient = getAuthzClient();
+ PermissionResponse response = authzClient.protection("marta", "password").permission().create(permissionRequest);
+
+ assertNotNull(response.getTicket());
+
+ AuthorizationRequest request = new AuthorizationRequest();
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+ try {
+ authzClient.authorization().authorize(request);
+ } catch (Exception e) {
+
+ }
+
+ AuthorizationResource authorization = getClient(getRealm()).authorization();
+ ResourceScopesResource scopes = authorization.scopes();
+ ScopeRepresentation removedScope = scopes.findByName("ScopeA");
+ List permissions = authzClient.protection().permission().findByScope(removedScope.getId());
+ assertFalse(permissions.isEmpty());
+
+ resource.setScopes(new HashSet<>());
+ resource.addScope("ScopeB");
+
+ authorization.resources().resource(resource.getId()).update(resource);
+ permissions = authzClient.protection().permission().findByScope(removedScope.getId());
+ assertTrue(permissions.isEmpty());
+
+ ScopeRepresentation scopeB = scopes.findByName("ScopeB");
+ permissions = authzClient.protection().permission().findByScope(scopeB.getId());
+ assertFalse(permissions.isEmpty());
+ }
+
+ @Test
+ public void testCreatePermissionTicketWithResourceWithoutManagedAccess() throws Exception {
+ ResourceRepresentation resource = addResource("Resource A");
+ PermissionResponse response = getAuthzClient().protection().permission().create(new PermissionRequest(resource.getName()));
+ assertNotNull(response.getTicket());
+ assertTrue(getAuthzClient().protection().permission().findByResource(resource.getId()).isEmpty());
+ }
+
+ @Test
+ public void testTicketNotCreatedWhenResourceOwner() throws Exception {
+ ResourceRepresentation resource = addResource("Resource A", "marta", true);
+ AuthzClient authzClient = getAuthzClient();
+ PermissionResponse response = authzClient.protection("marta", "password").permission().create(new PermissionRequest(resource.getId()));
+ assertNotNull(response.getTicket());
+ AuthorizationRequest request = new AuthorizationRequest();
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("marta", "password").getToken());
+
+ try {
+ authzClient.authorization().authorize(request);
+ } catch (Exception e) {
+
+ }
+
+ List permissions = authzClient.protection().permission().findByResource(resource.getId());
+ assertTrue(permissions.isEmpty());
+
+ response = authzClient.protection("kolo", "password").permission().create(new PermissionRequest(resource.getId()));
+ assertNotNull(response.getTicket());
+ request = new AuthorizationRequest();
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("kolo", "password").getToken());
+
+ try {
+ authzClient.authorization().authorize(request);
+ } catch (Exception e) {
+
+ }
+ permissions = authzClient.protection().permission().findByResource(resource.getId());
+ assertFalse(permissions.isEmpty());
+ assertEquals(1, permissions.size());
+ }
+
+ @Test
+ public void testPermissionForTypedScope() throws Exception {
+ ResourceRepresentation typedResource = addResource("Typed Resource", "ScopeC");
+
+ typedResource.setType("typed-resource");
+
+ getClient(getRealm()).authorization().resources().resource(typedResource.getId()).update(typedResource);
+
+ ResourceRepresentation resourceA = addResource("Resource A", "marta", true, "ScopeA", "ScopeB");
+
+ resourceA.setType(typedResource.getType());
+
+ getClient(getRealm()).authorization().resources().resource(resourceA.getId()).update(resourceA);
+
+ PermissionRequest permissionRequest = new PermissionRequest("Resource A");
+
+ permissionRequest.setScopes(new HashSet<>(Arrays.asList("ScopeA", "ScopeC")));
+
+ AuthzClient authzClient = getAuthzClient();
+ PermissionResponse response = authzClient.protection("kolo", "password").permission().create(permissionRequest);
+
+ AuthorizationRequest request = new AuthorizationRequest();
+ request.setTicket(response.getTicket());
+ request.setClaimToken(authzClient.obtainAccessToken("kolo", "password").getToken());
+
+ try {
+ authzClient.authorization().authorize(request);
+ } catch (Exception e) {
+
+ }
+
+ assertPersistence(response, resourceA, "ScopeA", "ScopeC");
+ }
+
+ @Test
+ public void testSameTicketForSamePermissionRequest() throws Exception {
+ ResourceRepresentation resource = addResource("Resource A", true);
+ PermissionResponse response = getAuthzClient().protection("marta", "password").permission().create(new PermissionRequest(resource.getName()));
+ assertNotNull(response.getTicket());
+ }
+
+ private void assertPersistence(PermissionResponse response, ResourceRepresentation resource, String... scopeNames) throws Exception {
+ String ticket = response.getTicket();
+ assertNotNull(ticket);
+
+ int expectedPermissions = scopeNames.length > 0 ? scopeNames.length : 1;
+ List<PermissionTicketRepresentation> tickets = getAuthzClient().protection().permission().findByResource(resource.getId());
+ assertEquals(expectedPermissions, tickets.size());
+
+ PermissionTicketToken token = new JWSInput(ticket).readJsonContent(PermissionTicketToken.class);
+
+ List<PermissionTicketToken.ResourcePermission> tokenPermissions = token.getResources();
+ assertNotNull(tokenPermissions);
+ assertEquals(expectedPermissions, scopeNames.length > 0 ? scopeNames.length : tokenPermissions.size());
+
+ Iterator<PermissionTicketToken.ResourcePermission> permissionIterator = tokenPermissions.iterator();
+
+ while (permissionIterator.hasNext()) {
+ PermissionTicketToken.ResourcePermission resourcePermission = permissionIterator.next();
+ long count = tickets.stream().filter(representation -> representation.getResource().equals(resourcePermission.getResourceId())).count();
+ if (count == (scopeNames.length > 0 ? scopeNames.length : 1)) {
+ permissionIterator.remove();
+ }
+ }
+
+ assertTrue(tokenPermissions.isEmpty());
+
+ ArrayList<PermissionTicketRepresentation> expectedTickets = new ArrayList<>(tickets);
+ Iterator<PermissionTicketRepresentation> ticketIterator = expectedTickets.iterator();
+
+ while (ticketIterator.hasNext()) {
+ PermissionTicketRepresentation ticketRep = ticketIterator.next();
+
+ assertFalse(ticketRep.isGranted());
+
+ if (ticketRep.getScope() != null) {
+ ScopeRepresentation scope = getClient(getRealm()).authorization().scopes().scope(ticketRep.getScope()).toRepresentation();
+
+ if (Arrays.asList(scopeNames).contains(scope.getName())) {
+ ticketIterator.remove();
+ }
+ } else if (ticketRep.getResource().equals(resource.getId())) {
+ ticketIterator.remove();
+ }
+ }
+
+ assertTrue(expectedTickets.isEmpty());
+ }
+
+ @Test
+ public void failInvalidResource() {
+ try {
+ getAuthzClient().protection().permission().create(new PermissionRequest("Invalid Resource"));
+ fail("Should fail, resource does not exist");
+ } catch (RuntimeException cause) {
+ assertTrue(HttpResponseException.class.isInstance(cause.getCause()));
+ assertEquals(400, HttpResponseException.class.cast(cause.getCause()).getStatusCode());
+ assertTrue(new String(HttpResponseException.class.cast(cause.getCause()).getBytes()).contains("invalid_resource_id"));
+ }
+ try {
+ getAuthzClient().protection().permission().create(new PermissionRequest());
+ fail("Should fail, resource is empty");
+ } catch (RuntimeException cause) {
+ cause.printStackTrace();
+ assertTrue(HttpResponseException.class.isInstance(cause.getCause()));
+ assertEquals(400, HttpResponseException.class.cast(cause.getCause()).getStatusCode());
+ assertTrue(new String((HttpResponseException.class.cast(cause.getCause()).getBytes())).contains("invalid_resource_id"));
+ }
+ }
+
+ @Test
+ public void failInvalidScope() throws Exception {
+ addResource("Resource A", "ScopeA", "ScopeB");
+ try {
+ PermissionRequest permissionRequest = new PermissionRequest("Resource A");
+
+ permissionRequest.setScopes(new HashSet<>(Arrays.asList("ScopeA", "ScopeC")));
+
+ getAuthzClient().protection().permission().create(permissionRequest);
+ fail("Should fail, resource does not exist");
+ } catch (RuntimeException cause) {
+ assertTrue(HttpResponseException.class.isInstance(cause.getCause()));
+ assertEquals(400, HttpResponseException.class.cast(cause.getCause()).getStatusCode());
+ assertTrue(new String((HttpResponseException.class.cast(cause.getCause()).getBytes())).contains("invalid_scope"));
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RolePolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RolePolicyTest.java
index 994e52e..e1a0fe6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RolePolicyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RolePolicyTest.java
@@ -16,14 +16,12 @@
*/
package org.keycloak.testsuite.authz;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
-import java.util.function.Predicate;
import org.junit.Before;
import org.junit.Test;
@@ -34,19 +32,16 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authorization.client.AuthorizationDeniedException;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.AuthorizationRequest;
-import org.keycloak.authorization.client.representation.AuthorizationResponse;
-import org.keycloak.authorization.client.representation.PermissionRequest;
-import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
-import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.AuthorizationRequest;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
-import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.GroupBuilder;
@@ -102,24 +97,19 @@ public class RolePolicyTest extends AbstractAuthzTest {
@Test
public void testUserWithExpectedRole() {
AuthzClient authzClient = getAuthzClient();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Resource A");
+ PermissionRequest request = new PermissionRequest("Resource A");
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ String ticket = authzClient.protection().permission().create(request).getTicket();
AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
- assertNotNull(response.getRpt());
+ assertNotNull(response.getToken());
}
@Test
public void testUserWithoutExpectedRole() {
AuthzClient authzClient = getAuthzClient();
- PermissionRequest request = new PermissionRequest();
-
- request.setResourceSetName("Resource A");
-
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ PermissionRequest request = new PermissionRequest("Resource A");
+ String ticket = authzClient.protection().permission().create(request).getTicket();
try {
authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
@@ -128,16 +118,16 @@ public class RolePolicyTest extends AbstractAuthzTest {
}
- request.setResourceSetName("Resource B");
- ticket = authzClient.protection().permission().forResource(request).getTicket();
+ request.setResourceId("Resource B");
+ ticket = authzClient.protection().permission().create(request).getTicket();
assertNotNull(authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket)));
UserRepresentation user = getRealm().users().search("kolo").get(0);
RoleRepresentation roleA = getRealm().roles().get("Role A").toRepresentation();
getRealm().users().get(user.getId()).roles().realmLevel().add(Arrays.asList(roleA));
- request.setResourceSetName("Resource A");
- ticket = authzClient.protection().permission().forResource(request).getTicket();
+ request.setResourceId("Resource A");
+ ticket = authzClient.protection().permission().create(request).getTicket();
assertNotNull(authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket)));
}
@@ -146,9 +136,9 @@ public class RolePolicyTest extends AbstractAuthzTest {
AuthzClient authzClient = getAuthzClient();
PermissionRequest request = new PermissionRequest();
- request.setResourceSetName("Resource C");
+ request.setResourceId("Resource C");
- String ticket = authzClient.protection().permission().forResource(request).getTicket();
+ String ticket = authzClient.protection().permission().create(request).getTicket();
assertNotNull(authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket)));
UserRepresentation user = getRealm().users().search("alice").get(0);
@@ -162,8 +152,8 @@ public class RolePolicyTest extends AbstractAuthzTest {
}
- request.setResourceSetName("Resource A");
- ticket = authzClient.protection().permission().forResource(request).getTicket();
+ request.setResourceId("Resource A");
+ ticket = authzClient.protection().permission().create(request).getTicket();
try {
authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaDiscoveryDocumentTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaDiscoveryDocumentTest.java
new file mode 100644
index 0000000..65c7f6a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaDiscoveryDocumentTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2018 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.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.URI;
+import java.util.List;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.junit.Test;
+import org.keycloak.authorization.config.UmaConfiguration;
+import org.keycloak.authorization.config.UmaWellKnownProviderFactory;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.admin.AbstractAdminTest;
+import org.keycloak.testsuite.util.OAuthClient;
+
+public class UmaDiscoveryDocumentTest extends AbstractKeycloakTest {
+
+ @ArquillianResource
+ protected OAuthClient oauth;
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation realm = AbstractAdminTest.loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+ testRealms.add(realm);
+ }
+
+ @Test
+ public void testFetchDiscoveryDocument() {
+ Client client = ClientBuilder.newClient();
+ UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
+ URI oidcDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder).build("test", UmaWellKnownProviderFactory.PROVIDER_ID);
+ WebTarget oidcDiscoveryTarget = client.target(oidcDiscoveryUri);
+
+ Response response = oidcDiscoveryTarget.request().get();
+
+ assertEquals("no-cache, must-revalidate, no-transform, no-store", response.getHeaders().getFirst("Cache-Control"));
+
+ UmaConfiguration configuration = response.readEntity(UmaConfiguration.class);
+
+ assertEquals(configuration.getAuthorizationEndpoint(), OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)).build("test").toString());
+ assertEquals(configuration.getTokenEndpoint(), oauth.getAccessTokenUrl());
+ assertEquals(configuration.getJwksUri(), oauth.getCertsUrl("test"));
+ assertEquals(configuration.getTokenIntrospectionEndpoint(), oauth.getTokenIntrospectionUrl());
+
+ String registrationUri = UriBuilder
+ .fromUri(OAuthClient.AUTH_SERVER_ROOT)
+ .path(RealmsResource.class).path(RealmsResource.class, "getRealmResource").build(realmsResouce().realm("test").toRepresentation().getRealm()).toString();
+
+ assertEquals(registrationUri + "/authz/protection/permission", configuration.getPermissionEndpoint().toString());
+ assertEquals(registrationUri + "/authz/protection/resource_set", configuration.getResourceRegistrationEndpoint().toString());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java
new file mode 100644
index 0000000..41448cf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
+
+import java.net.URI;
+import java.util.List;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
+import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.PermissionRequest;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.util.BasicAuthHelper;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaGrantTypeTest extends AbstractResourceServerTest {
+
+ private ResourceRepresentation resourceA;
+
+ @Before
+ public void configureAuthorization() throws Exception {
+ ClientResource client = getClient(getRealm());
+ AuthorizationResource authorization = client.authorization();
+
+ JSPolicyRepresentation policy = new JSPolicyRepresentation();
+
+ policy.setName("Default Policy");
+ policy.setCode("$evaluation.grant();");
+
+ Response response = authorization.policies().js().create(policy);
+ response.close();
+
+ ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+ resourceA = addResource("Resource A", "ScopeA", "ScopeB", "ScopeC");
+
+ permission.setName(resourceA.getName() + " Permission");
+ permission.addResource(resourceA.getName());
+ permission.addPolicy(policy.getName());
+
+ response = authorization.permissions().resource().create(permission);
+ response.close();
+ }
+
+ @Test
+ public void testObtainRptWithClientAdditionalScopes() throws Exception {
+ AuthorizationResponse response = authorize("marta", "password", "Resource A", new String[] {"ScopeA", "ScopeB"}, new String[] {"ScopeC"});
+ AccessToken accessToken = toAccessToken(response.getToken());
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB", "ScopeC");
+ assertTrue(permissions.isEmpty());
+ }
+
+ @Test
+ public void testObtainRptWithUpgrade() throws Exception {
+ AuthorizationResponse response = authorize("marta", "password", "Resource A", new String[] {"ScopeA", "ScopeB"});
+ String rpt = response.getToken();
+ AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization();
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
+ assertTrue(permissions.isEmpty());
+
+ response = authorize("marta", "password", "Resource A", new String[] {"ScopeC"}, rpt);
+ assertTrue(response.isUpgraded());
+
+ authorization = toAccessToken(response.getToken()).getAuthorization();
+ permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB", "ScopeC");
+ assertTrue(permissions.isEmpty());
+ }
+
+ @Test
+ public void testObtainRptWithOwnerManagedResource() throws Exception {
+ ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+ ResourceRepresentation resourceA = addResource("Resource Marta", "marta", true, "ScopeA", "ScopeB", "ScopeC");
+
+ permission.setName(resourceA.getName() + " Permission");
+ permission.addResource(resourceA.getName());
+ permission.addPolicy("Default Policy");
+
+ getClient(getRealm()).authorization().permissions().resource().create(permission).close();
+
+ ResourceRepresentation resourceB = addResource("Resource B", "marta", "ScopeA", "ScopeB", "ScopeC");
+
+ permission.setName(resourceB.getName() + " Permission");
+ permission.addResource(resourceB.getName());
+ permission.addPolicy("Default Policy");
+
+ getClient(getRealm()).authorization().permissions().resource().create(permission).close();
+
+ AuthorizationResponse response = authorize("marta", "password",
+ new PermissionRequest(resourceA.getName(), "ScopeA", "ScopeB"),
+ new PermissionRequest(resourceB.getName(), "ScopeC"));
+ String rpt = response.getToken();
+ AccessToken.Authorization authorization = toAccessToken(rpt).getAuthorization();
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, resourceA.getName(), "ScopeA", "ScopeB");
+ assertPermissions(permissions, resourceB.getName(), "ScopeC");
+ assertTrue(permissions.isEmpty());
+ }
+
+ @Test
+ public void testObtainRptWithClientCredentials() throws Exception {
+ AuthorizationResponse response = authorize("Resource A", new String[] {"ScopeA", "ScopeB"});
+ String rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ AccessToken accessToken = toAccessToken(rpt);
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
+
+ assertTrue(permissions.isEmpty());
+ }
+
+ @Test
+ public void testObtainRptUsingAccessToken() throws Exception {
+ AccessTokenResponse accessTokenResponse = getAuthzClient().obtainAccessToken("marta", "password");
+ AuthorizationResponse response = authorize(null, null, null, null, accessTokenResponse.getToken(), null, null, new PermissionRequest("Resource A", "ScopeA", "ScopeB"));
+ String rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ AccessToken accessToken = toAccessToken(rpt);
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
+ assertTrue(permissions.isEmpty());
+ }
+
+ @Test
+ public void testRefreshRpt() throws Exception {
+ AccessTokenResponse accessTokenResponse = getAuthzClient().obtainAccessToken("marta", "password");
+ AuthorizationResponse response = authorize(null, null, null, null, accessTokenResponse.getToken(), null, null, new PermissionRequest("Resource A", "ScopeA", "ScopeB"));
+ String rpt = response.getToken();
+
+ assertNotNull(rpt);
+
+ AccessToken accessToken = toAccessToken(rpt);
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
+ assertTrue(permissions.isEmpty());
+
+ String refreshToken = response.getRefreshToken();
+
+ assertNotNull(refreshToken);
+
+ Client client = ClientBuilder.newClient();
+ UriBuilder builder = UriBuilder.fromUri(AUTH_SERVER_ROOT);
+ URI uri = OIDCLoginProtocolService.tokenUrl(builder).build(REALM_NAME);
+ WebTarget target = client.target(uri);
+
+ Form parameters = new Form();
+
+ parameters.param("grant_type", OAuth2Constants.REFRESH_TOKEN);
+ parameters.param(OAuth2Constants.REFRESH_TOKEN, refreshToken);
+
+ AccessTokenResponse refreshTokenResponse = target.request()
+ .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("resource-server-test", "secret"))
+ .post(Entity.form(parameters)).readEntity(AccessTokenResponse.class);
+
+ assertNotNull(refreshTokenResponse.getToken());
+
+ AccessToken refreshedToken = toAccessToken(rpt);
+ authorization = refreshedToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
+ assertTrue(permissions.isEmpty());
+ }
+
+ @Test
+ public void testObtainRptWithIDToken() throws Exception {
+ String idToken = getIdToken("marta", "password");
+ AuthorizationResponse response = authorize("Resource A", new String[] {"ScopeA", "ScopeB"}, idToken, "http://openid.net/specs/openid-connect-core-1_0.html#IDToken");
+ String rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ AccessToken accessToken = toAccessToken(rpt);
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
+
+ assertTrue(permissions.isEmpty());
+ }
+
+ private String getIdToken(String username, String password) {
+ oauth.realm("authz-test");
+ oauth.clientId("test-app");
+ oauth.openLoginForm();
+ OAuthClient.AuthorizationEndpointResponse resp = oauth.doLogin(username, password);
+ String code = resp.getCode();
+ OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, password);
+ return response.getIdToken();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedAccessTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedAccessTest.java
new file mode 100644
index 0000000..f8dd3de
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedAccessTest.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2017 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.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.ws.rs.core.Response;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.resource.PermissionResource;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.authorization.AuthorizationResponse;
+import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.PermissionTicketRepresentation;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UserManagedAccessTest extends AbstractResourceServerTest {
+
+ private ResourceRepresentation resource;
+
+ @Before
+ public void configureAuthorization() throws Exception {
+ ClientResource client = getClient(getRealm());
+ AuthorizationResource authorization = client.authorization();
+
+ JSPolicyRepresentation policy = new JSPolicyRepresentation();
+
+ policy.setName("Only Owner Policy");
+ policy.setCode("print($evaluation.getPermission().getResource().getOwner());print($evaluation.getContext().getIdentity().getId());if ($evaluation.getContext().getIdentity().getId() == $evaluation.getPermission().getResource().getOwner()) {$evaluation.grant();}");
+
+ Response response = authorization.policies().js().create(policy);
+ response.close();
+ }
+
+ @Test
+ public void testOnlyOwnerCanAccess() throws Exception {
+ ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+ resource = addResource("Resource A", "marta", true, "ScopeA", "ScopeB");
+
+ permission.setName(resource.getName() + " Permission");
+ permission.addResource(resource.getName());
+ permission.addPolicy("Only Owner Policy");
+
+ getClient(getRealm()).authorization().permissions().resource().create(permission).close();
+
+ AuthorizationResponse response = authorize("marta", "password", resource.getName(), new String[] {"ScopeA", "ScopeB"});
+ String rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ AccessToken accessToken = toAccessToken(rpt);
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, resource.getName(), "ScopeA", "ScopeB");
+ assertTrue(permissions.isEmpty());
+
+ try {
+ response = authorize("kolo", "password", resource.getName(), new String[] {"ScopeA", "ScopeB"});
+ fail("User should have access to resource from another user");
+ } catch (AuthorizationDeniedException ade) {
+
+ }
+ }
+
+ @Test
+ public void testUserGrantsAccessToResource() throws Exception {
+ ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+ resource = addResource("Resource A", "marta", true, "ScopeA", "ScopeB");
+
+ permission.setName(resource.getName() + " Permission");
+ permission.addResource(resource.getName());
+ permission.addPolicy("Only Owner Policy");
+
+ getClient(getRealm()).authorization().permissions().resource().create(permission).close();
+
+ AuthorizationResponse response = authorize("marta", "password", "Resource A", new String[] {"ScopeA", "ScopeB"});
+ String rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ AccessToken accessToken = toAccessToken(rpt);
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
+ assertTrue(permissions.isEmpty());
+
+ try {
+ response = authorize("kolo", "password", "Resource A", new String[] {});
+ fail("User should have access to resource from another user");
+ } catch (AuthorizationDeniedException ade) {
+
+ }
+
+ PermissionResource permissionResource = getAuthzClient().protection().permission();
+ List<PermissionTicketRepresentation> permissionTickets = permissionResource.findByResource(resource.getId());
+
+ assertFalse(permissionTickets.isEmpty());
+ assertEquals(2, permissionTickets.size());
+
+ for (PermissionTicketRepresentation ticket : permissionTickets) {
+ assertFalse(ticket.isGranted());
+
+ ticket.setGranted(true);
+
+ permissionResource.update(ticket);
+ }
+
+ permissionTickets = permissionResource.findByResource(resource.getId());
+
+ assertFalse(permissionTickets.isEmpty());
+ assertEquals(2, permissionTickets.size());
+
+ for (PermissionTicketRepresentation ticket : permissionTickets) {
+ assertTrue(ticket.isGranted());
+ }
+
+ response = authorize("kolo", "password", resource.getName(), new String[] {"ScopeA", "ScopeB"});
+ rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ accessToken = toAccessToken(rpt);
+ authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, resource.getName(), "ScopeA", "ScopeB");
+ assertTrue(permissions.isEmpty());
+ }
+
+ @Test
+ public void testUserGrantsAccessToResourceWithoutScopes() throws Exception {
+ ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+ resource = addResource("Resource A", "marta", true);
+
+ permission.setName(resource.getName() + " Permission");
+ permission.addResource(resource.getName());
+ permission.addPolicy("Only Owner Policy");
+
+ getClient(getRealm()).authorization().permissions().resource().create(permission).close();
+
+ AuthorizationResponse response = authorize("marta", "password", "Resource A", new String[] {});
+ String rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ AccessToken accessToken = toAccessToken(rpt);
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A");
+ assertTrue(permissions.isEmpty());
+
+ try {
+ response = authorize("kolo", "password", "Resource A", new String[] {});
+ fail("User should have access to resource from another user");
+ } catch (AuthorizationDeniedException ade) {
+
+ }
+
+ PermissionResource permissionResource = getAuthzClient().protection().permission();
+ List<PermissionTicketRepresentation> permissionTickets = permissionResource.findByResource(resource.getId());
+
+ assertFalse(permissionTickets.isEmpty());
+ assertEquals(1, permissionTickets.size());
+
+ for (PermissionTicketRepresentation ticket : permissionTickets) {
+ assertFalse(ticket.isGranted());
+
+ ticket.setGranted(true);
+
+ permissionResource.update(ticket);
+ }
+
+ permissionTickets = permissionResource.findByResource(resource.getId());
+
+ assertFalse(permissionTickets.isEmpty());
+ assertEquals(1, permissionTickets.size());
+
+ for (PermissionTicketRepresentation ticket : permissionTickets) {
+ assertTrue(ticket.isGranted());
+ }
+
+ response = authorize("kolo", "password", resource.getName(), new String[] {});
+ rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ accessToken = toAccessToken(rpt);
+ authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, resource.getName());
+ assertTrue(permissions.isEmpty());
+
+ response = authorize("kolo", "password", resource.getName(), new String[] {});
+ rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ accessToken = toAccessToken(rpt);
+ authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, resource.getName());
+ assertTrue(permissions.isEmpty());
+
+ permissionTickets = permissionResource.findByResource(resource.getId());
+
+ assertFalse(permissionTickets.isEmpty());
+ assertEquals(1, permissionTickets.size());
+
+ for (PermissionTicketRepresentation ticket : permissionTickets) {
+ assertTrue(ticket.isGranted());
+ }
+ }
+
+ @Test
+ public void testUserGrantsAccessToScope() throws Exception {
+ ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+ resource = addResource("Resource A", "marta", true, "ScopeA", "ScopeB");
+
+ permission.setName(resource.getName() + " Permission");
+ permission.addResource(resource.getName());
+ permission.addPolicy("Only Owner Policy");
+
+ getClient(getRealm()).authorization().permissions().resource().create(permission).close();
+
+ AuthorizationResponse response = authorize("marta", "password", "Resource A", new String[] {"ScopeA", "ScopeB"});
+ String rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ AccessToken accessToken = toAccessToken(rpt);
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ List<Permission> permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, "Resource A", "ScopeA", "ScopeB");
+ assertTrue(permissions.isEmpty());
+
+ try {
+ response = authorize("kolo", "password", "Resource A", new String[] {"ScopeA"});
+ fail("User should have access to resource from another user");
+ } catch (AuthorizationDeniedException ade) {
+
+ }
+
+ PermissionResource permissionResource = getAuthzClient().protection().permission();
+ List<PermissionTicketRepresentation> permissionTickets = permissionResource.findByResource(resource.getId());
+
+ assertFalse(permissionTickets.isEmpty());
+ assertEquals(1, permissionTickets.size());
+
+ PermissionTicketRepresentation ticket = permissionTickets.get(0);
+ assertFalse(ticket.isGranted());
+
+ ticket.setGranted(true);
+
+ permissionResource.update(ticket);
+
+ response = authorize("kolo", "password", resource.getName(), new String[] {"ScopeA", "ScopeB"});
+ rpt = response.getToken();
+
+ assertNotNull(rpt);
+ assertFalse(response.isUpgraded());
+
+ accessToken = toAccessToken(rpt);
+ authorization = accessToken.getAuthorization();
+
+ assertNotNull(authorization);
+
+ permissions = authorization.getPermissions();
+
+ assertNotNull(permissions);
+ assertPermissions(permissions, resource.getName(), "ScopeA");
+ assertTrue(permissions.isEmpty());
+
+ permissionTickets = permissionResource.findByResource(resource.getId());
+
+ assertFalse(permissionTickets.isEmpty());
+ // must have two permission tickets, one persisted during the first authorize call for ScopeA and another for the second call to authorize for ScopeB
+ assertEquals(2, permissionTickets.size());
+
+ for (PermissionTicketRepresentation representation : new ArrayList<>(permissionTickets)) {
+ if (representation.isGranted()) {
+ permissionTickets.remove(representation);
+ }
+ }
+
+ assertEquals(1, permissionTickets.size());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/default-keycloak-uma2.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/default-keycloak-uma2.json
new file mode 100644
index 0000000..7308c71
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/default-keycloak-uma2.json
@@ -0,0 +1,8 @@
+{
+ "realm": "authz-test",
+ "auth-server-url" : "http://localhost:8180/auth",
+ "resource" : "resource-server-test",
+ "credentials": {
+ "secret": "secret"
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
index d631859..a90ff60 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
@@ -331,7 +331,7 @@
<artifactId>integration-arquillian-test-apps-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
- <includes>**/*realm.json,**/*authz-service.json,**/testsaml.json</includes>
+ <includes>**/*realm.json,**/*authz-service.json,**/testsaml.json,**/*-keycloak.json</includes>
</artifactItem>
</artifactItems>
<outputDirectory>${examples.home}</outputDirectory>
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
index c4d2b2b..59b2177 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
@@ -40,6 +40,9 @@ public class ResourceForm extends Form {
@FindBy(id = "name")
private WebElement name;
+ @FindBy(id = "displayName")
+ private WebElement displayName;
+
@FindBy(id = "type")
private WebElement type;
@@ -63,6 +66,7 @@ public class ResourceForm extends Form {
public void populate(ResourceRepresentation expected) {
setInputValue(name, expected.getName());
+ setInputValue(displayName, expected.getDisplayName());
setInputValue(type, expected.getType());
setInputValue(uri, expected.getUri());
setInputValue(iconUri, expected.getIconUri());
@@ -102,6 +106,7 @@ public class ResourceForm extends Form {
ResourceRepresentation representation = new ResourceRepresentation();
representation.setName(getInputValue(name));
+ representation.setDisplayName(getInputValue(displayName));
representation.setType(getInputValue(type));
representation.setUri(getInputValue(uri));
representation.setIconUri(getInputValue(iconUri));
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scope.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scope.java
index 5137125..3206078 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scope.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scope.java
@@ -17,6 +17,7 @@
package org.keycloak.testsuite.console.page.clients.authorization.scope;
import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -29,4 +30,8 @@ public class Scope {
public ScopeForm form() {
return form;
}
+
+ public ScopeRepresentation toRepresentation() {
+ return form.toRepresentation();
+ }
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java
index 29ec514..afc6e85 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/ScopeForm.java
@@ -30,6 +30,9 @@ public class ScopeForm extends Form {
@FindBy(id = "name")
private WebElement name;
+ @FindBy(id = "displayName")
+ private WebElement displayName;
+
@FindBy(id = "iconUri")
private WebElement iconUri;
@@ -41,6 +44,7 @@ public class ScopeForm extends Form {
public void populate(ScopeRepresentation expected) {
setInputValue(name, expected.getName());
+ setInputValue(displayName, expected.getDisplayName());
setInputValue(iconUri, expected.getIconUri());
save();
}
@@ -49,4 +53,14 @@ public class ScopeForm extends Form {
deleteButton.click();
modalDialog.confirmDeletion();
}
+
+ public ScopeRepresentation toRepresentation() {
+ ScopeRepresentation representation = new ScopeRepresentation();
+
+ representation.setName(getInputValue(name));
+ representation.setDisplayName(getInputValue(displayName));
+ representation.setIconUri(getInputValue(iconUri));
+
+ return representation;
+ }
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java
index 7df2fb5..06b3201 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/scope/Scopes.java
@@ -16,16 +16,18 @@
*/
package org.keycloak.testsuite.console.page.clients.authorization.scope;
+import static org.keycloak.testsuite.util.UIUtils.clickLink;
+import static org.openqa.selenium.By.tagName;
+
import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.testsuite.console.page.fragment.ModalDialog;
import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
-import static org.keycloak.testsuite.util.UIUtils.clickLink;
-import static org.openqa.selenium.By.tagName;
-
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -81,4 +83,16 @@ public class Scopes extends Form {
}
}
}
+
+ public Scope name(String name) {
+ for (WebElement row : scopes().rows()) {
+ ScopeRepresentation actual = scopes().toRepresentation(row);
+ if (actual.getName().equalsIgnoreCase(name)) {
+ clickLink(row.findElements(tagName("a")).get(0));
+ WaitUtils.waitForPageToLoad();
+ return scope;
+ }
+ }
+ return null;
+ }
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java
index 3d29c03..e9e57f4 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ResourceManagementTest.java
@@ -53,6 +53,7 @@ public class ResourceManagementTest extends AbstractAuthorizationSettingsTest {
String previousName = expected.getName();
expected.setName("changed");
+ expected.setDisplayName("changed");
expected.setType("changed");
expected.setUri("changed");
expected.setScopes(Arrays.asList("Scope A", "Scope B", "Scope C").stream().map(name -> new ScopeRepresentation(name)).collect(Collectors.toSet()));
@@ -93,6 +94,7 @@ public class ResourceManagementTest extends AbstractAuthorizationSettingsTest {
ResourceRepresentation expected = new ResourceRepresentation();
expected.setName("Test Resource");
+ expected.setDisplayName("Test Display Name");
expected.setType("Test Type");
expected.setUri("/test/resource");
@@ -112,6 +114,9 @@ public class ResourceManagementTest extends AbstractAuthorizationSettingsTest {
assertEquals(expected.getIconUri(), actual.getIconUri());
ResourceRepresentation resource = authorizationPage.authorizationTabs().resources().name(expected.getName()).toRepresentation();
+
+ assertEquals(expected.getDisplayName(), resource.getDisplayName());
+
Set<ScopeRepresentation> associatedScopes = resource.getScopes();
if (expected.getScopes() != null) {
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java
index 9bd5738..8897e63 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/ScopeManagementTest.java
@@ -21,6 +21,8 @@ import static org.junit.Assert.assertNull;
import org.junit.Test;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.testsuite.console.page.clients.authorization.resource.Resource;
+import org.keycloak.testsuite.console.page.clients.authorization.scope.Scope;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -33,9 +35,11 @@ public class ScopeManagementTest extends AbstractAuthorizationSettingsTest {
String previousName = expected.getName();
expected.setName("changed");
+ expected.setDisplayName("changed");
authorizationPage.navigateTo();
authorizationPage.authorizationTabs().scopes().update(previousName, expected);
+
assertAlertSuccess();
assertScope(expected);
}
@@ -62,9 +66,11 @@ public class ScopeManagementTest extends AbstractAuthorizationSettingsTest {
ScopeRepresentation expected = new ScopeRepresentation();
expected.setName("Test Scope");
+ expected.setDisplayName("Test Scope Display Name");
authorizationPage.authorizationTabs().scopes().create(expected);
assertAlertSuccess();
+ assertScope(expected);
return expected;
}
@@ -75,5 +81,9 @@ public class ScopeManagementTest extends AbstractAuthorizationSettingsTest {
assertEquals(expected.getName(), actual.getName());
assertEquals(expected.getIconUri(), actual.getIconUri());
+
+ ScopeRepresentation scope = authorizationPage.authorizationTabs().scopes().name(expected.getName()).toRepresentation();
+
+ assertEquals(expected.getDisplayName(), scope.getDisplayName());
}
}
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 0281d68..93f8a39 100755
--- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -123,6 +123,7 @@ missingLastNameMessage=Please specify last name.
missingEmailMessage=Please specify email.
missingPasswordMessage=Please specify password.
notMatchPasswordMessage=Passwords don''t match.
+invalidUserMessage=Invalid user
missingTotpMessage=Please specify authenticator code.
invalidPasswordExistingMessage=Invalid existing password.
@@ -169,6 +170,31 @@ invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last
invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted.
invalidPasswordGenericMessage=Invalid password: new password doesn''t match password policies.
+# Authorization
+myResources=My Resources
+myResourcesSub=My resources
+doDeny=Deny
+doRevoke=Revoke
+doApprove=Approve
+doRemoveSharing=Remove Sharing
+doRemoveRequest=Remove Request
+peopleAccessResource=People with access to this resource
+name=Name
+scopes=Scopes
+resource=Resource
+user=User
+peopleSharingThisResource=People sharing this resource
+shareWithOthers=Share with others
+needMyApproval=Need my approval
+requestsWaitingApproval=Your requests waiting approval
+icon=Icon
+requestor=Requestor
+owner=Owner
+resourcesSharedWithMe=Resources shared with me
+permissionRequestion=Permission Requestion
+permission=Permission
+shares=share(s)
+
locale_ca=Catal\u00E0
locale_de=Deutsch
locale_en=English
diff --git a/themes/src/main/resources/theme/base/account/resource-detail.ftl b/themes/src/main/resources/theme/base/account/resource-detail.ftl
new file mode 100755
index 0000000..fd4e5ec
--- /dev/null
+++ b/themes/src/main/resources/theme/base/account/resource-detail.ftl
@@ -0,0 +1,225 @@
+<#import "template.ftl" as layout>
+<@layout.mainLayout active='authorization' bodyClass='authorization'; section>
+
+ <style>
+ .search-box,.close-icon,.search-wrapper {
+ position: relative;
+ }
+ .search-wrapper {
+ width: 500px;
+ margin: auto;
+ margin-top: 50px;
+ }
+ .search-box {
+ font-weight: 600;
+ color: white;
+ border: 1px solid #006e9c;
+ outline: 0;
+ border-radius: 15px;
+ background-color: #0085cf;
+ padding: 2px 5px;
+
+ }
+ .search-box:focus {
+ box-shadow: 0 0 15px 5px #b0e0ee;
+ border: 2px solid #bebede;
+ }
+ .close-icon {
+ border:1px solid transparent;
+ background-color: transparent;
+ display: inline-block;
+ float: right;
+ outline: 0;
+ cursor: pointer;
+ }
+ .close-icon:after {
+ display: block;
+ width: 15px;
+ height: 15px;
+ background-color: #FA9595;
+ z-index:1;
+ right: 35px;
+ top: 0;
+ bottom: 0;
+ margin: auto;
+ padding: 2px;
+ border-radius: 50%;
+ text-align: center;
+ color: white;
+ font-weight: normal;
+ font-size: 12px;
+ box-shadow: 0 0 2px #E50F0F;
+ cursor: pointer;
+ }
+ .search-box:not(:valid) ~ .close-icon {
+ display: none;
+ }
+ </style>
+ <script>
+ function removeScopeElm(elm) {
+ var td = elm.parentNode;
+ var tr = td.parentNode;
+ var tbody = tr.parentNode;
+
+ td.removeChild(elm);
+
+ var childCount = td.childNodes.length - 1;
+
+ for (i = 0; i < td.childNodes.length; i++) {
+ if (!td.childNodes[i].tagName || td.childNodes[i].tagName.toUpperCase() != 'DIV') {
+ td.removeChild(td.childNodes[i]);
+ childCount--;
+ }
+ }
+
+ if (childCount <= 0) {
+ tbody.removeChild(tr);
+ }
+ }
+
+ function removeAllScopes(id) {
+ var scopesElm = document.getElementsByName('removeScope-' + id);
+
+ for (i = 0; i < scopesElm.length; i++) {
+ var td = scopesElm[i].parentNode.parentNode;
+ var tr = td.parentNode;
+ var tbody = tr.parentNode;
+ tbody.removeChild(tr);
+ }
+ }
+
+ function getChildren(parent, childId) {
+ var childNodes = [];
+
+ for (i = 0; i < parent.childNodes.length; i++) {
+ if (parent.childNodes[i].id == childId) {
+ childNodes.push(parent.childNodes[i]);
+ }
+ }
+
+ return childNodes;
+ }
+ </script>
+
+ <div class="row">
+ <div class="col-md-10">
+ <h2>
+ <a href="${url.resourceUrl}">My Resources</a> <i class="fa fa-angle-right"></i> <#if authorization.resource.displayName??>${authorization.resource.displayName}<#else>${authorization.resource.name}</#if>
+ </h2>
+ </div>
+ </div>
+
+ <#if authorization.resource.iconUri??>
+ <img src="${authorization.resource.iconUri}">
+ <br/>
+ </#if>
+
+ <div class="row">
+ <div class="col-md-10">
+ <h3>
+ ${msg("peopleAccessResource")}
+ </h3>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th>${msg("user")}</th>
+ <th>${msg("permission")}</th>
+ <th>${msg("date")}</th>
+ <th>${msg("action")}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <#if authorization.resource.shares?size != 0>
+ <#list authorization.resource.shares as permission>
+ <form action="${url.getResourceGrant(authorization.resource.id)}" name="revokeForm-${authorization.resource.id}-${permission.requester.username}" method="post">
+ <input type="hidden" name="action" value="revoke">
+ <input type="hidden" name="requester" value="${permission.requester.username}">
+ <tr>
+ <td>
+ <#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username}</#if>
+ </td>
+ <td>
+ <#if permission.scopes?size != 0>
+ <#list permission.scopes as scope>
+ <#if scope.granted>
+ <div class="search-box">
+ <#if scope.scope.displayName??>
+ ${scope.scope.displayName}
+ <#else>
+ ${scope.scope.name}
+ </#if>
+ <button class="close-icon" type="button" name="removeScope-${authorization.resource.id}-${permission.requester.username}" onclick="removeScopeElm(this.parentNode);document.forms['revokeForm-${authorization.resource.id}-${permission.requester.username}'].submit();"><i class="fa fa-times" aria-hidden="true"></i></button>
+ <input type="hidden" name="permission_id" value="${scope.id}"/>
+ </div>
+ </#if>
+ </#list>
+ <#else>
+ Any action
+ </#if>
+ </td>
+ <td>
+ ${permission.createdDate?datetime}
+ </td>
+ <td width="20%" align="middle" style="vertical-align: middle">
+ <a href="#" id="revoke-${authorization.resource.name}-${permission.requester.username}" onclick="removeAllScopes('${authorization.resource.id}-${permission.requester.username}');document.forms['revokeForm-${authorization.resource.id}-${permission.requester.username}'].submit();" type="submit" class="btn btn-primary">${msg("doRevoke")}</a>
+ </td>
+ </tr>
+ </form>
+ </#list>
+ <#else>
+ <tr>
+ <td colspan="4">The resource is not being shared</td>
+ </tr>
+ </#if>
+ </tbody>
+ </table>
+ </form>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-10">
+ <h3>
+ ${msg("shareWithOthers")}
+ </h3>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-10">
+ <form action="${url.getResourceShare(authorization.resource.id)}" name="shareForm" method="post">
+ <div class="col-sm-3 col-md-3">
+ <label for="password" class="control-label">${msg("username")} or ${msg("email")} </label> <span class="required">*</span>
+ </div>
+ <div class="col-sm-8 col-md-8">
+ <div class="row">
+ <div class="col-md-12">
+ <input type="text" class="form-control" id="user_id" name="user_id" autofocus autocomplete="off">
+ </div>
+ <div class="col-md-12">
+ <br/>
+ <#list authorization.resource.scopes as scope>
+ <div id="scope" class="search-box">
+ <#if scope.displayName??>
+ ${scope.displayName}
+ <#else>
+ ${scope.name}
+ </#if>
+ <button class="close-icon" id="share-remove-scope-${authorization.resource.name}-${scope.name}" type="button" onclick="if (getChildren(this.parentNode.parentNode, 'scope').length > 1) {removeScopeElm(this.parentNode)}"><i class="fa fa-times" aria-hidden="true"></i></button>
+ <input type="hidden" name="scope_id" value="${scope.id}"/>
+ </div>
+ </#list>
+ </div>
+ <div class="col-md-12">
+ <br/>
+ <a href="#" onclick="document.forms['shareForm'].submit()" type="submit" id="share-button" class="btn btn-primary">${msg("Share")}</a>
+ </div>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ <br/>
+</@layout.mainLayout>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/account/resources.ftl b/themes/src/main/resources/theme/base/account/resources.ftl
new file mode 100755
index 0000000..31b1d3f
--- /dev/null
+++ b/themes/src/main/resources/theme/base/account/resources.ftl
@@ -0,0 +1,385 @@
+<#import "template.ftl" as layout>
+<@layout.mainLayout active='authorization' bodyClass='authorization'; section>
+ <style>
+ .search-box,.close-icon,.search-wrapper {
+ position: relative;
+ }
+ .search-wrapper {
+ width: 500px;
+ margin: auto;
+ margin-top: 50px;
+ }
+ .search-box {
+ font-weight: 600;
+ color: white;
+ border: 1px solid #006e9c;
+ outline: 0;
+ border-radius: 15px;
+ background-color: #0085cf;
+ padding: 2px 5px;
+ }
+ .search-box:focus {
+ box-shadow: 0 0 15px 5px #b0e0ee;
+ border: 2px solid #bebede;
+ }
+ .close-icon {
+ border:1px solid transparent;
+ background-color: transparent;
+ display: inline-block;
+ float: right;
+ outline: 0;
+ cursor: pointer;
+ }
+ .close-icon:after {
+ display: block;
+ width: 15px;
+ height: 15px;
+ background-color: #FA9595;
+ z-index:1;
+ right: 35px;
+ top: 0;
+ bottom: 0;
+ margin: auto;
+ padding: 2px;
+ border-radius: 50%;
+ text-align: center;
+ color: white;
+ font-weight: normal;
+ font-size: 12px;
+ box-shadow: 0 0 2px #E50F0F;
+ cursor: pointer;
+ }
+ .search-box:not(:valid) ~ .close-icon {
+ display: none;
+ }
+ </style>
+ <script>
+ function showHideActions(elm) {
+ if (elm.style.display == 'none') {
+ elm.style.display = '';
+ } else {
+ elm.style.display = 'none';
+ }
+ }
+ function removeScopeElm(elm) {
+ var td = elm.parentNode;
+ var tr = td.parentNode;
+ var tbody = tr.parentNode;
+
+ td.removeChild(elm);
+
+ var childCount = td.childNodes.length - 1;
+
+ for (i = 0; i < td.childNodes.length; i++) {
+ if (!td.childNodes[i].tagName || td.childNodes[i].tagName.toUpperCase() != 'DIV') {
+ td.removeChild(td.childNodes[i]);
+ childCount--;
+ }
+ }
+
+ if (childCount <= 0) {
+ tbody.removeChild(tr);
+ }
+ }
+
+ function removeAllScopes(id) {
+ var scopesElm = document.getElementsByName('removeScope-' + id);
+
+ for (i = 0; i < scopesElm.length; i++) {
+ var td = scopesElm[i].parentNode.parentNode;
+ var tr = td.parentNode;
+ var tbody = tr.parentNode;
+ tbody.removeChild(tr);
+ }
+ }
+
+ function selectAllCheckBoxes(formName, elm, name) {
+ var shares = document.forms[formName].getElementsByTagName('input');
+
+ for (i = 0; i < shares.length; i++) {
+ if (shares[i].name == name) {
+ shares[i].checked = elm.checked;
+ }
+ }
+ }
+ </script>
+ <div class="row">
+ <div class="col-md-10">
+ <h2>
+ ${msg("myResources")}
+ </h2>
+ </div>
+ </div>
+
+ <#if authorization.resourcesWaitingApproval?size != 0>
+ <div class="row">
+ <div class="col-md-12">
+ <h3>
+ ${msg("needMyApproval")}
+ </h3>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th>${msg("resource")}</th>
+ <th>${msg("requestor")}</th>
+ <th>${msg("permissionRequestion")}</th>
+ <th>${msg("action")}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <#list authorization.resourcesWaitingApproval as resource>
+ <#list resource.permissions as permission>
+ <form action="${url.getResourceGrant(resource.id)}" name="approveForm-${resource.id}-${permission.requester.username}" method="post">
+ <input type="hidden" name="action" value="grant">
+ <input type="hidden" name="requester" value="${permission.requester.username}">
+ <tr>
+ <td>
+ <#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
+ </td>
+ <td>
+ <#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username}</#if>
+ </td>
+ <td>
+ <#list permission.scopes as scope>
+ <div class="search-box">
+ <#if scope.scope.displayName??>
+ ${scope.scope.displayName}
+ <#else>
+ ${scope.scope.name}
+ </#if>
+ <button class="close-icon" type="button" id="grant-remove-scope-${resource.name}-${permission.requester.username}-${scope.scope.name}" name="removeScope-${resource.id}-${permission.requester.username}" onclick="removeScopeElm(this.parentNode);document.forms['approveForm-${resource.id}-${permission.requester.username}']['action'].value = 'deny';document.forms['approveForm-${resource.id}-${permission.requester.username}'].submit();"><i class="fa fa-times" aria-hidden="true"></i></button>
+ <input type="hidden" name="permission_id" value="${scope.id}"/>
+ </div>
+ </#list>
+ </td>
+ <td width="20%" align="middle" style="vertical-align: middle">
+ <a href="#" id="grant-${resource.name}-${permission.requester.username}" onclick="document.forms['approveForm-${resource.id}-${permission.requester.username}']['action'].value = 'grant';document.forms['approveForm-${resource.id}-${permission.requester.username}'].submit();" type="submit" class="btn btn-primary">${msg("doApprove")}</a>
+ <a href="#" id="deny-${resource.name}-${permission.requester.username}" onclick="removeAllScopes('${resource.id}-${permission.requester.username}');document.forms['approveForm-${resource.id}-${permission.requester.username}']['action'].value = 'deny';document.forms['approveForm-${resource.id}-${permission.requester.username}'].submit();" type="submit" class="btn btn-danger">${msg("doDeny")}</a>
+ </td>
+ </tr>
+ </form>
+ </#list>
+ </#list>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </#if>
+
+ <div class="row">
+ <div class="col-md-12">
+ <h3>
+ ${msg("myResourcesSub")}
+ </h3>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th>${msg("resource")}</th>
+ <th>${msg("application")}</th>
+ <th>${msg("peopleSharingThisResource")}</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <#if authorization.resources?size != 0>
+ <#list authorization.resources as resource>
+ <tr>
+ <td>
+ <a id="detail-${resource.name}" href="${url.getResourceDetailUrl(resource.id)}">
+ <#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
+ </a>
+ </td>
+ <td>
+ <a href="${resource.resourceServer.redirectUri}">${resource.resourceServer.name}</a>
+ </td>
+ <td>
+ <#if resource.shares?size != 0>
+ <a href="${url.getResourceDetailUrl(resource.id)}">${resource.shares?size} <i class="fa fa-users"></i></a>
+ <#else>
+ This resource is not being shared.
+ </#if>
+ </td>
+ </tr>
+ </#list>
+ <#else>
+ <tr>
+ <td colspan="4">You don't have any resource</td>
+ </tr>
+ </#if>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-md-12">
+ <h3>
+ ${msg("resourcesSharedWithMe")}
+ </h3>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <form action="${url.resourceUrl}" name="shareForm" method="post">
+ <input type="hidden" name="action" value="cancel"/>
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th width="5%"><input type="checkbox" onclick="selectAllCheckBoxes('shareForm', this, 'resource_id');" <#if authorization.sharedResources?size == 0>disabled="true"</#if></td>
+ <th>${msg("resource")}</th>
+ <th>${msg("owner")}</th>
+ <th>${msg("application")}</th>
+ <th>${msg("permission")}</th>
+ <th>${msg("date")}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <#if authorization.sharedResources?size != 0>
+ <#list authorization.sharedResources as resource>
+ <tr>
+ <td>
+ <input type="checkbox" name="resource_id" value="${resource.id}"/>
+ </td>
+ <td>
+ <#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
+ </td>
+ <td>
+ <#if resource.owner.email??>${resource.owner.email}<#else>${resource.owner.username}</#if>
+ </td>
+ <td>
+ <a href="${resource.resourceServer.redirectUri}">${resource.resourceServer.name}</a>
+ </td>
+ <td>
+ <#if resource.permissions?size != 0>
+ <ul>
+ <#list resource.permissions as permission>
+ <#list permission.scopes as scope>
+ <#if scope.granted>
+ <li>
+ <#if scope.scope.displayName??>
+ ${scope.scope.displayName}
+ <#else>
+ ${scope.scope.name}
+ </#if>
+ </li>
+ </#if>
+ </#list>
+ </#list>
+ </ul>
+ <#else>
+ Any action
+ </#if>
+ </td>
+ <td>
+ ${resource.permissions[0].grantedDate?datetime}
+ </td>
+ </tr>
+ </#list>
+ <#else>
+ <tr>
+ <td colspan="5">There are no resources shared with you</td>
+ </tr>
+ </#if>
+ </tbody>
+ </table>
+ </form>
+ </div>
+ <#if authorization.sharedResources?size != 0>
+ <div class="col-md-12">
+ <a href="#" onclick="document.forms['shareForm'].submit();" type="submit" class="btn btn-danger">${msg("doRemoveSharing")}</a>
+ </div>
+ </#if>
+ </div>
+
+ <#if authorization.resourcesWaitingOthersApproval?size != 0>
+ <br/>
+ <div class="row">
+ <div class="col-md-12">
+ <h3>
+ ${msg("requestsWaitingApproval")}
+ </h3>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <#if authorization.resourcesWaitingOthersApproval?size != 0>
+ <i class="pficon pficon-info"></i> You have ${authorization.resourcesWaitingOthersApproval?size} permission request(s) <a href="#" onclick="document.getElementById('waitingApproval').style.display=''">waiting</a> for approval.
+ <#else>
+ You have no permission requests waiting for approval.
+ </#if>
+ <div class="row">
+ <div class="col-md-12"></div>
+ </div>
+ <div class="row">
+ <div class="col-md-12"></div>
+ </div>
+ <div class="row">
+ <div class="col-md-12"></div>
+ </div>
+ <div class="row" id="waitingApproval" style="display:none">
+ <div class="col-md-12">
+ <form action="${url.resourceUrl}" name="waitingApprovalForm" method="post">
+ <input type="hidden" name="action" value="cancelRequest"/>
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th width="5%"><input type="checkbox" onclick="selectAllCheckBoxes('waitingApprovalForm', this, 'resource_id');" <#if authorization.resourcesWaitingOthersApproval?size == 0>disabled="true"</#if></th>
+ <th>${msg("resource")}</th>
+ <th>${msg("owner")}</th>
+ <th>${msg("action")}</th>
+ <th>${msg("date")}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <#list authorization.resourcesWaitingOthersApproval as resource>
+ <tr>
+ <td>
+ <input type="checkbox" name="resource_id" value="${resource.id}"/>
+ </td>
+ <td>
+ <#if resource.displayName??>${resource.displayName}<#else>${resource.name}</#if>
+ </td>
+ <td>
+ <#if resource.owner.email??>${resource.owner.email}<#else>${resource.owner.username}</#if>
+ </td>
+ <td>
+ <ul>
+ <#list resource.permissions as permission>
+ <#list permission.scopes as scope>
+ <li>
+ <#if scope.scope.displayName??>
+ ${scope.scope.displayName}
+ <#else>
+ ${scope.scope.name}
+ </#if>
+ </li>
+ </#list>
+ </#list>
+ </ul>
+ </td>
+ <td>
+ ${resource.permissions[0].createdDate?datetime}
+ </td>
+ </tr>
+ </#list>
+ </tbody>
+ </table>
+ </form>
+ </div>
+ <div class="col-md-12">
+ <a href="#" onclick="document.forms['waitingApprovalForm'].submit();" type="submit" class="btn btn-danger">${msg("doRemoveRequest")}</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </#if>
+
+</@layout.mainLayout>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/account/template.ftl b/themes/src/main/resources/theme/base/account/template.ftl
index aa0ca41..e49c664 100644
--- a/themes/src/main/resources/theme/base/account/template.ftl
+++ b/themes/src/main/resources/theme/base/account/template.ftl
@@ -61,6 +61,7 @@
<li class="<#if active=='sessions'>active</#if>"><a href="${url.sessionsUrl}">${msg("sessions")}</a></li>
<li class="<#if active=='applications'>active</#if>"><a href="${url.applicationsUrl}">${msg("applications")}</a></li>
<#if features.log><li class="<#if active=='log'>active</#if>"><a href="${url.logUrl}">${msg("log")}</a></li></#if>
+ <#if realm.userManagedAccessAllowed && features.authorization><li class="<#if active=='authorization'>active</#if>"><a href="${url.resourceUrl}">${msg("myResources")}</a></li></#if>
</ul>
</div>
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 9dcf980..ba0412f 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -24,6 +24,8 @@ endpoints=Endpoints
# Realm settings
realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled
realm-detail.oidc-endpoints.tooltip=Shows the configuration of the OpenID Connect endpoints
+realm-detail.userManagedAccess.tooltip=If enabled, users are allowed to manage their resources and permissions using the Account Management Console.
+userManagedAccess=User-Managed Access
registrationAllowed=User registration
registrationAllowed.tooltip=Enable/disable the registration page. A link for registration will show on login page too.
registrationEmailAsUsername=Email as username
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
index e3bda1e..c68b0e2 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
@@ -22,6 +22,13 @@
</div>
<kc-tooltip>{{:: 'authz-resource-name.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">{{:: 'displayName' | translate}} <span class="required" data-ng-show="create">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="displayName" name="displayName" data-ng-model="resource.displayName">
+ </div>
+ <kc-tooltip>{{:: 'authz-resource-name.tooltip' | translate}}</kc-tooltip>
+ </div>
<div class="form-group" data-ng-hide="create">
<label class="col-md-2 control-label" for="resource.owner.name">{{:: 'authz-owner' | translate}} </label>
<div class="col-sm-6">
@@ -59,6 +66,13 @@
</div>
<kc-tooltip>{{:: 'authz-icon-uri.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="resource.ownerManagedAccess">User-Managed Access Enabled</label>
+ <div class="col-md-6">
+ <input ng-model="resource.ownerManagedAccess" id="resource.ownerManagedAccess" onoffswitch />
+ </div>
+ <kc-tooltip>{{:: 'authz-permission-resource-apply-to-resource-type.tooltip' | translate}}</kc-tooltip>
+ </div>
</fieldset>
<div class="form-group" data-ng-show="access.manageAuthorization">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-detail.html
index a505bbc..d296abd 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-detail.html
@@ -23,6 +23,13 @@
<kc-tooltip>{{:: 'authz-scope-name.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="displayName">{{:: 'displayName' | translate}} </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="displayName" name="displayName" data-ng-model="scope.displayName">
+ </div>
+ <kc-tooltip>{{:: 'authz-scope-name.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
<label class="col-md-2 control-label" for="name">{{:: 'authz-icon-uri' | translate}} </label>
<div class="col-sm-6">
<input class="form-control" type="text" id="iconUri" name="name" data-ng-model="scope.iconUri" autofocus>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html
index 88416f3..dadb0c7 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html
@@ -32,6 +32,14 @@
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="enabled">{{:: 'userManagedAccess' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="realm.userManagedAccessAllowed" name="userManagedAccessAllowed" id="userManagedAccessAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+ <kc-tooltip>{{:: 'realm-detail.userManagedAccess.tooltip' | translate}}</kc-tooltip>
+ </div>
+
+ <div class="form-group">
<label class="col-md-2 control-label">{{:: 'endpoints' | translate}}</label>
<div class="col-md-6">
<a class="form-control" ng-href="{{authUrl}}/realms/{{realm.realm}}/.well-known/openid-configuration" target="_blank">OpenID Endpoint Configuration</a>