From 94f583e935f4c7045e3897c21389ca48594b5b87 Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Wed, 20 Jul 2016 17:37:48 +0200 Subject: [PATCH 01/13] KEYCLOAK-2981 Upload-certificate admin endpoint does not nullify private keys --- .../resources/admin/ClientAttributeCertificateResource.java | 3 ++- .../org/keycloak/testsuite/admin/client/CredentialsTest.java | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java index 895bd647ab..4f41596a1c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java @@ -171,7 +171,7 @@ public class ClientAttributeCertificateResource { * * @param uriInfo * @param input - * @return + * @return information extracted from uploaded certificate - not necessarily the new state of certificate on the server * @throws IOException */ @POST @@ -189,6 +189,7 @@ public class ClientAttributeCertificateResource { if (info.getCertificate() != null) { client.setAttribute(certificateAttribute, info.getCertificate()); + client.removeAttribute(privateAttribute); } else { throw new ErrorResponseException("certificate-not-found", "Certificate with given alias not found in the keystore", Response.Status.BAD_REQUEST); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java index a9d038ed90..0b2ffafbf2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/CredentialsTest.java @@ -147,8 +147,7 @@ public class CredentialsTest extends AbstractClientTest { // Get the certificate - to make sure cert was properly updated, and privateKey is null cert = certRsc.getKeyInfo(); assertEquals("cert properly set", certificate2, cert.getCertificate()); - // TODO: KEYCLOAK-2981 - //assertNull("privateKey nullified", cert.getPrivateKey()); + assertNull("privateKey nullified", cert.getPrivateKey()); // Re-upload the private key certRsc.uploadJks(keyCertForm); From 59e0570cdf155175ed95af0cd3a0ca7339515947 Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Tue, 26 Jul 2016 21:32:57 +0200 Subject: [PATCH 02/13] KEYCLOAK-2571 RESET_PASSWORD_ERROR and UPDATE_PASSWORD_ERROR events not fired --- .../main/java/org/keycloak/events/Errors.java | 4 +++ .../requiredactions/UpdatePassword.java | 10 +++++++ .../services/resources/AccountService.java | 17 +++++++++-- .../resources/LoginActionsService.java | 2 +- .../testsuite/account/AccountTest.java | 28 ++++++------------- .../testsuite/forms/ResetPasswordTest.java | 3 ++ 6 files changed, 41 insertions(+), 23 deletions(-) diff --git a/server-spi/src/main/java/org/keycloak/events/Errors.java b/server-spi/src/main/java/org/keycloak/events/Errors.java index 0981806a83..ea2b8877e4 100755 --- a/server-spi/src/main/java/org/keycloak/events/Errors.java +++ b/server-spi/src/main/java/org/keycloak/events/Errors.java @@ -71,4 +71,8 @@ public interface Errors { String INVALID_EMAIL = "invalid_email"; String IDENTITY_PROVIDER_LOGIN_FAILURE = "identity_provider_login_failure"; String IDENTITY_PROVIDER_ERROR = "identity_provider_error"; + + String PASSWORD_CONFIRM_ERROR = "password_confirm_error"; + String PASSWORD_MISSING = "password_missing"; + String PASSWORD_REJECTED = "password_rejected"; } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java index 47c9d5baf6..7918815eec 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java @@ -21,6 +21,8 @@ import org.keycloak.Config; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.models.KeycloakSession; @@ -84,17 +86,23 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); + EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR) + .client(context.getClientSession().getClient()) + .user(context.getClientSession().getUserSession().getUser()); + if (Validation.isBlank(passwordNew)) { Response challenge = context.form() .setError(Messages.MISSING_PASSWORD) .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); context.challenge(challenge); + errorEvent.error(Errors.PASSWORD_MISSING); return; } else if (!passwordNew.equals(passwordConfirm)) { Response challenge = context.form() .setError(Messages.NOTMATCH_PASSWORD) .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); context.challenge(challenge); + errorEvent.error(Errors.PASSWORD_CONFIRM_ERROR); return; } @@ -102,12 +110,14 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac context.getSession().users().updateCredential(context.getRealm(), context.getUser(), UserCredentialModel.password(passwordNew)); context.success(); } catch (ModelException me) { + errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED); Response challenge = context.form() .setError(me.getMessage(), me.getParameters()) .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); context.challenge(challenge); return; } catch (Exception ape) { + errorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED); Response challenge = context.form() .setError(ape.getMessage()) .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 251cd8e569..bf6fb885ed 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -16,7 +16,7 @@ */ package org.keycloak.services.resources; -import org.jboss.logging.Logger; +import org.keycloak.events.Errors; import org.keycloak.forms.account.AccountPages; import org.keycloak.forms.account.AccountProvider; import org.keycloak.events.Details; @@ -612,26 +612,34 @@ public class AccountService extends AbstractSecuredLocalService { String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); + EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR) + .client(auth.getClient()) + .user(auth.getClientSession().getUserSession().getUser()); + if (requireCurrent) { if (Validation.isBlank(password)) { setReferrerOnPage(); + errorEvent.error(Errors.PASSWORD_MISSING); return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD); } UserCredentialModel cred = UserCredentialModel.password(password); if (!session.users().validCredentials(session, realm, user, cred)) { setReferrerOnPage(); + errorEvent.error(Errors.INVALID_USER_CREDENTIALS); return account.setError(Messages.INVALID_PASSWORD_EXISTING).createResponse(AccountPages.PASSWORD); } } if (Validation.isBlank(passwordNew)) { setReferrerOnPage(); + errorEvent.error(Errors.PASSWORD_MISSING); return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD); } if (!passwordNew.equals(passwordConfirm)) { setReferrerOnPage(); + errorEvent.error(Errors.PASSWORD_CONFIRM_ERROR); return account.setError(Messages.INVALID_PASSWORD_CONFIRM).createResponse(AccountPages.PASSWORD); } @@ -639,14 +647,17 @@ public class AccountService extends AbstractSecuredLocalService { session.users().updateCredential(realm, user, UserCredentialModel.password(passwordNew)); } catch (ModelReadOnlyException mre) { setReferrerOnPage(); + errorEvent.error(Errors.NOT_ALLOWED); return account.setError(Messages.READ_ONLY_PASSWORD).createResponse(AccountPages.PASSWORD); - }catch (ModelException me) { + } catch (ModelException me) { logger.failedToUpdatePassword(me); setReferrerOnPage(); + errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED); return account.setError(me.getMessage(), me.getParameters()).createResponse(AccountPages.PASSWORD); - }catch (Exception ape) { + } catch (Exception ape) { logger.failedToUpdatePassword(ape); setReferrerOnPage(); + errorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED); return account.setError(ape.getMessage()).createResponse(AccountPages.PASSWORD); } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index cf8791edd2..66dee2e651 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -892,7 +892,7 @@ public class LoginActionsService { return AuthenticationManager.finishedRequiredActions(session, userSession, clientSession, clientConnection, request, uriInfo, event); } - } + } if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { return context.getChallenge(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java index a4f7257baa..5f8a51df68 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -21,6 +21,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.events.Details; +import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.services.resources.AccountService; @@ -168,22 +169,20 @@ public class AccountTest extends TestRealmKeycloakTest { EventRepresentation event = events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent(); String sessionId = event.getSessionId(); String userId = event.getUserId(); - changePasswordPage.changePassword("", "new-password", "new-password"); + changePasswordPage.changePassword("", "new-password", "new-password"); Assert.assertEquals("Please specify password.", profilePage.getError()); + events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_MISSING).assertEvent(); changePasswordPage.changePassword("password", "new-password", "new-password2"); - Assert.assertEquals("Password confirmation doesn't match.", profilePage.getError()); + events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_CONFIRM_ERROR).assertEvent(); changePasswordPage.changePassword("password", "new-password", "new-password"); - Assert.assertEquals("Your password has been updated.", profilePage.getSuccess()); - events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent(); changePasswordPage.logout(); - events.expectLogout(sessionId).detail(Details.REDIRECT_URI, changePasswordPage.getPath()).assertEvent(); loginPage.open(); @@ -191,7 +190,7 @@ public class AccountTest extends TestRealmKeycloakTest { Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().session((String) null).error("invalid_user_credentials") + events.expectLogin().session((String) null).error(Errors.INVALID_USER_CREDENTIALS) .removeDetail(Details.CONSENT) .assertEvent(); @@ -214,18 +213,14 @@ public class AccountTest extends TestRealmKeycloakTest { changePasswordPage.open(); loginPage.login("test-user@localhost", "password"); - - events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent(); changePasswordPage.changePassword("", "new", "new"); - Assert.assertEquals("Please specify password.", profilePage.getError()); + events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_MISSING).assertEvent(); changePasswordPage.changePassword("password", "new-password", "new-password"); - Assert.assertEquals("Your password has been updated.", profilePage.getSuccess()); - events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent(); } @@ -235,31 +230,26 @@ public class AccountTest extends TestRealmKeycloakTest { changePasswordPage.open(); loginPage.login("test-user@localhost", "password"); - events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent(); changePasswordPage.changePassword("password", "password", "password"); - Assert.assertEquals("Invalid password: must not be equal to any of last 2 passwords.", profilePage.getError()); + events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).assertEvent(); changePasswordPage.changePassword("password", "password1", "password1"); - Assert.assertEquals("Your password has been updated.", profilePage.getSuccess()); - events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent(); changePasswordPage.changePassword("password1", "password", "password"); - Assert.assertEquals("Invalid password: must not be equal to any of last 2 passwords.", profilePage.getError()); + events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).assertEvent(); changePasswordPage.changePassword("password1", "password1", "password1"); - Assert.assertEquals("Invalid password: must not be equal to any of last 2 passwords.", profilePage.getError()); + events.expectAccount(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).assertEvent(); changePasswordPage.changePassword("password1", "password2", "password2"); - Assert.assertEquals("Your password has been updated.", profilePage.getSuccess()); - events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 98a3fe6135..78d20d9ceb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -314,6 +314,7 @@ public class ResetPasswordTest extends TestRealmKeycloakTest { assertTrue(updatePasswordPage.isCurrent()); assertEquals(error, updatePasswordPage.getError()); + events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); } @Test @@ -544,6 +545,8 @@ public class ResetPasswordTest extends TestRealmKeycloakTest { assertEquals("Invalid password: minimum length 8.", resetPasswordPage.getErrorMessage()); + events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy"); String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); From 322597b4200eb7c0fe08e0d51c10575d8c3283fb Mon Sep 17 00:00:00 2001 From: Scott Rossillo Date: Wed, 27 Jul 2016 12:35:21 -0400 Subject: [PATCH 03/13] KEYCLOAK-3097: Fix JBoss Logging scope JBoss Logging Framework required by Keycloak Core. --- adapters/oidc/spring-security/pom.xml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/adapters/oidc/spring-security/pom.xml b/adapters/oidc/spring-security/pom.xml index 415266bb45..f19cdf6e99 100755 --- a/adapters/oidc/spring-security/pom.xml +++ b/adapters/oidc/spring-security/pom.xml @@ -71,6 +71,13 @@ org.slf4j slf4j-api ${slf4j.version} + compile + + + org.jboss.logging + jboss-logging + ${jboss.logging.version} + compile org.apache.httpcomponents @@ -121,12 +128,6 @@ ${slf4j.version} test - - org.jboss.logging - jboss-logging - ${jboss.logging.version} - test - From 7e1b97888ae4a97c6f0b6270d49e458ca9d93aa0 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 27 Jul 2016 15:15:14 -0300 Subject: [PATCH 04/13] [KEYCLOAK-3338] - Adding client roles to role policy and UX improvements --- .../provider/role/RolePolicyProvider.java | 39 ++- .../role/RolePolicyProviderFactory.java | 26 +- .../idm/RoleRepresentation.java | 18 ++ .../authorization/ResourceRepresentation.java | 3 + .../authorization/ScopeRepresentation.java | 9 + .../hello-world-authz-service.json | 12 +- .../src/main/webapp/index.html | 2 +- .../src/main/webapp/js/app.js | 7 +- .../main/webapp/partials/admin/albums.html | 2 +- .../src/main/webapp/partials/home.html | 4 +- examples/authz/photoz/photoz-realm.json | 2 +- .../photoz-restful-api-authz-service.json | 37 ++- .../example/photoz/album/AlbumService.java | 8 +- .../infinispan/CachedPolicyStore.java | 7 +- .../infinispan/CachedResourceStore.java | 22 +- .../jpa/store/JPAResourceStore.java | 2 +- .../authorization/attribute/Attributes.java | 2 +- .../authorization/identity/Identity.java | 48 ++- .../migration/migrators/MigrateTo2_1_0.java | 54 ++++ .../models/utils/ModelToRepresentation.java | 2 + .../admin/PolicyEvaluationService.java | 20 +- .../authorization/admin/PolicyService.java | 7 +- .../admin/ResourceServerService.java | 284 +++++------------- .../PolicyEvaluationResponse.java | 12 +- .../authorization/admin/util/Models.java | 17 +- .../common/KeycloakIdentity.java | 66 ++-- .../protection/ProtectionService.java | 31 +- .../authorization/util/Permissions.java | 30 +- .../src/main/webapp/index.html | 2 +- .../src/main/webapp/js/app.js | 7 +- .../main/webapp/partials/admin/albums.html | 24 +- .../src/main/webapp/partials/home.html | 14 +- .../test-apps/photoz/photoz-realm.json | 2 +- .../photoz-restful-api-authz-service.json | 39 +-- .../example/photoz/album/AlbumService.java | 8 +- .../page/PhotozClientAuthzTestApp.java | 15 +- .../AbstractPhotozExampleAdapterTest.java | 120 +++++++- .../AbstractPhotozAdminTest.java | 17 +- .../messages/admin-messages_en.properties | 5 +- .../resources/js/authz/authz-controller.js | 124 +++++++- .../resource-server-policy-role-detail.html | 71 ++++- ...esource-server-policy-evaluate-result.html | 4 +- .../authz/resource-server-scope-list.html | 22 ++ 43 files changed, 830 insertions(+), 417 deletions(-) diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java index af5f322cd2..c8a99fe7c1 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProvider.java @@ -23,9 +23,12 @@ import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.evaluation.Evaluation; import org.keycloak.authorization.policy.evaluation.EvaluationContext; import org.keycloak.authorization.policy.provider.PolicyProvider; +import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import java.util.Map; + import static org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory.getRoles; /** @@ -47,23 +50,41 @@ public class RolePolicyProvider implements PolicyProvider { @Override public void evaluate(Evaluation evaluation) { - EvaluationContext context = evaluation.getContext(); - String[] roleIds = getRoles(this.policy); + Map[] roleIds = getRoles(this.policy); if (roleIds.length > 0) { - Identity identity = context.getIdentity(); + Identity identity = evaluation.getContext().getIdentity(); - for (String roleId : roleIds) { - RoleModel role = getCurrentRealm().getRoleById(roleId); + for (Map current : roleIds) { + RoleModel role = getCurrentRealm().getRoleById((String) current.get("id")); - if (role != null && identity.hasRole(role.getName())) { - evaluation.grant(); - break; + if (role != null) { + boolean hasRole = hasRole(identity, role); + + if (!hasRole && Boolean.valueOf(isRequired(current))) { + evaluation.deny(); + return; + } else if (hasRole) { + evaluation.grant(); + } } } } } + private boolean isRequired(Map current) { + return (boolean) current.getOrDefault("required", false); + } + + private boolean hasRole(Identity identity, RoleModel role) { + String roleName = role.getName(); + if (role.isClientRole()) { + ClientModel clientModel = getCurrentRealm().getClientById(role.getContainerId()); + return identity.hasClientRole(clientModel.getClientId(), roleName); + } + return identity.hasRealmRole(roleName); + } + private RealmModel getCurrentRealm() { return this.authorization.getKeycloakSession().getContext().getRealm(); } @@ -72,4 +93,4 @@ public class RolePolicyProvider implements PolicyProvider { public void close() { } -} +} \ No newline at end of file diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java index 598a41ef20..301bb7b9b9 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/role/RolePolicyProviderFactory.java @@ -34,7 +34,9 @@ import org.keycloak.util.JsonSerialization; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * @author Pedro Igor @@ -81,11 +83,17 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory { RoleModel removedRole = ((RoleRemovedEvent) event).getRole(); policyStore.findByType(getId()).forEach(policy -> { - List roles = new ArrayList<>(); + List roles = new ArrayList<>(); - for (String roleId : getRoles(policy)) { - if (!roleId.equals(removedRole.getId())) { - roles.add(roleId); + for (Map role : getRoles(policy)) { + if (!role.get("id").equals(removedRole.getId())) { + Map updated = new HashMap(); + updated.put("id", role.get("id")); + Object required = role.get("required"); + if (required != null) { + updated.put("required", required); + } + roles.add(updated); } } @@ -96,7 +104,9 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory { }); policyStore.delete(policy.getId()); } else { - policy.getConfig().put("roles", JsonSerialization.writeValueAsString(roles)); + Map config = policy.getConfig(); + config.put("roles", JsonSerialization.writeValueAsString(roles)); + policy.setConfig(config); } } catch (IOException e) { throw new RuntimeException("Error while synchronizing roles with policy [" + policy.getName() + "].", e); @@ -116,17 +126,17 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory { return "role"; } - static String[] getRoles(Policy policy) { + static Map[] getRoles(Policy policy) { String roles = policy.getConfig().get("roles"); if (roles != null) { try { - return JsonSerialization.readValue(roles.getBytes(), String[].class); + return JsonSerialization.readValue(roles.getBytes(), Map[].class); } catch (IOException e) { throw new RuntimeException("Could not parse roles [" + roles + "] from policy config [" + policy.getName() + ".", e); } } - return new String[]{}; + return new Map[] {}; } } diff --git a/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java index e9b470a10a..9614ca311d 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java @@ -32,6 +32,8 @@ public class RoleRepresentation { protected Boolean scopeParamRequired; protected boolean composite; protected Composites composites; + private Boolean clientRole; + private String containerId; public static class Composites { protected Set realm; @@ -122,4 +124,20 @@ public class RoleRepresentation { public void setComposite(boolean composite) { this.composite = composite; } + + public Boolean getClientRole() { + return clientRole; + } + + public void setClientRole(Boolean clientRole) { + this.clientRole = clientRole; + } + + public String getContainerId() { + return containerId; + } + + public void setContainerId(String containerId) { + this.containerId = containerId; + } } 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 6ccac3372a..2776b0a330 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 @@ -16,6 +16,7 @@ */ package org.keycloak.representations.idm.authorization; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.net.URI; @@ -39,12 +40,14 @@ public class ResourceRepresentation { private String name; private String uri; private String type; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private Set scopes; @JsonProperty("icon_uri") private String iconUri; private ResourceOwnerRepresentation owner; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private List policies; /** 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 39aa9c7bc2..3a1f2525f9 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 @@ -34,6 +34,7 @@ public class ScopeRepresentation { private String name; private String iconUri; private List policies; + private List resources; /** * Creates an instance. @@ -94,6 +95,14 @@ public class ScopeRepresentation { this.policies = policies; } + public List getResources() { + return this.resources; + } + + public void setResources(List resources) { + this.resources = resources; + } + public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; diff --git a/examples/authz/hello-world-authz-service/hello-world-authz-service.json b/examples/authz/hello-world-authz-service/hello-world-authz-service.json index ea56e6268d..6e509c70ba 100644 --- a/examples/authz/hello-world-authz-service/hello-world-authz-service.json +++ b/examples/authz/hello-world-authz-service/hello-world-authz-service.json @@ -1,4 +1,6 @@ { + "allowRemoteResourceManagement": false, + "policyEnforcementMode": "ENFORCING", "resources": [ { "name": "Default Resource", @@ -8,22 +10,26 @@ ], "policies": [ { - "name": "Only From Realm Policy", + "name": "Default Policy", "description": "A policy that grants access only for users within this realm", "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", "config": { "applyPolicies": "[]", - "code": "var context = $evaluation.getContext();\n\n// using attributes from the evaluation context to obtain the realm\nvar contextAttributes = context.getAttributes();\nvar realmName = contextAttributes.getValue('kc.realm.name').asString(0);\n\n// using attributes from the identity to obtain the issuer\nvar identity = context.getIdentity();\nvar identityAttributes = identity.getAttributes();\nvar issuer = identityAttributes.getValue('iss').asString(0);\n\n// only users from the realm have access granted \nif (issuer.endsWith(realmName)) {\n $evaluation.grant();\n}" + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" } }, { "name": "Default Permission", "description": "A permission that applies to the default resource type", "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", "config": { "defaultResourceType": "urn:hello-world-authz-service:resources:default", "default": "true", - "applyPolicies": "[\"Only From Realm Policy\"]" + "applyPolicies": "[\"Default Policy\"]" } } ] 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 98f0856db0..203b6e2095 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 @@ -19,7 +19,7 @@ -Show Requesting Party Token | Show Access Token | Request Entitlements +Show Requesting Party Token | Show Access Token | Request Entitlements | 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 2990675a06..e58c5f55d7 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 @@ -64,6 +64,8 @@ module.controller('TokenCtrl', function ($scope, Identity) { $scope.requestEntitlements = function () { Identity.authorization.entitlement('photoz-restful-api').then(function (rpt) {}); } + + $scope.Identity = Identity; }); module.controller('AlbumCtrl', function ($scope, $http, $routeParams, $location, Album) { @@ -83,14 +85,13 @@ module.controller('ProfileCtrl', function ($scope, $http, $routeParams, $locatio $scope.profile = Profile.get(); }); -module.controller('AdminAlbumCtrl', function ($scope, $http, $route, AdminAlbum, Album) { +module.controller('AdminAlbumCtrl', function ($scope, $http, $route, $location, AdminAlbum, Album) { $scope.albums = {}; $http.get(apiUrl + '/admin/album').success(function (data) { $scope.albums = data; }); $scope.deleteAlbum = function (album) { - var newAlbum = new Album(album); - newAlbum.$delete({id: album.id}, function () { + new Album(album).$delete({id: album.id}, function () { $route.reload(); }); } diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html index da78224f91..c0cc6e1834 100644 --- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html +++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html @@ -10,7 +10,7 @@ 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 bd5853ea8e..78c252a85c 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 @@ -1,4 +1,4 @@ -

Welcome To Photoz, {{Identity.claims.name}} [Sign Out]

+

Welcome To Photoz, {{Identity.claims.name}}

Administration: [All Albums]


@@ -15,7 +15,7 @@ - {{p.name}} - [X] + {{p.name}} - [X] diff --git a/examples/authz/photoz/photoz-realm.json b/examples/authz/photoz/photoz-realm.json index b3b2b8132a..baa8f66330 100644 --- a/examples/authz/photoz/photoz-realm.json +++ b/examples/authz/photoz/photoz-realm.json @@ -53,7 +53,7 @@ } ], "realmRoles": [ - "user", "admin", "uma_authorization" + "admin", "uma_authorization" ], "clientRoles": { "realm-management": [ diff --git a/examples/authz/photoz/photoz-restful-api-authz-service.json b/examples/authz/photoz/photoz-restful-api-authz-service.json index e8d8862607..6c786e7577 100644 --- a/examples/authz/photoz/photoz-restful-api-authz-service.json +++ b/examples/authz/photoz/photoz-restful-api-authz-service.json @@ -21,10 +21,10 @@ "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:delete" + "name": "urn:photoz.com:scopes:album:create" } ] }, @@ -44,12 +44,15 @@ "name": "Only Owner Policy", "description": "Defines that only the resource owner is allowed to do something", "type": "drools", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", "config": { "mavenArtifactVersion": "2.1.0-SNAPSHOT", "mavenArtifactId": "photoz-authz-policy", "sessionName": "MainOwnerSession", "mavenArtifactGroupId": "org.keycloak", "moduleName": "PhotozAuthzOwnerPolicy", + "applyPolicies": "[]", "scannerPeriod": "1", "scannerPeriodUnit": "Hours" } @@ -58,16 +61,22 @@ "name": "Any Admin Policy", "description": "Defines that adminsitrators can do something", "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", "config": { - "roles": "[\"admin\"]" + "applyPolicies": "[]", + "roles": "[{\"id\":\"admin\",\"required\":true}]" } }, { "name": "Any User Policy", "description": "Defines that any user can do something", "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", "config": { - "roles": "[\"user\"]" + "applyPolicies": "[]", + "roles": "[{\"id\":\"user\"}]" } }, { @@ -77,6 +86,7 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { + "applyPolicies": "[]", "code": "var contextAttributes = $evaluation.getContext().getAttributes();\n\nif (contextAttributes.containsValue('kc.client.network.ip_address', '127.0.0.1')) {\n $evaluation.grant();\n}" } }, @@ -84,6 +94,8 @@ "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\"]" } @@ -92,35 +104,28 @@ "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\"]" + "applyPolicies": "[\"Only Owner Policy\",\"Administration Policy\"]" } }, { "name": "Only From @keycloak.org or Admin", "description": "Defines that only users from @keycloak.org", "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", "config": { "applyPolicies": "[]", "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}" } }, - { - "name": "Only in the Period", - "description": "Access granted only during the morning", - "type": "time", - "config": { - "noa": "2016-01-03 23:59:59", - "expirationUnit": "Minutes", - "nbf": "2016-01-01 00:00:00", - "expirationTime": "1" - } - }, { "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", 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 388c9e4c8b..c28bca3bc0 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 @@ -109,13 +109,7 @@ public class AlbumService { private void createProtectedResource(Album album) { try { - HashSet scopes = new HashSet<>(); - - scopes.add(new ScopeRepresentation(SCOPE_ALBUM_VIEW)); - scopes.add(new ScopeRepresentation(SCOPE_ALBUM_CREATE)); - scopes.add(new ScopeRepresentation(SCOPE_ALBUM_DELETE)); - - ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album"); + ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), new HashSet(), "/album/" + album.getId(), "http://photoz.com/album"); albumResource.setOwner(album.getUserId()); diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java index 6bdf96fefc..6e3271bc82 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java @@ -38,6 +38,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; /** * @author Pedro Igor @@ -99,7 +100,7 @@ public class CachedPolicyStore implements PolicyStore { @Override public List findByResourceServer(String resourceServerId) { - return getDelegate().findByResourceServer(resourceServerId); + return getDelegate().findByResourceServer(resourceServerId).stream().map(policy -> findById(policy.getId())).collect(Collectors.toList()); } @Override @@ -179,12 +180,12 @@ public class CachedPolicyStore implements PolicyStore { @Override public List findByType(String type) { - return getDelegate().findByType(type); + return getDelegate().findByType(type).stream().map(policy -> findById(policy.getId())).collect(Collectors.toList()); } @Override public List findDependentPolicies(String id) { - return getDelegate().findDependentPolicies(id); + return getDelegate().findDependentPolicies(id).stream().map(policy -> findById(policy.getId())).collect(Collectors.toList()); } private String getCacheKeyForPolicy(String policyId) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java index ee638f6180..495df61484 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java @@ -68,7 +68,21 @@ public class CachedResourceStore implements ResourceStore { @Override public void delete(String id) { - this.cache.remove(getCacheKeyForResource(id)); + List removed = this.cache.remove(getCacheKeyForResource(id)); + + if (removed != null) { + CachedResource cachedResource = removed.get(0); + List byOwner = this.cache.get(getResourceOwnerCacheKey(cachedResource.getOwner())); + + if (byOwner != null) { + byOwner.remove(id); + + if (byOwner.isEmpty()) { + this.cache.remove(getResourceOwnerCacheKey(cachedResource.getOwner())); + } + } + } + getDelegate().delete(id); } @@ -109,12 +123,12 @@ public class CachedResourceStore implements ResourceStore { @Override public List findByResourceServer(String resourceServerId) { - return getDelegate().findByResourceServer(resourceServerId); + return getDelegate().findByResourceServer(resourceServerId).stream().map(resource -> findById(resource.getId())).collect(Collectors.toList()); } @Override public List findByScope(String... id) { - return getDelegate().findByScope(id); + return getDelegate().findByScope(id).stream().map(resource -> findById(resource.getId())).collect(Collectors.toList()); } @Override @@ -283,7 +297,7 @@ public class CachedResourceStore implements ResourceStore { return; } cached = new ArrayList<>(); - this.cache.put(getResourceOwnerCacheKey(resource.getOwner()), cached); + this.cache.put(cacheKey, cached); } if (cached != null && !cached.contains(resource.getId())) { 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 802989c973..986d007808 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 @@ -98,7 +98,7 @@ public class JPAResourceStore implements ResourceStore { @Override public List findByScope(String... id) { - Query query = entityManager.createQuery("from ResourceEntity r inner join r.scopes s where s.id in (:scopeIds)"); + Query query = entityManager.createQuery("select r from ResourceEntity r inner join r.scopes s where s.id in (:scopeIds)"); query.setParameter("scopeIds", Arrays.asList(id)); diff --git a/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java index ce2bc51921..a70d3c075e 100644 --- a/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java +++ b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java @@ -95,7 +95,7 @@ public interface Attributes { private final String[] values; private final String name; - Entry(String name, Collection values) { + public Entry(String name, Collection values) { this.name = name; this.values = values.toArray(new String[values.size()]); } diff --git a/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java b/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java index ebc9679994..f16e6c3397 100644 --- a/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java +++ b/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java @@ -19,6 +19,10 @@ package org.keycloak.authorization.identity; import org.keycloak.authorization.attribute.Attributes; +import java.util.Collection; +import java.util.Map; +import java.util.function.Predicate; + /** *

Represents a security identity, which can be a person or non-person entity that was previously authenticated. * @@ -45,13 +49,53 @@ public interface Identity { Attributes getAttributes(); /** - * Indicates if this identity is granted with a role with the given roleName. + * Indicates if this identity is granted with a role (realm or client) with the given roleName. * * @param roleName the name of the role * * @return true if the identity has the given role. Otherwise, it returns false. */ default boolean hasRole(String roleName) { - return getAttributes().containsValue("roles", roleName); + return hasRealmRole(roleName) || hasClientRole(roleName); + } + + /** + * Indicates if this identity is granted with a realm role with the given roleName. + * + * @param roleName the name of the role + * + * @return true if the identity has the given role. Otherwise, it returns false. + */ + default boolean hasRealmRole(String roleName) { + return getAttributes().containsValue("kc.realm.roles", roleName); + } + + /** + * Indicates if this identity is granted with a client role with the given roleName. + * + * @param clientId the client id + * @param roleName the name of the role + * + * @return true if the identity has the given role. Otherwise, it returns false. + */ + default boolean hasClientRole(String clientId, String roleName) { + return getAttributes().containsValue("kc.client." + clientId + ".roles", roleName); + } + + /** + * Indicates if this identity is granted with a client role with the given roleName. + * + * @param roleName the name of the role + * + * @return true if the identity has the given role. Otherwise, it returns false. + */ + default boolean hasClientRole(String roleName) { + return getAttributes().toMap().entrySet().stream().filter(entry -> { + String key = entry.getKey(); + if (key.startsWith("kc.client") && key.endsWith(".roles")) { + return getAttributes().containsValue(key, roleName); + } + return false; + }).findFirst().isPresent(); } } diff --git a/server-spi/src/main/java/org/keycloak/migration/migrators/MigrateTo2_1_0.java b/server-spi/src/main/java/org/keycloak/migration/migrators/MigrateTo2_1_0.java index e5230f92af..8ff3664ad3 100644 --- a/server-spi/src/main/java/org/keycloak/migration/migrators/MigrateTo2_1_0.java +++ b/server-spi/src/main/java/org/keycloak/migration/migrators/MigrateTo2_1_0.java @@ -17,11 +17,22 @@ package org.keycloak.migration.migrators; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; import org.keycloak.migration.ModelVersion; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.UserModel; +import org.keycloak.util.JsonSerialization; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; /** * @@ -33,6 +44,7 @@ public class MigrateTo2_1_0 { public void migrate(KeycloakSession session) { for (RealmModel realm : session.realms().getRealms()) { migrateDefaultRequiredAction(realm); + migrateRolePolicies(realm, session); } } @@ -46,4 +58,46 @@ public class MigrateTo2_1_0 { otpAction.setName("Configure OTP"); } + + // KEYCLOAK-3338: Changes to how role policy config is stored" + private void migrateRolePolicies(RealmModel realm, KeycloakSession session) { + AuthorizationProvider authorizationProvider = session.getProvider(AuthorizationProvider.class); + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + realm.getClients().forEach(clientModel -> { + ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(clientModel.getId()); + + if (resourceServer != null) { + policyStore.findByType("role").forEach(policy -> { + Map config = policy.getConfig(); + String roles = config.get("roles"); + List roleConfig; + + try { + roleConfig = JsonSerialization.readValue(roles, List.class); + } catch (Exception e) { + throw new RuntimeException("Malformed configuration for role policy [" + policy.getName() + "].", e); + } + + if (!roleConfig.isEmpty() && roleConfig.get(0) instanceof String) { + try { + config.put("roles", JsonSerialization.writeValueAsString(roleConfig.stream().map(new Function() { + @Override + public Map apply(String roleId) { + Map updated = new HashMap(); + + updated.put("id", roleId); + + return updated; + } + }).collect(Collectors.toList()))); + policy.setConfig(config); + } catch (Exception e) { + throw new RuntimeException("Failed to migrate role policy [" + policy.getName() + "].", e); + } + } + }); + } + }); + } } diff --git a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 7f7f7e0d04..203736f893 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -218,6 +218,8 @@ public class ModelToRepresentation { rep.setDescription(role.getDescription()); rep.setScopeParamRequired(role.isScopeParamRequired()); rep.setComposite(role.isComposite()); + rep.setClientRole(role.isClientRole()); + rep.setContainerId(role.getContainerId()); return rep; } 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 f4d8febfd2..8e4ec5e318 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java @@ -136,11 +136,19 @@ public class PolicyEvaluationService { } private List createPermissions(PolicyEvaluationRequest representation, EvaluationContext evaluationContext, AuthorizationProvider authorization) { - if (representation.isEntitlements()) { + List resources = representation.getResources(); + + for (PolicyEvaluationRequest.Resource resource : new ArrayList<>(resources)) { + if (resource.getId() == null && (resource.getScopes() == null || resource.getScopes().isEmpty())) { + resources.remove(resource); + } + } + + if (representation.isEntitlements() || resources.isEmpty()) { return Permissions.all(this.resourceServer, evaluationContext.getIdentity(), authorization); } - return representation.getResources().stream().flatMap((Function>) resource -> { + return resources.stream().flatMap((Function>) resource -> { Set givenScopes = resource.getScopes(); if (givenScopes == null) { @@ -157,7 +165,13 @@ public class PolicyEvaluationService { } else if (resource.getType() != null) { return storeFactory.getResourceStore().findByType(resource.getType()).stream().map(resource1 -> new ResourcePermission(resource1, scopes, resourceServer)); } else { - return scopes.stream().map(scope -> new ResourcePermission(null, asList(scope), resourceServer)); + List collect = scopes.stream().map(scope -> new ResourcePermission(null, asList(scope), resourceServer)).collect(Collectors.toList()); + + for (Scope scope : scopes) { + collect.addAll(storeFactory.getResourceStore().findByScope(scope.getId()).stream().map(resource12 -> new ResourcePermission(resource12, asList(scope), resourceServer)).collect(Collectors.toList())); + } + + return collect.stream(); } }).collect(Collectors.toList()); } diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java index d2d623cffd..7c529e992f 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java @@ -154,7 +154,11 @@ public class PolicyService { } policyStore.findDependentPolicies(id).forEach(dependentPolicy -> { - dependentPolicy.removeAssociatedPolicy(policy); + if (dependentPolicy.getAssociatedPolicies().size() == 1) { + policyStore.delete(dependentPolicy.getId()); + } else { + dependentPolicy.removeAssociatedPolicy(policy); + } }); policyStore.delete(policy.getId()); @@ -271,7 +275,6 @@ public class PolicyService { } StoreFactory storeFactory = authorization.getStoreFactory(); - PolicyStore policyStore = storeFactory.getPolicyStore(); for (String scopeId : scopeIds) { boolean hasScope = false; diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java index 59ba844511..93c5ce806f 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java @@ -17,8 +17,6 @@ */ package org.keycloak.authorization.admin; -import org.jboss.resteasy.plugins.providers.multipart.InputPart; -import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.admin.util.Models; @@ -146,7 +144,11 @@ public class ResourceServerService { .stream().map(resource -> { ResourceRepresentation rep = Models.toRepresentation(resource, resourceServer, authorization); - rep.getOwner().setId(null); + if (rep.getOwner().getId().equals(this.resourceServer.getClientId())) { + rep.setOwner(null); + } else { + rep.getOwner().setId(null); + } rep.setId(null); rep.setPolicies(null); rep.getScopes().forEach(scopeRepresentation -> { @@ -175,15 +177,8 @@ public class ResourceServerService { ScopeRepresentation rep = Models.toRepresentation(scope, authorization); rep.setId(null); - - rep.getPolicies().forEach(policyRepresentation -> { - policyRepresentation.setId(null); - policyRepresentation.setConfig(null); - policyRepresentation.setType(null); - policyRepresentation.setDecisionStrategy(null); - policyRepresentation.setDescription(null); - policyRepresentation.setDependentPolicies(null); - }); + rep.setPolicies(null); + rep.setResources(null); return rep; }).collect(Collectors.toList()); @@ -258,131 +253,74 @@ public class ResourceServerService { String roles = config.get("roles"); if (roles != null && !roles.isEmpty()) { - roles = roles.replace("[", ""); - roles = roles.replace("]", ""); - - if (!roles.isEmpty()) { - String roleNames = ""; - - for (String role : roles.split(",")) { - if (!roleNames.isEmpty()) { - roleNames = roleNames + ","; - } - - role = role.replace("\"", ""); - - roleNames = roleNames + "\"" + this.realm.getRole(role).getId() + "\""; - } - - config.put("roles", "[" + roleNames + "]"); + try { + List rolesMap = JsonSerialization.readValue(roles, List.class); + config.put("roles", JsonSerialization.writeValueAsString(rolesMap.stream().map(roleConfig -> { + roleConfig.put("id", realm.getRole(roleConfig.get("id").toString()).getId()); + return roleConfig; + }).collect(Collectors.toList()))); + } catch (Exception e) { + throw new RuntimeException("Error while exporting policy [" + policyRepresentation.getName() + "].", e); } } String users = config.get("users"); - if (users != null) { - users = users.replace("[", ""); - users = users.replace("]", ""); - - if (!users.isEmpty()) { - String userNames = ""; - - for (String user : users.split(",")) { - if (!userNames.isEmpty()) { - userNames = userNames + ","; - } - - user = user.replace("\"", ""); - - userNames = userNames + "\"" + this.session.users().getUserByUsername(user, this.realm).getId() + "\""; - } - - config.put("users", "[" + userNames + "]"); + if (users != null && !users.isEmpty()) { + try { + List usersMap = JsonSerialization.readValue(users, List.class); + config.put("users", JsonSerialization.writeValueAsString(usersMap.stream().map(userName -> this.session.users().getUserByUsername(userName, this.realm).getId()).collect(Collectors.toList()))); + } catch (Exception e) { + throw new RuntimeException("Error while exporting policy [" + policyRepresentation.getName() + "].", e); } } String scopes = config.get("scopes"); if (scopes != null && !scopes.isEmpty()) { - scopes = scopes.replace("[", ""); - scopes = scopes.replace("]", ""); - - if (!scopes.isEmpty()) { - String scopeNames = ""; - - for (String scope : scopes.split(",")) { - if (!scopeNames.isEmpty()) { - scopeNames = scopeNames + ","; - } - - scope = scope.replace("\"", ""); - - Scope newScope = scopeStore.findByName(scope, resourceServer.getId()); + try { + List scopesMap = JsonSerialization.readValue(scopes, List.class); + config.put("scopes", JsonSerialization.writeValueAsString(scopesMap.stream().map(scopeName -> { + Scope newScope = scopeStore.findByName(scopeName, resourceServer.getId()); if (newScope == null) { - throw new RuntimeException("Scope with name [" + scope + "] not defined."); + throw new RuntimeException("Scope with name [" + scopeName + "] not defined."); } - scopeNames = scopeNames + "\"" + newScope.getId() + "\""; - } - - config.put("scopes", "[" + scopeNames + "]"); + return newScope.getId(); + }).collect(Collectors.toList()))); + } catch (Exception e) { + throw new RuntimeException("Error while exporting policy [" + policyRepresentation.getName() + "].", e); } } String policyResources = config.get("resources"); if (policyResources != null && !policyResources.isEmpty()) { - policyResources = policyResources.replace("[", ""); - policyResources = policyResources.replace("]", ""); - - if (!policyResources.isEmpty()) { - String resourceNames = ""; - - for (String resource : policyResources.split(",")) { - if (!resourceNames.isEmpty()) { - resourceNames = resourceNames + ","; - } - - resource = resource.replace("\"", ""); - - if ("".equals(resource)) { - continue; - } - - resourceNames = resourceNames + "\"" + storeFactory.getResourceStore().findByName(resource, resourceServer.getId()).getId() + "\""; - } - - config.put("resources", "[" + resourceNames + "]"); + try { + List resources = JsonSerialization.readValue(policyResources, List.class); + config.put("resources", JsonSerialization.writeValueAsString(resources.stream().map(resourceName -> storeFactory.getResourceStore().findByName(resourceName, resourceServer.getId()).getId()).collect(Collectors.toList()))); + } catch (Exception e) { + throw new RuntimeException("Error while exporting policy [" + policyRepresentation.getName() + "].", e); } } String applyPolicies = config.get("applyPolicies"); if (applyPolicies != null && !applyPolicies.isEmpty()) { - applyPolicies = applyPolicies.replace("[", ""); - applyPolicies = applyPolicies.replace("]", ""); - - if (!applyPolicies.isEmpty()) { - String policyNames = ""; - - for (String pId : applyPolicies.split(",")) { - if (!policyNames.isEmpty()) { - policyNames = policyNames + ","; - } - - pId = pId.replace("\"", "").trim(); - - Policy policy = policyStore.findByName(pId, resourceServer.getId()); + try { + List policies = JsonSerialization.readValue(applyPolicies, List.class); + config.put("applyPolicies", JsonSerialization.writeValueAsString(policies.stream().map(policyName -> { + Policy policy = policyStore.findByName(policyName, resourceServer.getId()); if (policy == null) { - throw new RuntimeException("Policy with name [" + pId + "] not defined."); + throw new RuntimeException("Policy with name [" + policyName + "] not defined."); } - policyNames = policyNames + "\"" + policy.getId() + "\""; - } - - config.put("applyPolicies", "[" + policyNames + "]"); + return policy.getId(); + }).collect(Collectors.toList()))); + } catch (Exception e) { + throw new RuntimeException("Error while exporting policy [" + policyRepresentation.getName() + "].", e); } } @@ -491,123 +429,59 @@ public class ResourceServerService { } private PolicyRepresentation createPolicyRepresentation(StoreFactory storeFactory, Policy policy) { - PolicyRepresentation rep = Models.toRepresentation(policy, authorization); + try { + PolicyRepresentation rep = Models.toRepresentation(policy, authorization); - rep.setId(null); - rep.setDependentPolicies(null); + rep.setId(null); + rep.setDependentPolicies(null); - Map config = rep.getConfig(); + Map config = rep.getConfig(); - String roles = config.get("roles"); + String roles = config.get("roles"); - if (roles != null && !roles.isEmpty()) { - roles = roles.replace("[", ""); - roles = roles.replace("]", ""); - - if (!roles.isEmpty()) { - String roleNames = ""; - - for (String role : roles.split(",")) { - if (!roleNames.isEmpty()) { - roleNames = roleNames + ","; - } - - role = role.replace("\"", ""); - - roleNames = roleNames + "\"" + this.realm.getRoleById(role).getName() + "\""; - } - - config.put("roles", "[" + roleNames + "]"); + if (roles != null && !roles.isEmpty()) { + List rolesMap = JsonSerialization.readValue(roles, List.class); + config.put("roles", JsonSerialization.writeValueAsString(rolesMap.stream().map(roleMap -> { + roleMap.put("id", realm.getRoleById(roleMap.get("id").toString()).getName()); + return roleMap; + }).collect(Collectors.toList()))); } - } - String users = config.get("users"); + String users = config.get("users"); - if (users != null) { - users = users.replace("[", ""); - users = users.replace("]", ""); - - if (!users.isEmpty()) { + if (users != null && !users.isEmpty()) { UserFederationManager userManager = this.session.users(); - String userNames = ""; - - for (String user : users.split(",")) { - if (!userNames.isEmpty()) { - userNames = userNames + ","; - } - - user = user.replace("\"", ""); - - userNames = userNames + "\"" + userManager.getUserById(user, this.realm).getUsername() + "\""; - } - - config.put("users", "[" + userNames + "]"); + List userIds = JsonSerialization.readValue(users, List.class); + config.put("users", JsonSerialization.writeValueAsString(userIds.stream().map(userId -> userManager.getUserById(userId, this.realm).getUsername()).collect(Collectors.toList()))); } - } - String scopes = config.get("scopes"); + String scopes = config.get("scopes"); - if (scopes != null && !scopes.isEmpty()) { - scopes = scopes.replace("[", ""); - scopes = scopes.replace("]", ""); - - if (!scopes.isEmpty()) { + if (scopes != null && !scopes.isEmpty()) { ScopeStore scopeStore = storeFactory.getScopeStore(); - String scopeNames = ""; - - for (String scope : scopes.split(",")) { - if (!scopeNames.isEmpty()) { - scopeNames = scopeNames + ","; - } - - scope = scope.replace("\"", ""); - - scopeNames = scopeNames + "\"" + scopeStore.findById(scope).getName() + "\""; - } - - config.put("scopes", "[" + scopeNames + "]"); + List scopeIds = JsonSerialization.readValue(scopes, List.class); + config.put("scopes", JsonSerialization.writeValueAsString(scopeIds.stream().map(scopeId -> scopeStore.findById(scopeId).getName()).collect(Collectors.toList()))); } - } - String policyResources = config.get("resources"); + String policyResources = config.get("resources"); - if (policyResources != null && !policyResources.isEmpty()) { - policyResources = policyResources.replace("[", ""); - policyResources = policyResources.replace("]", ""); - - if (!policyResources.isEmpty()) { + if (policyResources != null && !policyResources.isEmpty()) { ResourceStore resourceStore = storeFactory.getResourceStore(); - String resourceNames = ""; - - for (String resource : policyResources.split(",")) { - if (!resourceNames.isEmpty()) { - resourceNames = resourceNames + ","; - } - - resource = resource.replace("\"", ""); - - resourceNames = resourceNames + "\"" + resourceStore.findById(resource).getName() + "\""; - } - - config.put("resources", "[" + resourceNames + "]"); - } - } - - String policyNames = ""; - Set associatedPolicies = policy.getAssociatedPolicies(); - - if (!associatedPolicies.isEmpty()) { - for (Policy associatedPolicy : associatedPolicies) { - if (!policyNames.isEmpty()) { - policyNames = policyNames + ","; - } - - policyNames = policyNames + "\"" + associatedPolicy.getName() + "\""; + List resourceIds = JsonSerialization.readValue(policyResources, List.class); + config.put("resources", JsonSerialization.writeValueAsString(resourceIds.stream().map(resourceId -> resourceStore.findById(resourceId).getName()).collect(Collectors.toList()))); } - config.put("applyPolicies", "[" + policyNames + "]"); - } + Set associatedPolicies = policy.getAssociatedPolicies(); - return rep; + if (!associatedPolicies.isEmpty()) { + config.put("applyPolicies", JsonSerialization.writeValueAsString(associatedPolicies.stream().map(associated -> associated.getName()).collect(Collectors.toList()))); + } + + rep.setAssociatedPolicies(null); + + return rep; + } catch (Exception e) { + throw new RuntimeException("Error while exporting policy [" + policy.getName() + "].", e); + } } } diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java index 77fe01e2a4..9a3c9b361a 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java @@ -21,14 +21,10 @@ package org.keycloak.authorization.admin.representation; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.Decision.Effect; import org.keycloak.authorization.admin.util.Models; -import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.policy.evaluation.Result; import org.keycloak.authorization.policy.evaluation.Result.PolicyResult; -import org.keycloak.authorization.store.StoreFactory; -import org.keycloak.authorization.util.Permissions; -import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; @@ -90,9 +86,17 @@ public class PolicyEvaluationResponse { policies.add(toRepresentation(policy, authorization)); } + if (rep.getResource().getId() != null) { + if (!rep.getScopes().isEmpty()) { + rep.getResource().setName(rep.getResource().getName() + " with scopes " + rep.getScopes().stream().map(ScopeRepresentation::getName).collect(Collectors.toList())); + } + } + rep.setPolicies(policies); } + resultsRep.sort((o1, o2) -> o1.getResource().getName().compareTo(o2.getResource().getName())); + response.results = resultsRep; return response; diff --git a/services/src/main/java/org/keycloak/authorization/admin/util/Models.java b/services/src/main/java/org/keycloak/authorization/admin/util/Models.java index 60dc51eb8c..7e9d66d9e2 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/util/Models.java +++ b/services/src/main/java/org/keycloak/authorization/admin/util/Models.java @@ -63,13 +63,18 @@ public final class Models { scope.setId(model.getId()); scope.setName(model.getName()); scope.setIconUri(model.getIconUri()); + + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + + scope.setResources(new ArrayList<>()); + + storeFactory.getResourceStore().findByScope(model.getId()).forEach(resource -> scope.getResources().add(toRepresentation(resource, resource.getResourceServer(), authorizationProvider))); + + PolicyStore policyStore = storeFactory.getPolicyStore(); + scope.setPolicies(new ArrayList<>()); - Set policies = new HashSet<>(); - - policies.addAll(authorizationProvider.getStoreFactory().getPolicyStore().findByScopeIds(Arrays.asList(model.getId()), model.getResourceServer().getId())); - - for (Policy policyModel : policies) { + policyStore.findByScopeIds(Arrays.asList(model.getId()), model.getResourceServer().getId()).forEach(policyModel -> { PolicyRepresentation policy = new PolicyRepresentation(); policy.setId(policyModel.getId()); @@ -79,7 +84,7 @@ public final class Models { if (!scope.getPolicies().contains(policy)) { scope.getPolicies().add(policy); } - } + }); return scope; } 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 5ddd431279..c40c38a7fb 100644 --- a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java +++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java @@ -28,6 +28,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.representations.AccessToken; +import org.keycloak.saml.common.util.StringUtil; import org.keycloak.services.ErrorResponseException; import org.keycloak.util.JsonSerialization; @@ -37,6 +38,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; /** * @author Pedro Igor @@ -53,61 +55,59 @@ public class KeycloakIdentity implements Identity { } public KeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession) { - this.accessToken = accessToken; - - if (this.accessToken == null) { + if (accessToken == null) { throw new ErrorResponseException("invalid_bearer_token", "Could not obtain bearer access_token from request.", Status.FORBIDDEN); } - + if (keycloakSession == null) { + throw new ErrorResponseException("no_keycloak_session", "No keycloak session", Status.FORBIDDEN); + } + this.accessToken = accessToken; this.keycloakSession = keycloakSession; this.realm = keycloakSession.getContext().getRealm(); - HashMap> attributes = new HashMap<>(); + Map> attributes = new HashMap<>(); try { ObjectNode objectNode = JsonSerialization.createObjectNode(this.accessToken); Iterator iterator = objectNode.fieldNames(); - List roleNames = new ArrayList<>(); while (iterator.hasNext()) { String fieldName = iterator.next(); JsonNode fieldValue = objectNode.get(fieldName); List values = new ArrayList<>(); - values.add(fieldValue.asText()); + if (fieldValue.isArray()) { + Iterator valueIterator = fieldValue.iterator(); - if (fieldName.equals("realm_access")) { - JsonNode grantedRoles = fieldValue.get("roles"); - - if (grantedRoles != null) { - Iterator rolesIt = grantedRoles.iterator(); - - while (rolesIt.hasNext()) { - roleNames.add(rolesIt.next().asText()); - } + while (valueIterator.hasNext()) { + values.add(valueIterator.next().asText()); } + } else { + String value = fieldValue.asText(); + + if (StringUtil.isNullOrEmpty(value)) { + continue; + } + + values.add(value); } - if (fieldName.equals("resource_access")) { - Iterator resourceAccessIt = fieldValue.iterator(); - - while (resourceAccessIt.hasNext()) { - JsonNode grantedRoles = resourceAccessIt.next().get("roles"); - - if (grantedRoles != null) { - Iterator rolesIt = grantedRoles.iterator(); - - while (rolesIt.hasNext()) { - roleNames.add(rolesIt.next().asText()); - } - } - } + if (!values.isEmpty()) { + attributes.put(fieldName, values); } - - attributes.put(fieldName, values); } - attributes.put("roles", roleNames); + AccessToken.Access realmAccess = accessToken.getRealmAccess(); + + if (realmAccess != null) { + attributes.put("kc.realm.roles", realmAccess.getRoles()); + } + + Map resourceAccess = accessToken.getResourceAccess(); + + if (resourceAccess != null) { + resourceAccess.forEach((clientId, access) -> attributes.put("kc.client." + clientId + ".roles", access.getRoles())); + } } catch (Exception e) { throw new RuntimeException("Error while reading attributes from security token.", e); } 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 f3695d00ef..45fb6fe8f8 100644 --- a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java +++ b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java @@ -28,6 +28,7 @@ import org.keycloak.authorization.protection.permission.PermissionService; import org.keycloak.authorization.protection.permission.PermissionsService; import org.keycloak.authorization.protection.resource.ResourceService; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.ErrorResponseException; @@ -48,16 +49,12 @@ public class ProtectionService { @Path("/resource_set") public Object resource() { KeycloakIdentity identity = createIdentity(); - - if (!identity.hasRole("uma_protection")) { - throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN); - } - - ResourceSetService resourceManager = new ResourceSetService(getResourceServer(identity), this.authorization, null); + ResourceServer resourceServer = getResourceServer(identity); + ResourceSetService resourceManager = new ResourceSetService(resourceServer, this.authorization, null); ResteasyProviderFactory.getInstance().injectProperties(resourceManager); - ResourceService resource = new ResourceService(getResourceServer(identity), identity, resourceManager, this.authorization); + ResourceService resource = new ResourceService(resourceServer, identity, resourceManager, this.authorization); ResteasyProviderFactory.getInstance().injectProperties(resource); @@ -68,10 +65,6 @@ public class ProtectionService { public Object permission() { KeycloakIdentity identity = createIdentity(); - if (!identity.hasRole("uma_protection")) { - throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN); - } - PermissionService resource = new PermissionService(identity, getResourceServer(identity), this.authorization); ResteasyProviderFactory.getInstance().injectProperties(resource); @@ -83,10 +76,6 @@ public class ProtectionService { public Object permissions() { KeycloakIdentity identity = createIdentity(); - if (!identity.hasRole("uma_protection")) { - throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN); - } - PermissionsService resource = new PermissionsService(identity, getResourceServer(identity), this.authorization); ResteasyProviderFactory.getInstance().injectProperties(resource); @@ -95,7 +84,17 @@ public class ProtectionService { } private KeycloakIdentity createIdentity() { - return new KeycloakIdentity(this.authorization.getKeycloakSession()); + 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.getClientId()); + + 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) { 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 f2cce514b5..ed49697d60 100644 --- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java +++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java @@ -61,20 +61,38 @@ public final class Permissions { StoreFactory storeFactory = authorization.getStoreFactory(); ResourceStore resourceStore = storeFactory.getResourceStore(); - resourceStore.findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource))); - resourceStore.findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource))); + resourceStore.findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resourceServer, authorization))); + resourceStore.findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resourceServer, authorization))); return permissions; } - public static List createResourcePermissions(Resource resource) { + public static List createResourcePermissions(Resource resource, ResourceServer resourceServer, AuthorizationProvider authorization) { List permissions = new ArrayList<>(); List scopes = resource.getScopes(); - permissions.add(new ResourcePermission(resource, Collections.emptyList(), resource.getResourceServer())); + if (scopes.isEmpty()) { + String type = resource.getType(); - for (Scope scope : scopes) { - permissions.add(new ResourcePermission(resource, Arrays.asList(scope), resource.getResourceServer())); + // check if there is a typed resource whose scopes are inherited by the resource being requested. In this case, we assume that parent resource + // is owned by the resource server itself + if (type != null) { + StoreFactory storeFactory = authorization.getStoreFactory(); + ResourceStore resourceStore = storeFactory.getResourceStore(); + resourceStore.findByType(type).forEach(resource1 -> { + if (resource1.getOwner().equals(resourceServer.getClientId())) { + scopes.addAll(resource1.getScopes()); + } + }); + } + } + + if (scopes.size() > 1) { + for (Scope scope : scopes) { + permissions.add(new ResourcePermission(resource, Arrays.asList(scope), resource.getResourceServer())); + } + } else { + permissions.add(new ResourcePermission(resource, Collections.emptyList(), resource.getResourceServer())); } return permissions; 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 5ac24eaf3f..bec9faea7b 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 @@ -19,7 +19,7 @@ -Show Requesting Party Token | Show Access Token | Request Entitlements +Show Requesting Party Token | Show Access Token | Request Entitlements | 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 2990675a06..e58c5f55d7 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 @@ -64,6 +64,8 @@ module.controller('TokenCtrl', function ($scope, Identity) { $scope.requestEntitlements = function () { Identity.authorization.entitlement('photoz-restful-api').then(function (rpt) {}); } + + $scope.Identity = Identity; }); module.controller('AlbumCtrl', function ($scope, $http, $routeParams, $location, Album) { @@ -83,14 +85,13 @@ module.controller('ProfileCtrl', function ($scope, $http, $routeParams, $locatio $scope.profile = Profile.get(); }); -module.controller('AdminAlbumCtrl', function ($scope, $http, $route, AdminAlbum, Album) { +module.controller('AdminAlbumCtrl', function ($scope, $http, $route, $location, AdminAlbum, Album) { $scope.albums = {}; $http.get(apiUrl + '/admin/album').success(function (data) { $scope.albums = data; }); $scope.deleteAlbum = function (album) { - var newAlbum = new Album(album); - newAlbum.$delete({id: album.id}, function () { + new Album(album).$delete({id: album.id}, function () { $route.reload(); }); } diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html index da78224f91..00f52d7018 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html @@ -1,19 +1,19 @@

All Albums

- - - + + + - - - + + +
{{key}}
{{key}}
- -
+ +
\ No newline at end of file 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 bd5853ea8e..e144d1b240 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 @@ -1,4 +1,4 @@ -

Welcome To Photoz, {{Identity.claims.name}} [Sign Out]

+

Welcome To Photoz, {{Identity.claims.name}}

Administration: [All Albums]


@@ -9,14 +9,14 @@ You don't have any albums, yet. - - - + + + - - - + + +
Your Albums
Your Albums
{{p.name}} - [X]
{{p.name}} - [X]
\ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json index b3b2b8132a..baa8f66330 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json @@ -53,7 +53,7 @@ } ], "realmRoles": [ - "user", "admin", "uma_authorization" + "admin", "uma_authorization" ], "clientRoles": { "realm-management": [ 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 cc8c8f866f..6c786e7577 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 @@ -21,10 +21,10 @@ "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:delete" + "name": "urn:photoz.com:scopes:album:create" } ] }, @@ -44,12 +44,15 @@ "name": "Only Owner Policy", "description": "Defines that only the resource owner is allowed to do something", "type": "drools", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", "config": { "mavenArtifactVersion": "2.1.0-SNAPSHOT", "mavenArtifactId": "photoz-authz-policy", "sessionName": "MainOwnerSession", - "mavenArtifactGroupId": "org.keycloak.testsuite", + "mavenArtifactGroupId": "org.keycloak", "moduleName": "PhotozAuthzOwnerPolicy", + "applyPolicies": "[]", "scannerPeriod": "1", "scannerPeriodUnit": "Hours" } @@ -58,16 +61,22 @@ "name": "Any Admin Policy", "description": "Defines that adminsitrators can do something", "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", "config": { - "roles": "[\"admin\"]" + "applyPolicies": "[]", + "roles": "[{\"id\":\"admin\",\"required\":true}]" } }, { "name": "Any User Policy", "description": "Defines that any user can do something", "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", "config": { - "roles": "[\"user\"]" + "applyPolicies": "[]", + "roles": "[{\"id\":\"user\"}]" } }, { @@ -77,6 +86,7 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { + "applyPolicies": "[]", "code": "var contextAttributes = $evaluation.getContext().getAttributes();\n\nif (contextAttributes.containsValue('kc.client.network.ip_address', '127.0.0.1')) {\n $evaluation.grant();\n}" } }, @@ -84,6 +94,8 @@ "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\"]" } @@ -92,35 +104,28 @@ "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\"]" + "applyPolicies": "[\"Only Owner Policy\",\"Administration Policy\"]" } }, { "name": "Only From @keycloak.org or Admin", "description": "Defines that only users from @keycloak.org", "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", "config": { "applyPolicies": "[]", "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}" } }, - { - "name": "Only in the Period", - "description": "Access granted only during the morning", - "type": "time", - "config": { - "noa": "2016-01-03 23:59:59", - "expirationUnit": "Minutes", - "nbf": "2016-01-01 00:00:00", - "expirationTime": "1" - } - }, { "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", 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 388c9e4c8b..c28bca3bc0 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 @@ -109,13 +109,7 @@ public class AlbumService { private void createProtectedResource(Album album) { try { - HashSet scopes = new HashSet<>(); - - scopes.add(new ScopeRepresentation(SCOPE_ALBUM_VIEW)); - scopes.add(new ScopeRepresentation(SCOPE_ALBUM_CREATE)); - scopes.add(new ScopeRepresentation(SCOPE_ALBUM_DELETE)); - - ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album"); + ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), new HashSet(), "/album/" + album.getId(), "http://photoz.com/album"); albumResource.setOwner(album.getUserId()); 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 09bf6a92a4..0c3b108976 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 @@ -76,17 +76,28 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl { pause(500); } - public void login(String username, String password) { + public void login(String username, String password) throws InterruptedException { navigateTo(); - + Thread.sleep(2000); if (this.driver.getCurrentUrl().startsWith(getInjectedUrl().toString())) { + Thread.sleep(2000); logOut(); + navigateTo(); } + Thread.sleep(2000); + this.loginPage.form().login(username, password); } public boolean wasDenied() { return this.driver.findElement(By.id("output")).getText().contains("You can not access"); } + + public void viewAlbum(String name) { + By id = By.id("view-" + name); + WaitUtils.waitUntilElement(id); + this.driver.findElements(id).forEach(WebElement::click); + pause(500); + } } 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 32f23bed49..59a7a31071 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,12 +16,10 @@ */ package org.keycloak.testsuite.adapter.example.authorization; -import org.apache.commons.io.IOUtils; import org.jboss.arquillian.container.test.api.Deployer; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.test.api.ArquillianResource; -import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Test; import org.keycloak.admin.client.resource.AuthorizationResource; @@ -39,7 +37,6 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; -import java.util.function.Consumer; import java.util.stream.Collectors; import static org.junit.Assert.assertFalse; @@ -144,7 +141,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } - this.clientPage.login("admin", "admin"); this.clientPage.navigateToAdminAlbum(); this.clientPage.deleteAlbum("Alice-Family-Album"); @@ -186,7 +182,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } - this.clientPage.login("admin", "admin"); this.clientPage.navigateToAdminAlbum(); assertTrue(this.clientPage.wasDenied()); @@ -195,6 +190,121 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } + @Test + public void testAdminWithoutPermissionsToTypedResource() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); + this.clientPage.createAlbum("Alice Family Album"); + + this.clientPage.login("admin", "admin"); + this.clientPage.navigateToAdminAlbum(); + + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.viewAlbum("Alice Family Album"); + + assertFalse(this.clientPage.wasDenied()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Album Resource Permission".equals(policy.getName())) { + policy.getConfig().put("applyPolicies", "[\"Any User Policy\"]"); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.viewAlbum("Alice Family Album"); + + assertTrue(this.clientPage.wasDenied()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Album Resource Permission".equals(policy.getName())) { + policy.getConfig().put("applyPolicies", "[\"Any User Policy\", \"Administration Policy\"]"); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.viewAlbum("Alice Family Album"); + + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.deleteAlbum("Alice Family Album"); + + List resources = getAuthorizationResource().resources().resources(); + + assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + @Test + public void testAdminWithoutPermissionsToDeleteScopePermission() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); + this.clientPage.createAlbum("Alice Family Album"); + + this.clientPage.login("admin", "admin"); + this.clientPage.navigateToAdminAlbum(); + + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.deleteAlbum("Alice Family Album"); + + assertFalse(this.clientPage.wasDenied()); + + List resources = getAuthorizationResource().resources().resources(); + + assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Delete Album Permission".equals(policy.getName())) { + policy.getConfig().put("applyPolicies", "[\"Only Owner Policy\"]"); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + this.clientPage.login("alice", "alice"); + this.clientPage.createAlbum("Alice Family Album"); + + this.clientPage.login("admin", "admin"); + this.clientPage.navigateToAdminAlbum(); + this.clientPage.viewAlbum("Alice Family Album"); + + assertFalse(this.clientPage.wasDenied()); + resources = getAuthorizationResource().resources().resources(); + + assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + + this.clientPage.navigateToAdminAlbum(); + + this.clientPage.deleteAlbum("Alice Family Album"); + assertTrue(this.clientPage.wasDenied()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Delete Album Permission".equals(policy.getName())) { + policy.getConfig().put("applyPolicies", "[\"Only Owner and Administrators Policy\"]"); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.deleteAlbum("Alice Family Album"); + + assertFalse(this.clientPage.wasDenied()); + + resources = getAuthorizationResource().resources().resources(); + + assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + private void importResourceServerSettings() throws FileNotFoundException { getAuthorizationResource().importSettings(loadJson(new FileInputStream(new File(TEST_APPS_HOME_DIR + "/photoz/photoz-restful-api-authz-service.json")), ResourceServerRepresentation.class)); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java index 31b221b4a1..1faf21e4c0 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java @@ -49,6 +49,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import java.io.IOException; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -56,6 +57,8 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import static org.jboss.aesh.terminal.Key.e; + /** * @author Pedro Igor */ @@ -279,8 +282,12 @@ public abstract class AbstractPhotozAdminTest extends AbstractAuthorizationTest RealmModel realm = authorizationProvider.getKeycloakSession().realms().getRealmByName(TEST_REALM_NAME); RoleModel adminRole = realm.getRole("admin"); + Map role = new HashMap(); + + role.put("id", adminRole.getId()); + try { - config.put("roles", JsonSerialization.writeValueAsString(new String[] {adminRole.getId()})); + config.put("roles", JsonSerialization.writeValueAsString(new Map[] {role})); } catch (IOException e) { throw new RuntimeException(e); } @@ -352,10 +359,14 @@ public abstract class AbstractPhotozAdminTest extends AbstractAuthorizationTest Policy policy = policyStore.create("Any User Policy", "role", resourceServer); HashedMap config = new HashedMap(); RealmModel realm = authorizationProvider.getKeycloakSession().realms().getRealmByName(TEST_REALM_NAME); - RoleModel adminRole = realm.getRole("user"); + RoleModel userRole = realm.getRole("user"); + + Map role = new HashMap(); + + role.put("id", userRole.getId()); try { - config.put("roles", JsonSerialization.writeValueAsString(new String[] {adminRole.getId()})); + config.put("roles", JsonSerialization.writeValueAsString(new Map[] {role})); } catch (IOException e) { throw new RuntimeException(e); } 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 fbfe64b54d..50d591960b 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 @@ -953,6 +953,7 @@ authz-no-resources=No resources authz-result=Result authz-authorization-services-enabled=Authorization Enabled authz-authorization-services-enabled.tooltip=Enable/Disable fine-grained authorization support for a client +authz-required=Required # Authz Settings authz-import-config.tooltip=Import a JSON file containing authorization settings for this resource server. @@ -1016,7 +1017,9 @@ authz-select-a-policy=Select a policy # Authz Role Policy Detail authz-add-role-policy=Add Role Policy authz-no-roles-assigned=No roles assigned. -authz-policy-role-roles.tooltip=Specifies which role(s) are allowed by this policy. +authz-policy-role-realm-roles.tooltip=Specifies which *realm* role(s) are allowed by this policy. +authz-policy-role-clients.tooltip=Selects a client in order to filter the client roles that can be applied to this policy. +authz-policy-role-client-roles.tooltip=Specifies which *client* role(s) are allowed by this policy. # Authz User Policy Detail authz-add-user-policy=Add User Policy diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js index 680cb5b837..bbe0fd0a45 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js @@ -150,6 +150,14 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r client : client.id, rsrid : $route.current.params.rsrid, }, function(data) { + if (!data.scopes) { + data.scopes = []; + } + + if (!data.policies) { + data.policies = []; + } + $scope.resource = angular.copy(data); $scope.changed = false; @@ -157,9 +165,7 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r $scope.resource.scopes[i] = $scope.resource.scopes[i].name; } - data = angular.copy($scope.resource); - - $scope.originalResource = data; + $scope.originalResource = angular.copy($scope.resource); $scope.$watch('resource', function() { if (!angular.equals($scope.resource, data)) { @@ -237,6 +243,10 @@ module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $lo ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) { $scope.scopes = data; }); + + $scope.createPolicy = function(scope) { + $location.path('/realms/' + $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/permission/scope/create').search({scpid: scope.id}); + } }); }); @@ -534,7 +544,7 @@ module.controller('ResourceServerPolicyResourceDetailCtrl', function($scope, $ro }, realm, client, $scope); }); -module.controller('ResourceServerPolicyScopeDetailCtrl', function($scope, $route, realm, client, PolicyController, ResourceServerPolicy, ResourceServerResource, ResourceServerScope) { +module.controller('ResourceServerPolicyScopeDetailCtrl', function($scope, $route, $location, realm, client, PolicyController, ResourceServerPolicy, ResourceServerResource, ResourceServerScope) { PolicyController.onInit({ getPolicyType : function() { return "scope"; @@ -624,6 +634,12 @@ module.controller('ResourceServerPolicyScopeDetailCtrl', function($scope, $route newPolicy.decisionStrategy = 'UNANIMOUS'; newPolicy.config = {}; newPolicy.config.resources = ''; + + var scopeId = $location.search()['scpid']; + + if (scopeId) { + newPolicy.config.scopes = [scopeId]; + } }, onCreate : function() { @@ -712,7 +728,7 @@ module.controller('ResourceServerPolicyUserDetailCtrl', function($scope, $route, }, realm, client, $scope); }); -module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, realm, client, PolicyController, Role, RoleById) { +module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, realm, client, Client, ClientRole, PolicyController, Role, RoleById) { PolicyController.onInit({ getPolicyType : function() { return "role"; @@ -723,6 +739,10 @@ module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, $scope.roles = data; }); + Client.query({realm: $route.current.params.realm}, function (data) { + $scope.clients = data; + }); + $scope.selectedRoles = []; $scope.selectRole = function(role) { @@ -732,10 +752,55 @@ module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, $scope.selectedRole = {}; $scope.selectedRoles.push(role); + + var clientRoles = []; + + if ($scope.clientRoles) { + for (i = 0; i < $scope.clientRoles.length; i++) { + if ($scope.clientRoles[i].id != role.id) { + clientRoles.push($scope.clientRoles[i]); + } + } + $scope.clientRoles = clientRoles; + } } - $scope.removeFromList = function(list, index) { - list.splice(index, 1); + $scope.removeFromList = function(role) { + if ($scope.clientRoles && $scope.selectedClient && $scope.selectedClient.id == role.containerId) { + $scope.clientRoles.push(role); + } + var index = $scope.selectedRoles.indexOf(role); + if (index != -1) { + $scope.selectedRoles.splice(index, 1); + } + } + + $scope.selectClient = function() { + if (!$scope.selectedClient) { + $scope.clientRoles = []; + return; + } + ClientRole.query({realm: $route.current.params.realm, client: $scope.selectedClient.id}, function(data) { + var roles = []; + + for (j = 0; j < data.length; j++) { + var defined = false; + + for (i = 0; i < $scope.selectedRoles.length; i++) { + if ($scope.selectedRoles[i].id == data[j].id) { + defined = true; + break; + } + } + + if (!defined) { + data[j].container = {}; + data[j].container.name = $scope.selectedClient.clientId; + roles.push(data[j]); + } + } + $scope.clientRoles = roles; + }); } }, @@ -746,7 +811,18 @@ module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, var roles = eval(policy.config.roles); for (i = 0; i < roles.length; i++) { - RoleById.get({realm: $route.current.params.realm, role: roles[i]}, function(data) { + RoleById.get({realm: $route.current.params.realm, role: roles[i].id}, function(data) { + for (i = 0; i < roles.length; i++) { + if (roles[i].id == data.id) { + data.required = roles[i].required ? true : false; + } + } + for (i = 0; i < $scope.clients.length; i++) { + if ($scope.clients[i].id == data.containerId) { + data.container = {}; + data.container.name = $scope.clients[i].clientId; + } + } selectedRoles.push(data); $scope.selectedRoles = angular.copy(selectedRoles); }); @@ -764,7 +840,12 @@ module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, var roles = []; for (i = 0; i < $scope.selectedRoles.length; i++) { - roles.push($scope.selectedRoles[i].id); + var role = {}; + role.id = $scope.selectedRoles[i].id; + if ($scope.selectedRoles[i].required) { + role.required = $scope.selectedRoles[i].required; + } + roles.push(role); } $scope.policy.config.roles = JSON.stringify(roles); @@ -774,12 +855,35 @@ module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, var roles = []; for (i = 0; i < $scope.selectedRoles.length; i++) { - roles.push($scope.selectedRoles[i].id); + var role = {}; + role.id = $scope.selectedRoles[i].id; + if ($scope.selectedRoles[i].required) { + role.required = $scope.selectedRoles[i].required; + } + roles.push(role); } $scope.policy.config.roles = JSON.stringify(roles); } }, realm, client, $scope); + + $scope.hasRealmRole = function () { + for (i = 0; i < $scope.selectedRoles.length; i++) { + if (!$scope.selectedRoles[i].clientRole) { + return true; + } + } + return false; + } + + $scope.hasClientRole = function () { + for (i = 0; i < $scope.selectedRoles.length; i++) { + if ($scope.selectedRoles[i].clientRole) { + return true; + } + } + return false; + } }); module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) { diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-role-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-role-detail.html index 9b0c199d23..8904bb35b3 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-role-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-role-detail.html @@ -49,31 +49,85 @@ {{:: 'authz-policy-description.tooltip' | translate}}
- + -
+
- {{:: 'authz-policy-role-roles.tooltip' | translate}} + {{:: 'authz-policy-role-realm-roles.tooltip' | translate}}
-
+
- - + + + + + + + + + + + + + + + + +
{{:: 'name' | translate}}
{{:: 'name' | translate}}{{:: 'authz-required' | translate}}{{:: 'actions' | translate}}
{{role.name}} + +
{{:: 'authz-no-roles-assigned' | translate}}
+
+
+
+ + +
+ +
+ {{:: 'authz-policy-role-clients.tooltip' | translate}} +
+
+ + +
+ +
+ + {{:: 'authz-policy-role-client-roles.tooltip' | translate}} +
+
+ +
+ + + + + + - + + + @@ -98,7 +152,6 @@ -
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html index 8a748f4702..524d55d371 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html @@ -43,14 +43,14 @@
  • {{policyResult.policy.name}} + href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{policyResult.policy.type}}/{{policyResult.policy.id}}">{{policyResult.policy.name}} decision was {{policyResult.status}} {{policyResult.status}} by {{policyResult.policy.decisionStrategy}} decision.
    • {{subPolicy.policy.name}} + href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy/{{subPolicy.policy.type}}/{{subPolicy.policy.id}}">{{subPolicy.policy.name}} voted to {{subPolicy.status}} {{subPolicy.status}}. diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html index 8d1b0e1979..39cfc776f3 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html @@ -24,11 +24,33 @@
+ + + + + + From 8cfa50f13410f0e3078f49c288db23b77e9ae813 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 28 Jul 2016 12:31:46 -0300 Subject: [PATCH 05/13] [KEYCLOAK-3338] More testing and improvements when importing role policies --- examples/authz/photoz/photoz-realm.json | 19 ++- .../photoz-restful-api-authz-service.json | 8 +- .../admin/ResourceServerService.java | 33 ++++- .../test-apps/photoz/photoz-realm.json | 19 ++- .../photoz-restful-api-authz-service.json | 8 +- .../page/PhotozClientAuthzTestApp.java | 47 +++++- .../AbstractPhotozExampleAdapterTest.java | 138 +++++++++++++++++- 7 files changed, 251 insertions(+), 21 deletions(-) diff --git a/examples/authz/photoz/photoz-realm.json b/examples/authz/photoz/photoz-realm.json index baa8f66330..b0aeb5d37d 100644 --- a/examples/authz/photoz/photoz-realm.json +++ b/examples/authz/photoz/photoz-realm.json @@ -22,7 +22,12 @@ ], "realmRoles": [ "user", "uma_authorization" - ] + ], + "clientRoles": { + "photoz-restful-api": [ + "manage-albums" + ] + } }, { "username": "jdoe", @@ -38,7 +43,12 @@ ], "realmRoles": [ "user", "uma_authorization" - ] + ], + "clientRoles": { + "photoz-restful-api": [ + "manage-albums" + ] + } }, { "username": "admin", @@ -58,6 +68,9 @@ "clientRoles": { "realm-management": [ "realm-admin" + ], + "photoz-restful-api": [ + "manage-albums" ] } }, @@ -90,6 +103,8 @@ "adminUrl": "/photoz-html5-client", "baseUrl": "/photoz-html5-client", "publicClient": true, + "consentRequired" : true, + "fullScopeAllowed" : true, "redirectUris": [ "/photoz-html5-client/*" ], diff --git a/examples/authz/photoz/photoz-restful-api-authz-service.json b/examples/authz/photoz/photoz-restful-api-authz-service.json index 6c786e7577..6547d2fcc4 100644 --- a/examples/authz/photoz/photoz-restful-api-authz-service.json +++ b/examples/authz/photoz/photoz-restful-api-authz-service.json @@ -70,13 +70,13 @@ }, { "name": "Any User Policy", - "description": "Defines that any user can do something", + "description": "Defines that only users from well known clients are allowed to access", "type": "role", "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { "applyPolicies": "[]", - "roles": "[{\"id\":\"user\"}]" + "roles": "[{\"id\":\"user\"},{\"id\":\"manage-albums\",\"required\":true}]" } }, { @@ -97,7 +97,7 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "applyPolicies": "[\"Any Admin Policy\",\"Only From a Specific Client Address\"]" + "applyPolicies": "[\"Only From a Specific Client Address\",\"Any Admin Policy\"]" } }, { @@ -107,7 +107,7 @@ "logic": "POSITIVE", "decisionStrategy": "AFFIRMATIVE", "config": { - "applyPolicies": "[\"Only Owner Policy\",\"Administration Policy\"]" + "applyPolicies": "[\"Administration Policy\",\"Only Owner Policy\"]" } }, { diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java index 93c5ce806f..567675f23d 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java @@ -45,6 +45,7 @@ import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.services.resources.admin.RealmAuth; import org.keycloak.util.JsonSerialization; +import javax.management.relation.Role; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -61,6 +62,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -256,7 +259,35 @@ public class ResourceServerService { try { List rolesMap = JsonSerialization.readValue(roles, List.class); config.put("roles", JsonSerialization.writeValueAsString(rolesMap.stream().map(roleConfig -> { - roleConfig.put("id", realm.getRole(roleConfig.get("id").toString()).getId()); + String roleName = roleConfig.get("id").toString(); + String clientId = null; + int clientIdSeparator = roleName.indexOf("/"); + + if (clientIdSeparator != -1) { + clientId = roleName.substring(0, clientIdSeparator); + roleName = roleName.substring(clientIdSeparator + 1); + } + + RoleModel role; + + if (clientId == null) { + role = realm.getRole(roleName); + } else { + role = realm.getClientByClientId(clientId).getRole(roleName); + } + + // fallback to find any client role with the given name + if (role == null) { + String finalRoleName = roleName; + role = realm.getClients().stream().map(clientModel -> clientModel.getRole(finalRoleName)).filter(roleModel -> roleModel != null) + .findFirst().orElse(null); + } + + if (role == null) { + throw new RuntimeException("Error while importing configuration. Role [" + role + "] could not be found."); + } + + roleConfig.put("id", role.getId()); return roleConfig; }).collect(Collectors.toList()))); } catch (Exception e) { diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json index baa8f66330..b0aeb5d37d 100644 --- a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json @@ -22,7 +22,12 @@ ], "realmRoles": [ "user", "uma_authorization" - ] + ], + "clientRoles": { + "photoz-restful-api": [ + "manage-albums" + ] + } }, { "username": "jdoe", @@ -38,7 +43,12 @@ ], "realmRoles": [ "user", "uma_authorization" - ] + ], + "clientRoles": { + "photoz-restful-api": [ + "manage-albums" + ] + } }, { "username": "admin", @@ -58,6 +68,9 @@ "clientRoles": { "realm-management": [ "realm-admin" + ], + "photoz-restful-api": [ + "manage-albums" ] } }, @@ -90,6 +103,8 @@ "adminUrl": "/photoz-html5-client", "baseUrl": "/photoz-html5-client", "publicClient": true, + "consentRequired" : true, + "fullScopeAllowed" : true, "redirectUris": [ "/photoz-html5-client/*" ], 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 6c786e7577..6547d2fcc4 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 @@ -70,13 +70,13 @@ }, { "name": "Any User Policy", - "description": "Defines that any user can do something", + "description": "Defines that only users from well known clients are allowed to access", "type": "role", "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { "applyPolicies": "[]", - "roles": "[{\"id\":\"user\"}]" + "roles": "[{\"id\":\"user\"},{\"id\":\"manage-albums\",\"required\":true}]" } }, { @@ -97,7 +97,7 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "applyPolicies": "[\"Any Admin Policy\",\"Only From a Specific Client Address\"]" + "applyPolicies": "[\"Only From a Specific Client Address\",\"Any Admin Policy\"]" } }, { @@ -107,7 +107,7 @@ "logic": "POSITIVE", "decisionStrategy": "AFFIRMATIVE", "config": { - "applyPolicies": "[\"Only Owner Policy\",\"Administration Policy\"]" + "applyPolicies": "[\"Administration Policy\",\"Only Owner Policy\"]" } }, { 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 0c3b108976..4721737daa 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 @@ -22,11 +22,13 @@ import org.jboss.arquillian.test.api.ArquillianResource; import org.keycloak.testsuite.auth.page.login.OIDCLogin; import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; import org.keycloak.testsuite.page.Form; +import org.keycloak.testsuite.pages.ConsentPage; import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import java.net.URL; +import java.util.List; import static org.keycloak.testsuite.util.WaitUtils.pause; @@ -44,6 +46,9 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl { @Page protected OIDCLogin loginPage; + @Page + protected ConsentPage consentPage; + public void createAlbum(String name) { this.driver.findElement(By.id("create-album")).click(); Form.setInputValue(this.driver.findElement(By.id("album.name")), name); @@ -88,13 +93,53 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl { Thread.sleep(2000); this.loginPage.form().login(username, password); + + // simple check if we are at the consent page, if so just click 'Yes' + if (this.consentPage.isCurrent()) { + consentPage.confirm(); + Thread.sleep(2000); + } + } + + public void loginWithScopes(String username, String password, String... scopes) throws Exception { + navigateTo(); + Thread.sleep(2000); + if (this.driver.getCurrentUrl().startsWith(getInjectedUrl().toString())) { + Thread.sleep(2000); + logOut(); + navigateTo(); + } + + Thread.sleep(2000); + + StringBuilder scopesValue = new StringBuilder(); + + for (String scope : scopes) { + if (scopesValue.length() != 0) { + scopesValue.append(" "); + } + scopesValue.append(scope); + } + + this.driver.navigate().to(this.driver.getCurrentUrl() + " " + scopesValue); + + Thread.sleep(2000); + + this.loginPage.form().login(username, password); + + // simple check if we are at the consent page, if so just click 'Yes' + if (this.consentPage.isCurrent()) { + consentPage.confirm(); + Thread.sleep(2000); + } } public boolean wasDenied() { return this.driver.findElement(By.id("output")).getText().contains("You can not access"); } - public void viewAlbum(String name) { + public void viewAlbum(String name) throws InterruptedException { + Thread.sleep(2000); By id = By.id("view-" + name); WaitUtils.waitUntilElement(id); this.driver.findElements(id).forEach(WebElement::click); 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 59a7a31071..8a8d483a24 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 @@ -23,20 +23,30 @@ import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Test; 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.RoleResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; 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.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import static org.junit.Assert.assertFalse; @@ -84,7 +94,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd importResourceServerSettings(); } - @Test public void testCreateDeleteAlbum() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -106,7 +115,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } - @Test public void testOnlyOwnerCanDeleteAlbum() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -152,7 +160,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } - @Test public void testRegularUserCanNotAccessAdminResources() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -165,7 +172,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } - @Test public void testAdminOnlyFromSpecificAddress() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -211,6 +217,23 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd policy.getConfig().put("applyPolicies", "[\"Any User Policy\"]"); getAuthorizationResource().policies().policy(policy.getId()).update(policy); } + if ("Any User Policy".equals(policy.getName())) { + ClientResource resourceServerClient = getClientResource(RESOURCE_SERVER_ID); + RoleResource manageAlbumRole = resourceServerClient.roles().get("manage-albums"); + RoleRepresentation roleRepresentation = manageAlbumRole.toRepresentation(); + List roles = JsonSerialization.readValue(policy.getConfig().get("roles"), List.class); + + roles = roles.stream().filter(new Predicate() { + @Override + public boolean test(Map map) { + return !map.get("id").equals(roleRepresentation.getId()); + } + }).collect(Collectors.toList()); + + policy.getConfig().put("roles", JsonSerialization.writeValueAsString(roles)); + + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } } this.clientPage.navigateToAdminAlbum(); @@ -241,7 +264,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } - @Test public void testAdminWithoutPermissionsToDeleteScopePermission() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -305,13 +327,115 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } + public void testClientRoleRepresentingUserConsent() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); + + assertFalse(this.clientPage.wasDenied()); + + UsersResource usersResource = realmsResouce().realm(REALM_NAME).users(); + List users = usersResource.search("alice", null, null, null, null, null); + + assertFalse(users.isEmpty()); + + UserRepresentation userRepresentation = users.get(0); + UserResource userResource = usersResource.get(userRepresentation.getId()); + + ClientResource html5ClientApp = getClientResource("photoz-html5-client"); + + userResource.revokeConsent(html5ClientApp.toRepresentation().getClientId()); + + ClientResource resourceServerClient = getClientResource(RESOURCE_SERVER_ID); + RoleResource roleResource = resourceServerClient.roles().get("manage-albums"); + RoleRepresentation roleRepresentation = roleResource.toRepresentation(); + + roleRepresentation.setScopeParamRequired(true); + + roleResource.update(roleRepresentation); + + this.clientPage.login("alice", "alice"); + + assertTrue(this.clientPage.wasDenied()); + + this.clientPage.loginWithScopes("alice", "alice", RESOURCE_SERVER_ID + "/manage-albums"); + + assertFalse(this.clientPage.wasDenied()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + public void testClientRoleNotRequired() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); + + assertFalse(this.clientPage.wasDenied()); + + UsersResource usersResource = realmsResouce().realm(REALM_NAME).users(); + List users = usersResource.search("alice", null, null, null, null, null); + + assertFalse(users.isEmpty()); + + UserRepresentation userRepresentation = users.get(0); + UserResource userResource = usersResource.get(userRepresentation.getId()); + + ClientResource html5ClientApp = getClientResource("photoz-html5-client"); + + userResource.revokeConsent(html5ClientApp.toRepresentation().getClientId()); + + ClientResource resourceServerClient = getClientResource(RESOURCE_SERVER_ID); + RoleResource manageAlbumRole = resourceServerClient.roles().get("manage-albums"); + RoleRepresentation roleRepresentation = manageAlbumRole.toRepresentation(); + + roleRepresentation.setScopeParamRequired(true); + + manageAlbumRole.update(roleRepresentation); + + this.clientPage.login("alice", "alice"); + + assertTrue(this.clientPage.wasDenied()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Any User Policy".equals(policy.getName())) { + List roles = JsonSerialization.readValue(policy.getConfig().get("roles"), List.class); + + roles.forEach(new Consumer() { + @Override + public void accept(Map role) { + String roleId = (String) role.get("id"); + if (roleId.equals(manageAlbumRole.toRepresentation().getId())) { + role.put("required", false); + } + } + }); + + policy.getConfig().put("roles", JsonSerialization.writeValueAsString(roles)); + + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + this.clientPage.login("alice", "alice"); + + assertFalse(this.clientPage.wasDenied()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + private void importResourceServerSettings() throws FileNotFoundException { getAuthorizationResource().importSettings(loadJson(new FileInputStream(new File(TEST_APPS_HOME_DIR + "/photoz/photoz-restful-api-authz-service.json")), ResourceServerRepresentation.class)); } private AuthorizationResource getAuthorizationResource() throws FileNotFoundException { + return getClientResource(RESOURCE_SERVER_ID).authorization(); + } + + private ClientResource getClientResource(String clientId) { ClientsResource clients = this.realmsResouce().realm(REALM_NAME).clients(); - ClientRepresentation resourceServer = clients.findByClientId(RESOURCE_SERVER_ID).get(0); - return clients.get(resourceServer.getId()).authorization(); + ClientRepresentation resourceServer = clients.findByClientId(clientId).get(0); + return clients.get(resourceServer.getId()); } } \ No newline at end of file From 3c8ed8e3d8463a16767bc8cde3dbb0a577a92b7b Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 29 Jul 2016 05:18:38 -0300 Subject: [PATCH 06/13] [KEYCLOAK-3372] - Code cleanup --- .../authorization/AbstractPolicyEnforcer.java | 27 +++-- .../KeycloakAdapterPolicyEnforcer.java | 2 +- .../authorization/PolicyEnforcer.java | 2 - .../adapters/config/PolicyEnforcerConfig.java | 7 +- .../authorization/PolicyRepresentation.java | 3 + .../photoz-restful-api-authz-service.json | 5 - .../example/photoz/album/AlbumService.java | 7 +- .../src/main/webapp/WEB-INF/keycloak.json | 13 ++- .../servlet-authz-app-config.json | 6 +- .../authorization/attribute/Attributes.java | 3 +- .../evaluation/DefaultPolicyEvaluator.java | 50 ++++++--- .../admin/PolicyEvaluationService.java | 12 +- .../AuthorizationTokenService.java | 18 ++- .../entitlement/EntitlementService.java | 19 ++-- .../authorization/util/Permissions.java | 105 +++++++++--------- .../example/photoz/album/AlbumService.java | 7 +- .../src/main/webapp/WEB-INF/keycloak.json | 15 ++- .../AbstractPhotozExampleAdapterTest.java | 7 ++ 18 files changed, 179 insertions(+), 129 deletions(-) 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 6b1fe19fd8..0c0fc236f9 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 @@ -126,11 +126,13 @@ public abstract class AbstractPolicyEnforcer { List permissions = authorization.getPermissions(); for (Permission permission : permissions) { - Set allowedScopes = permission.getScopes(); - if (permission.getResourceSetId() != null) { if (isResourcePermission(actualPathConfig, permission)) { - if (((allowedScopes == null || allowedScopes.isEmpty()) && requiredScopes.isEmpty()) || allowedScopes.containsAll(requiredScopes)) { + if (actualPathConfig.isInstance() && !matchResourcePermission(actualPathConfig, permission)) { + continue; + + } + if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) { LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions); if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) { this.paths.remove(actualPathConfig); @@ -138,11 +140,6 @@ public abstract class AbstractPolicyEnforcer { return true; } } - } else { - if ((allowedScopes.isEmpty() && requiredScopes.isEmpty()) || allowedScopes.containsAll(requiredScopes)) { - LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions); - return true; - } } } @@ -151,6 +148,11 @@ public abstract class AbstractPolicyEnforcer { return false; } + private boolean hasResourceScopePermission(Set requiredScopes, Permission permission, PathConfig actualPathConfig) { + Set allowedScopes = permission.getScopes(); + return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty()); + } + protected AuthzClient getAuthzClient() { return this.authzClient; } @@ -210,7 +212,6 @@ public abstract class AbstractPolicyEnforcer { config.setPath(targetResource.getUri()); config.setScopes(originalConfig.getScopes()); config.setMethods(originalConfig.getMethods()); - config.setInstance(true); config.setParentConfig(originalConfig); this.paths.add(config); @@ -244,13 +245,17 @@ public abstract class AbstractPolicyEnforcer { private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) { // first we try a match using resource id - boolean resourceMatch = permission.getResourceSetId().equals(actualPathConfig.getId()); + boolean resourceMatch = matchResourcePermission(actualPathConfig, permission); // as a fallback, check if the current path is an instance and if so, check if parent's id matches the permission if (!resourceMatch && actualPathConfig.isInstance()) { - resourceMatch = permission.getResourceSetId().equals(actualPathConfig.getParentConfig().getId()); + resourceMatch = matchResourcePermission(actualPathConfig.getParentConfig(), permission); } return resourceMatch; } + + private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) { + return permission.getResourceSetId().equals(actualPathConfig.getId()); + } } 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 e151f7620e..a77676292c 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 @@ -50,7 +50,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { int retry = 2; AccessToken original = accessToken; - while (retry >= 0) { + while (retry > 0) { if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) { original.setAuthorization(accessToken.getAuthorization()); return true; 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 5c2612449d..aa6d3d25d0 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 @@ -45,7 +45,6 @@ public class PolicyEnforcer { private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class); private final KeycloakDeployment deployment; - private final PathMatcher pathMatcher; private final AuthzClient authzClient; private final PolicyEnforcerConfig enforcerConfig; private final List paths; @@ -54,7 +53,6 @@ public class PolicyEnforcer { this.deployment = deployment; this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig(); this.authzClient = AuthzClient.create(new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient())); - this.pathMatcher = new PathMatcher(); this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig); if (LOGGER.isDebugEnabled()) { 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 b2c5757faa..9cf710ac5d 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 @@ -109,7 +109,6 @@ public class PolicyEnforcerConfig { private List methods = new ArrayList<>(); private List scopes = Collections.emptyList(); private String id; - private boolean instance; @JsonIgnore private PathConfig parentConfig; @@ -178,11 +177,7 @@ public class PolicyEnforcerConfig { } public boolean isInstance() { - return instance; - } - - public void setInstance(boolean instance) { - this.instance = instance; + return this.parentConfig != null; } public void setParentConfig(PathConfig parentConfig) { diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java index dd219012dc..9a51031782 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java @@ -16,6 +16,8 @@ */ package org.keycloak.representations.idm.authorization; +import com.fasterxml.jackson.annotation.JsonInclude; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -35,6 +37,7 @@ public class PolicyRepresentation { private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; private Map config = new HashMap(); private List dependentPolicies; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private List associatedPolicies = new ArrayList<>(); public String getId() { diff --git a/examples/authz/photoz/photoz-restful-api-authz-service.json b/examples/authz/photoz/photoz-restful-api-authz-service.json index 6547d2fcc4..455948c493 100644 --- a/examples/authz/photoz/photoz-restful-api-authz-service.json +++ b/examples/authz/photoz/photoz-restful-api-authz-service.json @@ -52,7 +52,6 @@ "sessionName": "MainOwnerSession", "mavenArtifactGroupId": "org.keycloak", "moduleName": "PhotozAuthzOwnerPolicy", - "applyPolicies": "[]", "scannerPeriod": "1", "scannerPeriodUnit": "Hours" } @@ -64,7 +63,6 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "applyPolicies": "[]", "roles": "[{\"id\":\"admin\",\"required\":true}]" } }, @@ -75,7 +73,6 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "applyPolicies": "[]", "roles": "[{\"id\":\"user\"},{\"id\":\"manage-albums\",\"required\":true}]" } }, @@ -86,7 +83,6 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "applyPolicies": "[]", "code": "var contextAttributes = $evaluation.getContext().getAttributes();\n\nif (contextAttributes.containsValue('kc.client.network.ip_address', '127.0.0.1')) {\n $evaluation.grant();\n}" } }, @@ -117,7 +113,6 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "applyPolicies": "[]", "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}" } }, 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 c28bca3bc0..a5d7f161b4 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 @@ -109,7 +109,12 @@ public class AlbumService { private void createProtectedResource(Album album) { try { - ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), new HashSet(), "/album/" + album.getId(), "http://photoz.com/album"); + HashSet scopes = new HashSet<>(); + + scopes.add(new ScopeRepresentation(SCOPE_ALBUM_VIEW)); + scopes.add(new ScopeRepresentation(SCOPE_ALBUM_DELETE)); + + ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album"); albumResource.setOwner(album.getUserId()); 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 95fb58bf70..6849d0771b 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 @@ -9,17 +9,18 @@ "secret": "secret" }, "policy-enforcer": { + "user-managed-access" : {}, "paths": [ { "path" : "/album/*", "methods" : [ - { - "method": "GET", - "scopes" : ["urn:photoz.com:scopes:album:view"] - }, { "method": "POST", "scopes" : ["urn:photoz.com:scopes:album:create"] + }, + { + "method": "GET", + "scopes" : ["urn:photoz.com:scopes:album:view"] } ] }, @@ -30,6 +31,10 @@ { "method": "DELETE", "scopes" : ["urn:photoz.com:scopes:album:delete"] + }, + { + "method": "GET", + "scopes" : ["urn:photoz.com:scopes:album:view"] } ] }, diff --git a/examples/authz/servlet-authz/servlet-authz-app-config.json b/examples/authz/servlet-authz/servlet-authz-app-config.json index d5fb1cb811..43ebde42b8 100644 --- a/examples/authz/servlet-authz/servlet-authz-app-config.json +++ b/examples/authz/servlet-authz/servlet-authz-app-config.json @@ -54,7 +54,7 @@ "description": "Defines that adminsitrators can do something", "type": "role", "config": { - "roles": "[\"admin\"]" + "roles": "[{\"id\":\"admin\"}]" } }, { @@ -62,7 +62,7 @@ "description": "Defines that any user can do something", "type": "role", "config": { - "roles": "[\"user\"]" + "roles": "[{\"id\":\"user\"}]" } }, { @@ -71,7 +71,7 @@ "type": "role", "logic": "POSITIVE", "config": { - "roles": "[\"user_premium\"]" + "roles": "[{\"id\":\"user_premium\"}]" } }, { diff --git a/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java index a70d3c075e..a0a7b6c0d1 100644 --- a/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java +++ b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java @@ -67,7 +67,8 @@ public interface Attributes { * @return true if any attribute with name and value exist. Otherwise, returns false. */ default boolean containsValue(String name, String value) { - return toMap().getOrDefault(name, emptyList()).stream().anyMatch(value::equals); + Collection values = toMap().get(name); + return values != null && values.stream().anyMatch(value::equals); } /** diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java index e2ef2f96d1..d5fa9cc9fa 100644 --- a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java @@ -32,8 +32,10 @@ import org.keycloak.authorization.store.StoreFactory; import org.keycloak.representations.idm.authorization.PolicyEnforcementMode; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -130,27 +132,49 @@ public class DefaultPolicyEvaluator implements PolicyEvaluator { return true; } - if (policy.getScopes().isEmpty()) { - return true; - } + Set scopes = new HashSet<>(policy.getScopes()); - boolean hasScope = true; + if (scopes.isEmpty()) { + Set resources = new HashSet<>(); - for (Scope givenScope : policy.getScopes()) { - boolean hasGivenScope = false; + resources.addAll(policy.getResources()); - for (Scope scope : permission.getScopes()) { - if (givenScope.getId().equals(scope.getId())) { - hasGivenScope = true; - break; + for (Resource resource : resources) { + scopes.addAll(resource.getScopes()); + } + + if (!resources.isEmpty() && scopes.isEmpty()) { + return false; + } + + if (scopes.isEmpty()) { + Resource resource = permission.getResource(); + String type = resource.getType(); + + if (type != null) { + List resourcesByType = authorization.getStoreFactory().getResourceStore().findByType(type); + + for (Resource resourceType : resourcesByType) { + if (resourceType.getOwner().equals(resource.getResourceServer().getClientId())) { + resources.add(resourceType); + } + } } } - if (!hasGivenScope) { - return false; + for (Resource resource : resources) { + scopes.addAll(resource.getScopes()); } } - return hasScope; + for (Scope givenScope : scopes) { + for (Scope scope : permission.getScopes()) { + if (givenScope.getId().equals(scope.getId())) { + return true; + } + } + } + + return false; } } 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 8e4ec5e318..6712a9dac3 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java @@ -240,10 +240,16 @@ public class PolicyEvaluationService { } } - accessToken.addAccess(clientModel.getClientId()); - AccessToken.Access resourceAccess = accessToken.getResourceAccess(clientModel.getClientId()); + AccessToken.Access clientAccess = accessToken.addAccess(clientModel.getClientId()); + clientAccess.roles(new HashSet<>()); - userModel.getClientRoleMappings(clientModel).stream().map(RoleModel::getName).forEach(roleName -> resourceAccess.addRole(roleName)); + userModel.getClientRoleMappings(clientModel).stream().map(RoleModel::getName).forEach(roleName -> clientAccess.addRole(roleName)); + + ClientModel resourceServerClient = realm.getClientById(resourceServer.getClientId()); + AccessToken.Access resourceServerAccess = accessToken.addAccess(resourceServerClient.getClientId()); + resourceServerAccess.roles(new HashSet<>()); + + userModel.getClientRoleMappings(resourceServerClient).stream().map(RoleModel::getName).forEach(roleName -> resourceServerAccess.addRole(roleName)); } } } 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 405675a544..0fd347488d 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -17,6 +17,7 @@ package org.keycloak.authorization.authorization; 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; @@ -50,8 +51,6 @@ import javax.ws.rs.container.Suspended; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -104,8 +103,12 @@ public class AuthorizationTokenService { List entitlements = Permissions.allPermits(results); if (entitlements.isEmpty()) { + HashMap error = new HashMap<>(); + + error.put(OAuth2Constants.ERROR, "not_authorized"); + asyncResponse.resume(Cors.add(httpRequest, Response.status(Status.FORBIDDEN) - .entity(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN))) + .entity(error)) .allowedOrigins(identity.getAccessToken()) .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build()); } else { @@ -193,14 +196,7 @@ public class AuthorizationTokenService { return permissionsToEvaluate.entrySet().stream() .flatMap((Function>, Stream>) entry -> { Resource entryResource = storeFactory.getResourceStore().findById(entry.getKey()); - if (entry.getValue().isEmpty()) { - return Arrays.asList(new ResourcePermission(entryResource, Collections.emptyList(), entryResource.getResourceServer())).stream(); - } else { - return entry.getValue().stream() - .map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, entryResource.getResourceServer().getId())) - .filter(scope -> scope != null) - .map(scope -> new ResourcePermission(entryResource, Arrays.asList(scope), entryResource.getResourceServer())); - } + return Permissions.createResourcePermissions(entryResource, entry.getValue(), authorization).stream(); }).collect(Collectors.toList()); } diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java index ccc457d6b9..e5986b78ae 100644 --- a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java +++ b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java @@ -18,6 +18,7 @@ package org.keycloak.authorization.entitlement; import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.common.KeycloakEvaluationContext; @@ -55,8 +56,6 @@ import javax.ws.rs.container.Suspended; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -119,8 +118,12 @@ public class EntitlementService { List entitlements = Permissions.allPermits(results); if (entitlements.isEmpty()) { + HashMap error = new HashMap<>(); + + error.put(OAuth2Constants.ERROR, "not_authorized"); + asyncResponse.resume(Cors.add(request, Response.status(Status.FORBIDDEN) - .entity(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN))) + .entity(error)) .allowedOrigins(identity.getAccessToken()) .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build()); } else { @@ -249,15 +252,7 @@ public class EntitlementService { return permissionsToEvaluate.entrySet().stream() .flatMap((Function>, Stream>) entry -> { Resource entryResource = storeFactory.getResourceStore().findById(entry.getKey()); - - if (entry.getValue().isEmpty()) { - return Arrays.asList(new ResourcePermission(entryResource, Collections.emptyList(), entryResource.getResourceServer())).stream(); - } else { - return entry.getValue().stream() - .map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, entryResource.getResourceServer().getId())) - .filter(scope -> scope != null) - .map(scope -> new ResourcePermission(entryResource, Arrays.asList(scope), entryResource.getResourceServer())); - } + return Permissions.createResourcePermissions(entryResource, entry.getValue(), authorization).stream(); }).collect(Collectors.toList()); } } 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 ed49697d60..ebc57c2951 100644 --- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java +++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java @@ -27,6 +27,7 @@ import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.permission.ResourcePermission; 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.Permission; @@ -61,91 +62,95 @@ public final class Permissions { StoreFactory storeFactory = authorization.getStoreFactory(); ResourceStore resourceStore = storeFactory.getResourceStore(); - resourceStore.findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resourceServer, authorization))); - resourceStore.findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resourceServer, authorization))); + resourceStore.findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resource.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()), authorization))); + resourceStore.findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource, resource.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()), authorization))); return permissions; } - public static List createResourcePermissions(Resource resource, ResourceServer resourceServer, AuthorizationProvider authorization) { + public static List createResourcePermissions(Resource resource, Set requestedScopes, AuthorizationProvider authorization) { List permissions = new ArrayList<>(); - List scopes = resource.getScopes(); - - if (scopes.isEmpty()) { - String type = resource.getType(); + String type = resource.getType(); + ResourceServer resourceServer = resource.getResourceServer(); + List scopes; + if (requestedScopes.isEmpty()) { + scopes = resource.getScopes(); // check if there is a typed resource whose scopes are inherited by the resource being requested. In this case, we assume that parent resource // is owned by the resource server itself - if (type != null) { + if (type != null && !resource.getOwner().equals(resourceServer.getClientId())) { StoreFactory storeFactory = authorization.getStoreFactory(); ResourceStore resourceStore = storeFactory.getResourceStore(); resourceStore.findByType(type).forEach(resource1 -> { if (resource1.getOwner().equals(resourceServer.getClientId())) { - scopes.addAll(resource1.getScopes()); + for (Scope typeScope : resource1.getScopes()) { + if (!scopes.contains(typeScope)) { + scopes.add(typeScope); + } + } } }); } + } else { + ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore(); + scopes = requestedScopes.stream().map(scopeName -> { + Scope byName = scopeStore.findByName(scopeName, resource.getResourceServer().getId()); + return byName; + }).collect(Collectors.toList()); } - if (scopes.size() > 1) { + if (scopes.isEmpty()) { + permissions.add(new ResourcePermission(resource, Collections.emptyList(), resource.getResourceServer())); + } else { for (Scope scope : scopes) { permissions.add(new ResourcePermission(resource, Arrays.asList(scope), resource.getResourceServer())); } - } else { - permissions.add(new ResourcePermission(resource, Collections.emptyList(), resource.getResourceServer())); } return permissions; } public static List allPermits(List evaluation) { - List permissions = evaluation.stream() - .filter(evaluationResult -> evaluationResult.getEffect().equals(Effect.PERMIT)) - .map(evaluationResult -> { - ResourcePermission permission = evaluationResult.getPermission(); - String resourceId = null; - String resourceName = null; + Map permissions = new HashMap<>(); - Resource resource = permission.getResource(); - - if (resource != null) { - resourceId = resource.getId(); - resourceName = resource.getName(); - } - - Set scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()); - - return new Permission(resourceId, resourceName, scopes); - }).collect(Collectors.toList()); - - Map perms = new HashMap<>(); - - permissions.forEach(permission -> { - Permission evalPermission = perms.get(permission.getResourceSetId()); - - if (evalPermission == null) { - evalPermission = permission; - perms.put(permission.getResourceSetId(), evalPermission); + for (Result evaluationResult : evaluation) { + ResourcePermission permission = evaluationResult.getPermission(); + Set scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()); + if (evaluationResult.getEffect().equals(Effect.DENY)) { + continue; } + Resource resource = permission.getResource(); - Set permissionScopes = permission.getScopes(); + if (resource != null) { + String resourceId = resource.getId(); + String resourceName = resource.getName(); + Permission evalPermission = permissions.get(resource.getId()); - if (permissionScopes != null && !permissionScopes.isEmpty()) { - Set scopes = evalPermission.getScopes(); - - if (scopes == null) { - scopes = new HashSet(); - evalPermission.setScopes(scopes); + if (evalPermission == null) { + evalPermission = new Permission(resourceId, resourceName, scopes); + permissions.put(resourceId, evalPermission); } - for (String scopeName : permissionScopes) { - if (!scopes.contains(scopeName)) { - scopes.add(scopeName); + if (scopes != null && !scopes.isEmpty()) { + Set finalScopes = evalPermission.getScopes(); + + if (finalScopes == null) { + finalScopes = new HashSet(); + evalPermission.setScopes(finalScopes); + } + + for (String scopeName : scopes) { + if (!finalScopes.contains(scopeName)) { + finalScopes.add(scopeName); + } } } + } else { + Permission scopePermission = new Permission(null, null, scopes); + permissions.put(scopePermission.toString(), scopePermission); } - }); + } - return perms.values().stream().collect(Collectors.toList()); + return permissions.values().stream().collect(Collectors.toList()); } } 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 c28bca3bc0..a5d7f161b4 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 @@ -109,7 +109,12 @@ public class AlbumService { private void createProtectedResource(Album album) { try { - ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), new HashSet(), "/album/" + album.getId(), "http://photoz.com/album"); + HashSet scopes = new HashSet<>(); + + scopes.add(new ScopeRepresentation(SCOPE_ALBUM_VIEW)); + scopes.add(new ScopeRepresentation(SCOPE_ALBUM_DELETE)); + + ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album"); albumResource.setOwner(album.getUserId()); 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 95fb58bf70..a3ac697cb3 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 @@ -1,7 +1,7 @@ { "realm": "photoz", "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", - "auth-server-url": "http://localhost:8080/auth", + "auth-server-url": "http://localhost:8180/auth", "ssl-required": "external", "resource": "photoz-restful-api", "bearer-only" : true, @@ -9,17 +9,18 @@ "secret": "secret" }, "policy-enforcer": { + "user-managed-access" : {}, "paths": [ { "path" : "/album/*", "methods" : [ - { - "method": "GET", - "scopes" : ["urn:photoz.com:scopes:album:view"] - }, { "method": "POST", "scopes" : ["urn:photoz.com:scopes:album:create"] + }, + { + "method": "GET", + "scopes" : ["urn:photoz.com:scopes:album:view"] } ] }, @@ -30,6 +31,10 @@ { "method": "DELETE", "scopes" : ["urn:photoz.com:scopes:album:delete"] + }, + { + "method": "GET", + "scopes" : ["urn:photoz.com:scopes:album:view"] } ] }, 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 8a8d483a24..28662fa573 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 @@ -94,6 +94,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd importResourceServerSettings(); } + @Test public void testCreateDeleteAlbum() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -115,6 +116,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } + @Test public void testOnlyOwnerCanDeleteAlbum() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -160,6 +162,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } + @Test public void testRegularUserCanNotAccessAdminResources() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -172,6 +175,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } + @Test public void testAdminOnlyFromSpecificAddress() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -264,6 +268,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } + @Test public void testAdminWithoutPermissionsToDeleteScopePermission() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -327,6 +332,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } + @Test public void testClientRoleRepresentingUserConsent() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -366,6 +372,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } } + @Test public void testClientRoleNotRequired() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); From 824890323dfec65d08f817da15b5c457236e9a46 Mon Sep 17 00:00:00 2001 From: mhajas Date: Mon, 18 Jul 2016 14:01:26 +0200 Subject: [PATCH 07/13] Migrate SAML Filter tests to integration arquillian tests --- testsuite/integration-arquillian/pom.xml | 5 + .../page/BadClientSalesPostSigServlet.java | 2 +- .../page/BadRealmSalesPostSigServlet.java | 2 +- .../adapter/page/Employee2Servlet.java | 2 +- .../adapter/page/EmployeeSigFrontServlet.java | 2 +- .../adapter/page/EmployeeSigServlet.java | 2 +- ...ervletWithLogout.java => SAMLServlet.java} | 15 +- .../adapter/page/SalesMetadataServlet.java | 2 +- .../adapter/page/SalesPostEncServlet.java | 2 +- .../adapter/page/SalesPostPassiveServlet.java | 2 +- .../adapter/page/SalesPostServlet.java | 2 +- .../page/SalesPostSigEmailServlet.java | 2 +- .../page/SalesPostSigPersistentServlet.java | 2 +- .../adapter/page/SalesPostSigServlet.java | 2 +- .../page/SalesPostSigTransientServlet.java | 2 +- .../adapter/servlet/SendUsernameServlet.java | 129 ++++++++++-------- .../DeploymentArchiveProcessor.java | 57 ++++++-- .../arquillian/SAMLFilterDependency.java | 87 ++++++++++++ .../annotation/UseServletFilter.java | 20 +++ .../AbstractSAMLFilterServletAdapterTest.java | 54 ++++++++ .../AbstractSAMLServletsAdapterTest.java | 61 +++++---- .../jboss-deployment-structure.xml | 2 +- .../adapter-test/keycloak-saml/web.xml | 7 +- .../adapter/EAPSAMLFilterAdapterTest.java | 12 ++ .../adapter/EAP6SAMLFilterAdapterTest.java | 12 ++ .../adapter/WildflySAMLFilterAdapterTest.java | 11 ++ .../Wildfly9SAMLFilterAdapterTest.java | 12 ++ .../integration-arquillian/tests/pom.xml | 1 + 28 files changed, 394 insertions(+), 117 deletions(-) rename testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/{SAMLServletWithLogout.java => SAMLServlet.java} (70%) create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SAMLFilterDependency.java create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/UseServletFilter.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPSAMLFilterAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6SAMLFilterAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflySAMLFilterAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9SAMLFilterAdapterTest.java diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml index f1bc7e9552..dbbaaf4d59 100644 --- a/testsuite/integration-arquillian/pom.xml +++ b/testsuite/integration-arquillian/pom.xml @@ -69,6 +69,11 @@ pom import + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-impl-maven + test + org.jboss.arquillian.extension arquillian-drone-bom diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadClientSalesPostSigServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadClientSalesPostSigServlet.java index e85d43e4a7..163d3f1009 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadClientSalesPostSigServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadClientSalesPostSigServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class BadClientSalesPostSigServlet extends SAMLServletWithLogout { +public class BadClientSalesPostSigServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "bad-client-sales-post-sig"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadRealmSalesPostSigServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadRealmSalesPostSigServlet.java index 08fd844fed..f4dcc0c9c5 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadRealmSalesPostSigServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/BadRealmSalesPostSigServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class BadRealmSalesPostSigServlet extends SAMLServletWithLogout { +public class BadRealmSalesPostSigServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "bad-realm-sales-post-sig"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/Employee2Servlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/Employee2Servlet.java index 391c122e32..4257c2febf 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/Employee2Servlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/Employee2Servlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class Employee2Servlet extends SAMLServletWithLogout { +public class Employee2Servlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "employee2"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigFrontServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigFrontServlet.java index ec21b58974..0e7886cd04 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigFrontServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigFrontServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class EmployeeSigFrontServlet extends SAMLServletWithLogout { +public class EmployeeSigFrontServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "employee-sig-front"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigServlet.java index 24d92d0eb5..910ef46d17 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class EmployeeSigServlet extends SAMLServletWithLogout { +public class EmployeeSigServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "employee-sig"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServletWithLogout.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java similarity index 70% rename from testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServletWithLogout.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java index 15744ece89..fe856dc14a 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServletWithLogout.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java @@ -24,11 +24,24 @@ import static org.keycloak.testsuite.util.WaitUtils.pause; /** * @author mhajas */ -public abstract class SAMLServletWithLogout extends AbstractPageWithInjectedUrl { +public abstract class SAMLServlet extends AbstractPageWithInjectedUrl { public void logout() { driver.navigate().to(getUriBuilder().queryParam("GLO", "true").build().toASCIIString()); getUriBuilder().replaceQueryParam("GLO", null); pause(300); } + + public void checkRoles(boolean check) { + if (check) { + getUriBuilder().queryParam("checkRoles", true); + } else { + getUriBuilder().replaceQueryParam("checkRoles", null); + } + } + + public void checkRolesEndPoint() { + driver.navigate().to(getUriBuilder().build().toASCIIString() + "/checkRoles"); + pause(300); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesMetadataServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesMetadataServlet.java index 730ed2efc3..93f2043a9e 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesMetadataServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesMetadataServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class SalesMetadataServlet extends SAMLServletWithLogout { +public class SalesMetadataServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "sales-metadata"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostEncServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostEncServlet.java index 892848a3cb..874b1e892d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostEncServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostEncServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class SalesPostEncServlet extends SAMLServletWithLogout { +public class SalesPostEncServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "sales-post-enc"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostPassiveServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostPassiveServlet.java index 47afaae3d3..a5879c0c3d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostPassiveServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostPassiveServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class SalesPostPassiveServlet extends SAMLServletWithLogout { +public class SalesPostPassiveServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "sales-post-passive"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java index e14acd4c48..cd9ea11854 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class SalesPostServlet extends SAMLServletWithLogout { +public class SalesPostServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "sales-post"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigEmailServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigEmailServlet.java index 789469ea69..77c68f10cb 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigEmailServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigEmailServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class SalesPostSigEmailServlet extends SAMLServletWithLogout { +public class SalesPostSigEmailServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "sales-post-sig-email"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigPersistentServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigPersistentServlet.java index 059202fc69..5ccb96f90d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigPersistentServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigPersistentServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class SalesPostSigPersistentServlet extends SAMLServletWithLogout { +public class SalesPostSigPersistentServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "sales-post-sig-persistent"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigServlet.java index 77c57f110e..b4ab9bb580 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class SalesPostSigServlet extends SAMLServletWithLogout { +public class SalesPostSigServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "sales-post-sig"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigTransientServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigTransientServlet.java index 17ca8a846f..697ac83e0b 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigTransientServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SalesPostSigTransientServlet.java @@ -25,7 +25,7 @@ import java.net.URL; /** * @author mhajas */ -public class SalesPostSigTransientServlet extends SAMLServletWithLogout { +public class SalesPostSigTransientServlet extends SAMLServlet { public static final String DEPLOYMENT_NAME = "sales-post-sig-transient"; @ArquillianResource diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java index 6a075949df..58feae3080 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java @@ -17,78 +17,91 @@ package org.keycloak.testsuite.adapter.servlet; + +import org.jboss.resteasy.annotations.cache.NoCache; + import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; import java.io.IOException; -import java.io.OutputStream; import java.security.Principal; -import java.util.List; /** -* @author Bill Burke -* @version $Revision: 1 $ -*/ -public class SendUsernameServlet extends HttpServlet { + * @author Bill Burke + * @author mhajas + * @version $Revision: 1 $ + */ +@Path("/") +public class SendUsernameServlet { - public static Principal sentPrincipal; - public static List checkRoles; + private static boolean checkRoles = false; - @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { - System.out.println("In SendUsername Servlet doGet()"); - if (checkRoles != null) { - for (String role : checkRoles) { - System.out.println("check role: " + role); - //Assert.assertTrue(req.isUserInRole(role)); - if (!req.isUserInRole(role)) { - resp.sendError(403); - return; - } - } + @Context + private HttpServletRequest httpServletRequest; + @GET + @NoCache + public Response doGet(@QueryParam("checkRoles") boolean checkRolesFlag) throws ServletException, IOException { + System.out.println("In SendUsername Servlet doGet() check roles is " + (checkRolesFlag || checkRoles)); + if (httpServletRequest.getUserPrincipal() != null && (checkRolesFlag || checkRoles) && !checkRoles()) { + return Response.status(Response.Status.FORBIDDEN).entity("Forbidden").build(); } - resp.setContentType("text/plain"); - OutputStream stream = resp.getOutputStream(); - Principal principal = req.getUserPrincipal(); - stream.write("request-path: ".getBytes()); - stream.write(req.getServletPath().getBytes()); - stream.write("\n".getBytes()); - stream.write("principal=".getBytes()); - if (principal == null) { - stream.write("null".getBytes()); - return; - } - String name = principal.getName(); - stream.write(name.getBytes()); - sentPrincipal = principal; + return Response.ok(getOutput(), MediaType.TEXT_PLAIN).build(); } - @Override - protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { - System.out.println("In SendUsername Servlet doPost()"); - if (checkRoles != null) { - for (String role : checkRoles) { - System.out.println("check role: " + role); - if (!req.isUserInRole(role)) { - throw new RuntimeException("User: " + req.getUserPrincipal() + " is not in Role: " + role); - } - } + + @POST + @NoCache + public Response doPost(@QueryParam("checkRoles") boolean checkRolesFlag) throws ServletException, IOException { + System.out.println("In SendUsername Servlet doPost() check roles is " + (checkRolesFlag || checkRoles)); + + if (httpServletRequest.getUserPrincipal() != null && (checkRolesFlag || checkRoles) && !checkRoles()) { + throw new RuntimeException("User: " + httpServletRequest.getUserPrincipal() + " do not have required role"); } - resp.setContentType("text/plain"); - OutputStream stream = resp.getOutputStream(); - Principal principal = req.getUserPrincipal(); - stream.write("request-path: ".getBytes()); - stream.write(req.getServletPath().getBytes()); - stream.write("\n".getBytes()); - stream.write("principal=".getBytes()); + + return Response.ok(getOutput(), MediaType.TEXT_PLAIN).build(); + } + + @GET + @Path("{path}") + public Response doGetElseWhere(@PathParam("path") String path, @QueryParam("checkRoles") boolean checkRolesFlag) throws ServletException, IOException { + System.out.println("In SendUsername Servlet doGetElseWhere() - path: " + path); + return doGet(checkRolesFlag); + } + + @POST + @Path("{path}") + public Response doPostElseWhere(@PathParam("path") String path, @QueryParam("checkRoles") boolean checkRolesFlag) throws ServletException, IOException { + System.out.println("In SendUsername Servlet doPostElseWhere() - path: " + path); + return doPost(checkRolesFlag); + } + + @GET + @Path("checkRoles") + public String checkRolesEndPoint() { + checkRoles = true; + System.out.println("Setting checkRoles to true"); + return "Roles will be checked"; + } + + private boolean checkRoles() { + return httpServletRequest.isUserInRole("manager"); + } + + private String getOutput() { + String output = "request-path: "; + output += httpServletRequest.getServletPath(); + output += "\n"; + output += "principal="; + Principal principal = httpServletRequest.getUserPrincipal(); + if (principal == null) { - stream.write("null".getBytes()); - return; + return output + "null"; } - String name = principal.getName(); - stream.write(name.getBytes()); - sentPrincipal = principal; + + return output + principal.getName(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java index a63595e3bf..68d7183a06 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java @@ -25,7 +25,9 @@ import org.jboss.logging.Logger; import org.jboss.logging.Logger.Level; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; import org.keycloak.representations.adapters.config.AdapterConfig; +import org.keycloak.testsuite.arquillian.annotation.UseServletFilter; import org.keycloak.testsuite.util.IOUtil; import org.keycloak.util.JsonSerialization; import org.w3c.dom.Document; @@ -35,11 +37,9 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.hasAppServerContainerAnnotation; -import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isRelative; -import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.isTomcatAppServer; -import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.*; +import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.*; +import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot; import static org.keycloak.testsuite.util.IOUtil.*; ; @@ -125,7 +125,7 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { adapterConfig.setAuthServerUrl(getAuthServerContextRoot() + "/auth"); adapterConfig.setRealmKey(REALM_KEY); } - + if ("true".equals(System.getProperty("app.server.ssl.required"))) { adapterConfig.setSslRequired("all"); } @@ -155,17 +155,46 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { } protected void modifyWebXml(Archive archive, TestClass testClass) { - if (isTomcatAppServer(testClass.getJavaClass())) { - try { - String webXmlContent = IOUtils.toString( - archive.get(WEBXML_PATH).getAsset().openStream()); - + try { + String webXmlContent = IOUtils.toString( + archive.get(WEBXML_PATH).getAsset().openStream()); + if (isTomcatAppServer(testClass.getJavaClass())) { webXmlContent = webXmlContent.replace("KEYCLOAK", "BASIC"); - - archive.add(new StringAsset((webXmlContent)), WEBXML_PATH); - } catch (IOException ex) { - throw new RuntimeException("Cannot load web.xml from archive."); } + + if (testClass.getJavaClass().isAnnotationPresent(UseServletFilter.class)) { + //We need to add filter declaration to web.xml + log.info("Adding filter to " + testClass.getAnnotation(UseServletFilter.class).filterClass() + " with mapping " + testClass.getAnnotation(UseServletFilter.class).filterPattern() + " for " + archive.getName()); + String filter = "\n\n" + + "" + testClass.getAnnotation(UseServletFilter.class).filterName() + "\n" + + "" + testClass.getAnnotation(UseServletFilter.class).filterClass() + "\n" + + "\n" + + "\n\n" + + "" + testClass.getAnnotation(UseServletFilter.class).filterName() + "\n" + + "" + testClass.getAnnotation(UseServletFilter.class).filterPattern() + "\n"; + if (!testClass.getAnnotation(UseServletFilter.class).dispatcherType().isEmpty()) { + filter += "" + testClass.getAnnotation(UseServletFilter.class).dispatcherType() + "\n"; + } + filter += "\n"; + + webXmlContent = webXmlContent.replace("", " " + filter); + + //Also we need to add all dependencies within war lib directory, because filter needs to work without installed adapter + log.info("Adding SAMLFilter dependencies to " + archive.getName()); + ((WebArchive) archive).addAsLibraries(new SAMLFilterDependency().getDependencies()); + + + //finally we need to remove all keycloak related configuration from web.xml + int start = webXmlContent.indexOf(""); + int end = webXmlContent.indexOf("") + "".length(); + + + webXmlContent = webXmlContent.substring(0, start) + webXmlContent.substring(end); + } + + archive.add(new StringAsset((webXmlContent)), WEBXML_PATH); + } catch (IOException ex) { + throw new RuntimeException("Cannot load web.xml from archive."); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SAMLFilterDependency.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SAMLFilterDependency.java new file mode 100644 index 0000000000..808a3839da --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SAMLFilterDependency.java @@ -0,0 +1,87 @@ +package org.keycloak.testsuite.arquillian; + +import org.jboss.logging.Logger; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.jboss.shrinkwrap.resolver.api.maven.PackagingType; +import org.jboss.shrinkwrap.resolver.api.maven.ScopeType; +import org.jboss.shrinkwrap.resolver.api.maven.coordinate.MavenDependency; +import org.jboss.shrinkwrap.resolver.api.maven.coordinate.MavenDependencyExclusion; + +import java.io.File; +import java.util.Collections; +import java.util.Set; + +/** + * @author mhajas + */ +public class SAMLFilterDependency implements MavenDependency { + + private static File[] files; + + protected final Logger log = org.jboss.logging.Logger.getLogger(this.getClass()); + + @Override + public Set getExclusions() { + return Collections.EMPTY_SET; + } + + @Override + public ScopeType getScope() { + return ScopeType.COMPILE; + } + + @Override + public boolean isOptional() { + return false; + } + + @Override + public PackagingType getPackaging() { + return PackagingType.JAR; + } + + @Override + public PackagingType getType() { + return PackagingType.JAR; + } + + @Override + public String getClassifier() { + return null; + } + + @Override + public String getVersion() { + return System.getProperty("project.version"); + } + + @Override + public String getGroupId() { + return "org.keycloak"; + } + + @Override + public String getArtifactId() { + return "keycloak-saml-servlet-filter-adapter"; + } + + @Override + public String toCanonicalForm() { + return getGroupId() + ":" + getArtifactId() + ":" + getVersion(); + } + + private void resolve() { + log.info("Resolving SAMLFilter dependencies"); + files = Maven.configureResolver().addDependency(this) + .resolve().withTransitivity().asFile(); + log.info("Resolving dependencies is finished with " + files.length + " files"); + } + + public File[] getDependencies() { + if (files == null) { + resolve(); + } + + return files; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/UseServletFilter.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/UseServletFilter.java new file mode 100644 index 0000000000..dc0b3f4f61 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/UseServletFilter.java @@ -0,0 +1,20 @@ +package org.keycloak.testsuite.arquillian.annotation; + +import java.lang.annotation.*; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author mhajas + */ +@Documented +@Retention(RUNTIME) +@Target({ElementType.TYPE}) +@Inherited +public @interface UseServletFilter { + + String filterName(); + String filterClass(); + String filterPattern() default "/*"; + String dispatcherType() default ""; +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java new file mode 100644 index 0000000000..0df65a4f6c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java @@ -0,0 +1,54 @@ +package org.keycloak.testsuite.adapter.servlet; + +import org.junit.After; +import org.junit.Before; +import org.keycloak.testsuite.arquillian.annotation.UseServletFilter; + +/** + * @author mhajas + */ + +@UseServletFilter(filterName = "saml-filter", filterClass = "org.keycloak.adapters.saml.servlet.SamlFilter") +public abstract class AbstractSAMLFilterServletAdapterTest extends AbstractSAMLServletsAdapterTest { + + @Before + public void checkRoles() { + badClientSalesPostSigServletPage.checkRoles(true); + badRealmSalesPostSigServletPage.checkRoles(true); + employeeSigServletPage.checkRoles(true); + employeeSigFrontServletPage.checkRoles(true); + salesMetadataServletPage.checkRoles(true); + salesPostServletPage.checkRoles(true); + salesPostEncServletPage.checkRoles(true); + salesPostSigServletPage.checkRoles(true); + salesPostPassiveServletPage.checkRoles(true); + salesPostSigEmailServletPage.checkRoles(true); + salesPostSigPersistentServletPage.checkRoles(true); + salesPostSigTransientServletPage.checkRoles(true); + employee2ServletPage.navigateTo(); + + //using endpoint instead of query param because we are not able to put query param to IDP initiated login + testRealmLoginPage.form().login(bburkeUser); + employee2ServletPage.checkRolesEndPoint(); + employee2ServletPage.logout(); + + forbiddenIfNotAuthenticated = false; + } + + @After + public void uncheckRoles() { + badClientSalesPostSigServletPage.checkRoles(false); + badRealmSalesPostSigServletPage.checkRoles(false); + employee2ServletPage.checkRoles(false); + employeeSigServletPage.checkRoles(false); + employeeSigFrontServletPage.checkRoles(false); + salesMetadataServletPage.checkRoles(false); + salesPostServletPage.checkRoles(false); + salesPostEncServletPage.checkRoles(false); + salesPostSigServletPage.checkRoles(false); + salesPostPassiveServletPage.checkRoles(false); + salesPostSigEmailServletPage.checkRoles(false); + salesPostSigPersistentServletPage.checkRoles(false); + salesPostSigTransientServletPage.checkRoles(false); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java index 23c365f9bd..5d3d72dc70 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java @@ -50,46 +50,48 @@ import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement; */ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAdapterTest { @Page - private BadClientSalesPostSigServlet badClientSalesPostSigServletPage; + protected BadClientSalesPostSigServlet badClientSalesPostSigServletPage; @Page - private BadRealmSalesPostSigServlet badRealmSalesPostSigServletPage; + protected BadRealmSalesPostSigServlet badRealmSalesPostSigServletPage; @Page - private Employee2Servlet employee2ServletPage; + protected Employee2Servlet employee2ServletPage; @Page - private EmployeeSigServlet employeeSigServletPage; + protected EmployeeSigServlet employeeSigServletPage; @Page - private EmployeeSigFrontServlet employeeSigFrontServletPage; + protected EmployeeSigFrontServlet employeeSigFrontServletPage; @Page - private SalesMetadataServlet salesMetadataServletPage; + protected SalesMetadataServlet salesMetadataServletPage; @Page - private SalesPostServlet salesPostServletPage; + protected SalesPostServlet salesPostServletPage; @Page - private SalesPostEncServlet salesPostEncServletPage; + protected SalesPostEncServlet salesPostEncServletPage; @Page - private SalesPostPassiveServlet salesPostPassiveServletPage; + protected SalesPostPassiveServlet salesPostPassiveServletPage; @Page - private SalesPostSigServlet salesPostSigServletPage; + protected SalesPostSigServlet salesPostSigServletPage; @Page - private SalesPostSigEmailServlet salesPostSigEmailServletPage; + protected SalesPostSigEmailServlet salesPostSigEmailServletPage; @Page - private SalesPostSigPersistentServlet salesPostSigPersistentServletPage; + protected SalesPostSigPersistentServlet salesPostSigPersistentServletPage; @Page - private SalesPostSigTransientServlet salesPostSigTransientServletPage; + protected SalesPostSigTransientServlet salesPostSigTransientServletPage; @Page - private SAMLIDPInitiatedLogin samlidpInitiatedLogin; + protected SAMLIDPInitiatedLogin samlidpInitiatedLogin; + + protected boolean forbiddenIfNotAuthenticated = true; @Deployment(name = BadClientSalesPostSigServlet.DEPLOYMENT_NAME) protected static WebArchive badClientSalesPostSig() { @@ -196,7 +198,7 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd waitUntilElement(By.xpath("//body")).text().contains("principal=bburke"); } - private void testSuccessfulAndUnauthorizedLogin(SAMLServletWithLogout page, Login loginPage) { + private void testSuccessfulAndUnauthorizedLogin(SAMLServlet page, Login loginPage) { assertSuccessfulLogin(page, bburkeUser, loginPage); page.logout(); assertForbiddenLogin(page, "unauthorized", "password", loginPage); @@ -223,7 +225,6 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd assertForbidden(employee2ServletPage); assertForbidden(employeeSigFrontServletPage); assertForbidden(salesPostSigPersistentServletPage); - salesPostServletPage.logout(); } @@ -243,8 +244,12 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd assertCurrentUrlStartsWith(testRealmSAMLRedirectLoginPage); salesPostPassiveServletPage.navigateTo(); - waitUntilElement(By.xpath("//body")).text().not().contains("principal="); - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("") || driver.getPageSource().equals("")); + if (forbiddenIfNotAuthenticated) { + waitUntilElement(By.xpath("//body")).text().not().contains("principal="); + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("") || driver.getPageSource().equals("")); + } else { + waitUntilElement(By.xpath("//body")).text().contains("principal=null"); + } salesPostSigEmailServletPage.navigateTo(); assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); @@ -320,9 +325,13 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd public void salesPostPassiveTest() { salesPostPassiveServletPage.navigateTo(); - waitUntilElement(By.xpath("//body")).text().not().contains("principal="); - //Different 403 status page on EAP and Wildfly - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("") || driver.getPageSource().equals("")); + if (forbiddenIfNotAuthenticated) { + waitUntilElement(By.xpath("//body")).text().not().contains("principal="); + //Different 403 status page on EAP and Wildfly + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("") || driver.getPageSource().equals("")); + } else { + waitUntilElement(By.xpath("//body")).text().contains("principal=null"); + } assertSuccessfulLogin(salesPostServletPage, bburkeUser, testRealmSAMLPostLoginPage); @@ -331,9 +340,13 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd salesPostPassiveServletPage.logout(); salesPostPassiveServletPage.navigateTo(); - waitUntilElement(By.xpath("//body")).text().not().contains("principal="); - //Different 403 status page on EAP and Wildfly - assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("") || driver.getPageSource().equals("")); + if (forbiddenIfNotAuthenticated) { + waitUntilElement(By.xpath("//body")).text().not().contains("principal="); + //Different 403 status page on EAP and Wildfly + assertTrue(driver.getPageSource().contains("Forbidden") || driver.getPageSource().contains("") || driver.getPageSource().equals("")); + } else { + waitUntilElement(By.xpath("//body")).text().contains("principal=null"); + } assertForbiddenLogin(salesPostServletPage, "unauthorized", "password", testRealmSAMLPostLoginPage); assertForbidden(salesPostPassiveServletPage); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/jboss-deployment-structure.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/jboss-deployment-structure.xml index 6f4b9c290a..6b5322defc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/jboss-deployment-structure.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/jboss-deployment-structure.xml @@ -27,7 +27,7 @@ - + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/web.xml index 44aa653c05..4207f91795 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/web.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/web.xml @@ -23,13 +23,8 @@ %CONTEXT_PATH% - - Servlet - org.keycloak.testsuite.adapter.servlet.SendUsernameServlet - - - Servlet + javax.ws.rs.core.Application /* diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPSAMLFilterAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPSAMLFilterAdapterTest.java new file mode 100644 index 0000000000..bf739a73e9 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap/src/test/java/org/keycloak/testsuite/adapter/EAPSAMLFilterAdapterTest.java @@ -0,0 +1,12 @@ +package org.keycloak.testsuite.adapter; + +import org.keycloak.testsuite.adapter.servlet.AbstractSAMLFilterServletAdapterTest; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; +import org.keycloak.testsuite.arquillian.annotation.UseServletFilter; + +/** + * @author mhajas + */ +@AppServerContainer("app-server-eap") +public class EAPSAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest { +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6SAMLFilterAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6SAMLFilterAdapterTest.java new file mode 100644 index 0000000000..627e5f3d8e --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/eap6/src/test/java/org/keycloak/testsuite/adapter/EAP6SAMLFilterAdapterTest.java @@ -0,0 +1,12 @@ +package org.keycloak.testsuite.adapter; + +import org.keycloak.testsuite.adapter.servlet.AbstractSAMLFilterServletAdapterTest; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; +import org.keycloak.testsuite.arquillian.annotation.UseServletFilter; + +/** + * @author mhajas + */ +@AppServerContainer("app-server-eap6") +public class EAPSAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest { +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflySAMLFilterAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflySAMLFilterAdapterTest.java new file mode 100644 index 0000000000..d5e837d8a4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/WildflySAMLFilterAdapterTest.java @@ -0,0 +1,11 @@ +package org.keycloak.testsuite.adapter; + +import org.keycloak.testsuite.adapter.servlet.AbstractSAMLFilterServletAdapterTest; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +/** + * @author mhajas + */ +@AppServerContainer("app-server-wildfly") +public class WildflySAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest { +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9SAMLFilterAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9SAMLFilterAdapterTest.java new file mode 100644 index 0000000000..230cafabef --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly9/src/test/java/org/keycloak/testsuite/adapter/Wildfly9SAMLFilterAdapterTest.java @@ -0,0 +1,12 @@ +package org.keycloak.testsuite.adapter; + +import org.keycloak.testsuite.adapter.servlet.AbstractSAMLFilterServletAdapterTest; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; +import org.keycloak.testsuite.arquillian.annotation.UseServletFilter; + +/** + * @author mhajas + */ +@AppServerContainer("app-server-wildfly9") +public class Wildfly9SAMLFilterAdapterTest extends AbstractSAMLFilterServletAdapterTest { +} diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 14890097f1..0db29a32c6 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -173,6 +173,7 @@ ${browser} ${firefox_binary} + ${project.version} From 607b305c2f5a8cc620fc3a1f3f18161c22c786a6 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 29 Jul 2016 12:42:53 -0300 Subject: [PATCH 08/13] [KEYCLOAK-3372] - Adding tests for servlet authorization --- .../src/main/webapp/logout-include.jsp | 4 +- .../integration-arquillian/test-apps/pom.xml | 1 + .../test-apps/servlet-authz/README.md | 54 +++ .../test-apps/servlet-authz/pom.xml | 53 +++ .../servlet-authz-app-authz-service.json | 147 ++++++++ .../servlet-authz/servlet-authz-realm.json | 95 +++++ .../META-INF/jboss-deployment-structure.xml | 25 ++ .../src/main/webapp/WEB-INF/keycloak.json | 14 + .../src/main/webapp/WEB-INF/web.xml | 47 +++ .../src/main/webapp/accessDenied.jsp | 6 + .../servlet-authz/src/main/webapp/index.jsp | 35 ++ .../src/main/webapp/logout-include.jsp | 11 + .../main/webapp/protected/admin/onlyAdmin.jsp | 6 + .../src/main/webapp/protected/dynamicMenu.jsp | 48 +++ .../webapp/protected/premium/onlyPremium.jsp | 6 + .../test-apps/test-apps-dist/build.xml | 8 + .../AbstractServletAuthzAdapterTest.java | 352 ++++++++++++++++++ .../WildflyServletAuthzAdapterTest.java | 33 ++ .../tests/other/adapters/pom.xml | 6 + 19 files changed, 949 insertions(+), 2 deletions(-) create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/README.md create mode 100755 testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-realm.json create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/keycloak.json create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/accessDenied.jsp create mode 100755 testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/index.jsp create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/logout-include.jsp create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp create mode 100644 testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java create mode 100644 testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyServletAuthzAdapterTest.java 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 95365ea195..364d8877ab 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 to logout.

\ No newline at end of file +

Click here ">Sign Out

\ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml index 391e0d8206..fa4b0c19ab 100644 --- a/testsuite/integration-arquillian/test-apps/pom.xml +++ b/testsuite/integration-arquillian/test-apps/pom.xml @@ -20,5 +20,6 @@ js-database photoz hello-world-authz-service + servlet-authz \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/README.md b/testsuite/integration-arquillian/test-apps/servlet-authz/README.md new file mode 100644 index 0000000000..f93acb52ca --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/README.md @@ -0,0 +1,54 @@ +# About the Example Application + +This is a simple Servlet-based application that will introduce you to some of the main concepts around Keycloak Authorization Services. + +For this application, users can be regular users, premium users or administrators, where: + +* Regular users have very limited access. +* Premium users have access to the *premium area* +* Administrators have access to the *administration area* + +In Keycloak, all the paths being protected are resources on the server. + +This application will also show you how to create a dynamic menu with the permissions granted to an user. + +## Create the Example Realm and a Resource Server + +Considering that your Keycloak Server is up and running, log in to the Keycloak Administration Console. + +Now, create a new realm based on the following configuration file: + + examples/authz/servlet-authz/servlet-authz-realm.json + +That will import a pre-configured realm with everything you need to run this example. For more details about how to import a realm +into Keycloak, check the Keycloak's reference documentation. + +After importing that file, you'll have a new realm called ``servlet-authz``. + +Now, let's import another configuration using the Administration Console in order to configure the client application ``servlet-authz-app`` as a resource server with all resources, scopes, permissions and policies. + +Click on ``Clients`` on the left side menu. Click on the ``servlet-authz-app`` on the client listing page. This will +open the ``Client Details`` page. Once there, click on the `Authorization` tab. + +Click on the ``Select file`` button, which means you want to import a resource server configuration. Now select the file that is located at: + + examples/authz/servlet-authz/servlet-authz-app-config.json + +Now click ``Upload`` and the resource server will be updated accordingly. + +## Deploy and Run the Example Applications + +To deploy the example application, follow these steps: + + cd examples/authz/servlet-authz + mvn clean package wildfly:deploy + +Now, try to access the client application using the following URL: + + http://localhost:8080/servlet-authz-app + +If everything is correct, you will be redirect to Keycloak login page. You can login to the application with the following credentials: + +* username: jdoe / password: jdoe +* username: alice / password: alice +* username: admin / password: admin \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml new file mode 100755 index 0000000000..fc9f6b0641 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + org.keycloak.testsuite + integration-arquillian-test-apps + 2.1.0-SNAPSHOT + + + servlet-authz-app + war + + Keycloak Authz: Servlet Authorization Test + Servlet Authorization Test + + + + + org.keycloak + keycloak-authz-client + ${project.version} + provided + + + org.keycloak + keycloak-core + ${project.version} + provided + + + + + ${project.artifactId} + + + org.jboss.as.plugins + jboss-as-maven-plugin + + false + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + + diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json new file mode 100644 index 0000000000..43ebde42b8 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json @@ -0,0 +1,147 @@ +{ + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Admin Resource", + "uri": "/protected/admin/*", + "type": "http://servlet-authz/protected/admin", + "scopes": [ + { + "name": "urn:servlet-authz:protected:admin:access" + } + ] + }, + { + "name": "Protected Resource", + "uri": "/*", + "type": "http://servlet-authz/protected/resource", + "scopes": [ + { + "name": "urn:servlet-authz:protected:resource:access" + } + ] + }, + { + "name": "Premium Resource", + "uri": "/protected/premium/*", + "type": "urn:servlet-authz:protected:resource", + "scopes": [ + { + "name": "urn:servlet-authz:protected:premium:access" + } + ] + }, + { + "name": "Main Page", + "type": "urn:servlet-authz:protected:resource", + "scopes": [ + { + "name": "urn:servlet-authz:page:main:actionForAdmin" + }, + { + "name": "urn:servlet-authz:page:main:actionForUser" + }, + { + "name": "urn:servlet-authz:page:main:actionForPremiumUser" + } + ] + } + ], + "policies": [ + { + "name": "Any Admin Policy", + "description": "Defines that adminsitrators can do something", + "type": "role", + "config": { + "roles": "[{\"id\":\"admin\"}]" + } + }, + { + "name": "Any User Policy", + "description": "Defines that any user can do something", + "type": "role", + "config": { + "roles": "[{\"id\":\"user\"}]" + } + }, + { + "name": "Only Premium User Policy", + "description": "Defines that only premium users can do something", + "type": "role", + "logic": "POSITIVE", + "config": { + "roles": "[{\"id\":\"user_premium\"}]" + } + }, + { + "name": "All Users Policy", + "description": "Defines that all users can do something", + "type": "aggregate", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "applyPolicies": "[\"Any User Policy\",\"Any Admin Policy\",\"Only Premium User Policy\"]" + } + }, + { + "name": "Premium Resource Permission", + "description": "A policy that defines access to premium resources", + "type": "resource", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Premium Resource\"]", + "applyPolicies": "[\"Only Premium User Policy\"]" + } + }, + { + "name": "Administrative Resource Permission", + "description": "A policy that defines access to administrative resources", + "type": "resource", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Admin Resource\"]", + "applyPolicies": "[\"Any Admin Policy\"]" + } + }, + { + "name": "Protected Resource Permission", + "description": "A policy that defines access to any protected resource", + "type": "resource", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "resources": "[\"Protected Resource\"]", + "applyPolicies": "[\"All Users Policy\"]" + } + }, + { + "name": "Action 1 on Main Page Resource Permission", + "description": "A policy that defines access to action 1 on the main page", + "type": "scope", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "scopes": "[\"urn:servlet-authz:page:main:actionForAdmin\"]", + "applyPolicies": "[\"Any Admin Policy\"]" + } + }, + { + "name": "Action 2 on Main Page Resource Permission", + "description": "A policy that defines access to action 2 on the main page", + "type": "scope", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "scopes": "[\"urn:servlet-authz:page:main:actionForUser\"]", + "applyPolicies": "[\"Any User Policy\"]" + } + }, + { + "name": "Action 3 on Main Page Resource Permission", + "description": "A policy that defines access to action 3 on the main page", + "type": "scope", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "scopes": "[\"urn:servlet-authz:page:main:actionForPremiumUser\"]", + "applyPolicies": "[\"Only Premium User Policy\"]" + } + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-realm.json b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-realm.json new file mode 100644 index 0000000000..371e4510f5 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-realm.json @@ -0,0 +1,95 @@ +{ + "realm": "servlet-authz", + "enabled": true, + "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", + "requiredCredentials": [ + "password" + ], + "users": [ + { + "username": "alice", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "alice" + } + ], + "realmRoles": [ + "user" + ] + }, + { + "username": "jdoe", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "jdoe" + } + ], + "realmRoles": [ + "user", + "user_premium" + ] + }, + { + "username": "admin", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "admin" + } + ], + "realmRoles": [ + "user", + "admin" + ], + "clientRoles": { + "realm-management": [ + "realm-admin" + ] + } + }, + { + "username": "service-account-servlet-authz-app", + "enabled": true, + "serviceAccountClientId": "servlet-authz-app", + "clientRoles": { + "servlet-authz-app" : ["uma_protection"] + } + } + ], + "roles": { + "realm": [ + { + "name": "user", + "description": "User privileges" + }, + { + "name": "admin", + "description": "Administrator privileges" + }, + { + "name": "user_premium", + "description": "User Premium privileges" + } + ] + }, + "clients": [ + { + "clientId": "servlet-authz-app", + "enabled": true, + "baseUrl": "/servlet-authz-app", + "adminUrl": "/servlet-authz-app", + "bearerOnly": false, + "authorizationServicesEnabled": true, + "redirectUris": [ + "/servlet-authz-app/*" + ], + "secret": "secret" + } + ] +} diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml new file mode 100644 index 0000000000..515ffa5c73 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/keycloak.json new file mode 100644 index 0000000000..7b362a782f --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/keycloak.json @@ -0,0 +1,14 @@ +{ + "realm": "servlet-authz", + "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url" : "http://localhost:8180/auth", + "ssl-required" : "external", + "resource" : "servlet-authz-app", + "public-client" : false, + "credentials": { + "secret": "secret" + }, + "policy-enforcer": { + "on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp" + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..14d0615978 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,47 @@ + + + + servlet-authz-app + + + + All Resources + /* + + + user + + + + + + All Resources + /* + + + admin + + + + + KEYCLOAK + servlet-authz + + + + admin + + + + user + + + + 403 + /accessDenied.jsp + + + diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/accessDenied.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/accessDenied.jsp new file mode 100644 index 0000000000..6f25023af0 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/accessDenied.jsp @@ -0,0 +1,6 @@ + + +

You can not access this resource.

+ <%@include file="logout-include.jsp"%> + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/index.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/index.jsp new file mode 100755 index 0000000000..3fbfca269c --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/index.jsp @@ -0,0 +1,35 @@ +<%@page import="org.keycloak.AuthorizationContext" %> +<%@ page import="org.keycloak.KeycloakSecurityContext" %> +<%@ page import="org.keycloak.representations.idm.authorization.Permission" %> + +<% + KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); + AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext(); +%> + + + + <%@include file="logout-include.jsp"%> +

This is a public resource. Try to access one of these protected resources:

+ +

Dynamic Menu

+

User Premium

+

Administration

+ +

Your permissions are:

+ +
    + <% + for (Permission permission : authzContext.getPermissions()) { + %> +
  • +

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

    +

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

    +

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

    +
  • + <% + } + %> +
+ + diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/logout-include.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/logout-include.jsp new file mode 100644 index 0000000000..21ef2edebf --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/logout-include.jsp @@ -0,0 +1,11 @@ +<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %> +<%@ page import="org.keycloak.constants.ServiceUrlConstants" %> +<% + String scheme = request.getScheme(); + String host = request.getServerName(); + int port = request.getServerPort(); + String contextPath = request.getContextPath(); + String redirectUri = scheme + "://" + host + ":" + port + contextPath; +%> +

Click here ">Sign Out

\ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp new file mode 100644 index 0000000000..5946cd660c --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp @@ -0,0 +1,6 @@ + + +

Only Administrators can access this page.

+ <%@include file="../../logout-include.jsp"%> + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp new file mode 100644 index 0000000000..1473d223f3 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp @@ -0,0 +1,48 @@ +<%@page import="org.keycloak.AuthorizationContext" %> +<%@ page import="org.keycloak.KeycloakSecurityContext" %> + +<% + KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); + AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext(); +%> + + + +

Any authenticated user can access this page.

+<%@include file="../logout-include.jsp"%> + +

Here is a dynamic menu built from the permissions returned by the server:

+ +
    + <% + if (authzContext.hasResourcePermission("Protected Resource")) { + %> +
  • + Do user thing +
  • + <% + } + %> + + <% + if (authzContext.hasResourcePermission("Premium Resource")) { + %> +
  • + Do user premium thing +
  • + <% + } + %> + + <% + if (authzContext.hasPermission("Admin Resource", "urn:servlet-authz:protected:admin:access")) { + %> +
  • + Do administration thing +
  • + <% + } + %> +
+ + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp new file mode 100644 index 0000000000..9244f9ca5e --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp @@ -0,0 +1,6 @@ + + +

Only for premium users.

+<%@include file="../../logout-include.jsp"%> + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml index 9621ae53ef..3e905447b0 100755 --- a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml +++ b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml @@ -43,5 +43,13 @@ + + + + + + + + diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java new file mode 100644 index 0000000000..2753c5e5bd --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java @@ -0,0 +1,352 @@ +/* + * Copyright 2016 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.adapter.example.authorization; + +import org.jboss.arquillian.container.test.api.Deployer; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +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.admin.client.resource.RoleResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; +import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest; +import org.keycloak.testsuite.util.WaitUtils; +import org.keycloak.util.JsonSerialization; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +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.pause; + +/** + * @author Pedro Igor + */ +public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAdapterTest { + + private static final String REALM_NAME = "servlet-authz"; + private static final String RESOURCE_SERVER_ID = "servlet-authz-app"; + + @ArquillianResource + private Deployer deployer; + + @Override + public void addAdapterTestRealms(List testRealms) { + testRealms.add( + loadRealm(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/servlet-authz-realm.json"))); + } + + @Deployment(name = RESOURCE_SERVER_ID, managed = false) + public static WebArchive deployment() throws IOException { + return exampleDeployment(RESOURCE_SERVER_ID); + } + + @Override + public void beforeAbstractKeycloakTest() throws Exception { + super.beforeAbstractKeycloakTest(); + importResourceServerSettings(); + } + + @Test + public void testUserPermissions() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + + login("alice", "alice"); + + assertFalse(wasDenied()); + + assertTrue(hasLink("User Premium")); + assertTrue(hasLink("Administration")); + assertTrue(hasText("urn:servlet-authz:page:main:actionForUser")); + assertFalse(hasText("urn:servlet-authz:page:main:actionForAdmin")); + assertFalse(hasText("urn:servlet-authz:page:main:actionForPremiumUser")); + + navigateToDynamicMenuPage(); + + assertTrue(hasText("Do user thing")); + assertFalse(hasText("Do user premium thing")); + assertFalse(hasText("Do administration thing")); + + + navigateToUserPremiumPage(); + + assertTrue(wasDenied()); + + navigateToAdminPage(); + + assertTrue(wasDenied()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + @Test + public void testUserPremiumPermissions() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + + login("jdoe", "jdoe"); + + assertFalse(wasDenied()); + assertTrue(hasLink("User Premium")); + assertTrue(hasLink("Administration")); + assertTrue(hasText("urn:servlet-authz:page:main:actionForUser")); + assertTrue(hasText("urn:servlet-authz:page:main:actionForPremiumUser")); + assertFalse(hasText("urn:servlet-authz:page:main:actionForAdmin")); + + navigateToDynamicMenuPage(); + + assertTrue(hasText("Do user thing")); + assertTrue(hasText("Do user premium thing")); + assertFalse(hasText("Do administration thing")); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + @Test + public void testAdminPermissions() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + + login("admin", "admin"); + + assertFalse(wasDenied()); + + assertTrue(hasLink("User Premium")); + assertTrue(hasLink("Administration")); + assertTrue(hasText("urn:servlet-authz:page:main:actionForUser")); + assertTrue(hasText("urn:servlet-authz:page:main:actionForAdmin")); + assertFalse(hasText("urn:servlet-authz:page:main:actionForPremiumUser")); + + navigateToDynamicMenuPage(); + + assertTrue(hasText("Do user thing")); + assertTrue(hasText("Do administration thing")); + assertFalse(hasText("Do user premium thing")); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + @Test + public void testGrantPremiumAccess() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + + login("alice", "alice"); + + assertFalse(wasDenied()); + + navigateToUserPremiumPage(); + + assertTrue(wasDenied()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Premium Resource Permission".equals(policy.getName())) { + policy.getConfig().put("applyPolicies", "[\"Any User Policy\"]"); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + login("alice", "alice"); + + navigateToUserPremiumPage(); + + assertFalse(wasDenied()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Premium Resource Permission".equals(policy.getName())) { + policy.getConfig().put("applyPolicies", "[\"Only Premium User Policy\"]"); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + login("alice", "alice"); + navigateToUserPremiumPage(); + + assertTrue(wasDenied()); + + PolicyRepresentation onlyAlicePolicy = new PolicyRepresentation(); + + onlyAlicePolicy.setName("Temporary Premium Access Policy"); + onlyAlicePolicy.setType("user"); + HashMap config = new HashMap<>(); + UsersResource usersResource = realmsResouce().realm(REALM_NAME).users(); + List users = usersResource.search("alice", null, null, null, null, null); + + assertFalse(users.isEmpty()); + + config.put("users", JsonSerialization.writeValueAsString(Arrays.asList(users.get(0).getId()))); + + onlyAlicePolicy.setConfig(config); + getAuthorizationResource().policies().create(onlyAlicePolicy); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Premium Resource Permission".equals(policy.getName())) { + policy.getConfig().put("applyPolicies", "[\"Temporary Premium Access Policy\"]"); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + logOut(); + login("alice", "alice"); + navigateToUserPremiumPage(); + + assertFalse(wasDenied()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + @Test + public void testGrantAdministrativePermissions() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + + login("jdoe", "jdoe"); + + navigateToAdminPage(); + assertTrue(wasDenied()); + + RealmResource realmResource = realmsResouce().realm(REALM_NAME); + UsersResource usersResource = realmResource.users(); + List users = usersResource.search("jdoe", null, null, null, null, null); + + assertFalse(users.isEmpty()); + + UserResource userResource = usersResource.get(users.get(0).getId()); + + RoleRepresentation adminRole = realmResource.roles().get("admin").toRepresentation(); + userResource.roles().realmLevel().add(Arrays.asList(adminRole)); + + login("jdoe", "jdoe"); + + navigateToAdminPage(); + assertFalse(wasDenied()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + private boolean hasLink(String text) { + return getLink(text) != null; + } + + private boolean hasText(String text) { + return this.driver.getPageSource().contains(text); + } + + private WebElement getLink(String text) { + return this.driver.findElement(By.xpath("//a[text() = '" + text + "']")); + } + + private void importResourceServerSettings() throws FileNotFoundException { + getAuthorizationResource().importSettings(loadJson(new FileInputStream(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/servlet-authz-app-authz-service.json")), ResourceServerRepresentation.class)); + } + + private AuthorizationResource getAuthorizationResource() throws FileNotFoundException { + return getClientResource(RESOURCE_SERVER_ID).authorization(); + } + + private ClientResource getClientResource(String clientId) { + ClientsResource clients = this.realmsResouce().realm(REALM_NAME).clients(); + ClientRepresentation resourceServer = clients.findByClientId(clientId).get(0); + return clients.get(resourceServer.getId()); + } + + private void logOut() { + navigateTo(); + By by = By.xpath("//a[text() = 'Sign Out']"); + WaitUtils.waitUntilElement(by); + this.driver.findElement(by).click(); + pause(500); + } + + private void login(String username, String password) throws InterruptedException { + navigateTo(); + Thread.sleep(2000); + if (this.driver.getCurrentUrl().startsWith(getResourceServerUrl().toString())) { + Thread.sleep(2000); + logOut(); + navigateTo(); + } + + Thread.sleep(2000); + + this.loginPage.form().login(username, password); + } + + private void navigateTo() { + this.driver.navigate().to(getResourceServerUrl()); + WaitUtils.waitUntilElement(By.xpath("//a[text() = 'Dynamic Menu']")); + } + + private boolean wasDenied() { + return this.driver.getPageSource().contains("You can not access this resource."); + } + + private URL getResourceServerUrl() { + try { + return new URL(this.appServerContextRootPage + "/" + RESOURCE_SERVER_ID); + } catch (MalformedURLException e) { + throw new RuntimeException("Could not obtain resource server url.", e); + } + } + + private void navigateToDynamicMenuPage() { + navigateTo(); + getLink("Dynamic Menu").click(); + } + + private void navigateToUserPremiumPage() { + navigateTo(); + getLink("User Premium").click(); + } + + private void navigateToAdminPage() { + navigateTo(); + getLink("Administration").click(); + } +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyServletAuthzAdapterTest.java new file mode 100644 index 0000000000..9a9a49b8de --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyServletAuthzAdapterTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 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.adapter.example; + +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.keycloak.testsuite.adapter.example.authorization.AbstractDefaultAuthzConfigAdapterTest; +import org.keycloak.testsuite.adapter.example.authorization.AbstractServletAuthzAdapterTest; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +/** + * + * @author tkyjovsk + */ +@RunAsClient +@AppServerContainer("app-server-wildfly") +//@AdapterLibsLocationProperty("adapter.libs.wildfly") +public class WildflyServletAuthzAdapterTest extends AbstractServletAuthzAdapterTest { + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml index b8c715f229..7ae36b2c53 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml @@ -287,6 +287,12 @@ ${project.version} war + + org.keycloak.testsuite + servlet-authz-app + ${project.version} + war + ${examples.home} true From 7983ed064fa6d2910d8501e0ac6de22dcd55de83 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 29 Jul 2016 15:25:36 -0300 Subject: [PATCH 09/13] [KEYCLOAK-3372] - More tests and code cleanup --- .../page/PhotozClientAuthzTestApp.java | 5 +- ...AbstractDefaultAuthzConfigAdapterTest.java | 27 +- .../AbstractPhotozExampleAdapterTest.java | 238 +++++++++++++++--- .../AbstractServletAuthzAdapterTest.java | 39 ++- .../WildflyDefaultAuthzConfigAdapterTest.java | 2 +- .../WildflyPhotozExampleAdapterTest.java | 2 +- .../WildflyServletAuthzAdapterTest.java | 2 +- 7 files changed, 232 insertions(+), 83 deletions(-) rename testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/{ => authorization}/WildflyDefaultAuthzConfigAdapterTest.java (94%) rename testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/{ => authorization}/WildflyPhotozExampleAdapterTest.java (94%) rename testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/{ => authorization}/WildflyServletAuthzAdapterTest.java (95%) 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 4721737daa..0e07157fe4 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 @@ -50,7 +50,10 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl { protected ConsentPage consentPage; public void createAlbum(String name) { - this.driver.findElement(By.id("create-album")).click(); + navigateTo(); + By id = By.id("create-album"); + WaitUtils.waitUntilElement(id); + this.driver.findElement(id).click(); Form.setInputValue(this.driver.findElement(By.id("album.name")), name); this.driver.findElement(By.id("save-album")).click(); pause(500); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java index 82b3ec4133..46666747ca 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java @@ -59,31 +59,28 @@ public abstract class AbstractDefaultAuthzConfigAdapterTest extends AbstractExam @Test public void testDefaultAuthzConfig() throws Exception { - configureAuthorizationServices(); - deploy(); - navigateToResourceServer(); - login(); + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + configureAuthorizationServices(); - assertTrue(this.driver.getPageSource().contains("Your permissions are")); - assertTrue(this.driver.getPageSource().contains("Default Resource")); + login(); + + assertTrue(this.driver.getPageSource().contains("Your permissions are")); + assertTrue(this.driver.getPageSource().contains("Default Resource")); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } } - private void login() { - this.loginPage.form().login("alice", "alice"); - } - - private void navigateToResourceServer() throws MalformedURLException { + private void login() throws MalformedURLException { this.driver.navigate().to(getResourceServerUrl()); + this.loginPage.form().login("alice", "alice"); } private URL getResourceServerUrl() throws MalformedURLException { return this.appServerContextRootPage.getUriBuilder().path(RESOURCE_SERVER_ID).build().toURL(); } - private void deploy() { - this.deployer.deploy(RESOURCE_SERVER_ID); - } - private void configureAuthorizationServices() { ClientsResource clients = realmsResouce().realm(REALM_NAME).clients(); ClientRepresentation client = clients.findByClientId(RESOURCE_SERVER_ID).get(0); 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 28662fa573..9a0fb262dd 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 @@ -25,6 +25,7 @@ import org.junit.Test; 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.ResourcesResource; import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; @@ -43,6 +44,10 @@ 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.HashSet; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -95,7 +100,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } @Test - public void testCreateDeleteAlbum() throws Exception { + public void testUserCanCreateAndDeleteAlbum() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); @@ -103,13 +108,11 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd this.clientPage.createAlbum("Alice Family Album"); List resources = getAuthorizationResource().resources().resources(); - assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); this.clientPage.deleteAlbum("Alice Family Album"); resources = getAuthorizationResource().resources().resources(); - assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); @@ -122,11 +125,11 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd this.deployer.deploy(RESOURCE_SERVER_ID); this.clientPage.login("alice", "alice"); this.clientPage.createAlbum("Alice-Family-Album"); + this.clientPage.login("admin", "admin"); this.clientPage.navigateToAdminAlbum(); List resources = getAuthorizationResource().resources().resources(); - assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { @@ -137,11 +140,11 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } this.clientPage.login("admin", "admin"); + this.clientPage.navigateToAdminAlbum(); this.clientPage.deleteAlbum("Alice-Family-Album"); - + assertTrue(this.clientPage.wasDenied()); resources = getAuthorizationResource().resources().resources(); - assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { @@ -153,9 +156,8 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd this.clientPage.navigateToAdminAlbum(); this.clientPage.deleteAlbum("Alice-Family-Album"); - + assertFalse(this.clientPage.wasDenied()); resources = getAuthorizationResource().resources().resources(); - assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); @@ -166,9 +168,9 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd public void testRegularUserCanNotAccessAdminResources() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); this.clientPage.navigateToAdminAlbum(); - assertTrue(this.clientPage.wasDenied()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); @@ -179,9 +181,9 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd public void testAdminOnlyFromSpecificAddress() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("admin", "admin"); this.clientPage.navigateToAdminAlbum(); - assertFalse(this.clientPage.wasDenied()); for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { @@ -193,7 +195,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } this.clientPage.navigateToAdminAlbum(); - assertTrue(this.clientPage.wasDenied()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); @@ -204,16 +205,15 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd public void testAdminWithoutPermissionsToTypedResource() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); this.clientPage.createAlbum("Alice Family Album"); this.clientPage.login("admin", "admin"); this.clientPage.navigateToAdminAlbum(); - assertFalse(this.clientPage.wasDenied()); this.clientPage.viewAlbum("Alice Family Album"); - assertFalse(this.clientPage.wasDenied()); for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { @@ -242,7 +242,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd this.clientPage.navigateToAdminAlbum(); this.clientPage.viewAlbum("Alice Family Album"); - assertTrue(this.clientPage.wasDenied()); for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { @@ -254,14 +253,11 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd this.clientPage.navigateToAdminAlbum(); this.clientPage.viewAlbum("Alice Family Album"); - assertFalse(this.clientPage.wasDenied()); this.clientPage.navigateToAdminAlbum(); this.clientPage.deleteAlbum("Alice Family Album"); - List resources = getAuthorizationResource().resources().resources(); - assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); @@ -269,23 +265,20 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd } @Test - public void testAdminWithoutPermissionsToDeleteScopePermission() throws Exception { + public void testAdminWithoutPermissionsToDeleteAlbum() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); this.clientPage.createAlbum("Alice Family Album"); this.clientPage.login("admin", "admin"); this.clientPage.navigateToAdminAlbum(); - assertFalse(this.clientPage.wasDenied()); this.clientPage.deleteAlbum("Alice Family Album"); - assertFalse(this.clientPage.wasDenied()); - List resources = getAuthorizationResource().resources().resources(); - assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { @@ -301,14 +294,11 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd this.clientPage.login("admin", "admin"); this.clientPage.navigateToAdminAlbum(); this.clientPage.viewAlbum("Alice Family Album"); - assertFalse(this.clientPage.wasDenied()); resources = getAuthorizationResource().resources().resources(); - assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); this.clientPage.navigateToAdminAlbum(); - this.clientPage.deleteAlbum("Alice Family Album"); assertTrue(this.clientPage.wasDenied()); @@ -321,11 +311,8 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd this.clientPage.navigateToAdminAlbum(); this.clientPage.deleteAlbum("Alice Family Album"); - assertFalse(this.clientPage.wasDenied()); - resources = getAuthorizationResource().resources().resources(); - assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); @@ -336,8 +323,8 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd public void testClientRoleRepresentingUserConsent() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); - this.clientPage.login("alice", "alice"); + this.clientPage.login("alice", "alice"); assertFalse(this.clientPage.wasDenied()); UsersResource usersResource = realmsResouce().realm(REALM_NAME).users(); @@ -361,11 +348,9 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd roleResource.update(roleRepresentation); this.clientPage.login("alice", "alice"); - assertTrue(this.clientPage.wasDenied()); this.clientPage.loginWithScopes("alice", "alice", RESOURCE_SERVER_ID + "/manage-albums"); - assertFalse(this.clientPage.wasDenied()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); @@ -376,6 +361,7 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd public void testClientRoleNotRequired() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); assertFalse(this.clientPage.wasDenied()); @@ -401,37 +387,207 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd manageAlbumRole.update(roleRepresentation); this.clientPage.login("alice", "alice"); - assertTrue(this.clientPage.wasDenied()); for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { if ("Any User Policy".equals(policy.getName())) { List roles = JsonSerialization.readValue(policy.getConfig().get("roles"), List.class); - roles.forEach(new Consumer() { - @Override - public void accept(Map role) { - String roleId = (String) role.get("id"); - if (roleId.equals(manageAlbumRole.toRepresentation().getId())) { - role.put("required", false); - } + roles.forEach(role -> { + String roleId = (String) role.get("id"); + if (roleId.equals(manageAlbumRole.toRepresentation().getId())) { + role.put("required", false); } }); policy.getConfig().put("roles", JsonSerialization.writeValueAsString(roles)); - getAuthorizationResource().policies().policy(policy.getId()).update(policy); } } this.clientPage.login("alice", "alice"); - assertFalse(this.clientPage.wasDenied()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); } } + @Test + public void testOverridePermissionFromResourceParent() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + + this.clientPage.login("alice", "alice"); + String resourceName = "My Resource Instance"; + this.clientPage.createAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.viewAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.navigateTo(); + this.clientPage.deleteAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.createAlbum(resourceName); + + this.clientPage.login("admin", "admin"); + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.viewAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.navigateToAdminAlbum();; + this.clientPage.deleteAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.login("alice", "alice"); + this.clientPage.createAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + getAuthorizationResource().resources().resources().forEach(resource -> { + if (resource.getName().equals(resourceName)) { + try { + PolicyRepresentation resourceInstancePermission = new PolicyRepresentation(); + + resourceInstancePermission.setName(resourceName + "Permission"); + resourceInstancePermission.setType("resource"); + + Map config = new HashMap<>(); + + config.put("resources", JsonSerialization.writeValueAsString(Arrays.asList(resource.getId()))); + config.put("applyPolicies", JsonSerialization.writeValueAsString(Arrays.asList("Only Owner Policy"))); + + resourceInstancePermission.setConfig(config); + getAuthorizationResource().policies().create(resourceInstancePermission); + } catch (Exception e) { + throw new RuntimeException("Error creating policy.", e); + } + } + }); + + this.clientPage.login("admin", "admin"); + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.viewAlbum(resourceName); + assertTrue(this.clientPage.wasDenied()); + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.deleteAlbum(resourceName); + assertTrue(this.clientPage.wasDenied()); + + this.clientPage.login("alice", "alice"); + this.clientPage.deleteAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + ResourcesResource resourcesResource = getAuthorizationResource().resources(); + List resources = resourcesResource.resources(); + assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + @Test + public void testInheritPermissionFromResourceParent() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + + this.clientPage.login("alice", "alice"); + + String resourceName = "My Resource Instance"; + this.clientPage.createAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.viewAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.navigateTo(); + this.clientPage.deleteAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.createAlbum(resourceName); + + this.clientPage.login("admin", "admin"); + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.viewAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.navigateToAdminAlbum();; + this.clientPage.deleteAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.login("alice", "alice"); + this.clientPage.createAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + ResourcesResource resourcesResource = getAuthorizationResource().resources(); + resourcesResource.resources().forEach(resource -> { + if (resource.getName().equals(resourceName)) { + try { + PolicyRepresentation resourceInstancePermission = new PolicyRepresentation(); + + resourceInstancePermission.setName(resourceName + "Permission"); + resourceInstancePermission.setType("resource"); + + Map config = new HashMap<>(); + + config.put("resources", JsonSerialization.writeValueAsString(Arrays.asList(resource.getId()))); + config.put("applyPolicies", JsonSerialization.writeValueAsString(Arrays.asList("Only Owner Policy"))); + + resourceInstancePermission.setConfig(config); + getAuthorizationResource().policies().create(resourceInstancePermission); + } catch (Exception e) { + throw new RuntimeException("Error creating policy.", e); + } + } + }); + + this.clientPage.login("admin", "admin"); + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.viewAlbum(resourceName); + assertTrue(this.clientPage.wasDenied()); + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.deleteAlbum(resourceName); + assertTrue(this.clientPage.wasDenied()); + + 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())); + resourcesResource.resource(resource.getId()).update(resource); + } + }); + + this.clientPage.login("admin", "admin"); + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.viewAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + + this.clientPage.navigateToAdminAlbum(); + this.clientPage.deleteAlbum(resourceName); + assertTrue(this.clientPage.wasDenied()); + + this.clientPage.login("alice", "alice"); + this.clientPage.deleteAlbum(resourceName); + assertFalse(this.clientPage.wasDenied()); + List resources = resourcesResource.resources(); + assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + + resourcesResource.resources().forEach(resource -> { + if (resource.getName().equals(resourceName)) { + resource.setScopes(Collections.emptySet()); + resourcesResource.resource(resource.getId()).update(resource); + } + }); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + private void importResourceServerSettings() throws FileNotFoundException { getAuthorizationResource().importSettings(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/adapter/example/authorization/AbstractServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java index 2753c5e5bd..7f61556de9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java @@ -25,7 +25,6 @@ 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.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.representations.idm.ClientRepresentation; @@ -49,9 +48,6 @@ import java.net.URL; import java.util.Arrays; 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; @@ -88,14 +84,12 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda } @Test - public void testUserPermissions() throws Exception { + public void testRegularUserPermissions() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); login("alice", "alice"); - assertFalse(wasDenied()); - assertTrue(hasLink("User Premium")); assertTrue(hasLink("Administration")); assertTrue(hasText("urn:servlet-authz:page:main:actionForUser")); @@ -103,18 +97,14 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda assertFalse(hasText("urn:servlet-authz:page:main:actionForPremiumUser")); navigateToDynamicMenuPage(); - assertTrue(hasText("Do user thing")); assertFalse(hasText("Do user premium thing")); assertFalse(hasText("Do administration thing")); - navigateToUserPremiumPage(); - assertTrue(wasDenied()); navigateToAdminPage(); - assertTrue(wasDenied()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); @@ -127,7 +117,6 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda this.deployer.deploy(RESOURCE_SERVER_ID); login("jdoe", "jdoe"); - assertFalse(wasDenied()); assertTrue(hasLink("User Premium")); assertTrue(hasLink("Administration")); @@ -136,10 +125,15 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda assertFalse(hasText("urn:servlet-authz:page:main:actionForAdmin")); navigateToDynamicMenuPage(); - assertTrue(hasText("Do user thing")); assertTrue(hasText("Do user premium thing")); assertFalse(hasText("Do administration thing")); + + navigateToUserPremiumPage(); + assertFalse(wasDenied()); + + navigateToAdminPage(); + assertTrue(wasDenied()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); } @@ -151,9 +145,7 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda this.deployer.deploy(RESOURCE_SERVER_ID); login("admin", "admin"); - assertFalse(wasDenied()); - assertTrue(hasLink("User Premium")); assertTrue(hasLink("Administration")); assertTrue(hasText("urn:servlet-authz:page:main:actionForUser")); @@ -161,26 +153,29 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda assertFalse(hasText("urn:servlet-authz:page:main:actionForPremiumUser")); navigateToDynamicMenuPage(); - assertTrue(hasText("Do user thing")); assertTrue(hasText("Do administration thing")); assertFalse(hasText("Do user premium thing")); + + navigateToUserPremiumPage(); + assertTrue(wasDenied()); + + navigateToAdminPage(); + assertFalse(wasDenied()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); } } @Test - public void testGrantPremiumAccess() throws Exception { + public void testGrantPremiumAccessToUser() throws Exception { try { this.deployer.deploy(RESOURCE_SERVER_ID); login("alice", "alice"); - assertFalse(wasDenied()); navigateToUserPremiumPage(); - assertTrue(wasDenied()); for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { @@ -193,7 +188,6 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda login("alice", "alice"); navigateToUserPremiumPage(); - assertFalse(wasDenied()); for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { @@ -204,8 +198,8 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda } login("alice", "alice"); - navigateToUserPremiumPage(); + navigateToUserPremiumPage(); assertTrue(wasDenied()); PolicyRepresentation onlyAlicePolicy = new PolicyRepresentation(); @@ -230,10 +224,9 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda } } - logOut(); login("alice", "alice"); - navigateToUserPremiumPage(); + navigateToUserPremiumPage(); assertFalse(wasDenied()); } finally { this.deployer.undeploy(RESOURCE_SERVER_ID); diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyDefaultAuthzConfigAdapterTest.java similarity index 94% rename from testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java rename to testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyDefaultAuthzConfigAdapterTest.java index 712daa06f5..7e31ead134 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyDefaultAuthzConfigAdapterTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.testsuite.adapter.example; +package org.keycloak.testsuite.adapter.example.authorization; import org.keycloak.testsuite.adapter.example.authorization.AbstractDefaultAuthzConfigAdapterTest; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleAdapterTest.java similarity index 94% rename from testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java rename to testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleAdapterTest.java index d9e2c343fa..f35217c719 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleAdapterTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.testsuite.adapter.example; +package org.keycloak.testsuite.adapter.example.authorization; import org.keycloak.testsuite.adapter.example.authorization.AbstractPhotozExampleAdapterTest; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzAdapterTest.java similarity index 95% rename from testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyServletAuthzAdapterTest.java rename to testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzAdapterTest.java index 9a9a49b8de..d50cf29c54 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyServletAuthzAdapterTest.java +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzAdapterTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.testsuite.adapter.example; +package org.keycloak.testsuite.adapter.example.authorization; import org.jboss.arquillian.container.test.api.RunAsClient; import org.keycloak.testsuite.adapter.example.authorization.AbstractDefaultAuthzConfigAdapterTest; From bd5b4348947a1c4fb9ae7ed5481e815bacae83d7 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 29 Jul 2016 22:08:38 -0300 Subject: [PATCH 10/13] [KEYCLOAK-3376] - Show authorization data when evaluating authorization requests --- .../admin/PolicyEvaluationService.java | 24 ++++++------------- .../PolicyEvaluationResponse.java | 19 ++++++++++++++- .../messages/admin-messages_en.properties | 6 ++++- .../resources/js/authz/authz-controller.js | 14 +++++++++++ .../resource-server-policy-evaluate.html | 22 +++++++++++------ 5 files changed, 59 insertions(+), 26 deletions(-) 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 6712a9dac3..67ae7d4c9e 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java @@ -87,16 +87,17 @@ public class PolicyEvaluationService { @Consumes("application/json") @Produces("application/json") public void evaluate(PolicyEvaluationRequest evaluationRequest, @Suspended AsyncResponse asyncResponse) { - EvaluationContext evaluationContext = createEvaluationContext(evaluationRequest); - authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization), evaluationContext).evaluate(createDecisionCollector(evaluationRequest, authorization, asyncResponse)); + KeycloakIdentity identity = createIdentity(evaluationRequest); + EvaluationContext evaluationContext = createEvaluationContext(evaluationRequest, identity); + authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization), evaluationContext).evaluate(createDecisionCollector(evaluationRequest, authorization, identity, asyncResponse)); } - private DecisionResultCollector createDecisionCollector(PolicyEvaluationRequest evaluationRequest, AuthorizationProvider authorization, AsyncResponse asyncResponse) { + private DecisionResultCollector createDecisionCollector(PolicyEvaluationRequest evaluationRequest, AuthorizationProvider authorization, KeycloakIdentity identity, AsyncResponse asyncResponse) { return new DecisionResultCollector() { @Override protected void onComplete(List results) { try { - asyncResponse.resume(Response.ok(PolicyEvaluationResponse.build(evaluationRequest, results, resourceServer, authorization)).build()); + asyncResponse.resume(Response.ok(PolicyEvaluationResponse.build(evaluationRequest, results, resourceServer, authorization, identity)).build()); } catch (Throwable cause) { asyncResponse.resume(cause); } @@ -109,8 +110,8 @@ public class PolicyEvaluationService { }; } - private EvaluationContext createEvaluationContext(PolicyEvaluationRequest representation) { - return new KeycloakEvaluationContext(createIdentity(representation), this.authorization.getKeycloakSession()) { + private EvaluationContext createEvaluationContext(PolicyEvaluationRequest representation, KeycloakIdentity identity) { + return new KeycloakEvaluationContext(identity, this.authorization.getKeycloakSession()) { @Override public Attributes getAttributes() { Map> attributes = new HashMap<>(super.getAttributes().toMap()); @@ -137,17 +138,6 @@ public class PolicyEvaluationService { private List createPermissions(PolicyEvaluationRequest representation, EvaluationContext evaluationContext, AuthorizationProvider authorization) { List resources = representation.getResources(); - - for (PolicyEvaluationRequest.Resource resource : new ArrayList<>(resources)) { - if (resource.getId() == null && (resource.getScopes() == null || resource.getScopes().isEmpty())) { - resources.remove(resource); - } - } - - if (representation.isEntitlements() || resources.isEmpty()) { - return Permissions.all(this.resourceServer, evaluationContext.getIdentity(), authorization); - } - return resources.stream().flatMap((Function>) resource -> { Set givenScopes = resource.getScopes(); diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java index 9a3c9b361a..cccb38ff39 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java @@ -21,10 +21,15 @@ package org.keycloak.authorization.admin.representation; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.Decision.Effect; import org.keycloak.authorization.admin.util.Models; +import org.keycloak.authorization.common.KeycloakIdentity; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.policy.evaluation.Result; import org.keycloak.authorization.policy.evaluation.Result.PolicyResult; +import org.keycloak.authorization.util.Permissions; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.authorization.Permission; import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; @@ -42,14 +47,22 @@ public class PolicyEvaluationResponse { private List results; private boolean entitlements; private Effect status; + private AccessToken rpt; private PolicyEvaluationResponse() { } - public static PolicyEvaluationResponse build(PolicyEvaluationRequest evaluationRequest, List results, ResourceServer resourceServer, AuthorizationProvider authorization) { + public static PolicyEvaluationResponse build(PolicyEvaluationRequest evaluationRequest, List results, ResourceServer resourceServer, AuthorizationProvider authorization, KeycloakIdentity identity) { PolicyEvaluationResponse response = new PolicyEvaluationResponse(); List resultsRep = new ArrayList<>(); + AccessToken accessToken = identity.getAccessToken(); + AccessToken.Authorization authorizationData = new AccessToken.Authorization(); + + authorizationData.setPermissions(Permissions.allPermits(results)); + accessToken.setAuthorization(authorizationData); + + response.rpt = accessToken; if (results.stream().anyMatch(evaluationResult -> evaluationResult.getEffect().equals(Effect.DENY))) { response.status = Effect.DENY; @@ -124,6 +137,10 @@ public class PolicyEvaluationResponse { return entitlements; } + public AccessToken getRpt() { + return rpt; + } + public static class EvaluationResultRepresentation { private ResourceRepresentation resource; 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 50d591960b..1f39b06b66 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 @@ -1076,10 +1076,11 @@ authz-permission-scope-scope.tooltip=Specifies that this permission must be appl # Authz Evaluation authz-evaluation-identity-information=Identity Information authz-evaluation-identity-information.tooltip=The available options to configure the identity information that will be used when evaluating policies. -authz-evaluation-client.tooltip=Select the client making this authorization request. +authz-evaluation-client.tooltip=Select the client making this authorization request. If not provided, authorization requests would be done based on the client you are in. authz-evaluation-user.tooltip=Select an user whose identity is going to be used to query permissions from the server. authz-evaluation-role.tooltip=Select the roles you want to associate with the selected user. authz-evaluation-new=New Evaluation +authz-evaluation-re-evaluate=Re-Evaluate authz-evaluation-previous=Previous Evaluation authz-evaluation-contextual-info=Contextual Information authz-evaluation-contextual-info.tooltip=The available options to configure any contextual information that will be used when evaluating policies. @@ -1093,3 +1094,6 @@ authz-evaluation-no-policies-resource=No policies were found for this resource. authz-evaluation-result.tooltip=The overall result for this permission request. authz-evaluation-scopes.tooltip=The requested scopes. authz-evaluation-policies.tooltip=Details about which policies were evaluated and their decisions. +authz-evaluation-authorization-data=Response +authz-evaluation-authorization-data.tooltip=Represents a token carrying authorization data as a result of the processing of an authorization request. This representation is basically what Keycloak issues to clients asking for permissions. Check the 'authorization' claim for the permissions that were granted based on the current authorization request. +authz-show-authorization-data=Show Authorization Data diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js index bbe0fd0a45..d772f92f0e 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js @@ -1329,6 +1329,18 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio } } + $scope.reevaluate = function() { + if ($scope.authzRequest.entitlements) { + $scope.entitlements(); + } else { + $scope.save(); + } + } + + $scope.showAuthzData = function() { + $scope.showRpt = true; + } + $scope.save = function() { $scope.authzRequest.entitlements = false; if ($scope.applyResourceType) { @@ -1356,10 +1368,12 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio $scope.showResultTab = function() { $scope.showResult = true; + $scope.showRpt = false; } $scope.showRequestTab = function() { $scope.showResult = false; + $scope.showRpt = false; } User.query({realm: $route.current.params.realm}, function(data) { diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html index ce62c33529..d875e86159 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html @@ -12,6 +12,10 @@ +
+
+ +
+ +
+ {{:: 'authz-evaluation-authorization-data.tooltip' | translate}} +
+
+
@@ -56,12 +70,6 @@ {{:: 'authz-evaluation-user.tooltip' | translate}}
-
-
- -
-
-
@@ -268,7 +276,7 @@
-
+
\ No newline at end of file From cd94778decc2e5b717df4e123979e84e8880c68d Mon Sep 17 00:00:00 2001 From: wyvie Date: Fri, 22 Jul 2016 23:58:59 +0200 Subject: [PATCH 11/13] [KEYCLOAK-3035] Fixed broker tests, should not randomly fail anymore --- .../testsuite/broker/AbstractBrokerTest.java | 130 ++++++++++++------ .../testsuite/broker/KcSamlBrokerTest.java | 2 - .../broker/KcSamlSignedBrokerTest.java | 2 - 3 files changed, 90 insertions(+), 44 deletions(-) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java index a2a16b4bc8..29f796f6f5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java @@ -1,5 +1,13 @@ package org.keycloak.testsuite.broker; +import static org.junit.Assert.assertEquals; +import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient; +import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.List; + import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Test; @@ -9,7 +17,6 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.services.messages.Messages; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.pages.AccountPasswordPage; @@ -17,15 +24,12 @@ import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.UpdateAccountInformationPage; import org.keycloak.testsuite.util.RealmBuilder; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.List; - -import static org.jgroups.util.Util.assertTrue; -import static org.junit.Assert.assertEquals; -import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient; -import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword; +import org.openqa.selenium.By; +import org.openqa.selenium.TimeoutException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedCondition; +import org.openqa.selenium.support.ui.WebDriverWait; public abstract class AbstractBrokerTest extends AbstractKeycloakTest { @@ -128,17 +132,6 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest { return identityProviderRepresentation; } - private void waitForPage(String title) { - long startAt = System.currentTimeMillis(); - - while (!driver.getTitle().toLowerCase().contains(title) - && System.currentTimeMillis() - startAt < 200) { - try { - Thread.sleep(5); - } catch (InterruptedException ignore) {} - } - } - @Test public void logInAsUserInIDP() { driver.navigate().to(getAccountUrl(consumerRealmName())); @@ -146,9 +139,7 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest { log.debug("Clicking social " + getIDPAlias()); accountLoginPage.clickSocial(getIDPAlias()); - if (!driver.getCurrentUrl().contains("/auth/realms/" + providerRealmName() + "/")) { - log.debug("Not on provider realm page, url: " + driver.getCurrentUrl()); - } + waitForPage("log in to"); Assert.assertTrue("Driver should be on the provider realm page right now", driver.getCurrentUrl().contains("/auth/realms/" + providerRealmName() + "/")); @@ -166,9 +157,11 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest { updateAccountInformationPage.updateAccountInformation("Firstname", "Lastname"); UsersResource consumerUsers = adminClient.realm(consumerRealmName()).users(); - Assert.assertTrue("There must be at least one user", consumerUsers.count() > 0); - List users = consumerUsers.search("", 0, 5); + int userCount = consumerUsers.count(); + Assert.assertTrue("There must be at least one user", userCount > 0); + + List users = consumerUsers.search("", 0, userCount); boolean isUserFound = false; for (UserRepresentation user : users) { @@ -195,11 +188,12 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest { log.debug("Clicking social " + getIDPAlias()); accountLoginPage.clickSocial(getIDPAlias()); + waitForPage("log in to"); + Assert.assertTrue("Driver should be on the provider realm page right now", driver.getCurrentUrl().contains("/auth/realms/" + providerRealmName() + "/")); accountLoginPage.login(getUserLogin(), getUserPassword()); - System.out.println(driver.getPageSource()); assertEquals(accountPage.buildUri().toASCIIString().replace("master", "consumer") + "/", driver.getCurrentUrl()); assertEquals(userCount, adminClient.realm(consumerRealmName()).users().count()); @@ -219,18 +213,40 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest { driver.navigate().to(getAuthRoot() + "/auth/realms/" + providerRealmName() + "/protocol/" + "openid-connect" - + "/logout"); + + "/logout?redirect_uri=" + encodeUrl(getAccountUrl(providerRealmName()))); driver.navigate().to(getAccountUrl(consumerRealmName())); - accountLoginPage.login(getUserLogin(), "invalid"); - accountLoginPage.login(getUserLogin(), "invalid"); - accountLoginPage.login(getUserLogin(), "invalid"); + try { + waitForPage("log in to"); + } catch (TimeoutException e) { + log.debug(driver.getTitle()); + log.debug(driver.getPageSource()); + Assert.fail("Timeout while waiting for login page"); + } + + for (int i = 0; i < 3; i++) { + try { + waitForElementEnabled("login"); + } catch (TimeoutException e) { + Assert.fail("Timeout while waiting for login element enabled"); + } + + accountLoginPage.login(getUserLogin(), "invalid"); + } assertEquals("Invalid username or password.", accountLoginPage.getError()); accountLoginPage.clickSocial(getIDPAlias()); + try { + waitForPage("log in to"); + } catch (TimeoutException e) { + log.debug(driver.getTitle()); + log.debug(driver.getPageSource()); + Assert.fail("Timeout while waiting for login page"); + } + Assert.assertTrue("Driver should be on the provider realm page right now", driver.getCurrentUrl().contains("/auth/realms/" + providerRealmName() + "/")); accountLoginPage.login(getUserLogin(), getUserPassword()); @@ -238,24 +254,17 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest { assertEquals("Account is disabled, contact admin.", errorPage.getError()); } - protected void testSingleLogout() { + private void testSingleLogout() { log.debug("Testing single log out"); driver.navigate().to(getAccountUrl(providerRealmName())); Assert.assertTrue("Should be logged in the account page", driver.getTitle().endsWith("Account Management")); - String encodedAccount; - try { - encodedAccount = URLEncoder.encode(getAccountUrl(providerRealmName()), "UTF-8"); - } catch (UnsupportedEncodingException e) { - encodedAccount = getAccountUrl(providerRealmName()); - } - driver.navigate().to(getAuthRoot() + "/auth/realms/" + providerRealmName() + "/protocol/" + "openid-connect" - + "/logout?redirect_uri=" + encodedAccount); + + "/logout?redirect_uri=" + encodeUrl(getAccountUrl(providerRealmName()))); waitForPage("log in to " + providerRealmName()); @@ -274,4 +283,45 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest { private String getAccountPasswordUrl(String realmName) { return getAuthRoot() + "/auth/realms/" + realmName + "/account/password"; } + + private void waitForPage(final String title) { + WebDriverWait wait = new WebDriverWait(driver, 5); + + ExpectedCondition condition = new ExpectedCondition() { + @Override + public Boolean apply(WebDriver input) { + return input.getTitle().toLowerCase().contains(title); + } + }; + + wait.until(condition); + } + + private void waitForElementEnabled(final String elementName) { + WebDriverWait wait = new WebDriverWait(driver, 5); + + ExpectedCondition condition = new ExpectedCondition() { + @Override + public Boolean apply(WebDriver input) { + List elements = input.findElements(By.name(elementName)); + if (elements.size() == 0) + return false; + + return elements.get(0).isEnabled(); + } + }; + + wait.until(condition); + } + + private String encodeUrl(String url) { + String result; + try { + result = URLEncoder.encode(url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + result = url; + } + + return result; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java index 7a8fb9ceb3..cdacaa7874 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java @@ -1,6 +1,5 @@ package org.keycloak.testsuite.broker; -import org.junit.Ignore; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -13,7 +12,6 @@ import java.util.Map; import static org.keycloak.testsuite.broker.BrokerTestConstants.*; -@Ignore public class KcSamlBrokerTest extends AbstractBrokerTest { @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java index 1a2eca83d5..128d1e020e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java @@ -1,6 +1,5 @@ package org.keycloak.testsuite.broker; -import org.junit.Ignore; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -11,7 +10,6 @@ import java.util.Map; import static org.keycloak.testsuite.broker.BrokerTestConstants.*; -@Ignore public class KcSamlSignedBrokerTest extends KcSamlBrokerTest { @Override From fea277a7f5fe569c4c738559993e379e0a3a22a0 Mon Sep 17 00:00:00 2001 From: Dmitry Telegin Date: Sat, 30 Jul 2016 01:59:55 +0300 Subject: [PATCH 12/13] KEYCLOAK-3369: Fire RealmPostCreateEvent --- .../java/org/keycloak/models/RealmModel.java | 13 +++++++++---- .../services/managers/RealmManager.java | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) 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 88eb240265..af4d6d746d 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -38,6 +38,11 @@ public interface RealmModel extends RoleContainerModel { RealmModel getCreatedRealm(); } + interface RealmPostCreateEvent extends ProviderEvent { + RealmModel getCreatedRealm(); + KeycloakSession getKeycloakSession(); + } + interface RealmRemovedEvent extends ProviderEvent { RealmModel getRealm(); KeycloakSession getKeycloakSession(); @@ -342,19 +347,19 @@ public interface RealmModel extends RoleContainerModel { Set getEventsListeners(); void setEventsListeners(Set listeners); - + Set getEnabledEventTypes(); void setEnabledEventTypes(Set enabledEventTypes); - + boolean isAdminEventsEnabled(); void setAdminEventsEnabled(boolean enabled); - + boolean isAdminEventsDetailsEnabled(); void setAdminEventsDetailsEnabled(boolean enabled); - + ClientModel getMasterAdminClient(); void setMasterAdminClient(ClientModel client); diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 214abb3df4..1add03dbaa 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -121,6 +121,8 @@ public class RealmManager implements RealmImporter { setupOfflineTokens(realm); setupAuthorizationServices(realm); + fireRealmPostCreate(realm); + return realm; } @@ -491,6 +493,7 @@ public class RealmManager implements RealmImporter { } setupAuthorizationServices(realm); + fireRealmPostCreate(realm); return realm; } @@ -587,4 +590,19 @@ public class RealmManager implements RealmImporter { private void setupAuthorizationServices(RealmModel realm) { KeycloakModelUtils.setupAuthorizationServices(realm); } + + private void fireRealmPostCreate(RealmModel realm) { + session.getKeycloakSessionFactory().publish(new RealmModel.RealmPostCreateEvent() { + @Override + public RealmModel getCreatedRealm() { + return realm; + } + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); + + } + } From c5d40a76ff209ae9e7620ad757de27d93199a858 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 1 Aug 2016 12:31:16 +0200 Subject: [PATCH 13/13] Fix DB2 and Oracle --- .../src/main/resources/META-INF/db2-jpa-changelog-master.xml | 1 + model/jpa/src/main/resources/META-INF/jpa-changelog-2.1.0.xml | 2 +- model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/model/jpa/src/main/resources/META-INF/db2-jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/db2-jpa-changelog-master.xml index 6b9ab90282..1481095bb6 100644 --- a/model/jpa/src/main/resources/META-INF/db2-jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/db2-jpa-changelog-master.xml @@ -34,4 +34,5 @@ + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.1.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.1.0.xml index 79c3e9e568..988662d1e2 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.1.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.1.0.xml @@ -175,7 +175,7 @@ - +
{{:: 'name' | translate}}{{:: 'client' | translate}}{{:: 'authz-required' | translate}} {{:: 'actions' | translate}}
{{role.name}}{{role.container.name}} - +
{{:: 'name' | translate}}{{:: 'authz-resources' | translate}}{{:: 'authz-permissions' | translate}}{{:: 'actions' | translate}}
{{scope.name}} + {{:: 'authz-no-resources-assigned' | translate}} + + + {{resource.name}}{{$last ? '' : ', '}} + + + + {{:: 'authz-no-permission-assigned' | translate}} + + + {{policy.name}}{{$last ? '' : ', '}} + + + + +
{{:: 'no-results' | translate}}