From 91bdc4bde24a9095cf9f0ba3ea90e3bd6057e104 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 28 Feb 2018 04:53:10 -0300 Subject: [PATCH] [KEYCLOAK-3169] - UMA 2.0 (#4368) * [KEYCLOAK-3169] - UMA 2.0 Support * [KEYCLOAK-3169] - Changes to account service and more tests * [KEYCLOAK-3169] - Code cleanup and tests * [KEYCLOAK-3169] - Changes to account service and tests * [KEYCLOAK-3169] - Changes to account service and tests * [KEYCLOAK-3169] - More tests * [KEYCLOAK-3169] - Changes to adapter configuration * [KEYCLOAK-3169] - Reviewing UMA specs and more tests * [KEYCLOAK-3169] - Reviewing UMA specs and more tests * [KEYCLOAK-3169] - Changes to UMA Grant Type and refactoring * [KEYCLOAK-3169] - Refresh tokens for RPT responses and tests * [KEYCLOAK-3169] - Changes to account my resources and policy enforcers * [KEYCLOAK-3169] - Realm settings flag to enable/disable user-managed access in account mgmt console * [KEYCLOAK-3169] - More changes to my resource pages in account mgmt console * [KEYCLOAK-3169] - Need to enable user-managed on realm to run tests * [KEYCLOAK-3169] - Removing more UMA 1.0 related code * [KEYCLOAK-3169] - Only submit requests if ticket exists * [KEYCLOAK-3169] - Returning UMA 401 response when not authenticated * [KEYCLOAK-3169] - Removing unused code * [KEYCLOAK-3169] - Removing unused code * [KEYCLOAK-3169] - 403 response in case ticket is not created * [KEYCLOAK-3169] - Fixing AbstractPhotozExampleAdapterTest#testClientRoleRepresentingUserConsent * [KEYCLOAK-3169] - 403 status code only returned for non-bearer clients --- .../BearerTokenRequestAuthenticator.java | 4 + .../authorization/AbstractPolicyEnforcer.java | 79 ++-- .../BearerTokenPolicyEnforcer.java | 67 ++- .../KeycloakAdapterPolicyEnforcer.java | 78 ++-- .../adapters/authorization/PathMatcher.java | 6 +- .../authorization/PolicyEnforcer.java | 62 ++- .../js/src/main/resources/keycloak-authz.js | 168 ++++--- .../authorization/client/AuthzClient.java | 267 +++++++---- .../client/ClientAuthenticator.java | 5 +- .../authorization/client/Configuration.java | 53 ++- .../representation/AuthorizationRequest.java | 48 -- .../representation/AuthorizationResponse.java | 42 -- .../representation/EntitlementRequest.java | 84 ---- .../client/representation/ErrorResponse.java | 60 --- .../representation/PermissionRequest.java | 79 ---- .../representation/RegistrationResponse.java | 49 -- .../ResourceRepresentation.java | 24 +- .../representation/ServerConfiguration.java | 274 +++++------ .../TokenIntrospectionResponse.java | 4 +- .../resource/AuthorizationResource.java | 76 ++- .../client/resource/EntitlementResource.java | 43 -- .../client/resource/PermissionResource.java | 191 +++++++- .../client/resource/ProtectedResource.java | 202 ++++++-- .../client/resource/ProtectionResource.java | 34 +- .../authorization/client/util/Http.java | 16 +- .../authorization/client/util/HttpMethod.java | 47 +- .../client/util/HttpMethodAuthenticator.java | 84 +++- .../client/util/HttpMethodResponse.java | 24 +- .../authorization/client/util/Throwables.java | 68 ++- .../client/util/TokenCallable.java | 131 ++++++ .../java/org/keycloak/common/util/Time.java | 9 + .../org/keycloak/AuthorizationContext.java | 4 +- .../java/org/keycloak/OAuth2Constants.java | 2 + .../adapters/config/PolicyEnforcerConfig.java | 49 +- .../idm/RealmRepresentation.java | 10 + .../authorization/AuthorizationRequest.java | 190 ++++++++ .../authorization/AuthorizationResponse.java | 49 ++ .../idm/authorization/Permission.java | 24 +- .../idm/authorization/PermissionRequest.java | 72 +++ .../authorization}/PermissionResponse.java | 10 +- .../PermissionTicketRepresentation.java | 87 ++++ .../authorization/PermissionTicketToken.java | 86 ++++ .../ResourceOwnerRepresentation.java | 8 + .../authorization/ResourceRepresentation.java | 27 ++ .../authorization/ScopeRepresentation.java | 9 + .../src/main/webapp/index.jsp | 4 +- .../AuthorizationClientExample.java | 97 ++-- .../src/main/resources/keycloak.json | 2 +- .../src/main/webapp/index.html | 6 +- .../src/main/webapp/js/app.js | 65 ++- .../src/main/webapp/js/identity.js | 4 + .../src/main/webapp/keycloak.json | 2 +- .../src/main/webapp/partials/home.html | 22 +- examples/authz/photoz/photoz-realm.json | 17 +- .../example/photoz/album/AlbumService.java | 76 ++- .../example/photoz/album/SharedAlbum.java | 47 ++ .../keycloak/example/photoz/entity/Album.java | 14 + .../photoz-restful-api-authz-service.json | 157 +++---- .../src/main/webapp/WEB-INF/keycloak.json | 27 +- .../src/main/webapp/WEB-INF/keycloak.json | 2 +- .../servlet-authz/src/main/webapp/index.jsp | 4 +- .../src/main/webapp/logout-include.jsp | 2 +- .../models/cache/infinispan/RealmAdapter.java | 12 + .../PermissionTicketAdapter.java | 139 ++++++ .../authorization/PolicyAdapter.java | 4 +- .../authorization/ResourceAdapter.java | 41 +- .../authorization/ScopeAdapter.java | 12 + .../StoreFactoryCacheManager.java | 15 + .../StoreFactoryCacheSession.java | 146 +++++- .../entities/CachedPermissionTicket.java | 83 ++++ .../entities/CachedResource.java | 12 + .../authorization/entities/CachedScope.java | 7 +- .../entities/PermissionTicketListQuery.java | 42 ++ .../entities/PermissionTicketQuery.java | 34 +- .../PermissionTicketResourceListQuery.java | 42 ++ .../PermissionTicketScopeListQuery.java | 34 +- .../events/PermissionTicketRemovedEvent.java | 60 +++ .../events/PermissionTicketUpdatedEvent.java | 60 +++ .../infinispan/entities/CachedRealm.java | 6 + .../jpa/entities/PermissionTicketEntity.java | 161 +++++++ .../jpa/entities/ResourceEntity.java | 23 + .../jpa/entities/ScopeEntity.java | 15 + .../jpa/store/JPAPermissionTicketStore.java | 229 +++++++++ .../jpa/store/JPAResourceServerStore.java | 15 +- .../jpa/store/JPAResourceStore.java | 19 +- .../jpa/store/JPAStoreFactory.java | 8 + .../jpa/store/PermissionTicketAdapter.java | 132 ++++++ .../jpa/store/ResourceAdapter.java | 20 + .../authorization/jpa/store/ScopeAdapter.java | 10 + .../org/keycloak/models/jpa/RealmAdapter.java | 11 + .../models/jpa/entities/RealmEntity.java | 12 +- .../jpa-changelog-authz-4.0.0.CR1.xml | 78 ++++ .../META-INF/jpa-changelog-master.xml | 1 + .../main/resources/META-INF/persistence.xml | 1 + .../authorization/AuthorizationProvider.java | 442 ++++++++++++------ .../authorization/model/PermissionTicket.java | 76 +++ .../authorization/model/Resource.java | 17 + .../keycloak/authorization/model/Scope.java | 14 + .../IterablePermissionEvaluator.java | 3 +- .../evaluator/PermissionEvaluator.java | 2 +- .../evaluation/DecisionResultCollector.java | 12 +- .../evaluation/DefaultPolicyEvaluator.java | 17 +- ...ionTicketAwareDecisionResultCollector.java | 181 +++++++ .../store/PermissionTicketStore.java | 93 ++++ .../authorization/store/StoreFactory.java | 6 + .../java/org/keycloak/events/EventType.java | 5 +- .../keycloak/forms/account/AccountPages.java | 2 +- .../forms/account/AccountProvider.java | 2 +- .../models/utils/ModelToRepresentation.java | 36 ++ .../models/utils/RepresentationToModel.java | 27 ++ .../java/org/keycloak/models/RealmModel.java | 4 + .../authorization/AuthorizationService.java | 28 +- .../admin/PolicyEvaluationService.java | 4 +- .../admin/ResourceSetService.java | 119 +++-- .../authorization/admin/ScopeService.java | 2 +- .../PolicyEvaluationResponseBuilder.java | 10 +- .../AuthorizationTokenService.java | 432 +++++++++++------ .../representation/AuthorizationRequest.java | 50 -- .../AuthorizationRequestMetadata.java | 51 -- .../representation/AuthorizationResponse.java | 43 -- .../common/KeycloakEvaluationContext.java | 12 +- .../common/KeycloakIdentity.java | 39 +- .../authorization/config/Configuration.java | 269 ----------- .../config/UmaConfiguration.java | 89 ++++ .../config/UmaWellKnownProvider.java | 16 +- .../config/UmaWellKnownProviderFactory.java | 5 +- .../entitlement/EntitlementService.java | 334 ------------- .../representation/EntitlementRequest.java | 78 ---- .../representation/EntitlementResponse.java | 42 -- .../protection/ProtectionService.java | 45 +- .../introspect/RPTIntrospectionProvider.java | 47 +- .../permission/AbstractPermissionService.java | 126 ++--- .../permission/PermissionService.java | 106 ++++- .../permission/PermissionTicket.java | 58 --- .../permission/PermissionsService.java | 2 +- .../representation/PermissionRequest.java | 67 --- .../representation/PermissionResponse.java | 39 -- .../protection/resource/ResourceService.java | 121 ++--- .../UmaResourceRepresentation.java | 11 + .../authorization/util/Permissions.java | 64 +-- .../keycloak/authorization/util/Tokens.java | 10 +- .../freemarker/FreeMarkerAccountProvider.java | 27 +- .../forms/account/freemarker/Templates.java | 4 + .../freemarker/model/AuthorizationBean.java | 349 ++++++++++++++ .../freemarker/model/FeaturesBean.java | 8 +- .../account/freemarker/model/RealmBean.java | 4 + .../account/freemarker/model/UrlBean.java | 16 + .../AccessTokenIntrospectionProvider.java | 69 +-- .../keycloak/protocol/oidc/TokenManager.java | 4 + .../oidc/endpoints/TokenEndpoint.java | 115 ++++- .../KeycloakOIDCClientInstallation.java | 1 - .../main/java/org/keycloak/services/Urls.java | 16 + .../services/managers/AppAuthManager.java | 5 +- .../resources/account/AccountFormService.java | 201 +++++++- .../org/keycloak/theme/FreeMarkerUtil.java | 1 + .../src/main/webapp/index.jsp | 4 +- .../src/main/webapp/index.html | 3 +- .../src/main/webapp/js/app.js | 68 ++- .../src/main/webapp/js/identity.js | 5 + .../main/webapp/partials/album/create.html | 1 + .../src/main/webapp/partials/home.html | 2 +- .../test-apps/photoz/photoz-realm.json | 34 ++ .../photoz-restful-api-authz-service.json | 35 +- .../photoz/admin/AdminAlbumService.java | 2 +- .../example/photoz/album/AlbumService.java | 20 +- .../example/photoz/album/ProfileService.java | 2 +- .../keycloak/example/photoz/entity/Album.java | 12 + .../src/main/webapp/WEB-INF/keycloak.json | 20 +- ...=> keycloak-permissive-authz-service.json} | 1 + .../servlet-authz/src/main/webapp/index.jsp | 4 +- .../page/PhotozClientAuthzTestApp.java | 114 ++++- .../AbstractPermissiveModeAdapterTest.java | 2 +- .../AbstractPhotozExampleAdapterTest.java | 154 +++++- ...ResourceManagementWithAuthzClientTest.java | 6 +- .../testsuite/admin/realm/RealmTest.java | 6 +- .../testsuite/authz/AbstractAuthzTest.java | 1 - .../authz/AbstractResourceServerTest.java | 229 +++++++++ .../testsuite/authz/AuthorizationAPITest.java | 58 +-- .../authz/AuthzClientCredentialsTest.java | 67 +-- .../authz/ConflictingScopePermissionTest.java | 9 +- .../testsuite/authz/EntitlementAPITest.java | 125 +++-- .../testsuite/authz/GroupNamePolicyTest.java | 36 +- .../testsuite/authz/GroupPathPolicyTest.java | 32 +- .../testsuite/authz/PermissionClaimTest.java | 23 +- .../authz/PermissionManagementTest.java | 370 +++++++++++++++ .../testsuite/authz/RolePolicyTest.java | 42 +- .../authz/UmaDiscoveryDocumentTest.java | 77 +++ .../testsuite/authz/UmaGrantTypeTest.java | 276 +++++++++++ .../authz/UserManagedAccessTest.java | 358 ++++++++++++++ .../default-keycloak-uma2.json | 8 + .../tests/other/adapters/pom.xml | 2 +- .../authorization/resource/ResourceForm.java | 5 + .../clients/authorization/scope/Scope.java | 5 + .../authorization/scope/ScopeForm.java | 14 + .../clients/authorization/scope/Scopes.java | 20 +- .../authorization/ResourceManagementTest.java | 5 + .../authorization/ScopeManagementTest.java | 10 + .../account/messages/messages_en.properties | 26 ++ .../theme/base/account/resource-detail.ftl | 225 +++++++++ .../theme/base/account/resources.ftl | 385 +++++++++++++++ .../resources/theme/base/account/template.ftl | 1 + .../messages/admin-messages_en.properties | 2 + .../resource-server-resource-detail.html | 14 + .../authz/resource-server-scope-detail.html | 7 + .../resources/partials/realm-detail.html | 8 + 205 files changed, 8661 insertions(+), 3389 deletions(-) delete mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java delete mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java delete mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java delete mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java delete mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java delete mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java delete mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java create mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/util/TokenCallable.java create mode 100644 core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationRequest.java create mode 100644 core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationResponse.java create mode 100644 core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java rename {authz/client/src/main/java/org/keycloak/authorization/client/representation => core/src/main/java/org/keycloak/representations/idm/authorization}/PermissionResponse.java (79%) create mode 100644 core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketRepresentation.java create mode 100644 core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java create mode 100644 examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/SharedAlbum.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/PermissionTicketAdapter.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java create mode 100755 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketListQuery.java rename authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementResponse.java => model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketQuery.java (52%) create mode 100755 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketResourceListQuery.java rename authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java => model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketScopeListQuery.java (52%) mode change 100644 => 100755 create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketRemovedEvent.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/PermissionTicketUpdatedEvent.java create mode 100644 model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PermissionTicketEntity.java create mode 100644 model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPermissionTicketStore.java create mode 100644 model/jpa/src/main/java/org/keycloak/authorization/jpa/store/PermissionTicketAdapter.java create mode 100755 model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.CR1.xml create mode 100644 server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java create mode 100644 server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java create mode 100644 server-spi-private/src/main/java/org/keycloak/authorization/store/PermissionTicketStore.java delete mode 100644 services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java delete mode 100644 services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java delete mode 100644 services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java delete mode 100644 services/src/main/java/org/keycloak/authorization/config/Configuration.java create mode 100644 services/src/main/java/org/keycloak/authorization/config/UmaConfiguration.java delete mode 100644 services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java delete mode 100644 services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java delete mode 100644 services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java delete mode 100644 services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java delete mode 100644 services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java delete mode 100644 services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java create mode 100755 services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java rename testsuite/integration-arquillian/test-apps/servlet-authz/{keycloak.-permissive-authz-service.json => keycloak-permissive-authz-service.json} (94%) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractResourceServerTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionManagementTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaDiscoveryDocumentTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UmaGrantTypeTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedAccessTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/default-keycloak-uma2.json create mode 100755 themes/src/main/resources/theme/base/account/resource-detail.ftl create mode 100755 themes/src/main/resources/theme/base/account/resources.ftl 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 bb6840960b..966f8c0ee7 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); 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 15aa1e142a..96fbe5d113 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); - } - - LOGGER.debugf("Could not find a configuration for path [%s]", path); - - if (isDefaultAccessDeniedUri(request, enforcerConfig)) { - return createAuthorizationContext(accessToken, null); - } - - handleAccessDenied(httpFacade); - - return createEmptyAuthorizationContext(false); + if (pathConfig == null) { + if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) { + return createAuthorizationContext(accessToken, null); } - if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { - return createEmptyAuthorizationContext(true); + LOGGER.debugf("Could not find a configuration for path [%s]", path); + + if (isDefaultAccessDeniedUri(request, enforcerConfig)) { + return createAuthorizationContext(accessToken, null); } - MethodConfig methodConfig = getRequiredScopes(pathConfig, request); + handleAccessDenied(httpFacade); - if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) { - try { - return createAuthorizationContext(accessToken, pathConfig); - } catch (Exception e) { - throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e); - } + return createEmptyAuthorizationContext(false); + } + + if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) { + return createEmptyAuthorizationContext(true); + } + + 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); } + } - 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 172c745130..9e29735239 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 Pedro Igor @@ -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); + HttpFacade.Response response = facade.getResponse(); + AuthzClient authzClient = getAuthzClient(); + String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient); + + if (ticket == null) { + response.setStatus(403); + return true; + } + + String realm = authzClient.getConfiguration().getRealm(); + String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString(); + response.setStatus(401); + 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 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"); - } - } - - 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"; - response.setStatus(401); - response.setHeader("WWW-Authenticate", "UMA realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\",ticket=\"" + ticket + "\""); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Sending UMA challenge"); - } - } - 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 65fdc1e749..0732ee9c3b 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 Pedro Igor @@ -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(); + + permissionRequest.setResourceId(pathConfig.getId()); + permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes())); + + AccessToken accessToken = securityContext.getToken(); + AuthorizationRequest authzRequest; 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); - - if (authzResponse != null) { - return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment); - } - - return 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 c8bce94592..8e83de1887 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 search = resource.findByFilter("uri=" + path); + List 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 7f21eef242..fe8aa1aac1 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 Pedro Igor */ @@ -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 requestParams, HashMap requestHeaders) { - ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, requestParams); + public void configureClientCredentials(Map> requestParams, Map requestHeaders) { + Map formparams = new HashMap<>(); + ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, formparams); + for (Entry 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 paths = Collections.synchronizedMap(new HashMap()); for (PathConfig pathConfig : enforcerConfig.getPaths()) { - Set 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 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 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 paths = Collections.synchronizedMap(new HashMap()); 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/js/src/main/resources/keycloak-authz.js b/adapters/oidc/js/src/main/resources/keycloak-authz.js index 843b11d9fb..523727365a 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(','); + if (authorizationRequest && authorizationRequest.ticket) { + var request = new XMLHttpRequest(); - for (i = 0; i < params.length; i++) { - var param = params[i].split('='); + request.open('POST', _instance.config.token_endpoint, true); + request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token); - if (param[0] == 'ticket') { - var request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState == 4) { + var status = request.status; - 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 (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; } } - _instance.entitlement(clientId).then(onGrant, onDeny, onError); + if (_instance.rpt && (authorizationRequest.incrementalAuthorization == undefined || authorizationRequest.incrementalAuthorization)) { + params += "&rpt=" + _instance.rpt; + } + + 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(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) { + authorizationRequest = {}; } - request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token) - request.send(erJson); + var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId; + if (authorizationRequest.claimToken) { + params += "&claim_token=" + authorizationRequest.claimToken; + + if (authorizationRequest.claimTokenFormat) { + params += "&claim_token_format=" + authorizationRequest.claimTokenFormat; + } + } + + 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 1113fd2136..1bf5c8697f 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; - /** *

This is class serves as an entry point for clients looking for access to Keycloak Authorization Services. * + *

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 http(s)://{server}:{port}/auth/realms/{realm}/.well-known/uma-configuration. + * * @author Pedro Igor */ public class AuthzClient { private final Http http; - private Callable patSupplier; + private TokenCallable patSupplier; - public static AuthzClient create() { + /** + *

Creates a new instance. + * + *

This method expects a keycloak.json in the classpath, otherwise an exception will be thrown. + * + * @return a new instance + * @throws RuntimeException in case there is no keycloak.json 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,16 +64,158 @@ public class AuthzClient { } } + /** + *

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()); } + /** + *

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 final Configuration configuration; + + /** + *

Creates a {@link ProtectionResource} instance which can be used to access the Protection API. + * + *

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()); + } + + /** + *

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; + } + }); + } + + /** + *

Creates a {@link ProtectionResource} instance which can be used to access the Protection API. + * + *

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)); + } + + /** + *

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); + } + + /** + *

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; + } + }); + } + + /** + *

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.post(this.serverConfiguration.getTokenEndpoint()) + .authentication() + .client() + .response() + .json(AccessTokenResponse.class) + .execute(); + } + + /** + * Obtains an access token using the resource owner credentials. + * + * @return an {@link AccessTokenResponse} + */ + public AccessTokenResponse obtainAccessToken(String userName, String password) { + return this.http.post(this.serverConfiguration.getTokenEndpoint()) + .authentication() + .oauth2ResourceOwnerPassword(userName, password) + .response() + .json(AccessTokenResponse.class) + .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.configuration; + } private AuthzClient(Configuration configuration, ClientAuthenticator authenticator) { if (configuration == null) { @@ -78,14 +228,14 @@ public class AuthzClient { throw new IllegalArgumentException("Configuration URL can not be null."); } - configurationUrl += "/realms/" + configuration.getRealm() + "/.well-known/uma-configuration"; + configurationUrl += "/realms/" + configuration.getRealm() + "/.well-known/uma2-configuration"; - this.deployment = configuration; + this.configuration = configuration; this.http = new Http(configuration, authenticator != null ? authenticator : configuration.getClientAuthenticator()); try { - this.serverConfiguration = this.http.get(URI.create(configurationUrl)) + this.serverConfiguration = this.http.get(configurationUrl) .response().json(ServerConfiguration.class) .execute(); } catch (Exception e) { @@ -95,85 +245,18 @@ public class AuthzClient { this.http.setServerConfiguration(this.serverConfiguration); } - private AuthzClient(Configuration configuration) { - this(configuration, null); - } - - public ProtectionResource protection() { - return new ProtectionResource(this.http, createPatSupplier()); - } - - public AuthorizationResource authorization(String accesstoken) { - return new AuthorizationResource(this.http, accesstoken); - } - - public AuthorizationResource authorization(String userName, String password) { - return new AuthorizationResource(this.http, obtainAccessToken(userName, password).getToken()); - } - - public EntitlementResource entitlement(String eat) { - return new EntitlementResource(this.http, eat); - } - - public AccessTokenResponse obtainAccessToken() { - return this.http.post(this.serverConfiguration.getTokenEndpoint()) - .authentication() - .client() - .response() - .json(AccessTokenResponse.class) - .execute(); - } - - public AccessTokenResponse obtainAccessToken(String userName, String password) { - return this.http.post(this.serverConfiguration.getTokenEndpoint()) - .authentication() - .oauth2ResourceOwnerPassword(userName, password) - .response() - .json(AccessTokenResponse.class) - .execute(); - } - - public ServerConfiguration getServerConfiguration() { - return this.serverConfiguration; - } - - public Configuration getConfiguration() { - return this.deployment; - } - - private Callable createPatSupplier() { + private TokenCallable createPatSupplier(String userName, String password) { if (patSupplier == null) { - patSupplier = new Callable() { - 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.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 076c2dbd7a..d9077e5c92 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 Pedro Igor */ public interface ClientAuthenticator { - void configureClientCredentials(HashMap requestParams, HashMap requestHeaders); + void configureClientCredentials(Map> requestParams, Map 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 647891ff4a..7e6802e9bc 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 Pedro Igor @@ -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 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 requestParams, HashMap 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> requestParams, Map 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/AuthorizationRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java deleted file mode 100644 index ef535862d2..0000000000 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * - * Copyright 2015 Red Hat, Inc. and/or its affiliates. - * - * 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.representation; - -/** - * @author Pedro Igor - */ -public class AuthorizationRequest { - - private String ticket; - private String rpt; - - public AuthorizationRequest(String ticket, String rpt) { - this.ticket = ticket; - this.rpt = rpt; - } - - public AuthorizationRequest(String ticket) { - this(ticket, null); - } - - public AuthorizationRequest() { - this(null, null); - } - - public String getTicket() { - return this.ticket; - } - - public String getRpt() { - return this.rpt; - } -} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java deleted file mode 100644 index 472c89ae50..0000000000 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * - * Copyright 2015 Red Hat, Inc. and/or its affiliates. - * - * 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.representation; - -/** - * @author Pedro Igor - */ -public class AuthorizationResponse { - - private String rpt; - - public AuthorizationResponse(String rpt) { - this.rpt = rpt; - } - - public AuthorizationResponse() { - this(null); - } - - public String getRpt() { - return this.rpt; - } - - public void setRpt(final String rpt) { - this.rpt = rpt; - } -} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java deleted file mode 100644 index b3efa85c96..0000000000 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.keycloak.authorization.client.representation; - -import java.util.ArrayList; -import java.util.List; - -/** - *

An {@code {@link EntitlementRequest} represents a request sent to the server containing the permissions being requested. - * - *

Along with an entitlement request additional {@link AuthorizationRequestMetadata} information can be passed in order to define what clients expect from - * the server when evaluating the requested permissions and when returning with a response. - * - * @author Pedro Igor - */ -public class EntitlementRequest { - - private String rpt; - private AuthorizationRequestMetadata metadata; - - private List permissions = new ArrayList<>(); - - /** - * Returns the permissions being requested. - * - * @return the permissions being requested (not {@code null}) - */ - public List getPermissions() { - return permissions; - } - - /** - * Set the permissions being requested - * - * @param permissions the permissions being requests (not {@code null}) - */ - public void setPermissions(List permissions) { - this.permissions = permissions; - } - - /** - * Adds the given {@link PermissionRequest} to the list of requested permissions. - * - * @param request the permission to request (not {@code null}) - */ - public void addPermission(PermissionRequest request) { - getPermissions().add(request); - } - - /** - * Returns a {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested. - * - * @return a previously issued RPT (may be {@code null}) - */ - public String getRpt() { - return rpt; - } - - /** - * A {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested. - * - * @param rpt a previously issued RPT. If {@code null}, only the requested permissions are evaluated - */ - public void setRpt(String rpt) { - this.rpt = rpt; - } - - /** - * Return the {@link Metadata} associated with this request. - * - * @return - */ - public AuthorizationRequestMetadata getMetadata() { - return metadata; - } - - /** - * The {@link Metadata} associated with this request. The metadata defines specific information that should be considered - * by the server when evaluating and returning permissions. - * - * @param metadata the {@link Metadata} associated with this request (may be {@code null}) - */ - public void setMetadata(AuthorizationRequestMetadata metadata) { - this.metadata = metadata; - } -} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java deleted file mode 100644 index fd3dc632b0..0000000000 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * - * Copyright 2015 Red Hat, Inc. and/or its affiliates. - * - * 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.representation; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * @author Pedro Igor - */ -public class ErrorResponse { - - private String error; - - @JsonProperty("error_description") - private String description; - - @JsonProperty("error_uri") - private String uri; - - public ErrorResponse(final String error, final String description, final String uri) { - this.error = error; - this.description = description; - this.uri = uri; - } - - public ErrorResponse(final String error) { - this(error, null, null); - } - - public ErrorResponse() { - this(null, null, null); - } - - public String getError() { - return this.error; - } - - public String getDescription() { - return this.description; - } - - public String getUri() { - return this.uri; - } -} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java deleted file mode 100644 index 38d54710db..0000000000 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * - * Copyright 2015 Red Hat, Inc. and/or its affiliates. - * - * 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.representation; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.Set; - -/** - * @author Pedro Igor - */ -public class PermissionRequest { - - @JsonProperty("resource_set_id") - private String resourceSetId; - - @JsonProperty("resource_set_name") - private String resourceSetName; - - private Set scopes; - - public PermissionRequest() { - - } - - public PermissionRequest(String resourceSetId, String resourceSetName, Set scopes) { - this.resourceSetId = resourceSetId; - this.resourceSetName = resourceSetName; - this.scopes = scopes; - } - - public PermissionRequest(String resourceSetName) { - this.resourceSetName = resourceSetName; - } - - public PermissionRequest(String resourceSetName, Set scopes) { - this.resourceSetName = resourceSetName; - this.scopes = scopes; - } - - public String getResourceSetId() { - return this.resourceSetId; - } - - public void setResourceSetId(String resourceSetId) { - this.resourceSetId = resourceSetId; - } - - public Set getScopes() { - return this.scopes; - } - - public void setScopes(Set scopes) { - this.scopes = scopes; - } - - public String getResourceSetName() { - return this.resourceSetName; - } - - public void setResourceSetName(String resourceSetName) { - this.resourceSetName = resourceSetName; - } -} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java deleted file mode 100644 index 0f279bc62d..0000000000 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * - * Copyright 2015 Red Hat, Inc. and/or its affiliates. - * - * 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.representation; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; - -/** - * @author Pedro Igor - */ -public class RegistrationResponse { - - private final ResourceRepresentation resourceDescription; - - public RegistrationResponse(ResourceRepresentation resourceDescription) { - this.resourceDescription = resourceDescription; - } - - public RegistrationResponse() { - this(null); - } - - @JsonUnwrapped - public ResourceRepresentation getResourceDescription() { - return this.resourceDescription; - } - - public String getId() { - if (this.resourceDescription != null) { - return this.resourceDescription.getId(); - } - - return null; - } -} 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 65ec87c321..c2e8f8a9e1 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; + /** *

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 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 6716165838..f708e52af1 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 Pedro Igor */ public class ServerConfiguration { - private String version; - private URI issuer; - - @JsonProperty("pat_profiles_supported") - private Set patProfiles; - - @JsonProperty("pat_grant_types_supported") - private Set patGrantTypes; - - @JsonProperty("aat_profiles_supported") - private Set aatProfiles; - - @JsonProperty("aat_grant_types_supported") - private Set aatGrantTypes; - - @JsonProperty("rpt_profiles_supported") - private Set rptProfiles; - - @JsonProperty("claim_token_profiles_supported") - private Set claimTokenProfiles; - - @JsonProperty("dynamic_client_endpoint") - private URI dynamicClientEndpoint; - - @JsonProperty("token_endpoint") - private URI tokenEndpoint; + @JsonProperty("issuer") + private String issuer; @JsonProperty("authorization_endpoint") - private URI authorizationEndpoint; + private String authorizationEndpoint; - @JsonProperty("requesting_party_claims_endpoint") - private URI requestingPartyClaimsEndpoint; + @JsonProperty("token_endpoint") + private String tokenEndpoint; - @JsonProperty("resource_set_registration_endpoint") - private URI resourceSetRegistrationEndpoint; + @JsonProperty("token_introspection_endpoint") + private String tokenIntrospectionEndpoint; - @JsonProperty("introspection_endpoint") - private URI introspectionEndpoint; + @JsonProperty("userinfo_endpoint") + private String userinfoEndpoint; - @JsonProperty("permission_registration_endpoint") - private URI permissionRegistrationEndpoint; + @JsonProperty("end_session_endpoint") + private String logoutEndpoint; - @JsonProperty("rpt_endpoint") - private URI rptEndpoint; + @JsonProperty("jwks_uri") + private String jwksUri; - /** - * Non-standard, Keycloak specific configuration options - */ - private String realm; + @JsonProperty("check_session_iframe") + private String checkSessionIframe; - private String realmPublicKey; + @JsonProperty("grant_types_supported") + private List grantTypesSupported; - private URI serverUrl; + @JsonProperty("response_types_supported") + private List responseTypesSupported; - public String getVersion() { - return this.version; + @JsonProperty("subject_types_supported") + private List subjectTypesSupported; + + @JsonProperty("id_token_signing_alg_values_supported") + private List idTokenSigningAlgValuesSupported; + + @JsonProperty("userinfo_signing_alg_values_supported") + private List userInfoSigningAlgValuesSupported; + + @JsonProperty("request_object_signing_alg_values_supported") + private List requestObjectSigningAlgValuesSupported; + + @JsonProperty("response_modes_supported") + private List responseModesSupported; + + @JsonProperty("registration_endpoint") + private String registrationEndpoint; + + @JsonProperty("token_endpoint_auth_methods_supported") + private List tokenEndpointAuthMethodsSupported; + + @JsonProperty("token_endpoint_auth_signing_alg_values_supported") + private List tokenEndpointAuthSigningAlgValuesSupported; + + @JsonProperty("claims_supported") + private List claimsSupported; + + @JsonProperty("claim_types_supported") + private List claimTypesSupported; + + @JsonProperty("claims_parameter_supported") + private Boolean claimsParameterSupported; + + @JsonProperty("scopes_supported") + private List scopesSupported; + + @JsonProperty("request_parameter_supported") + private Boolean requestParameterSupported; + + @JsonProperty("request_uri_parameter_supported") + private Boolean requestUriParameterSupported; + + @JsonProperty("resource_registration_endpoint") + private String resourceRegistrationEndpoint; + + @JsonProperty("permission_endpoint") + private String permissionEndpoint; + + public String getIssuer() { + return issuer; } - void setVersion(final String version) { - this.version = version; + public String getAuthorizationEndpoint() { + return authorizationEndpoint; } - public URI getIssuer() { - return this.issuer; + public String getTokenEndpoint() { + return tokenEndpoint; } - void setIssuer(final URI issuer) { - this.issuer = issuer; + public String getTokenIntrospectionEndpoint() { + return tokenIntrospectionEndpoint; } - public Set getPatProfiles() { - return this.patProfiles; + public String getUserinfoEndpoint() { + return userinfoEndpoint; } - void setPatProfiles(final Set patProfiles) { - this.patProfiles = patProfiles; + public String getLogoutEndpoint() { + return logoutEndpoint; } - public Set getPatGrantTypes() { - return this.patGrantTypes; + public String getJwksUri() { + return jwksUri; } - void setPatGrantTypes(final Set patGrantTypes) { - this.patGrantTypes = patGrantTypes; + public String getCheckSessionIframe() { + return checkSessionIframe; } - public Set getAatProfiles() { - return this.aatProfiles; + public List getGrantTypesSupported() { + return grantTypesSupported; } - void setAatProfiles(final Set aatProfiles) { - this.aatProfiles = aatProfiles; + public List getResponseTypesSupported() { + return responseTypesSupported; } - public Set getAatGrantTypes() { - return this.aatGrantTypes; + public List getSubjectTypesSupported() { + return subjectTypesSupported; } - void setAatGrantTypes(final Set aatGrantTypes) { - this.aatGrantTypes = aatGrantTypes; + public List getIdTokenSigningAlgValuesSupported() { + return idTokenSigningAlgValuesSupported; } - public Set getRptProfiles() { - return this.rptProfiles; + public List getUserInfoSigningAlgValuesSupported() { + return userInfoSigningAlgValuesSupported; } - void setRptProfiles(final Set rptProfiles) { - this.rptProfiles = rptProfiles; + public List getRequestObjectSigningAlgValuesSupported() { + return requestObjectSigningAlgValuesSupported; } - public Set getClaimTokenProfiles() { - return this.claimTokenProfiles; + public List getResponseModesSupported() { + return responseModesSupported; } - void setClaimTokenProfiles(final Set claimTokenProfiles) { - this.claimTokenProfiles = claimTokenProfiles; + public String getRegistrationEndpoint() { + return registrationEndpoint; } - public URI getDynamicClientEndpoint() { - return this.dynamicClientEndpoint; + public List getTokenEndpointAuthMethodsSupported() { + return tokenEndpointAuthMethodsSupported; } - void setDynamicClientEndpoint(final URI dynamicClientEndpoint) { - this.dynamicClientEndpoint = dynamicClientEndpoint; + public List getTokenEndpointAuthSigningAlgValuesSupported() { + return tokenEndpointAuthSigningAlgValuesSupported; } - public URI getTokenEndpoint() { - return this.tokenEndpoint; + public List getClaimsSupported() { + return claimsSupported; } - void setTokenEndpoint(final URI tokenEndpoint) { - this.tokenEndpoint = tokenEndpoint; + public List getClaimTypesSupported() { + return claimTypesSupported; } - public URI getAuthorizationEndpoint() { - return this.authorizationEndpoint; + public Boolean getClaimsParameterSupported() { + return claimsParameterSupported; } - void setAuthorizationEndpoint(final URI authorizationEndpoint) { - this.authorizationEndpoint = authorizationEndpoint; + public List getScopesSupported() { + return scopesSupported; } - public URI getRequestingPartyClaimsEndpoint() { - return this.requestingPartyClaimsEndpoint; + public Boolean getRequestParameterSupported() { + return requestParameterSupported; } - void setRequestingPartyClaimsEndpoint(final URI requestingPartyClaimsEndpoint) { - this.requestingPartyClaimsEndpoint = requestingPartyClaimsEndpoint; + public Boolean getRequestUriParameterSupported() { + return requestUriParameterSupported; } - public URI getResourceSetRegistrationEndpoint() { - return this.resourceSetRegistrationEndpoint; + public String getResourceRegistrationEndpoint() { + return resourceRegistrationEndpoint; } - void setResourceSetRegistrationEndpoint(final URI resourceSetRegistrationEndpoint) { - this.resourceSetRegistrationEndpoint = resourceSetRegistrationEndpoint; - } - - public URI getIntrospectionEndpoint() { - return this.introspectionEndpoint; - } - - void setIntrospectionEndpoint(final URI introspectionEndpoint) { - this.introspectionEndpoint = introspectionEndpoint; - } - - public URI getPermissionRegistrationEndpoint() { - return this.permissionRegistrationEndpoint; - } - - void setPermissionRegistrationEndpoint(final URI permissionRegistrationEndpoint) { - this.permissionRegistrationEndpoint = permissionRegistrationEndpoint; - } - - public URI getRptEndpoint() { - return this.rptEndpoint; - } - - void setRptEndpoint(final URI rptEndpoint) { - this.rptEndpoint = rptEndpoint; - } - - public String getRealm() { - return this.realm; - } - - public void setRealm(final String realm) { - this.realm = realm; - } - - public String getRealmPublicKey() { - return this.realmPublicKey; - } - - public void setRealmPublicKey(String realmPublicKey) { - this.realmPublicKey = realmPublicKey; - } - - public URI getServerUrl() { - return this.serverUrl; - } - - 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 8fcc6f31e0..8bd0424322 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 Pedro Igor */ 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 0f22ebe89e..6b30b0d355 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 Pedro Igor */ 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; } - public AuthorizationResponse authorize(AuthorizationRequest request) { + /** + * 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()); + } + + /** + * 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 callable = new Callable() { + @Override + public AuthorizationResponse call() throws Exception { + request.setAudience(configuration.getResource()); + + HttpMethod method = http.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.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/EntitlementResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java deleted file mode 100644 index 1103c2d0e8..0000000000 --- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.keycloak.authorization.client.resource; - -import static org.keycloak.authorization.client.util.Throwables.handleAndWrapException; - -import org.keycloak.authorization.client.representation.EntitlementRequest; -import org.keycloak.authorization.client.representation.EntitlementResponse; -import org.keycloak.authorization.client.util.Http; -import org.keycloak.util.JsonSerialization; - -/** - * @author Pedro Igor - */ -public class EntitlementResource { - - private final Http http; - private final String eat; - - public EntitlementResource(Http http, String eat) { - this.http = http; - this.eat = eat; - } - - public EntitlementResponse getAll(String resourceServerId) { - try { - return this.http.get("/authz/entitlement/" + resourceServerId) - .authorizationBearer(eat) - .response().json(EntitlementResponse.class).execute(); - } catch (Exception cause) { - throw handleAndWrapException("Failed to obtain entitlements", cause); - } - } - - public EntitlementResponse get(String resourceServerId, EntitlementRequest request) { - try { - return this.http.post("/authz/entitlement/" + resourceServerId) - .authorizationBearer(eat) - .json(JsonSerialization.writeValueAsBytes(request)) - .response().json(EntitlementResponse.class).execute(); - } catch (Exception cause) { - throw handleAndWrapException("Failed to obtain entitlements", 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 785a3a6d4b..b6628a9ac8 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 Pedro Igor */ public class PermissionResource { private final Http http; - private final Callable pat; + private final ServerConfiguration serverConfiguration; + private final TokenCallable pat; - public PermissionResource(Http http, Callable 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 requests) { + if (requests == null || requests.isEmpty()) { + throw new IllegalArgumentException("Permission request must not be null or empty"); + } + Callable callable = new Callable() { + @Override + public PermissionResponse call() throws Exception { + return http.post(serverConfiguration.getPermissionEndpoint()) + .authorizationBearer(pat.call()) + .json(JsonSerialization.writeValueAsBytes(requests)) + .response().json(PermissionResponse.class).execute(); + } + }; try { - return this.http.post("/authz/protection/permission") - .authorizationBearer(this.pat.call()) - .json(JsonSerialization.writeValueAsBytes(request)) - .response().json(PermissionResponse.class).execute(); + return callable.call(); } catch (Exception cause) { - throw handleAndWrapException("Error obtaining permission ticket", cause); + return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Error creating permission ticket", cause); + } + } + + /** + * Query the server for any permission ticket associated with the given scopeId. + * + * @param scopeId the scope id (not {@code null}) + * @return a list of permission tickets associated with the given scopeId + */ + public List findByScope(final String scopeId) { + if (scopeId == null) { + throw new IllegalArgumentException("Scope id must not be null"); + } + Callable> callable = new Callable>() { + @Override + public List call() throws Exception { + return http.>get(serverConfiguration.getPermissionEndpoint()) + .authorizationBearer(pat.call()) + .param("scopeId", scopeId) + .response().json(new TypeReference>(){}).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 resourceId. + * + * @param resourceId the resource id (not {@code null}) + * @return a list of permission tickets associated with the given resourceId + */ + public List findByResource(final String resourceId) { + if (resourceId == null) { + throw new IllegalArgumentException("Resource id must not be null"); + } + Callable> callable = new Callable>() { + @Override + public List call() throws Exception { + return http.>get(serverConfiguration.getPermissionEndpoint()) + .authorizationBearer(pat.call()) + .param("resourceId", resourceId) + .response().json(new TypeReference>(){}).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 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> callable = new Callable>() { + @Override + public List call() throws Exception { + return http.>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>(){}).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.put(serverConfiguration.getPermissionEndpoint()) + .json(JsonSerialization.writeValueAsBytes(ticket)) + .authorizationBearer(pat.call()) + .response().json(List.class).execute(); + return null; + } + }; + try { + callable.call(); + } catch (Exception 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 fcf1e436f8..cc92712726 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 Pedro Igor */ public class ProtectedResource { private final Http http; - private final Callable pat; + private ServerConfiguration serverConfiguration; + private final TokenCallable pat; - public ProtectedResource(Http http, Callable 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 callable = new Callable() { + @Override + public ResourceRepresentation call() throws Exception { + return http.post(serverConfiguration.getResourceRegistrationEndpoint()) + .authorizationBearer(pat.call()) + .json(JsonSerialization.writeValueAsBytes(resource)) + .response().json(ResourceRepresentation.class).execute(); + } + }; try { - return this.http.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.put(serverConfiguration.getResourceRegistrationEndpoint() + "/" + resource.getId()) + .authorizationBearer(pat.call()) + .json(JsonSerialization.writeValueAsBytes(resource)).execute(); + return null; + } + }; try { - this.http.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 id. + * + * @param id the resource id + * @return a {@link ResourceRepresentation} + */ + public ResourceRepresentation findById(final String id) { + Callable callable = new Callable() { + @Override + public ResourceRepresentation call() throws Exception { + return http.get(serverConfiguration.getResourceRegistrationEndpoint() + "/" + id) + .authorizationBearer(pat.call()) + .response().json(ResourceRepresentation.class).execute(); + } + }; try { - return this.http.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 findByFilter(String filter) { + /** + * Query the server for a resource given its name. + * + * @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 callable = new Callable() { + @Override + public String[] call() throws Exception { + return http.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.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 findAll() { + /** + * Query the server for all resources. + * + * @return @return an array of strings with the resource ids + */ + public String[] findAll() { try { - return this.http.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 id. + * + * @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 findByUri(String uri) { + String[] ids = find(null, null, uri, null, null, null, null, null); + + if (ids.length == 0) { + return Collections.emptyList(); + } + + List 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 3d2eb2cfe7..7268fe954a 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 Pedro Igor */ public class ProtectionResource { - private final Callable pat; + private final TokenCallable pat; private final Http http; + private ServerConfiguration serverConfiguration; - public ProtectionResource(Http http, Callable 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 rpt using the token introspection endpoint. + * + * @param rpt the rpt to introspect + * @return the {@link TokenIntrospectionResponse} + */ public TokenIntrospectionResponse introspectRequestingPartyToken(String rpt) { - return this.http.post("/protocol/openid-connect/token/introspect") + return this.http.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 f72e6b7669..eecb7e30af 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 Pedro Igor */ @@ -39,27 +37,19 @@ public class Http { } public HttpMethod get(String path) { - return method(RequestBuilder.get().setUri(this.serverConfiguration.getIssuer() + path)); - } - - public HttpMethod get(URI path) { return method(RequestBuilder.get().setUri(path)); } - public HttpMethod post(URI path) { + public HttpMethod post(String path) { return method(RequestBuilder.post().setUri(path)); } - public HttpMethod post(String path) { - return method(RequestBuilder.post().setUri(this.serverConfiguration.getIssuer() + path)); - } - public HttpMethod put(String path) { - return method(RequestBuilder.put().setUri(this.serverConfiguration.getIssuer() + path)); + return method(RequestBuilder.put().setUri(path)); } public HttpMethod delete(String path) { - return method(RequestBuilder.delete().setUri(this.serverConfiguration.getIssuer() + path)); + return method(RequestBuilder.delete().setUri(path)); } private HttpMethod 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 9a7e51a72e..230b7f8c74 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 { private final HttpClient httpClient; private final ClientAuthenticator authenticator; - private final RequestBuilder builder; + protected final RequestBuilder builder; protected final Configuration configuration; - protected final HashMap headers; - protected final HashMap params; + protected final Map headers; + protected final Map> params; private HttpMethodResponse response; public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder) { - this(configuration, authenticator, builder, new HashMap(), new HashMap()); + this(configuration, authenticator, builder, new HashMap>(), new HashMap()); } - public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder, HashMap params, HashMap headers) { + public HttpMethod(Configuration configuration, ClientAuthenticator authenticator, RequestBuilder builder, Map> params, Map headers) { this.configuration = configuration; this.httpClient = configuration.getHttpClient(); this.authenticator = authenticator; @@ -108,8 +108,10 @@ public class HttpMethod { } protected void preExecute(RequestBuilder builder) { - for (Map.Entry param : params.entrySet()) { - builder.addParameter(param.getKey(), param.getValue()); + for (Map.Entry> param : params.entrySet()) { + for (String value : param.getValue()) { + builder.addParameter(param.getKey(), value); + } } } @@ -128,7 +130,30 @@ public class HttpMethod { } public HttpMethod param(String name, String value) { - this.params.put(name, value); + if (value != null) { + List values = params.get(name); + + if (values == null || !values.isEmpty()) { + values = new ArrayList<>(); + params.put(name, values); + } + + values.add(value); + } + return this; + } + + public HttpMethod params(String name, String value) { + if (value != null) { + List 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 { if (params != null) { List formparams = new ArrayList<>(); - for (Map.Entry param : params.entrySet()) { - formparams.add(new BasicNameValuePair(param.getKey(), param.getValue())); + for (Map.Entry> 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 8807d393bd..33674fbc0c 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 Pedro Igor @@ -34,16 +42,84 @@ public class HttpMethodAuthenticator { } public HttpMethod 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 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 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 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 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 fceca19d61..7cfba8ef0b 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 Pedro Igor */ @@ -58,4 +60,22 @@ public class HttpMethodResponse { } }; } + + public HttpMethodResponse json(final TypeReference responseType) { + return new HttpMethodResponse(this.method) { + @Override + public R execute() { + return method.execute(new HttpResponseProcessor() { + @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 d51b27ecc7..ae2eaf11a6 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 Pedro Igor @@ -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 exception the root exception - * @return a {@link RuntimeException} wrapping the given {@code exception} + * @param cause the root cause + * @return a {@link RuntimeException} wrapping the given {@code cause} */ - public static RuntimeException handleAndWrapException(String message, Exception exception) { - if (exception instanceof HttpResponseException) { - throw handleAndWrapHttpResponseException(message, HttpResponseException.class.cast(exception)); + public static RuntimeException handleWrapException(String message, Throwable cause) { + if (cause instanceof HttpResponseException) { + throw handleAndWrapHttpResponseException(message, HttpResponseException.class.cast(cause)); } - return new RuntimeException(message, exception); + return new RuntimeException(message, cause); + } + + /** + *

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)}. + * + *

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 cause the cause + * @param the result of the callable + * @return the result of the callable + * @throws RuntimeException in case the attempt to retry fails + */ + public static V retryAndWrapExceptionIfNecessary(Callable 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().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); + } + } + + 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 0000000000..b1c328020f --- /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 { + + 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.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.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.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 54809d8dab..e48f217a9c 100644 --- a/common/src/main/java/org/keycloak/common/util/Time.java +++ b/common/src/main/java/org/keycloak/common/util/Time.java @@ -51,6 +51,15 @@ public class Time { return new Date(((long) time ) * 1000); } + /** + * 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 diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java index e096e7e2e0..0a9b33259c 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 59e0eeed4e..df5411257d 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 26dc22005b..89dadbfdad 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 Pedro Igor */ @@ -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 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 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/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index cd52d7ce0d..1c039d3033 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 federatedUsers) { this.federatedUsers = federatedUsers; } + + public void setUserManagedAccessAllowed(Boolean userManagedAccessAllowed) { + this.userManagedAccessAllowed = userManagedAccessAllowed; + } + + public Boolean isUserManagedAccessAllowed() { + return userManagedAccessAllowed; + } } 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 0000000000..764ae02e6b --- /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 Pedro Igor + */ +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 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 permission = null; + + for (ResourcePermission resourcePermission : permissions.getResources()) { + if (resourcePermission.getResourceId().equals(resourceId)) { + permission = resourcePermission; + break; + } + } + + if (permission == null) { + permission = new ResourcePermission(resourceId, new HashSet()); + 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/AuthorizationResponse.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationResponse.java new file mode 100644 index 0000000000..449e1b75c5 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AuthorizationResponse.java @@ -0,0 +1,49 @@ +/* + * 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 org.keycloak.representations.AccessTokenResponse; + +/** + * @author Pedro Igor + */ +public class AuthorizationResponse extends AccessTokenResponse { + + private boolean upgraded; + + public AuthorizationResponse() { + } + + public AuthorizationResponse(AccessTokenResponse response, boolean upgraded) { + setToken(response.getToken()); + setTokenType("Bearer"); + setRefreshToken(response.getRefreshToken()); + setRefreshExpiresIn(response.getRefreshExpiresIn()); + setExpiresIn(response.getExpiresIn()); + setNotBeforePolicy(response.getNotBeforePolicy()); + this.upgraded = upgraded; + } + + public boolean isUpgraded() { + return upgraded; + } + + public void setUpgraded(boolean upgraded) { + this.upgraded = upgraded; + } +} 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 74df64fde5..ed392f07a7 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 scopes; @@ -44,19 +44,19 @@ public class Permission { this(null, null, null, null); } - public Permission(final String resourceSetId, String resourceSetName, final Set scopes, Map> claims) { - this.resourceSetId = resourceSetId; - this.resourceSetName = resourceSetName; + public Permission(final String resourceId, String resourceName, final Set scopes, Map> 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 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/PermissionRequest.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java new file mode 100644 index 0000000000..5830e160da --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionRequest.java @@ -0,0 +1,72 @@ +/* + * 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.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Pedro Igor + */ +public class PermissionRequest { + + private String resourceId; + private Set scopes; + private String resourceServerId; + + public PermissionRequest(String resourceId, String... scopes) { + this.resourceId = resourceId; + if (scopes != null) { + this.scopes = new HashSet(Arrays.asList(scopes)); + } + } + + public PermissionRequest() { + this(null, null); + } + + public String getResourceId() { + return resourceId; + } + + @JsonProperty("resource_id") + public void setResourceId(String resourceSetId) { + this.resourceId = resourceSetId; + } + + public Set getScopes() { + return scopes; + } + + @JsonProperty("resource_scopes") + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + @JsonProperty("resource_server_id") + public void setResourceServerId(String resourceServerId) { + this.resourceServerId = resourceServerId; + } + + public String getResourceServerId() { + return resourceServerId; + } +} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionResponse.java similarity index 79% rename from authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java rename to core/src/main/java/org/keycloak/representations/idm/authorization/PermissionResponse.java index 1002bb59c3..2d2d7bbeea 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionResponse.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, @@ -15,7 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.authorization.client.representation; + +package org.keycloak.representations.idm.authorization; /** * @author Pedro Igor 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 0000000000..2a3e020b2b --- /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 Pedro Igor + */ +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/PermissionTicketToken.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java new file mode 100644 index 0000000000..ff4a927a3a --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java @@ -0,0 +1,86 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.TokenIdGenerator; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.JsonWebToken; + +/** + * @author Pedro Igor + */ +public class PermissionTicketToken extends JsonWebToken { + + private final List resources; + + public PermissionTicketToken() { + this(new ArrayList()); + } + + public PermissionTicketToken(List resources, String audience, AccessToken accessToken) { + if (accessToken != null) { + id(TokenIdGenerator.generateId()); + subject(accessToken.getSubject()); + expiration(accessToken.getExpiration()); + notBefore(accessToken.getNotBefore()); + issuedAt(accessToken.getIssuedAt()); + issuedFor(accessToken.getIssuedFor()); + } + if (audience != null) { + audience(audience); + } + this.resources = resources; + } + + public PermissionTicketToken(List resources) { + this(resources, null, null); + } + + public List getResources() { + return this.resources; + } + + public static class ResourcePermission { + + @JsonProperty("id") + private String resourceId; + + @JsonProperty("scopes") + private Set scopes; + + public ResourcePermission() { + } + + public ResourcePermission(String resourceId, Set scopes) { + this.resourceId = resourceId; + this.scopes = scopes; + } + + public String getResourceId() { + return resourceId; + } + + public Set getScopes() { + return scopes; + } + } +} 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 c058b9d112..4188ab3fe5 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 acbd2f2430..ae876f0347 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 policies; private List 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 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 3a1f2525f9..e6445e4abc 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 policies; private List 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/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp b/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp index 0aea6b0452..c511b2da9d 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()) { %>

  • -

    Resource: <%= permission.getResourceSetName() %>

    -

    ID: <%= permission.getResourceSetId() %>

    +

    Resource: <%= permission.getResourceName() %>

    +

    ID: <%= permission.getResourceId() %>

  • <% } 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 ea37d60e9a..4c4573b0b1 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 Pedro Igor */ @@ -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 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 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 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 b337389c89..4f9b0e5904 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/photoz/photoz-html5-client/src/main/webapp/index.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html index 203b6e2095..158d89f426 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 @@ - - + + -Show Requesting Party Token | Show Access Token | Request Entitlements | Sign Out +Show Requesting Party Token | Show Access Token | Request Entitlements | My Account | Sign Out
    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 e58c5f55d7..b552391ca2 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 9a018e4747..4088f8075c 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 affafdd82c..d9354e380a 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 78c252a85c..fffcdeab9f 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 @@
    Create Album | My Profile
    -
    +

    Your Albums

    You don't have any albums, yet. - - - - - - - - - - + + + +
    Your Albums
    {{p.name}} - [X]
    {{p.name}} - [X]
    +

    Shared With Me

    +You don't have any shares, yet. + + + +
    {{p.album.name}} - [X]Request Delete Access
    \ No newline at end of file diff --git a/examples/authz/photoz/photoz-realm.json b/examples/authz/photoz/photoz-realm.json index 118b9828f1..4a15c389a6 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 056ff05f79..b49ba90123 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(); } @@ -88,6 +94,29 @@ public class AlbumService { return Response.ok(this.entityManager.createQuery("from Album where userId = :id").setParameter("id", request.getUserPrincipal().getName()).getResultList()).build(); } + @GET + @Path("/shares") + @Produces("application/json") + public Response findShares() { + List permissions = getAuthzClient().protection().permission().find(null, null, null, getKeycloakSecurityContext().getToken().getSubject(), true, true, null, null); + Map 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") @@ -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); - getAuthzClient().protection().resource().create(albumResource); + ResourceRepresentation response = 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 search = protection.resource().findByFilter("uri=" + uri); + List 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 0000000000..dfc5fb1281 --- /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 scopes; + + public SharedAlbum(Album album) { + this.album = album; + } + + public Album getAlbum() { + return album; + } + + public List 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 990595e58a..d8dda5fe21 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 28b87bc579..d94ce40f59 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 @@ -2,13 +2,23 @@ "allowRemoteResourceManagement": true, "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,28 +28,45 @@ "type": "http://photoz.com/album", "scopes": [ { - "name": "urn:photoz.com:scopes:album:view" + "name": "album:delete" }, { - "name": "urn:photoz.com:scopes: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", @@ -66,16 +93,6 @@ "roles": "[{\"id\":\"admin\",\"required\":true}]" } }, - { - "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", @@ -87,45 +104,13 @@ } }, { - "name": "Administration Policy", - "description": "Defines that only administrators from a specific network address can do something.", - "type": "aggregate", + "name": "Any User Policy", + "description": "Defines that only users from well known clients are allowed to access", + "type": "role", "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", - "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,8 +121,20 @@ "decisionStrategy": "UNANIMOUS", "config": { "defaultResourceType": "http://photoz.com/admin", - "default": "true", - "applyPolicies": "[\"Administration Policy\"]" + "applyPolicies": "[\"Administration Policy\"]", + "default": "true" + } + }, + { + "name": "Album Resource Permission", + "description": "A default permission that defines access for any album resource", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Album Resource\"]", + "scopes": "[\"album:view\",\"album:delete\"]", + "applyPolicies": "[\"Only Owner and Administrators Policy\"]" } }, { @@ -147,37 +144,9 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "applyPolicies": "[\"Only From @keycloak.org or Admin\"]", - "scopes": "[\"urn:photoz.com:scopes:profile:view\"]" + "scopes": "[\"profile:view\"]", + "applyPolicies": "[\"Only From @keycloak.org or Admin\"]" } - }, - { - "name": "Delete Album Permission", - "description": "A policy that only allows the owner to delete his albums.", - "type": "scope", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "config": { - "applyPolicies": "[\"Only Owner and Administrators Policy\"]", - "scopes": "[\"urn:photoz.com:scopes:album:delete\"]" - } - } - ], - "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 9e06730fe3..774845052b 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,35 +8,28 @@ "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/WEB-INF/keycloak.json b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json index 7983fa39f1..d2834c3ce8 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/examples/authz/servlet-authz/src/main/webapp/index.jsp b/examples/authz/servlet-authz/src/main/webapp/index.jsp index 3fbfca269c..345a69dffc 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()) { %>
  • -

    Resource: <%= permission.getResourceSetName() %>

    -

    ID: <%= permission.getResourceSetId() %>

    +

    Resource: <%= permission.getResourceName() %>

    +

    ID: <%= permission.getResourceId() %>

    Scopes: <%= permission.getScopes() %>

  • <% 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 364d8877ab..21ef2edebf 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; %> -

    Click here Click here ">Sign Out

    \ No newline at end of file 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 dd623775f7..7e6c3c7342 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 @@ -141,6 +141,18 @@ public class RealmAdapter implements CachedRealmModel { updated.setEnabled(enabled); } + @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(); 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 0000000000..d6a7e0758e --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class PermissionTicketAdapter implements PermissionTicket, CachedModel { + + 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 7660c96d1e..ae96113e94 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 { @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 38b68601aa..d310fcaeda 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 { 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 { } @Override - public void updateScopes(Set 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 scopes) { + Resource updated = getDelegateForUpdate(); + + for (Scope scope : updated.getScopes()) { + if (!scopes.contains(scope)) { + PermissionTicketStore permissionStore = cacheSession.getPermissionTicketStoreDelegate(); + List 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 d90b27a19f..a4492a7b71 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 @@ -91,6 +91,18 @@ public class ScopeAdapter implements Scope, CachedModel { } + @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(); 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 3f189a5a40..7b463bec3d 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 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 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 resources, Set resourceTypes, Set scopes, String serverId, Set invalidations) { policyUpdated(id, name, resources, resourceTypes, scopes, serverId, invalidations); } + public void permissionTicketRemoval(String id, String owner, String resource, String scope, String serverId, Set 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 cb5d0606d6..33c1ddc55b 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 managedScopes = new HashMap<>(); protected Map managedResources = new HashMap<>(); protected Map managedPolicies = new HashMap<>(); + protected Map managedPermissionTickets = new HashMap<>(); protected Set invalidations = new HashSet<>(); protected Set 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 getResourceTypes(Set 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 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 findByResourceServer(String resourceServerId) { + return getPermissionTicketStoreDelegate().findByResourceServer(resourceServerId); + } + + @Override + public List 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 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 find(Map attributes, String resourceServerId, int firstResult, int maxResult) { + return getPermissionTicketStoreDelegate().find(attributes, resourceServerId, firstResult, maxResult); + } + + @Override + public List 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 List cacheQuery(String cacheKey, Class queryType, Supplier> resultSupplier, BiFunction, 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 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/authorization/entities/CachedPermissionTicket.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedPermissionTicket.java new file mode 100644 index 0000000000..a906a7d465 --- /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 Pedro Igor + */ +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 f15388324a..383ab1c76a 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 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 7bc31edaae..e879134d23 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 0000000000..d8c397cba1 --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class PermissionTicketListQuery extends AbstractRevisioned implements PermissionTicketQuery { + + private final Set 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 permissions, String serverId) { + super(revision, id); + this.serverId = serverId; + this.permissions = permissions; + } + + @Override + public String getResourceServerId() { + return serverId; + } + + public Set getPermissions() { + return permissions; + } + + @Override + public boolean isInvalid(Set invalidations) { + return invalidations.contains(getId()) || invalidations.contains(getResourceServerId()); + } +} \ No newline at end of file diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementResponse.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketQuery.java similarity index 52% rename from authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementResponse.java rename to model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketQuery.java index 747c144db8..fb345c7aa4 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementResponse.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketQuery.java @@ -1,13 +1,12 @@ /* - * JBoss, Home of Professional Open Source - * - * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * 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 + * 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, @@ -15,28 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.authorization.client.representation; +package org.keycloak.models.cache.infinispan.authorization.entities; + +import java.util.Set; + +import org.keycloak.models.cache.infinispan.entities.Revisioned; /** * @author Pedro Igor */ -public class EntitlementResponse { +public interface PermissionTicketQuery extends InResourceServer, Revisioned { - private String rpt; - - public EntitlementResponse(String rpt) { - this.rpt = rpt; - } - - public EntitlementResponse() { - this(null); - } - - public String getRpt() { - return this.rpt; - } - - public void setRpt(final String rpt) { - this.rpt = rpt; - } + Set getPermissions(); + boolean isInvalid(Set invalidations); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketResourceListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketResourceListQuery.java new file mode 100755 index 0000000000..3165854119 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketResourceListQuery.java @@ -0,0 +1,42 @@ +/* + * 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 java.util.Set; + +/** + * @author Pedro Igor + */ +public class PermissionTicketResourceListQuery extends PermissionTicketListQuery implements InResource { + + private final String resourceId; + + public PermissionTicketResourceListQuery(Long revision, String id, String resourceId, Set permissions, String serverId) { + super(revision, id, permissions, serverId); + this.resourceId = resourceId; + } + + @Override + public boolean isInvalid(Set invalidations) { + return super.isInvalid(invalidations) || invalidations.contains(getResourceId()); + } + + @Override + public String getResourceId() { + return resourceId; + } +} \ No newline at end of file diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketScopeListQuery.java old mode 100644 new mode 100755 similarity index 52% rename from authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java rename to model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketScopeListQuery.java index ca45017410..ff3743eeb0 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/PermissionTicketScopeListQuery.java @@ -14,35 +14,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.authorization.client.representation; +package org.keycloak.models.cache.infinispan.authorization.entities; -import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Set; /** * @author Pedro Igor */ -public class AuthorizationRequestMetadata { +public class PermissionTicketScopeListQuery extends PermissionTicketListQuery implements InScope { - public static final String INCLUDE_RESOURCE_NAME = "include_resource_name"; + private final String scopeId; - @JsonProperty(INCLUDE_RESOURCE_NAME) - private boolean includeResourceName = true; - - private int limit; - - public boolean isIncludeResourceName() { - return includeResourceName; + public PermissionTicketScopeListQuery(Long revision, String id, String scopeId, Set permissions, String serverId) { + super(revision, id, permissions, serverId); + this.scopeId = scopeId; } - public void setIncludeResourceName(boolean includeResourceName) { - this.includeResourceName = includeResourceName; + @Override + public boolean isInvalid(Set invalidations) { + return super.isInvalid(invalidations) || invalidations.contains(getScopeId()); } - public void setLimit(int limit) { - this.limit = limit; + @Override + public String getScopeId() { + return scopeId; } - - public int getLimit() { - return limit; - } -} +} \ 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 0000000000..bbef9799d6 --- /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 Marek Posolda + */ +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 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 0000000000..1d830edd0c --- /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 Marek Posolda + */ +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 invalidations) { + cache.permissionTicketUpdated(id, owner, resource, scope, serverId, invalidations); + } +} 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 e8540021d7..f187e9c0ee 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 adminEnabledEventOperations = new HashSet(); protected boolean adminEventsDetailsEnabled; protected List defaultRoles; + private boolean allowUserManagedAccess; public Set 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 getAttributes() { return attributes; } + + public boolean isAllowUserManagedAccess() { + return allowUserManagedAccess; + } } 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 0000000000..c607bece68 --- /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 Pedro Igor + */ +@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 d91be733c1..6031a6e92a 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 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 5d86d7bc37..9d9d7d88b8 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 0000000000..ee51d5e124 --- /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 Pedro Igor + */ +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 findByResourceServer(final String resourceServerId) { + TypedQuery query = entityManager.createNamedQuery("findPolicyIdByServerId", String.class); + + query.setParameter("serverId", resourceServerId); + + List result = query.getResultList(); + List list = new LinkedList<>(); + for (String id : result) { + list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId)); + } + return list; + } + + @Override + public List findByResource(final String resourceId, String resourceServerId) { + TypedQuery query = entityManager.createNamedQuery("findPermissionIdByResource", String.class); + + query.setFlushMode(FlushModeType.COMMIT); + query.setParameter("resourceId", resourceId); + query.setParameter("serverId", resourceServerId); + + List result = query.getResultList(); + List list = new LinkedList<>(); + for (String id : result) { + list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId)); + } + return list; + } + + @Override + public List findByScope(String scopeId, String resourceServerId) { + if (scopeId==null) { + return Collections.emptyList(); + } + + // Use separate subquery to handle DB2 and MSSSQL + TypedQuery query = entityManager.createNamedQuery("findPermissionIdByScope", String.class); + + query.setFlushMode(FlushModeType.COMMIT); + query.setParameter("scopeId", scopeId); + query.setParameter("serverId", resourceServerId); + + List result = query.getResultList(); + List list = new LinkedList<>(); + for (String id : result) { + list.add(provider.getStoreFactory().getPermissionTicketStore().findById(id, resourceServerId)); + } + return list; + } + + @Override + public List find(Map attributes, String resourceServerId, int firstResult, int maxResult) { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery querybuilder = builder.createQuery(PermissionTicketEntity.class); + Root root = querybuilder.from(PermissionTicketEntity.class); + + querybuilder.select(root.get("id")); + + List 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 result = query.getResultList(); + List list = new LinkedList<>(); + PermissionTicketStore ticket = provider.getStoreFactory().getPermissionTicketStore(); + + for (String id : result) { + list.add(ticket.findById(id, resourceServerId)); + } + + return list; + } + + @Override + public List findByOwner(String owner, String resourceServerId) { + TypedQuery query = entityManager.createNamedQuery("findPolicyIdByType", String.class); + + query.setFlushMode(FlushModeType.COMMIT); + query.setParameter("serverId", resourceServerId); + query.setParameter("owner", owner); + + List result = query.getResultList(); + List 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 5e79badca0..308b073111 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 query = entityManager.createNamedQuery("findPermissionTicketIdByServerId", String.class); + + query.setParameter("serverId", id); + + List 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 result = query.getResultList(); - List 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 de83c427bb..c938afe239 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 findByOwner(String ownerId, String resourceServerId) { - TypedQuery query = entityManager.createNamedQuery("findResourceIdByOwner", String.class); + String queryName = "findResourceIdByOwner"; + + if (resourceServerId == null) { + queryName = "findAnyResourceIdByOwner"; + } + + TypedQuery 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 result = query.getResultList(); List list = new LinkedList<>(); @@ -161,13 +170,17 @@ public class JPAResourceStore implements ResourceStore { querybuilder.select(root.get("id")); List 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 855f66a53a..cd08f0c8e6 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 @@ -64,6 +67,11 @@ public class JPAStoreFactory implements StoreFactory { return scopeStore; } + @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 0000000000..e1c56a2593 --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class PermissionTicketAdapter implements PermissionTicket, JpaModel { + + 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 9ce0de200e..782f084fc0 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 @@ -62,6 +62,16 @@ public class ResourceAdapter implements Resource, JpaModel { return entity.getName(); } + @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); @@ -121,6 +131,16 @@ public class ResourceAdapter implements Resource, JpaModel { return entity.getOwner(); } + @Override + public boolean isOwnerManagedAccess() { + return entity.isOwnerManagedAccess(); + } + + @Override + public void setOwnerManagedAccess(boolean ownerManagedAccess) { + entity.setOwnerManagedAccess(ownerManagedAccess); + } + @Override public void updateScopes(Set toUpdate) { Set ids = new HashSet<>(); 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 f77310e195..1a83d27980 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 @@ -60,6 +60,16 @@ public class ScopeAdapter implements Scope, JpaModel { } + @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/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 26bfc2415c..e737a25fb4 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 @@ -114,6 +114,17 @@ public class RealmAdapter implements RealmModel, JpaModel { em.flush(); } + @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/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index fe6ee14961..4f6244f0d3 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/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 0000000000..aa3d17e3d1 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.CR1.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 fa824e2fdd..c9d0e166cd 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 @@ + diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml index 36e3fb40d2..86eda78567 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 @@ org.keycloak.authorization.jpa.entities.ResourceEntity org.keycloak.authorization.jpa.entities.ScopeEntity org.keycloak.authorization.jpa.entities.PolicyEntity + org.keycloak.authorization.jpa.entities.PermissionTicketEntity org.keycloak.storage.jpa.entity.BrokerLinkEntity 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 644b90ac37..81da0786ce 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,159 +124,6 @@ public final class AuthorizationProvider implements Provider { return storeFactoryDelegate; } - private StoreFactory createStoreFactory(StoreFactory storeFactory) { - return new StoreFactory() { - @Override - public ResourceStore getResourceStore() { - return storeFactory.getResourceStore(); - } - - @Override - public ResourceServerStore getResourceServerStore() { - return storeFactory.getResourceServerStore(); - } - - @Override - public ScopeStore getScopeStore() { - return storeFactory.getScopeStore(); - } - - @Override - public PolicyStore getPolicyStore() { - PolicyStore policyStore = storeFactory.getPolicyStore(); - return new PolicyStore() { - @Override - public Policy create(AbstractPolicyRepresentation representation, ResourceServer resourceServer) { - Set 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())); - } - - Set scopes = representation.getScopes(); - - if (scopes != null) { - representation.setScopes(scopes.stream().map(id -> { - Scope scope = getScopeStore().findById(id, resourceServer.getId()); - - if (scope == null) { - scope = getScopeStore().findByName(id, resourceServer.getId()); - } - - if (scope == null) { - throw new RuntimeException("Scope [" + id + "] does not exist"); - } - - return scope.getId(); - }).collect(Collectors.toSet())); - } - - - Set policies = representation.getPolicies(); - - if (policies != null) { - representation.setPolicies(policies.stream().map(id -> { - Policy policy = getPolicyStore().findById(id, resourceServer.getId()); - - if (policy == null) { - policy = getPolicyStore().findByName(id, resourceServer.getId()); - } - - if (policy == null) { - throw new RuntimeException("Policy [" + id + "] does not exist"); - } - - return policy.getId(); - }).collect(Collectors.toSet())); - } - - return RepresentationToModel.toModel(representation, AuthorizationProvider.this, policyStore.create(representation, resourceServer)); - } - - @Override - public void delete(String id) { - Policy policy = findById(id, null); - - if (policy != null) { - ResourceServer resourceServer = policy.getResourceServer(); - - findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> { - dependentPolicy.removeAssociatedPolicy(policy); - if (dependentPolicy.getAssociatedPolicies().isEmpty()) { - delete(dependentPolicy.getId()); - } - }); - - policyStore.delete(id); - } - } - - @Override - public Policy findById(String id, String resourceServerId) { - return policyStore.findById(id, resourceServerId); - } - - @Override - public Policy findByName(String name, String resourceServerId) { - return policyStore.findByName(name, resourceServerId); - } - - @Override - public List findByResourceServer(String resourceServerId) { - return policyStore.findByResourceServer(resourceServerId); - } - - @Override - public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { - return policyStore.findByResourceServer(attributes, resourceServerId, firstResult, maxResult); - } - - @Override - public List findByResource(String resourceId, String resourceServerId) { - return policyStore.findByResource(resourceId, resourceServerId); - } - - @Override - public List findByResourceType(String resourceType, String resourceServerId) { - return policyStore.findByResourceType(resourceType, resourceServerId); - } - - @Override - public List findByScopeIds(List scopeIds, String resourceServerId) { - return policyStore.findByScopeIds(scopeIds, resourceServerId); - } - - @Override - public List findByType(String type, String resourceServerId) { - return policyStore.findByType(type, resourceServerId); - } - - @Override - public List findDependentPolicies(String id, String resourceServerId) { - return policyStore.findDependentPolicies(id, resourceServerId); - } - }; - } - - @Override - public void close() { - storeFactory.close(); - } - }; - } - /** * Returns the registered {@link PolicyProviderFactory}. * @@ -324,4 +173,291 @@ public final class AuthorizationProvider implements Provider { public void close() { } + + private StoreFactory createStoreFactory(StoreFactory storeFactory) { + return new StoreFactory() { + + ResourceStore resourceStore; + ScopeStore scopeStore; + PolicyStore policyStore; + + @Override + public ResourceStore getResourceStore() { + if (resourceStore == null) { + resourceStore = createResourceStoreWrapper(storeFactory); + } + return resourceStore; + } + + @Override + public ResourceServerStore getResourceServerStore() { + return storeFactory.getResourceServerStore(); + } + + @Override + public ScopeStore getScopeStore() { + if (scopeStore == null) { + scopeStore = createScopeWrapper(storeFactory); + } + return scopeStore; + } + + @Override + public PolicyStore getPolicyStore() { + 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 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 findByResourceServer(String id) { + return delegate.findByResourceServer(id); + } + + @Override + public List findByResourceServer(Map 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 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"); + } + + return resource.getId(); + }).collect(Collectors.toSet())); + } + + Set scopes = representation.getScopes(); + + if (scopes != null) { + representation.setScopes(scopes.stream().map(id -> { + Scope scope = storeFactory.getScopeStore().findById(id, resourceServer.getId()); + + if (scope == null) { + scope = storeFactory.getScopeStore().findByName(id, resourceServer.getId()); + } + + if (scope == null) { + throw new RuntimeException("Scope [" + id + "] does not exist"); + } + + return scope.getId(); + }).collect(Collectors.toSet())); + } + + + Set policies = representation.getPolicies(); + + if (policies != null) { + representation.setPolicies(policies.stream().map(id -> { + Policy policy = storeFactory.getPolicyStore().findById(id, resourceServer.getId()); + + if (policy == null) { + policy = storeFactory.getPolicyStore().findByName(id, resourceServer.getId()); + } + + if (policy == null) { + throw new RuntimeException("Policy [" + id + "] does not exist"); + } + + return policy.getId(); + }).collect(Collectors.toSet())); + } + + return RepresentationToModel.toModel(representation, AuthorizationProvider.this, policyStore.create(representation, resourceServer)); + } + + @Override + public void delete(String id) { + Policy policy = findById(id, null); + + if (policy != null) { + ResourceServer resourceServer = policy.getResourceServer(); + + findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> { + dependentPolicy.removeAssociatedPolicy(policy); + if (dependentPolicy.getAssociatedPolicies().isEmpty()) { + delete(dependentPolicy.getId()); + } + }); + + policyStore.delete(id); + } + } + + @Override + public Policy findById(String id, String resourceServerId) { + return policyStore.findById(id, resourceServerId); + } + + @Override + public Policy findByName(String name, String resourceServerId) { + return policyStore.findByName(name, resourceServerId); + } + + @Override + public List findByResourceServer(String resourceServerId) { + return policyStore.findByResourceServer(resourceServerId); + } + + @Override + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + return policyStore.findByResourceServer(attributes, resourceServerId, firstResult, maxResult); + } + + @Override + public List findByResource(String resourceId, String resourceServerId) { + return policyStore.findByResource(resourceId, resourceServerId); + } + + @Override + public List findByResourceType(String resourceType, String resourceServerId) { + return policyStore.findByResourceType(resourceType, resourceServerId); + } + + @Override + public List findByScopeIds(List scopeIds, String resourceServerId) { + return policyStore.findByScopeIds(scopeIds, resourceServerId); + } + + @Override + public List findByType(String type, String resourceServerId) { + return policyStore.findByType(type, resourceServerId); + } + + @Override + public List findDependentPolicies(String id, String resourceServerId) { + return policyStore.findDependentPolicies(id, resourceServerId); + } + }; + } + + private ResourceStore createResourceStoreWrapper(StoreFactory storeFactory) { + return new ResourceStore() { + ResourceStore delegate = storeFactory.getResourceStore(); + + @Override + public Resource create(String name, ResourceServer resourceServer, String owner) { + return delegate.create(name, resourceServer, owner); + } + + @Override + public void delete(String id) { + Resource resource = findById(id, null); + PermissionTicketStore ticketStore = storeFactory.getPermissionTicketStore(); + List permissions = ticketStore.findByResource(id, resource.getResourceServer().getId()); + + for (PermissionTicket permission : permissions) { + ticketStore.delete(permission.getId()); + } + + delegate.delete(id); + } + + @Override + public Resource findById(String id, String resourceServerId) { + return delegate.findById(id, resourceServerId); + } + + @Override + public List findByOwner(String ownerId, String resourceServerId) { + return delegate.findByOwner(ownerId, resourceServerId); + } + + @Override + public List findByUri(String uri, String resourceServerId) { + return delegate.findByUri(uri, resourceServerId); + } + + @Override + public List findByResourceServer(String resourceServerId) { + return delegate.findByResourceServer(resourceServerId); + } + + @Override + public List findByResourceServer(Map attributes, String resourceServerId, int firstResult, int maxResult) { + return delegate.findByResourceServer(attributes, resourceServerId, firstResult, maxResult); + } + + @Override + public List findByScope(List id, String resourceServerId) { + return delegate.findByScope(id, resourceServerId); + } + + @Override + public Resource findByName(String name, String resourceServerId) { + return delegate.findByName(name, resourceServerId); + } + + @Override + public List 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 0000000000..39366d6de5 --- /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 Pedro Igor + */ +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 4c2521ccd7..cdfc0b6e12 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 @@ -49,6 +49,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. * @@ -112,5 +126,8 @@ public interface Resource { */ String getOwner(); + boolean isOwnerManagedAccess(); + void setOwnerManagedAccess(boolean ownerManagedAccess); + void updateScopes(Set 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 e13a789d8a..fd90eca207 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 @@ -47,6 +47,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. * 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 f2da3a5cc1..c43acacb7d 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 587856f35a..ae0d7fd581 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 evaluate(D decision); List 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 2ffc049ee4..bfba2c64d3 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 results); + protected void onDeny(Result result) { + result.setStatus(Effect.DENY); + } + private boolean isGranted(Result.PolicyResult policyResult) { List 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 c720504712..9b708c54ae 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 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 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 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 0000000000..708f7e1c5d --- /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 Pedro Igor + */ +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 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 filters = new HashMap<>(); + + filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.REQUESTER, identity.getId()); + filters.put(PermissionTicket.GRANTED, Boolean.TRUE.toString()); + + List permissions = authorization.getStoreFactory().getPermissionTicketStore().find(filters, resource.getResourceServer().getId(), -1, -1); + + if (!permissions.isEmpty()) { + List 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 scopes = permission.getScopes(); + + if (scopes.isEmpty()) { + scopes = resource.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()); + } + + if (scopes.isEmpty()) { + Map 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 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 filters = new HashMap<>(); + + filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.REQUESTER, identity.getId()); + filters.put(PermissionTicket.SCOPE, scope.getId()); + + List 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 results) { + this.results = results; + } + + public List 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 0000000000..654d68b727 --- /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 Pedro Igor + */ +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 id + * + * @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 resourceServerId. + * + * @param resourceServerId the identifier of a resource server + * @return a list of permissions belonging to the given resource server + */ + List findByResourceServer(String resourceServerId); + + /** + * Returns a list of {@link PermissionTicket} associated with the given owner. + * + * @param owner the identifier of a resource server + * @return a list of permissions belonging to the given owner + */ + List findByOwner(String owner, String resourceServerId); + + /** + * Returns a list of {@link PermissionTicket} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given resourceId. + * + * @param resourceId the identifier of a resource + * @param resourceServerId the resource server id + * @return a list of permissions associated with the given resource + */ + List findByResource(String resourceId, String resourceServerId); + + /** + * Returns a list of {@link PermissionTicket} associated with a {@link org.keycloak.authorization.core.model.Scope} with the given scopeId. + * + * @param scopeId the id of the scopes + * @param resourceServerId the resource server id + * @return a list of permissions associated with the given scopes + */ + List findByScope(String scopeId, String resourceServerId); + + List find(Map 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 4f50c11c49..a1123a94ac 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 b48e2433a7..29b38bcbae 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 f9b483505c..6c01f79aba 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 a61c0a941b..1568f18d2e 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 2e7d3ef92d..56c95e5b82 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 98ccf38dba..c738b818ab 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 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/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index 5eb18db8fe..c316c47617 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/services/src/main/java/org/keycloak/authorization/AuthorizationService.java b/services/src/main/java/org/keycloak/authorization/AuthorizationService.java index f519b40727..2d0d7d70b7 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 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; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.authorization.protection.ProtectionService; + /** * @author Pedro Igor */ @@ -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/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java index d1acce0c3a..3ba29ddbd9 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/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java index f4d685c9cd..ff294c975e 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 Pedro Igor */ @@ -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) resource1 -> { + ResourceRepresentation representation = new ResourceRepresentation(); + + representation.setId(resource1.getId()); + + return representation; + }); + } + + public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource, Function toRepresentation) { + Response response = create(resource, toRepresentation); audit(uriInfo, resource, resource.getId(), OperationType.CREATE); return response; } public Response create(ResourceRepresentation resource) { + return create(resource, (Function) resource1 -> { + ResourceRepresentation representation = new ResourceRepresentation(); + + representation.setId(resource1.getId()); + + return representation; + }); + } + + public Response create(ResourceRepresentation resource, Function 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 -> toRepresentation(resource, resourceServer, authorization, true)); + } + + public Response findById(@PathParam("id") String id, Function 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, 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 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 1ab354663b..294951cc70 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 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/admin/representation/PolicyEvaluationResponseBuilder.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java index 17002ab82e..b5023b56fd 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 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/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java index c86dd45a3d..a8075a09cb 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 Pedro Igor */ public class AuthorizationTokenService { - protected static final Logger logger = Logger.getLogger(AuthorizationTokenService.class); + private static final Logger logger = Logger.getLogger(AuthorizationTokenService.class); + private static Map> SUPPORTED_CLAIM_TOKEN_FORMATS; + + 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(); + + if (claimToken == null) { + claimToken = authorizationRequest.getAccessToken(); + } + + 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); + } + }); + } + + private final TokenManager tokenManager; + private final EventBuilder event; + private final HttpRequest httpRequest; private final AuthorizationProvider authorization; + private final Cors cors; - @Context - private HttpRequest httpRequest; - - @Context - private KeycloakSession session; - - public AuthorizationTokenService(AuthorizationProvider authorization) { + 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; } - @OPTIONS - public Response authorizepPreFlight() { - return Cors.add(this.httpRequest, Response.ok()).auth().preflight().build(); - } - - @POST - @Consumes("application/json") - @Produces("application/json") - public Response authorize(AuthorizationRequest authorizationRequest) { - KeycloakEvaluationContext evaluationContext = new KeycloakEvaluationContext(this.authorization.getKeycloakSession()); - KeycloakIdentity identity = (KeycloakIdentity) evaluationContext.getIdentity(); - - if (!identity.hasRealmRole("uma_authorization")) { - throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_authorization scope.", Status.FORBIDDEN); - } - - 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()); + PermissionTicketToken ticket = getPermissionTicket(request); + ResourceServer resourceServer = getResourceServer(ticket); + KeycloakEvaluationContext evaluationContext = createEvaluationContext(request); + KeycloakIdentity identity = KeycloakIdentity.class.cast(evaluationContext.getIdentity()); + List evaluation; - if (resourceServer == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN); + 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 = authorization.evaluators().from(createPermissions(ticket, authorizationRequest, authorization), evaluationContext).evaluate(); + List permissions = Permissions.permits(evaluation, request.getMetadata(), authorization, resourceServer); - List entitlements = Permissions.permits(result, authorizationRequest.getMetadata(), authorization, resourceServer); - - 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(); + 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); + } } + + ClientModel targetClient = this.authorization.getRealm().getClientById(resourceServer.getId()); + AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(identity, permissions, targetClient), request.getRpt() != null); + + 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); } - - HashMap error = new HashMap<>(); - - error.put(OAuth2Constants.ERROR, "not_authorized"); - - return Cors.add(httpRequest, Response.status(Status.FORBIDDEN) - .entity(error)) - .allowedOrigins(identity.getAccessToken()) - .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); } - private List createPermissions(PermissionTicket ticket, AuthorizationRequest request, AuthorizationProvider authorization) { - StoreFactory storeFactory = authorization.getStoreFactory(); - ResourceServer resourceServer = authorization.getStoreFactory().getResourceServerStore().findById(ticket.getResourceServerId()); + private List evaluatePermissions(AuthorizationRequest authorizationRequest, PermissionTicketToken ticket, ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) { + return authorization.evaluators() + .from(createPermissions(ticket, authorizationRequest, resourceServer, authorization), evaluationContext) + .evaluate(); + } - if (resourceServer == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN); + private List 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 evaluateAllPermissions(ResourceServer resourceServer, KeycloakEvaluationContext evaluationContext, KeycloakIdentity identity) { + return authorization.evaluators() + .from(Permissions.all(resourceServer, identity, authorization), evaluationContext) + .evaluate(); + } + + private AccessTokenResponse createRequestingPartyToken(KeycloakIdentity identity, List 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()); } - Map> permissionsToEvaluate = new HashMap<>(); + return responseBuilder.build(); + } - ticket.getResources().forEach(requestedResource -> { - Resource resource; + 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); + } - if (requestedResource.getId() != null) { - resource = storeFactory.getResourceStore().findById(requestedResource.getId(), ticket.getResourceServerId()); - } else { - resource = storeFactory.getResourceStore().findByName(requestedResource.getName(), ticket.getResourceServerId()); + // 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 ResourceServer getResourceServer(PermissionTicketToken ticket) { + StoreFactory storeFactory = authorization.getStoreFactory(); + 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 CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.BAD_REQUEST); + } + + return resourceServer; + } + + private KeycloakEvaluationContext createEvaluationContext(AuthorizationRequest authorizationRequest) { + String claimTokenFormat = authorizationRequest.getClaimTokenFormat(); + + if (claimTokenFormat == null) { + claimTokenFormat = "urn:ietf:params:oauth:token-type:jwt"; + } + + BiFunction 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 createPermissions(PermissionTicketToken ticket, AuthorizationRequest request, ResourceServer resourceServer, AuthorizationProvider authorization) { + StoreFactory storeFactory = authorization.getStoreFactory(); + Map> 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 requestedScopes = requestedResource.getScopes(); + + if (requestedResource.getScopes() == null) { + requestedScopes = new HashSet<>(); } - Set requestedScopes = requestedResource.getScopes(); - Set 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 resources = new ArrayList(); + if (requestedResource.getResourceId() != null) { + existingResource = resourceStore.findById(requestedResource.getResourceId(), resourceServer.getId()); - resources.addAll(resourceStore.findByScope(requestedScopes.stream().map(scopeRepresentation -> { - Scope scope = scopeStore.findByName(scopeRepresentation.getName(), ticket.getResourceServerId()); + if (existingResource == null) { + existingResource = resourceStore.findByName(requestedResource.getResourceId(), resourceServer.getId()); + } + } - if (scope == null) { - return null; + 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(); + + if (clientAdditionalScopes != null) { + requestedScopes.addAll(Arrays.asList(clientAdditionalScopes.split(" "))); + } + + if (existingResource != null) { + Set scopes = permissionsToEvaluate.get(existingResource.getId()); + + 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())); - - for (Resource resource1 : resources) { - permissionsToEvaluate.put(resource1.getId(), collect); } - permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", collect); + scopes.addAll(requestedScopes); + } else { + List resources = resourceStore.findByScope(new ArrayList<>(requestedScopes), ticket.getAudience()[0]); + + for (Resource resource : resources) { + permissionsToEvaluate.put(resource.getId(), requestedScopes); + if (limit != null) { + limit--; + } + } + + 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 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 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 scopePermission = permission.getScopes(); @@ -229,63 +386,52 @@ public class AuthorizationTokenService { scopes.addAll(scopePermission); } } - }); + } } } } } + ScopeStore scopeStore = storeFactory.getScopeStore(); + return permissionsToEvaluate.entrySet().stream() .flatMap((Function>, Stream>) entry -> { String key = entry.getKey(); - if ("$KC_SCOPE_PERMISSION".equals(key)) { - ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore(); List 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 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/authorization/representation/AuthorizationRequest.java b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java deleted file mode 100644 index 2faf12fe41..0000000000 --- a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.authorization.representation; - -/** - * @author Pedro Igor - */ -public class AuthorizationRequest { - - private AuthorizationRequestMetadata metadata; - private String ticket; - private String rpt; - - public AuthorizationRequest(String ticket, String rpt) { - this.ticket = ticket; - this.rpt = rpt; - } - - public AuthorizationRequest() { - this(null, null); - } - - public String getTicket() { - return this.ticket; - } - - public String getRpt() { - return this.rpt; - } - - public AuthorizationRequestMetadata getMetadata() { - return metadata; - } -} diff --git a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java deleted file mode 100644 index faa90ce2df..0000000000 --- a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.authorization.representation; - -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.sun.org.apache.xpath.internal.operations.Bool; - -/** - * @author Pedro Igor - */ -public class AuthorizationRequestMetadata { - - public static final String INCLUDE_RESOURCE_NAME = "include_resource_name"; - - @JsonProperty(INCLUDE_RESOURCE_NAME) - private boolean includeResourceName = true; - - private int limit; - - public boolean isIncludeResourceName() { - return includeResourceName; - } - - public void setIncludeResourceName(boolean includeResourceName) { - this.includeResourceName = includeResourceName; - } - - public int getLimit() { - return limit; - } - - public void setLimit(int limit) { - this.limit = limit; - } -} diff --git a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java deleted file mode 100644 index cd0a521eb9..0000000000 --- a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.authorization.representation; - -/** - * @author Pedro Igor - */ -public class AuthorizationResponse { - - private String rpt; - - public AuthorizationResponse(String rpt) { - this.rpt = rpt; - } - - public AuthorizationResponse() { - this(null); - } - - public String getRpt() { - return this.rpt; - } - - public void setRpt(final String rpt) { - this.rpt = rpt; - } -} 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 da3cb0fa9c..047ff5a976 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 Pedro Igor */ @@ -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 59963ba775..654193d0b8 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 Pedro Igor @@ -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> attributes = new HashMap<>(); try { - ObjectNode objectNode = JsonSerialization.createObjectNode(this.accessToken); + ObjectNode objectNode = JsonSerialization.createObjectNode(token); Iterator 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 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 resourceAccess = accessToken.getResourceAccess(); + Map 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/Configuration.java b/services/src/main/java/org/keycloak/authorization/config/Configuration.java deleted file mode 100644 index 6669edc027..0000000000 --- a/services/src/main/java/org/keycloak/authorization/config/Configuration.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * 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 com.fasterxml.jackson.annotation.JsonProperty; - -import java.net.URI; -import java.util.Arrays; -import java.util.List; - -import static org.keycloak.protocol.oidc.OIDCWellKnownProvider.DEFAULT_GRANT_TYPES_SUPPORTED; - -/** - * @author Pedro Igor - */ -public class Configuration { - - private static final String UMA_VERSION = "1.0"; - private static final List DEFAULT_TOKEN_PROFILES = Arrays.asList("bearer"); - - public static final Configuration fromDefault(String authzServerUri, - String realm, - URI authorizationEndpoint, - URI tokenEndpoint, String publicKeyPem) { - Configuration configuration = new Configuration(); - - if (authzServerUri.endsWith("/")) { - authzServerUri = authzServerUri.substring(0, authzServerUri.lastIndexOf("/")); - } - - configuration.setVersion(UMA_VERSION); - configuration.setIssuer(URI.create(authzServerUri)); - configuration.setPatProfiles(DEFAULT_TOKEN_PROFILES); - configuration.setAatProfiles(DEFAULT_TOKEN_PROFILES); - configuration.setRptProfiles(DEFAULT_TOKEN_PROFILES); - configuration.setPatGrantTypes(DEFAULT_GRANT_TYPES_SUPPORTED); - configuration.setAatGrantTypes(DEFAULT_GRANT_TYPES_SUPPORTED); - configuration.setTokenEndpoint(tokenEndpoint); - configuration.setAuthorizationEndpoint(authorizationEndpoint); - configuration.setResourceSetRegistrationEndpoint(URI.create(authzServerUri + "/authz/protection/resource_set")); - configuration.setPermissionRegistrationEndpoint(URI.create(authzServerUri + "/authz/protection/permission")); - configuration.setRptEndpoint(URI.create(authzServerUri + "/authz/authorize")); - configuration.setRealmPublicKey(publicKeyPem); - configuration.setServerUrl(URI.create(authzServerUri)); - configuration.setRealm(realm); - - return configuration; - } - - private String realmPublicKey; - private String version; - private URI issuer; - - @JsonProperty("pat_profiles_supported") - private List patProfiles; - - @JsonProperty("pat_grant_types_supported") - private List patGrantTypes; - - @JsonProperty("aat_profiles_supported") - private List aatProfiles; - - @JsonProperty("aat_grant_types_supported") - private List aatGrantTypes; - - @JsonProperty("rpt_profiles_supported") - private List rptProfiles; - - @JsonProperty("claim_token_profiles_supported") - private List claimTokenProfiles; - - @JsonProperty("dynamic_client_endpoint") - private URI dynamicClientEndpoint; - - @JsonProperty("token_endpoint") - private URI tokenEndpoint; - - @JsonProperty("authorization_endpoint") - private URI authorizationEndpoint; - - @JsonProperty("requesting_party_claims_endpoint") - private URI requestingPartyClaimsEndpoint; - - @JsonProperty("resource_set_registration_endpoint") - private URI resourceSetRegistrationEndpoint; - - @JsonProperty("introspection_endpoint") - private URI introspectionEndpoint; - - @JsonProperty("permission_registration_endpoint") - private URI permissionRegistrationEndpoint; - - @JsonProperty("rpt_endpoint") - private URI rptEndpoint; - - /** - * Non-standard, Keycloak specific configuration options - */ - private String realm; - - private URI serverUrl; - - public String getVersion() { - return this.version; - } - - void setVersion(final String version) { - this.version = version; - } - - public URI getIssuer() { - return this.issuer; - } - - void setIssuer(final URI issuer) { - this.issuer = issuer; - } - - public List getPatProfiles() { - return this.patProfiles; - } - - void setPatProfiles(final List patProfiles) { - this.patProfiles = patProfiles; - } - - public List getPatGrantTypes() { - return this.patGrantTypes; - } - - void setPatGrantTypes(final List patGrantTypes) { - this.patGrantTypes = patGrantTypes; - } - - public List getAatProfiles() { - return this.aatProfiles; - } - - void setAatProfiles(final List aatProfiles) { - this.aatProfiles = aatProfiles; - } - - public List getAatGrantTypes() { - return this.aatGrantTypes; - } - - void setAatGrantTypes(final List aatGrantTypes) { - this.aatGrantTypes = aatGrantTypes; - } - - public List getRptProfiles() { - return this.rptProfiles; - } - - void setRptProfiles(final List rptProfiles) { - this.rptProfiles = rptProfiles; - } - - public List getClaimTokenProfiles() { - return this.claimTokenProfiles; - } - - void setClaimTokenProfiles(final List claimTokenProfiles) { - this.claimTokenProfiles = claimTokenProfiles; - } - - public URI getDynamicClientEndpoint() { - return this.dynamicClientEndpoint; - } - - void setDynamicClientEndpoint(final URI dynamicClientEndpoint) { - this.dynamicClientEndpoint = dynamicClientEndpoint; - } - - public URI getTokenEndpoint() { - return this.tokenEndpoint; - } - - void setTokenEndpoint(final URI tokenEndpoint) { - this.tokenEndpoint = tokenEndpoint; - } - - public URI getAuthorizationEndpoint() { - return this.authorizationEndpoint; - } - - void setAuthorizationEndpoint(final URI authorizationEndpoint) { - this.authorizationEndpoint = authorizationEndpoint; - } - - public URI getRequestingPartyClaimsEndpoint() { - return this.requestingPartyClaimsEndpoint; - } - - void setRequestingPartyClaimsEndpoint(final URI requestingPartyClaimsEndpoint) { - this.requestingPartyClaimsEndpoint = requestingPartyClaimsEndpoint; - } - - public URI getResourceSetRegistrationEndpoint() { - return this.resourceSetRegistrationEndpoint; - } - - void setResourceSetRegistrationEndpoint(final URI resourceSetRegistrationEndpoint) { - this.resourceSetRegistrationEndpoint = resourceSetRegistrationEndpoint; - } - - public URI getIntrospectionEndpoint() { - return this.introspectionEndpoint; - } - - void setIntrospectionEndpoint(final URI introspectionEndpoint) { - this.introspectionEndpoint = introspectionEndpoint; - } - - public URI getPermissionRegistrationEndpoint() { - return this.permissionRegistrationEndpoint; - } - - void setPermissionRegistrationEndpoint(final URI permissionRegistrationEndpoint) { - this.permissionRegistrationEndpoint = permissionRegistrationEndpoint; - } - - public URI getRptEndpoint() { - return this.rptEndpoint; - } - - void setRptEndpoint(final URI rptEndpoint) { - this.rptEndpoint = rptEndpoint; - } - - public String getRealm() { - return this.realm; - } - - public void setRealm(final String realm) { - this.realm = realm; - } - - public URI getServerUrl() { - return this.serverUrl; - } - - public void setServerUrl(URI serverUrl) { - this.serverUrl = serverUrl; - } - - public void setRealmPublicKey(String realmPublicKey) { - this.realmPublicKey = realmPublicKey; - } - - public String getRealmPublicKey() { - return realmPublicKey; - } -} 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 0000000000..67fb296e28 --- /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 Pedro Igor + */ +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 222e754068..7410f459da 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 Pedro Igor */ @@ -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 7776720791..a5acd637cc 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 Pedro Igor */ 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/entitlement/EntitlementService.java b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java deleted file mode 100644 index f8f777e4c2..0000000000 --- a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * 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.entitlement; - -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.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.OPTIONS; -import javax.ws.rs.POST; -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 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.AuthorizationRequestMetadata; -import org.keycloak.authorization.common.KeycloakEvaluationContext; -import org.keycloak.authorization.common.KeycloakIdentity; -import org.keycloak.authorization.entitlement.representation.EntitlementRequest; -import org.keycloak.authorization.entitlement.representation.EntitlementResponse; -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; -import org.keycloak.authorization.protection.permission.representation.PermissionRequest; -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.jose.jws.JWSInput; -import org.keycloak.jose.jws.JWSInputException; -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakContext; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.protocol.oidc.TokenManager; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.idm.authorization.Permission; -import org.keycloak.representations.idm.authorization.ScopeRepresentation; -import org.keycloak.services.ErrorResponseException; -import org.keycloak.services.resources.Cors; - -/** - * @author Pedro Igor - */ -public class EntitlementService { - - protected static final Logger logger = Logger.getLogger(EntitlementService.class); - private final AuthorizationProvider authorization; - - @Context - private HttpRequest request; - - @Context - private KeycloakSession session; - - public EntitlementService(AuthorizationProvider authorization) { - this.authorization = authorization; - } - - @Path("{resource_server_id}") - @OPTIONS - public Response authorizePreFlight(@PathParam("resource_server_id") String resourceServerId) { - return Cors.add(this.request, Response.ok()).auth().preflight().build(); - } - - @Path("{resource_server_id}") - @GET() - @Produces("application/json") - @Consumes("application/json") - public Response getAll(@PathParam("resource_server_id") String resourceServerId) { - KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession()); - - if (resourceServerId == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Requires resource_server_id request parameter.", Status.BAD_REQUEST); - } - - RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm(); - ClientModel client = realm.getClientByClientId(resourceServerId); - - if (client == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Identifier is not associated with any client and resource server.", Status.BAD_REQUEST); - } - - StoreFactory storeFactory = authorization.getStoreFactory(); - ResourceServer resourceServer = storeFactory.getResourceServerStore().findById(client.getId()); - - if (resourceServer == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN); - } - - return evaluate(null, Permissions.all(resourceServer, identity, authorization), identity, resourceServer); - } - - @Path("{resource_server_id}") - @POST - @Consumes("application/json") - @Produces("application/json") - public Response get(@PathParam("resource_server_id") String resourceServerId, EntitlementRequest entitlementRequest) { - KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession()); - - if (entitlementRequest == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid entitlement request.", Status.BAD_REQUEST); - } - - if (resourceServerId == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid resource_server_id.", Status.BAD_REQUEST); - } - - RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm(); - - ClientModel client = realm.getClientByClientId(resourceServerId); - - if (client == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Identifier is not associated with any resource server.", Status.BAD_REQUEST); - } - - StoreFactory storeFactory = authorization.getStoreFactory(); - ResourceServer resourceServer = storeFactory.getResourceServerStore().findById(client.getId()); - - if (resourceServer == null) { - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN); - } - - return evaluate(entitlementRequest.getMetadata(), createPermissions(entitlementRequest, resourceServer, authorization), identity, resourceServer); - } - - private Response evaluate(AuthorizationRequestMetadata metadata, List permissions, KeycloakIdentity identity, ResourceServer resourceServer) { - try { - List result = authorization.evaluators().from(permissions, new KeycloakEvaluationContext(this.authorization.getKeycloakSession())).evaluate(); - List entitlements = Permissions.permits(result, metadata, authorization, resourceServer); - - if (!entitlements.isEmpty()) { - return Cors.add(request, Response.ok().entity(new EntitlementResponse(createRequestingPartyToken(entitlements, identity.getAccessToken(), resourceServer)))).allowedOrigins(identity.getAccessToken()).allowedMethods("GET").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); - } - } catch (Exception cause) { - logger.error("Failed to evaluate permissions", cause); - throw new ErrorResponseException(OAuthErrorException.SERVER_ERROR, "Error while evaluating permissions.", Status.INTERNAL_SERVER_ERROR); - } - - HashMap error = new HashMap<>(); - - error.put(OAuth2Constants.ERROR, "not_authorized"); - - return Cors.add(request, Response.status(Status.FORBIDDEN) - .entity(error)) - .allowedOrigins(identity.getAccessToken()) - .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); - } - - private String createRequestingPartyToken(List permissions, AccessToken accessToken, ResourceServer resourceServer) { - RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm(); - AccessToken.Authorization authorization = new AccessToken.Authorization(); - - authorization.setPermissions(permissions); - accessToken.setAuthorization(authorization); - - ClientModel clientModel = realm.getClientById(resourceServer.getId()); - - if (!accessToken.hasAudience(clientModel.getClientId())) { - accessToken.audience(clientModel.getClientId()); - } - - return new TokenManager().encodeToken(this.authorization.getKeycloakSession(), realm, accessToken); - } - - private List createPermissions(EntitlementRequest entitlementRequest, ResourceServer resourceServer, AuthorizationProvider authorization) { - StoreFactory storeFactory = authorization.getStoreFactory(); - Map> permissionsToEvaluate = new LinkedHashMap<>(); - AuthorizationRequestMetadata metadata = entitlementRequest.getMetadata(); - Integer limit = metadata != null && metadata.getLimit() > 0 ? metadata.getLimit() : null; - - for (PermissionRequest requestedResource : entitlementRequest.getPermissions()) { - if (limit != null && limit <= 0) { - break; - } - - Resource resource = null; - - if (requestedResource.getResourceSetId() != null) { - resource = storeFactory.getResourceStore().findById(requestedResource.getResourceSetId(), resourceServer.getId()); - if (resource == null) { - throw new ErrorResponseException("invalid_resource", "Resource with id [" + requestedResource.getResourceSetId() + "] does not exist.", Status.FORBIDDEN); - } - } else if (requestedResource.getResourceSetName() != null) { - resource = storeFactory.getResourceStore().findByName(requestedResource.getResourceSetName(), resourceServer.getId()); - if (resource == null) { - throw new ErrorResponseException("invalid_resource", "Resource with name [" + requestedResource.getResourceSetName() + "] does not exist.", Status.FORBIDDEN); - } - } - - if (resource == null && (requestedResource.getScopes() == null || requestedResource.getScopes().isEmpty())) { - throw new ErrorResponseException("invalid_request", "You must provide a resource and/or scopes.", Status.FORBIDDEN); - } - - Set requestedScopes = requestedResource.getScopes().stream().map(ScopeRepresentation::new).collect(Collectors.toSet()); - Set scopeNames = requestedScopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet()); - - if (resource != null) { - permissionsToEvaluate.put(resource.getId(), scopeNames); - if (limit != null) { - limit--; - } - } else { - ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore(); - ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore(); - List resources = new ArrayList<>(); - - resources.addAll(resourceStore.findByScope(requestedScopes.stream().map(scopeRepresentation -> { - Scope scope = scopeStore.findByName(scopeRepresentation.getName(), resourceServer.getId()); - - if (scope == null) { - return null; - } - - return scope.getId(); - }).filter(s -> s != null).collect(Collectors.toList()), resourceServer.getId())); - - for (Resource resource1 : resources) { - permissionsToEvaluate.put(resource1.getId(), scopeNames); - if (limit != null) { - limit--; - } - } - - permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", scopeNames); - } - } - - String rpt = entitlementRequest.getRpt(); - - if (rpt != null && !"".equals(rpt)) { - KeycloakContext context = authorization.getKeycloakSession().getContext(); - - if (!Tokens.verifySignature(session, context.getRealm(), rpt)) { - throw new ErrorResponseException("invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN); - } - - AccessToken requestingPartyToken; - - try { - requestingPartyToken = new JWSInput(rpt).readJsonContent(AccessToken.class); - } catch (JWSInputException e) { - throw new ErrorResponseException("invalid_rpt", "Invalid RPT", Status.FORBIDDEN); - } - - if (requestingPartyToken.isActive()) { - AccessToken.Authorization authorizationData = requestingPartyToken.getAuthorization(); - - if (authorizationData != null) { - List permissions = authorizationData.getPermissions(); - - if (permissions != null) { - for (Permission permission : permissions) { - if (limit != null && limit <= 0) { - break; - } - - Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId(), resourceServer.getId()); - - if (resourcePermission != null) { - Set scopes = permissionsToEvaluate.get(resourcePermission.getId()); - - if (scopes == null) { - scopes = new HashSet<>(); - permissionsToEvaluate.put(resourcePermission.getId(), scopes); - if (limit != null) { - limit--; - } - } - - Set scopePermission = permission.getScopes(); - - if (scopePermission != null) { - scopes.addAll(scopePermission); - } - } - } - } - } - } - } - - return permissionsToEvaluate.entrySet().stream() - .flatMap((Function>, Stream>) entry -> { - String key = entry.getKey(); - - if ("$KC_SCOPE_PERMISSION".equals(key)) { - ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore(); - List 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()); - return Permissions.createResourcePermissions(entryResource, entry.getValue(), authorization).stream(); - } - }).collect(Collectors.toList()); - } -} diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java deleted file mode 100644 index f5a5745035..0000000000 --- a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.keycloak.authorization.entitlement.representation; - -import java.util.ArrayList; -import java.util.List; - -import org.keycloak.authorization.authorization.representation.AuthorizationRequestMetadata; -import org.keycloak.authorization.protection.permission.representation.PermissionRequest; - -/** - *

    An {@code {@link EntitlementRequest} represents a request sent to the server containing the permissions being requested. - * - *

    Along with an entitlement request additional {@link AuthorizationRequestMetadata} information can be passed in order to define what clients expect from - * the server when evaluating the requested permissions and when returning with a response. - * - * @author Pedro Igor - */ -public class EntitlementRequest { - - private String rpt; - private AuthorizationRequestMetadata metadata; - - private List permissions = new ArrayList<>(); - - /** - * Returns the permissions being requested. - * - * @return the permissions being requested (not {@code null}) - */ - public List getPermissions() { - return permissions; - } - - /** - * Set the permissions being requested - * - * @param permissions the permissions being requests (not {@code null}) - */ - public void setPermissions(List permissions) { - this.permissions = permissions; - } - - /** - * Returns a {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested. - * - * @return a previously issued RPT (may be {@code null}) - */ - public String getRpt() { - return rpt; - } - - /** - * A {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested. - * - * @param rpt a previously issued RPT. If {@code null}, only the requested permissions are evaluated - */ - public void setRpt(String rpt) { - this.rpt = rpt; - } - - /** - * Return the {@link Metadata} associated with this request. - * - * @return - */ - public AuthorizationRequestMetadata getMetadata() { - return metadata; - } - - /** - * The {@link Metadata} associated with this request. The metadata defines specific information that should be considered - * by the server when evaluating and returning permissions. - * - * @param metadata the {@link Metadata} associated with this request (may be {@code null}) - */ - public void setMetadata(AuthorizationRequestMetadata metadata) { - this.metadata = metadata; - } -} diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java deleted file mode 100644 index 8e883cd321..0000000000 --- a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.entitlement.representation; - -/** - * @author Pedro Igor - */ -public class EntitlementResponse { - - private String rpt; - - public EntitlementResponse(String rpt) { - this.rpt = rpt; - } - - public EntitlementResponse() { - this(null); - } - - public String getRpt() { - return this.rpt; - } - - public void setRpt(final String rpt) { - this.rpt = rpt; - } -} 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 30afbc798b..8a811f149c 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 (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) { + clientModel = realm.getClientById(clientId); + + 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/introspect/RPTIntrospectionProvider.java b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java index a31e834e4b..989dbe1c99 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 1e669cfa04..7a8e05e27c 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 request) { - if (request == null) { + if (request == null || request.isEmpty()) { throw new ErrorResponseException("invalid_permission_request", "Invalid permission request.", Response.Status.BAD_REQUEST); } - List 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 verifyRequestedResource(List 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) { + resource = resourceStore.findByName(resourceSetId, this.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); - } + throw new ErrorResponseException("invalid_resource_id", "Resource set with id [" + resourceSetId + "] does not exists in this server.", Response.Status.BAD_REQUEST); } } - Set scopes = verifyRequestedScopes(request1, resource); + Set 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 verifyRequestedScopes(PermissionRequest request, Resource resource) { - return request.getScopes().stream().map(scopeName -> { + Set 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()); } - }).filter(scopeRepresentation -> scopeRepresentation != null).collect(Collectors.toSet()); + + if (scope == null) { + throw new ErrorResponseException("invalid_scope", "Scope [" + scopeName + "] is invalid", Response.Status.BAD_REQUEST); + } + + return ModelToRepresentation.toRepresentation(scope); + }).collect(Collectors.toSet()); } - private String createPermissionTicket(List resources) { + private String createPermissionTicket(List request) { + List permissions = verifyRequestedResource(request).stream().flatMap(resource -> { + List perms = new ArrayList<>(); + Set 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 4f2181fd03..e30f7d0bdf 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 Pedro Igor */ 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 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 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/PermissionTicket.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java deleted file mode 100644 index 8726ce674f..0000000000 --- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.protection.permission; - -import org.keycloak.TokenIdGenerator; -import org.keycloak.representations.AccessToken; -import org.keycloak.representations.JsonWebToken; -import org.keycloak.representations.idm.authorization.ResourceRepresentation; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author Pedro Igor - */ -public class PermissionTicket extends JsonWebToken { - - private final List resources = new ArrayList<>(); - private final String resourceServerId; - - public PermissionTicket() { - this.resourceServerId = null; - } - - public PermissionTicket(List resources, String resourceServerId, AccessToken accessToken) { - id(TokenIdGenerator.generateId()); - subject(accessToken.getSubject()); - expiration(accessToken.getExpiration()); - notBefore(accessToken.getNotBefore()); - issuedAt(accessToken.getIssuedAt()); - issuedFor(accessToken.getIssuedFor()); - this.resources.addAll(resources); - this.resourceServerId = resourceServerId; - } - - public List getResources() { - return this.resources; - } - - public String getResourceServerId() { - return this.resourceServerId; - } -} 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 eea21088f3..37944d25ca 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/permission/representation/PermissionRequest.java b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java deleted file mode 100644 index 31d6d559bb..0000000000 --- a/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.protection.permission.representation; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * @author Pedro Igor - */ -public class PermissionRequest { - - @JsonProperty("resource_set_id") - private final String resourceSetId; - - @JsonProperty("resource_set_name") - private final String resourceSetName; - - private final Set scopes; - - public PermissionRequest(String resourceSetId, String... scopes) { - this.resourceSetId = resourceSetId; - - if (scopes != null) { - this.scopes = new HashSet(Arrays.asList(scopes)); - } else { - this.scopes = new HashSet<>(); - } - - this.resourceSetName = null; - } - - public PermissionRequest() { - this(null, null); - } - - public String getResourceSetId() { - return this.resourceSetId; - } - - public String getResourceSetName() { - return resourceSetName; - } - - public Set getScopes() { - return this.scopes; - } -} diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java deleted file mode 100644 index 24efefa3a8..0000000000 --- a/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.protection.permission.representation; - -/** - * @author Pedro Igor - */ -public class PermissionResponse { - - private final String ticket; - - public PermissionResponse(String ticket) { - this.ticket = ticket; - } - - public PermissionResponse() { - this(null); - } - - public String getTicket() { - return this.ticket; - } -} 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 c2e11dce03..0cead7da3c 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) 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 -> toUmaRepresentation(resource)); } @GET + @NoCache @Produces("application/json") - public Set find(@QueryParam("filter") String filter) { - if (filter == null) { - return findAll(); - } else { - return findByFilter(filter); - } - } - - private Set findAll() { - Response response = this.resourceManager.find(null, null, null, null, null, null, true, -1, -1); - List resources = (List) response.getEntity(); - return resources.stream().map(ResourceRepresentation::getId).collect(Collectors.toSet()); - } - - private Set findByFilter(String filter) { - Set 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, 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/protection/resource/representation/UmaResourceRepresentation.java b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java index 2997498ac6..fbbe08efec 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 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/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java index 0c285bc92a..38b65b8ac3 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 permits(List evaluation, AuthorizationRequestMetadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) { + public static List permits(List evaluation, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) { + return permits(evaluation, null, authorizationProvider, resourceServer); + } + + public static List permits(List evaluation, Metadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) { Map 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 permissions, ResourcePermission permission, ResourceServer resourceServer, AuthorizationRequestMetadata metadata) { + private static void grantPermission(AuthorizationProvider authorizationProvider, Map permissions, ResourcePermission permission, ResourceServer resourceServer, Metadata metadata) { List resources = new ArrayList<>(); Resource resource = permission.getResource(); Set 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 d693a4c753..745d7c71bb 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()); - return authManager.extractAuthorizationHeaderToken(keycloakSession.getContext().getRequestHeaders()); + if (authResult != null) { + return authResult.getToken(); + } + + 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 64fc15b71f..2d68f49515 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 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 msg template attribute. Also load Theme properties and place them into properties 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 getTheme()) * @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/Templates.java b/services/src/main/java/org/keycloak/forms/account/freemarker/Templates.java index 5f98987289..48822937f3 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/forms/account/freemarker/model/AuthorizationBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AuthorizationBean.java new file mode 100755 index 0000000000..837a843f85 --- /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 Stian Thorgersen + */ +public class AuthorizationBean { + + private final UserModel user; + private final AuthorizationProvider authorization; + private final UriInfo uriInfo; + private ResourceBean resource; + private List resources; + private Collection userSharedResources; + private Collection requestsWaitingPermission; + private Collection resourcesWaitingOthersApproval; + + public AuthorizationBean(KeycloakSession session, UserModel user, UriInfo uriInfo) { + this.user = user; + this.uriInfo = uriInfo; + authorization = session.getProvider(AuthorizationProvider.class); + List 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 getResourcesWaitingOthersApproval() { + if (resourcesWaitingOthersApproval == null) { + HashMap filters = new HashMap<>(); + + filters.put(PermissionTicket.REQUESTER, user.getId()); + filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); + + resourcesWaitingOthersApproval = toResourceRepresentation(findPermissions(filters)); + } + + return resourcesWaitingOthersApproval; + } + + public Collection getResourcesWaitingApproval() { + if (requestsWaitingPermission == null) { + HashMap filters = new HashMap<>(); + + filters.put(PermissionTicket.OWNER, user.getId()); + filters.put(PermissionTicket.GRANTED, Boolean.FALSE.toString()); + + requestsWaitingPermission = toResourceRepresentation(findPermissions(filters)); + } + + return requestsWaitingPermission; + } + + public List 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 getSharedResources() { + if (userSharedResources == null) { + HashMap 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 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 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 permissions = new HashMap<>(); + private Collection 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 getScopes() { + return resource.getScopes().stream().map(ModelToRepresentation::toRepresentation).collect(Collectors.toList()); + } + + public Collection getShares() { + if (shares == null) { + Map 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 getPermissions() { + return permissions.values(); + } + + private void addPermission(PermissionTicket ticket, AuthorizationProvider authorization) { + permissions.computeIfAbsent(ticket.getRequester(), key -> new RequesterBean(ticket, authorization)).addScope(ticket); + } + } + + private Collection toPermissionRepresentation(List permissionRequests) { + Map 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 toResourceRepresentation(List tickets) { + Map 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 findPermissions(Map 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 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 fa41dda008..262063d882 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 d81887668b..a0407aae90 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 a67db6e624..0de20be4fe 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/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java index e89db0ff9a..6166071188 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 Pedro Igor */ @@ -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/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index dd94094202..e21f3ade1c 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/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index cbf93bb8e0..05f9f4c4dc 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 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 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 4fc73f7b20..60970d8ef7 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/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java index c2d492943d..ac456142fb 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/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java index bebcb2de49..68aca41b30 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 d3c026082a..d316e34ae0 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 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 tickets = ticketStore.find(filters, resource.getResourceServer().getId(), -1, -1); + Iterator 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 filters = new HashMap<>(); + + filters.put(PermissionTicket.RESOURCE, resource.getId()); + filters.put(PermissionTicket.OWNER, auth.getUser().getId()); + filters.put(PermissionTicket.REQUESTER, user.getId()); + + List 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 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 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/theme/FreeMarkerUtil.java b/services/src/main/java/org/keycloak/theme/FreeMarkerUtil.java index a8b1784395..7415bf1cc9 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 0aea6b0452..c511b2da9d 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()) { %>

  • -

    Resource: <%= permission.getResourceSetName() %>

    -

    ID: <%= permission.getResourceSetId() %>

    +

    Resource: <%= permission.getResourceName() %>

    +

    ID: <%= permission.getResourceId() %>

  • <% } 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 077f9dcbfb..692e05d174 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 @@ Show Requesting Party Token | Show Access Token | Request Entitlements | -Request Entitlement | +Request Entitlement | +My Account | Sign Out
    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 ecdbf0b533..3d2ed43afa 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 9a018e4747..3a892b5549 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 403adfa13f..ab65313b69 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: + 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 788763b2e2..b4208bbeec 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 @@

    -Create Album | My Profile | Any Scope Access | All Scope Access +Create Album | My Profile | Any Scope Access | All Scope Access | Get All Resources

    You don't have any albums, yet. diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json index 7ec7e02650..e44fec390c 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-authz-service.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json index 0b621f5b49..7327cba332 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/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 77bb3b6774..22b538814c 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 7969492735..90704168f0 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 search = protection.resource().findByFilter("uri=" + uri); + List 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 92e300dec5..6e3e3b0e12 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 cc8bea26fc..f887e2a129 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 f3db78da32..a0f8711eea 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/servlet-authz/keycloak.-permissive-authz-service.json b/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-permissive-authz-service.json similarity index 94% rename from testsuite/integration-arquillian/test-apps/servlet-authz/keycloak.-permissive-authz-service.json rename to testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-permissive-authz-service.json index 89c92f29ab..75a0532029 100644 --- a/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak.-permissive-authz-service.json +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-permissive-authz-service.json @@ -9,6 +9,7 @@ "secret": "secret" }, "policy-enforcer": { + "issue-permission-ticket": false, "enforcement-mode" : "PERMISSIVE", "on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp" } 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 3fbfca269c..345a69dffc 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()) { %>
  • -

    Resource: <%= permission.getResourceSetName() %>

    -

    ID: <%= permission.getResourceSetId() %>

    +

    Resource: <%= permission.getResourceName() %>

    +

    ID: <%= permission.getResourceId() %>

    Scopes: <%= permission.getScopes() %>

  • <% 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 27df53565e..1f61268ef2 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 ba670e3ccf..e1ad409350 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 edd264c923..a7cded4223 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 Pedro Igor */ @@ -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 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 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 5f07b2ff55..536d122f83 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 b4b493e542..56d2bee7ec 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 02d18653de..77952be1f7 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 0000000000..237d5e3955 --- /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 Pedro Igor + */ +public abstract class AbstractResourceServerTest extends AbstractKeycloakTest { + + protected static final String REALM_NAME = "authz-test"; + + @Override + public void addTestRealms(List 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 permissions, String expectedResource, String... expectedScopes) { + Iterator iterator = permissions.iterator(); + + while (iterator.hasNext()) { + Permission permission = iterator.next(); + + if (permission.getResourceName().equalsIgnoreCase(expectedResource)) { + Set 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 5529cfd442..61d27735c9 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(); + PermissionRequest request = new PermissionRequest("Resource A"); - request.setResourceSetName("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 cfeb153490..7fece477c6 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 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 requestParams, HashMap requestHeaders) { - ClientCredentialsProviderUtils.setClientCredentials(deployment, requestHeaders, requestParams); + public void configureClientCredentials(Map> requestParams, Map requestHeaders) { + Map formparams = new HashMap<>(); + ClientCredentialsProviderUtils.setClientCredentials(deployment, requestHeaders, formparams); + for (Entry 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 d7f8c6bb53..b55f348270 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 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 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 c0a8868c2b..54bcd2e5bb 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 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()); + 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 EntitlementRequest(); + 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 responseSupplier) { - AccessToken.Authorization authorization = toAccessToken(responseSupplier.get().getRpt()).getAuthorization(); + private void assertResponse(Metadata metadata, Supplier responseSupplier) { + AccessToken.Authorization authorization = toAccessToken(responseSupplier.get().getToken()).getAuthorization(); List 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 256c24c69b..f4fcce5442 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 9b3b72862c..ab0bfe9ef4 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 cde7fc2d63..7a9600f91a 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 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 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 0000000000..21bb9ee33f --- /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 Pedro Igor + */ +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 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 tickets = getAuthzClient().protection().permission().findByResource(resource.getId()); + assertEquals(expectedPermissions, tickets.size()); + + PermissionTicketToken token = new JWSInput(ticket).readJsonContent(PermissionTicketToken.class); + + List tokenPermissions = token.getResources(); + assertNotNull(tokenPermissions); + assertEquals(expectedPermissions, scopeNames.length > 0 ? scopeNames.length : tokenPermissions.size()); + + Iterator 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 expectedTickets = new ArrayList<>(tickets); + Iterator 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 994e52e835..e1a0fe64b3 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(); + PermissionRequest request = new PermissionRequest("Resource A"); - request.setResourceSetName("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 0000000000..65c7f6a416 --- /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 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 0000000000..41448cf175 --- /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 Pedro Igor + */ +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 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 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 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 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 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 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 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 0000000000..f8dd3def7c --- /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 Pedro Igor + */ +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 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 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 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 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 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 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 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 0000000000..7308c7191b --- /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 d6318594f9..a90ff60f3e 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml @@ -331,7 +331,7 @@ integration-arquillian-test-apps-dist ${project.version} zip - **/*realm.json,**/*authz-service.json,**/testsaml.json + **/*realm.json,**/*authz-service.json,**/testsaml.json,**/*-keycloak.json ${examples.home} 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 c4d2b2b9db..59b217708f 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 5137125a8c..3206078e38 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 Pedro Igor @@ -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 29ec514fc2..afc6e854c6 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 7df2fb5252..06b3201df5 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 Pedro Igor */ @@ -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 3d29c03f24..e9e57f4030 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 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 9bd5738c25..8897e6399f 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 Pedro Igor @@ -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 0281d685e0..93f8a39d1e 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 0000000000..fd4e5ecb7c --- /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> + + + + +
    +
    +

    + My Resources <#if authorization.resource.displayName??>${authorization.resource.displayName}<#else>${authorization.resource.name} +

    +
    +
    + + <#if authorization.resource.iconUri??> + +
    + + +
    +
    +

    + ${msg("peopleAccessResource")} +

    +
    +
    +
    +
    + + + + + + + + + + + <#if authorization.resource.shares?size != 0> + <#list authorization.resource.shares as permission> + + + + + + + + + + + + <#else> + + + + + +
    ${msg("user")}${msg("permission")}${msg("date")}${msg("action")}
    + <#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username} + + <#if permission.scopes?size != 0> + <#list permission.scopes as scope> + <#if scope.granted> + + + + <#else> + Any action + + + ${permission.createdDate?datetime} + + ${msg("doRevoke")} +
    The resource is not being shared
    + +
    +
    +
    +
    +

    + ${msg("shareWithOthers")} +

    +
    +
    +
    +
    +
    +
    + * +
    +
    +
    +
    + +
    +
    +
    + <#list authorization.resource.scopes as scope> + + +
    + +
    +
    +
    +
    +
    +
    + \ 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 0000000000..31b1d3f8e9 --- /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> + + +
    +
    +

    + ${msg("myResources")} +

    +
    +
    + + <#if authorization.resourcesWaitingApproval?size != 0> +
    +
    +

    + ${msg("needMyApproval")} +

    +
    +
    +
    +
    + + + + + + + + + + + <#list authorization.resourcesWaitingApproval as resource> + <#list resource.permissions as permission> + + + + + + + + + + + + + +
    ${msg("resource")}${msg("requestor")}${msg("permissionRequestion")}${msg("action")}
    + <#if resource.displayName??>${resource.displayName}<#else>${resource.name} + + <#if permission.requester.email??>${permission.requester.email}<#else>${permission.requester.username} + + <#list permission.scopes as scope> + + + + ${msg("doApprove")} + ${msg("doDeny")} +
    +
    +
    + + +
    +
    +

    + ${msg("myResourcesSub")} +

    +
    +
    +
    +
    + + + + + + + + + + + <#if authorization.resources?size != 0> + <#list authorization.resources as resource> + + + + + + + <#else> + + + + + +
    ${msg("resource")}${msg("application")}${msg("peopleSharingThisResource")}
    + + <#if resource.displayName??>${resource.displayName}<#else>${resource.name} + + + ${resource.resourceServer.name} + + <#if resource.shares?size != 0> + ${resource.shares?size} + <#else> + This resource is not being shared. + +
    You don't have any resource
    +
    +
    + +
    +
    +

    + ${msg("resourcesSharedWithMe")} +

    +
    +
    +
    +
    +
    + + + + + + + + + + + + + <#if authorization.sharedResources?size != 0> + <#list authorization.sharedResources as resource> + + + + + + + + + + <#else> + + + + + +
    disabled="true" + ${msg("resource")}${msg("owner")}${msg("application")}${msg("permission")}${msg("date")}
    + + + <#if resource.displayName??>${resource.displayName}<#else>${resource.name} + + <#if resource.owner.email??>${resource.owner.email}<#else>${resource.owner.username} + + ${resource.resourceServer.name} + + <#if resource.permissions?size != 0> +
      + <#list resource.permissions as permission> + <#list permission.scopes as scope> + <#if scope.granted> +
    • + <#if scope.scope.displayName??> + ${scope.scope.displayName} + <#else> + ${scope.scope.name} + +
    • + + + +
    + <#else> + Any action + +
    + ${resource.permissions[0].grantedDate?datetime} +
    There are no resources shared with you
    +
    +
    + <#if authorization.sharedResources?size != 0> + + +
    + + <#if authorization.resourcesWaitingOthersApproval?size != 0> +
    +
    +
    +

    + ${msg("requestsWaitingApproval")} +

    +
    +
    +
    +
    + <#if authorization.resourcesWaitingOthersApproval?size != 0> + You have ${authorization.resourcesWaitingOthersApproval?size} permission request(s) waiting for approval. + <#else> + You have no permission requests waiting for approval. + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + + \ 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 aa0ca414a7..e49c664a6e 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 @@
  • ${msg("sessions")}
  • ${msg("applications")}
  • <#if features.log>
  • ${msg("log")}
  • + <#if realm.userManagedAccessAllowed && features.authorization>
  • ${msg("myResources")}
  • 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 9dcf980c20..ba0412fde0 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 e3bda1e507..c68b0e27ac 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 @@
    {{:: 'authz-resource-name.tooltip' | translate}}
    +
    + +
    + +
    + {{:: 'authz-resource-name.tooltip' | translate}} +
    @@ -59,6 +66,13 @@
    {{:: 'authz-icon-uri.tooltip' | translate}}
    +
    + +
    + +
    + {{:: 'authz-permission-resource-apply-to-resource-type.tooltip' | translate}} +
    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 a505bbcd23..d296abd733 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 @@ -22,6 +22,13 @@
    {{:: 'authz-scope-name.tooltip' | translate}}
    +
    + +
    + +
    + {{:: 'authz-scope-name.tooltip' | translate}} +
    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 88416f3902..dadb0c79ab 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 @@ -31,6 +31,14 @@ {{:: 'realm-detail.enabled.tooltip' | translate}}
    +
    + +
    + +
    + {{:: 'realm-detail.userManagedAccess.tooltip' | translate}} +
    +