diff --git a/CHANGELOG.md b/CHANGELOG.md index c27f44e03..d4b0773e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Version 3.3.2 (2017-09-30) + +* [new] A `@RequiresCrudPermissions` annotation allows to add permission checks based on the detected CRUD action of the called method. +* [new] SPI `CrudActionResolver` has been added to security to allow for resolving the CRUD action of a particular method. +* [new] A JAX-RS implementation of `CrudActionResolver` detects the CRUD action based upon the JAX-RS annotations. + # Version 3.3.1 (2017-09-06) * [new] Configuration dump (`config` tool) now dumps inner properties for maps, collections, arrays and complex objects. diff --git a/cli/pom.xml b/cli/pom.xml index 4e9551ab7..1b60ca602 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-cli diff --git a/core/pom.xml b/core/pom.xml index fc9af598c..e39f44dcb 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-core diff --git a/pom.xml b/pom.xml index 9e06be98e..25c3e789d 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ org.seedstack.seed seed - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT pom diff --git a/rest/core/pom.xml b/rest/core/pom.xml index 9f2a0d310..f423e236f 100644 --- a/rest/core/pom.xml +++ b/rest/core/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed-rest - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-rest-core diff --git a/rest/core/src/main/java/org/seedstack/seed/rest/internal/RestCrudActionResolver.java b/rest/core/src/main/java/org/seedstack/seed/rest/internal/RestCrudActionResolver.java new file mode 100644 index 000000000..183313780 --- /dev/null +++ b/rest/core/src/main/java/org/seedstack/seed/rest/internal/RestCrudActionResolver.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.rest.internal; + +import org.seedstack.seed.security.CrudAction; +import org.seedstack.seed.security.spi.CrudActionResolver; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +class RestCrudActionResolver implements CrudActionResolver { + private final Map, CrudAction> annotationMap; + + RestCrudActionResolver() { + Map, CrudAction> map = new HashMap<>(); + map.put(javax.ws.rs.DELETE.class, CrudAction.DELETE); + map.put(javax.ws.rs.GET.class, CrudAction.READ); + map.put(javax.ws.rs.HEAD.class, CrudAction.READ); + map.put(javax.ws.rs.OPTIONS.class, CrudAction.READ); + map.put(javax.ws.rs.POST.class, CrudAction.CREATE); + map.put(javax.ws.rs.PUT.class, CrudAction.UPDATE); + annotationMap = Collections.unmodifiableMap(map); + } + + @Override + public Optional resolve(Method method) { + return Arrays.stream(method.getAnnotations()) + .map(Annotation::annotationType) + .map(x -> annotationMap.getOrDefault(x, null)) + .filter(Objects::nonNull) + .findFirst(); + } +} diff --git a/rest/core/src/test/java/org/seedstack/seed/rest/internal/RestCrudActionResolverTest.java b/rest/core/src/test/java/org/seedstack/seed/rest/internal/RestCrudActionResolverTest.java new file mode 100644 index 000000000..81070be4c --- /dev/null +++ b/rest/core/src/test/java/org/seedstack/seed/rest/internal/RestCrudActionResolverTest.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.rest.internal; + +import org.junit.Before; +import org.junit.Test; +import org.seedstack.seed.security.CrudAction; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HEAD; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RestCrudActionResolverTest { + private RestCrudActionResolver resolverUnderTest; + + @Before + public void setup() throws Exception { + resolverUnderTest = new RestCrudActionResolver(); + } + + @Test + public void test_that_resolves_to_the_right_verb() throws Exception { + assertThat(resolverUnderTest.resolve(Fixture.class.getMethod("delete"))).isPresent().contains(CrudAction.DELETE); + assertThat(resolverUnderTest.resolve(Fixture.class.getMethod("get"))).isPresent().contains(CrudAction.READ); + assertThat(resolverUnderTest.resolve(Fixture.class.getMethod("head"))).isPresent().contains(CrudAction.READ); + assertThat(resolverUnderTest.resolve(Fixture.class.getMethod("options"))).isPresent().contains(CrudAction.READ); + assertThat(resolverUnderTest.resolve(Fixture.class.getMethod("post"))).isPresent().contains(CrudAction.CREATE); + assertThat(resolverUnderTest.resolve(Fixture.class.getMethod("put"))).isPresent().contains(CrudAction.UPDATE); + assertThat(resolverUnderTest.resolve(Fixture.class.getMethod("none"))).isNotPresent(); + assertThat(resolverUnderTest.resolve(Fixture.class.getMethod("random"))).isNotPresent(); + } + + // Test Fixture + public static class Fixture { + + @DELETE + public void delete() { + + } + + @GET + public void get() { + + } + + @Deprecated + public void random() { + + } + + @HEAD + public void head() { + } + + @OPTIONS + public void options() { + + } + + @POST + public void post() { + + } + + @PUT + public void put() { + + } + + public void none() { + + } + + } + +} diff --git a/rest/jersey2/pom.xml b/rest/jersey2/pom.xml index 8622f8af9..ea5362639 100644 --- a/rest/jersey2/pom.xml +++ b/rest/jersey2/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed-rest - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-rest-jersey2 diff --git a/rest/pom.xml b/rest/pom.xml index a6c414a3c..409dc2121 100644 --- a/rest/pom.xml +++ b/rest/pom.xml @@ -13,7 +13,7 @@ org.seedstack.seed seed - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-rest diff --git a/rest/specs/pom.xml b/rest/specs/pom.xml index 1196c62dc..202c5f3cc 100644 --- a/rest/specs/pom.xml +++ b/rest/specs/pom.xml @@ -13,7 +13,7 @@ org.seedstack.seed seed-rest - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-rest-specs diff --git a/security/core/pom.xml b/security/core/pom.xml index 3106c8e6f..10f3316df 100644 --- a/security/core/pom.xml +++ b/security/core/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed-security - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-security-core @@ -72,7 +72,6 @@ ${javax.el.version} provided - org.glassfish javax.el diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/CrudSecurityIT.java b/security/core/src/it/java/org/seedstack/seed/security/internal/CrudSecurityIT.java new file mode 100644 index 000000000..ac894f917 --- /dev/null +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/CrudSecurityIT.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal; + +import org.apache.shiro.SecurityUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.seedstack.seed.it.SeedITRunner; +import org.seedstack.seed.security.AuthorizationException; +import org.seedstack.seed.security.SecuritySupport; +import org.seedstack.seed.security.WithUser; +import org.seedstack.seed.security.internal.fixtures.AnnotatedCrudClass4Security; +import org.seedstack.seed.security.internal.fixtures.AnnotatedCrudMethods4Security; + +import javax.inject.Inject; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@RunWith(SeedITRunner.class) +public class CrudSecurityIT { + + @Inject + private AnnotatedCrudClass4Security annotatedClass; + + @Inject + private AnnotatedCrudMethods4Security annotatedMethods; + + @Inject + private SecuritySupport securitySupport; + + @Test + @WithUser(id = "Obiwan", password = "yodarulez") + public void Obiwan_should_not_be_able_to_interpret_anything() { + assertThatThrownBy(() -> annotatedClass.read()).isInstanceOf(AuthorizationException.class); + assertThatThrownBy(() -> annotatedMethods.read()).isInstanceOf(AuthorizationException.class); + + } + + // RESTBOT + @Test + @WithUser(id = "R2D2", password = "beep") + public void r2d2_should_be_able_to_be_a_restbot() { + // Delete jabba! + assertThat(SecurityUtils.getSubject().isPermitted("jabba:delete")).isTrue(); + assertThat(securitySupport.isPermitted("jabba:delete")).isTrue(); + + // Update c3p0! + assertThat(SecurityUtils.getSubject().isPermitted("c3p0:update")).isTrue(); + assertThat(securitySupport.isPermitted("c3p0:update")).isTrue(); + + // Create X-Wing + assertThat(SecurityUtils.getSubject().isPermitted("xwing:create")).isTrue(); + assertThat(securitySupport.isPermitted("xwing:create")).isTrue(); + + // Read chewaka + assertThat(SecurityUtils.getSubject().isPermitted("chewaka:read")).isTrue(); + assertThat(securitySupport.isPermitted("chewaka:read")).isTrue(); + + // is a restbot + assertThat(SecurityUtils.getSubject().hasRole("restbot")).isTrue(); + assertThat(securitySupport.hasRole("restbot")).isTrue(); + } + + @Test + @WithUser(id = "R2D2", password = "beep") + public void r2d2_should_be_able_to_call_any_kind_of_method() { + + assertThat(annotatedClass.create()).isTrue(); + assertThat(annotatedClass.read()).isTrue(); + assertThat(annotatedClass.update()).isTrue(); + assertThat(annotatedClass.delete()).isTrue(); + + assertThat(annotatedMethods.create()).isTrue(); + assertThat(annotatedMethods.read()).isTrue(); + assertThat(annotatedMethods.update()).isTrue(); + assertThat(annotatedMethods.delete()).isTrue(); + + } + + // INTERPRETER + @Test + @WithUser(id = "C3P0", password = "ewokgod") + public void c3p0_should_only_be_able_to_read_rest_as_interpreter() { + // Read ewoks + assertThat(SecurityUtils.getSubject().isPermitted("ewok:read")).isTrue(); + assertThat(securitySupport.isPermitted("ewok:read")).isTrue(); + + // Should not be able to update itself + assertThat(SecurityUtils.getSubject().isPermitted("c3p0:update")).isFalse(); + assertThat(securitySupport.isPermitted("c3p0:update")).isFalse(); + + // Is an interpreter + assertThat(SecurityUtils.getSubject().hasRole("interpreter")).isTrue(); + assertThat(securitySupport.hasRole("interpreter")).isTrue(); + } + + @Test + @WithUser(id = "C3P0", password = "ewokgod") + public void c3p0_should_be_able_to_read_data() { + + assertThatThrownBy(() -> annotatedMethods.create()).isInstanceOf(AuthorizationException.class); + assertThatThrownBy(() -> annotatedMethods.update()).isInstanceOf(AuthorizationException.class); + assertThatThrownBy(() -> annotatedMethods.delete()).isInstanceOf(AuthorizationException.class); + assertThat(annotatedMethods.read()).isTrue(); + + assertThatThrownBy(() -> annotatedClass.create()).isInstanceOf(AuthorizationException.class); + assertThatThrownBy(() -> annotatedClass.update()).isInstanceOf(AuthorizationException.class); + assertThatThrownBy(() -> annotatedClass.delete()).isInstanceOf(AuthorizationException.class); + assertThat(annotatedClass.read()).isTrue(); + } +} diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/SecurityIT.java b/security/core/src/it/java/org/seedstack/seed/security/internal/SecurityIT.java index d0e086f2c..7d850fa9a 100644 --- a/security/core/src/it/java/org/seedstack/seed/security/internal/SecurityIT.java +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/SecurityIT.java @@ -7,7 +7,6 @@ */ package org.seedstack.seed.security.internal; -import com.google.inject.AbstractModule; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; @@ -16,7 +15,6 @@ import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; -import org.seedstack.seed.Install; import org.seedstack.seed.it.SeedITRunner; import org.seedstack.seed.security.AuthorizationException; import org.seedstack.seed.security.SecuritySupport; @@ -114,14 +112,4 @@ public void Anakin_should_not_be_able_to_teach() { public void Anakin_should_have_customized_principal() { Assertions.assertThat(Principals.getSimplePrincipalByName(securitySupport.getOtherPrincipals(), "foo").getValue()).isEqualTo("bar"); } - - @Install - public static class securityTestModule extends AbstractModule { - - @Override - protected void configure() { - bind(AnnotatedClass4Security.class); - } - - } } diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedClass4Security.java b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedClass4Security.java index 737c75e46..4fb6ecf74 100644 --- a/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedClass4Security.java +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedClass4Security.java @@ -7,20 +7,21 @@ */ package org.seedstack.seed.security.internal.fixtures; +import org.seedstack.seed.it.ITBind; import org.seedstack.seed.security.RequiresPermissions; import org.seedstack.seed.security.RequiresRoles; +@ITBind public class AnnotatedClass4Security { + final String place = "coruscant"; - final String place = "coruscant"; - - @RequiresRoles("jedi") - public boolean callTheForce() { - return true; - } - - @RequiresPermissions("academy:teach") - public boolean teach(){ - return true; - } + @RequiresRoles("jedi") + public boolean callTheForce() { + return true; + } + + @RequiresPermissions("academy:teach") + public boolean teach() { + return true; + } } \ No newline at end of file diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedCrudClass4Security.java b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedCrudClass4Security.java new file mode 100644 index 000000000..3d2156df0 --- /dev/null +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedCrudClass4Security.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.fixtures; + +import org.seedstack.seed.it.ITBind; +import org.seedstack.seed.security.RequiresCrudPermissions; +import org.seedstack.seed.security.internal.fixtures.annotations.CREATE; +import org.seedstack.seed.security.internal.fixtures.annotations.DELETE; +import org.seedstack.seed.security.internal.fixtures.annotations.READ; +import org.seedstack.seed.security.internal.fixtures.annotations.UPDATE; + +@ITBind +@RequiresCrudPermissions("crudTest") +public class AnnotatedCrudClass4Security { + + @DELETE + public boolean delete() { + return true; + } + + @READ + public boolean read() { + return true; + } + + @UPDATE + public boolean update() { + return true; + } + + @CREATE + public boolean create() { + return true; + } + + // Empty + public boolean none() { + return true; + } + +} diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedCrudMethods4Security.java b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedCrudMethods4Security.java new file mode 100644 index 000000000..1ca3d39d9 --- /dev/null +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedCrudMethods4Security.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.fixtures; + +import org.seedstack.seed.it.ITBind; +import org.seedstack.seed.security.RequiresCrudPermissions; +import org.seedstack.seed.security.internal.fixtures.annotations.CREATE; +import org.seedstack.seed.security.internal.fixtures.annotations.DELETE; +import org.seedstack.seed.security.internal.fixtures.annotations.READ; +import org.seedstack.seed.security.internal.fixtures.annotations.UPDATE; + +@ITBind +public class AnnotatedCrudMethods4Security { + + @DELETE + @RequiresCrudPermissions("crudTest") + public boolean delete() { + return true; + } + + @READ + @RequiresCrudPermissions("crudTest") + public boolean read() { + return true; + } + + @UPDATE + @RequiresCrudPermissions("crudTest") + public boolean update() { + return true; + } + + @CREATE + @RequiresCrudPermissions("crudTest") + public boolean create() { + return true; + } + + // Empty + public boolean none() { + return true; + } + +} diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/TestActionResolver.java b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/TestActionResolver.java new file mode 100644 index 000000000..fc33260ba --- /dev/null +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/TestActionResolver.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.fixtures; + +import org.seedstack.seed.security.CrudAction; +import org.seedstack.seed.security.internal.fixtures.annotations.CREATE; +import org.seedstack.seed.security.internal.fixtures.annotations.DELETE; +import org.seedstack.seed.security.internal.fixtures.annotations.READ; +import org.seedstack.seed.security.internal.fixtures.annotations.UPDATE; +import org.seedstack.seed.security.spi.CrudActionResolver; + +import java.lang.reflect.Method; +import java.util.Optional; + +public class TestActionResolver implements CrudActionResolver { + @Override + public Optional resolve(Method method) { + + if (method.getAnnotation(CREATE.class) != null) { + return Optional.of(CrudAction.CREATE); + } + + if (method.getAnnotation(READ.class) != null) { + return Optional.of(CrudAction.READ); + } + if (method.getAnnotation(UPDATE.class) != null) { + return Optional.of(CrudAction.UPDATE); + } + if (method.getAnnotation(DELETE.class) != null) { + return Optional.of(CrudAction.DELETE); + } + return Optional.empty(); + } +} diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/CREATE.java b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/CREATE.java new file mode 100644 index 000000000..878efd053 --- /dev/null +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/CREATE.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.fixtures.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({ TYPE, METHOD }) +public @interface CREATE { + +} diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/DELETE.java b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/DELETE.java new file mode 100644 index 000000000..d0fbdbb70 --- /dev/null +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/DELETE.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.fixtures.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({ TYPE, METHOD }) +public @interface DELETE { + +} diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/READ.java b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/READ.java new file mode 100644 index 000000000..f440c0870 --- /dev/null +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/READ.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package org.seedstack.seed.security.internal.fixtures.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({ TYPE, METHOD }) +public @interface READ { + +} diff --git a/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/UPDATE.java b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/UPDATE.java new file mode 100644 index 000000000..fb28cbe0a --- /dev/null +++ b/security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/UPDATE.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.fixtures.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Documented +@Retention(RUNTIME) +@Target({ TYPE, METHOD }) +public @interface UPDATE { + +} diff --git a/security/core/src/it/resources/application.yaml b/security/core/src/it/resources/application.yaml index 82d5ade86..063c3a24c 100644 --- a/security/core/src/it/resources/application.yaml +++ b/security/core/src/it/resources/application.yaml @@ -15,6 +15,12 @@ security: Anakin: password: imsodark roles: [SEED.PADAWAN, OTHER.UNKNOWN.ROLE] + R2D2: + password: beep + roles: [SEED.RESTBOT] + C3P0: + password: ewokgod + roles: [SEED.RESTBOT.INTERPRETER] ThePoltergeist: password: bouh roles: [SEED.JEDI, SEED.MU.GHOST, SEED.SX.GHOST] @@ -22,9 +28,13 @@ security: roles: padawan: SEED.PADAWAN jedi: SEED.JEDI + restbot: SEED.RESTBOT + interpreter: SEED.RESTBOT.INTERPRETER ghost: SEED.{scope}.GHOST nothing: '*' permissions: jedi: ['lightSaber:*', 'academy:*'] padawan: 'academy:learn' + restbot: ['*:read', '*:update', '*:create', '*:delete' ] + interpreter: '*:read' ghost: 'site:haunt' diff --git a/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityAopModule.java b/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityAopModule.java index f4f9baea3..b20af98ff 100644 --- a/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityAopModule.java +++ b/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityAopModule.java @@ -9,15 +9,42 @@ import com.google.inject.AbstractModule; import com.google.inject.matcher.Matchers; +import com.google.inject.multibindings.Multibinder; +import org.seedstack.seed.security.RequiresCrudPermissions; import org.seedstack.seed.security.RequiresPermissions; import org.seedstack.seed.security.RequiresRoles; +import org.seedstack.seed.security.internal.authorization.RequiresCrudPermissionsInterceptor; import org.seedstack.seed.security.internal.authorization.RequiresPermissionsInterceptor; import org.seedstack.seed.security.internal.authorization.RequiresRolesInterceptor; +import org.seedstack.seed.security.spi.CrudActionResolver; + +import java.util.Collection; class SecurityAopModule extends AbstractModule { + private final Collection> crudActionResolverClasses; + + SecurityAopModule(final Collection> crudActionResolverClasses) { + this.crudActionResolverClasses = crudActionResolverClasses; + } + @Override protected void configure() { - bindInterceptor(Matchers.any(), Matchers.annotatedWith(RequiresRoles.class), new RequiresRolesInterceptor(new ShiroSecuritySupport())); - bindInterceptor(Matchers.any(), Matchers.annotatedWith(RequiresPermissions.class), new RequiresPermissionsInterceptor(new ShiroSecuritySupport())); + bindInterceptor(Matchers.any(), Matchers.annotatedWith(RequiresRoles.class), new RequiresRolesInterceptor()); + bindInterceptor(Matchers.any(), Matchers.annotatedWith(RequiresPermissions.class), new RequiresPermissionsInterceptor()); + bindCrudInterceptor(); + } + + private void bindCrudInterceptor() { + Multibinder crudActionResolverMultibinder = Multibinder.newSetBinder(binder(), CrudActionResolver.class); + for (Class crudActionResolverClass : crudActionResolverClasses) { + crudActionResolverMultibinder.addBinding().to(crudActionResolverClass); + } + + RequiresCrudPermissionsInterceptor requiresCrudPermissionsInterceptor = new RequiresCrudPermissionsInterceptor(); + requestInjection(requiresCrudPermissionsInterceptor); + + // Allows a single annotation at class level, or multiple annotations / one per method + bindInterceptor(Matchers.annotatedWith(RequiresCrudPermissions.class), Matchers.any(), requiresCrudPermissionsInterceptor); + bindInterceptor(Matchers.any(), Matchers.annotatedWith(RequiresCrudPermissions.class), requiresCrudPermissionsInterceptor); } } diff --git a/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityModule.java b/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityModule.java index a8528fdc9..03452f023 100644 --- a/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityModule.java +++ b/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityModule.java @@ -21,6 +21,7 @@ import org.seedstack.seed.SeedException; import org.seedstack.seed.security.Scope; import org.seedstack.seed.security.internal.securityexpr.SecurityExpressionModule; +import org.seedstack.seed.security.spi.CrudActionResolver; import java.util.Collection; import java.util.Map; @@ -35,18 +36,20 @@ class SecurityModule extends AbstractModule { private final SecurityConfigurer securityConfigurer; private final boolean elAvailable; private final Collection securityProviders; + private final Collection> crudActionResolvers; - SecurityModule(SecurityConfigurer securityConfigurer, Map> scopeClasses, boolean elAvailable, Collection securityProviders) { + SecurityModule(SecurityConfigurer securityConfigurer, Map> scopeClasses, boolean elAvailable, Collection securityProviders, Collection> crudActionResolvers) { this.securityConfigurer = securityConfigurer; this.scopeClasses = scopeClasses; this.elAvailable = elAvailable; this.securityProviders = securityProviders; + this.crudActionResolvers = crudActionResolvers; } @Override protected void configure() { install(new SecurityInternalModule(securityConfigurer, scopeClasses)); - install(new SecurityAopModule()); + install(new SecurityAopModule(crudActionResolvers)); if (elAvailable) { install(new SecurityExpressionModule()); @@ -71,7 +74,6 @@ protected void configure() { install(removeSecurityManager(additionalSecurityModule)); } } - install(mainModuleToInstall); } diff --git a/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityPlugin.java b/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityPlugin.java index f232815f4..58583f143 100644 --- a/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityPlugin.java +++ b/security/core/src/main/java/org/seedstack/seed/security/internal/SecurityPlugin.java @@ -20,25 +20,31 @@ import org.seedstack.seed.security.RolePermissionResolver; import org.seedstack.seed.security.Scope; import org.seedstack.seed.security.SecurityConfig; +import org.seedstack.seed.security.spi.CrudActionResolver; import org.seedstack.seed.security.spi.SecurityScope; +import org.seedstack.shed.misc.PriorityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import static org.seedstack.shed.misc.PriorityUtils.sortByPriority; + /** * This plugin provides core security infrastructure, based on Apache Shiro * implementation. */ public class SecurityPlugin extends AbstractSeedPlugin { private static final Logger LOGGER = LoggerFactory.getLogger(SecurityPlugin.class); - private final Map> scopeClasses = new HashMap<>(); private final Set securityProviders = new HashSet<>(); + private final List> crudActionResolvers = new ArrayList<>(); private SecurityConfigurer securityConfigurer; private boolean elAvailable; @@ -59,61 +65,82 @@ public Collection classpathScanRequests() { .descendentTypeOf(RoleMapping.class) .descendentTypeOf(RolePermissionResolver.class) .descendentTypeOf(Scope.class) - .descendentTypeOf(PrincipalCustomizer.class).build(); + .descendentTypeOf(PrincipalCustomizer.class) + .descendentTypeOf(CrudActionResolver.class) + .build(); } @Override - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings({"unchecked"}) public InitState initialize(InitContext initContext) { SecurityConfig securityConfig = getConfiguration(SecurityConfig.class); Map, Collection>> scannedClasses = initContext.scannedSubTypesByAncestorClass(); configureScopes(scannedClasses.get(Scope.class)); + configureCrudActionResolvers(scannedClasses.get(CrudActionResolver.class)); securityProviders.addAll(initContext.dependencies(SecurityProvider.class)); elAvailable = initContext.dependency(ELPlugin.class).isFunctionMappingAvailable(); - Collection>> principalCustomizerClasses = (Collection) scannedClasses.get(PrincipalCustomizer.class); - securityConfigurer = new SecurityConfigurer(securityConfig, scannedClasses, principalCustomizerClasses); + securityConfigurer = new SecurityConfigurer( + securityConfig, + scannedClasses, + (Collection) scannedClasses.get(PrincipalCustomizer.class) + ); return InitState.INITIALIZED; } + @SuppressWarnings("unchecked") + private void configureCrudActionResolvers(Collection> candidates) { + if (candidates != null) { + candidates.stream() + .map(x -> (Class) x) + .forEach(resolver -> { + crudActionResolvers.add(resolver); + LOGGER.trace("Detected CRUD action resolver {}", resolver.getName()); + }); + sortByPriority(crudActionResolvers, PriorityUtils::priorityOfClassOf); + } + LOGGER.debug("Detected {} CRUD action resolver(s)", crudActionResolvers.size()); + } @SuppressWarnings("unchecked") - private void configureScopes(Collection> scopeClasses) { - if (scopeClasses != null) { - for (Class scopeCandidateClass : scopeClasses) { - if (Scope.class.isAssignableFrom(scopeCandidateClass)) { - SecurityScope securityScope = scopeCandidateClass.getAnnotation(SecurityScope.class); + private void configureScopes(Collection> candidates) { + if (candidates != null) { + for (Class candidate : candidates) { + if (Scope.class.isAssignableFrom(candidate)) { + SecurityScope securityScope = candidate.getAnnotation(SecurityScope.class); String scopeName; if (securityScope != null) { scopeName = securityScope.value(); } else { - scopeName = scopeCandidateClass.getSimpleName(); + scopeName = candidate.getSimpleName(); } try { - scopeCandidateClass.getConstructor(String.class); + candidate.getConstructor(String.class); } catch (NoSuchMethodException e) { throw SeedException.wrap(e, SecurityErrorCode.MISSING_ADEQUATE_SCOPE_CONSTRUCTOR) .put("scopeName", scopeName) - .put("class", scopeCandidateClass.getName()); + .put("class", candidate.getName()); } - if (this.scopeClasses.containsKey(scopeName)) { + if (scopeClasses.containsKey(scopeName)) { throw SeedException.createNew(SecurityErrorCode.DUPLICATE_SCOPE_NAME) .put("scopeName", scopeName) - .put("class1", this.scopeClasses.get(scopeName).getName()) - .put("class2", scopeCandidateClass.getName()); + .put("class1", scopeClasses.get(scopeName).getName()) + .put("class2", candidate.getName()); } - this.scopeClasses.put(scopeName, (Class) scopeCandidateClass); + LOGGER.trace("Detected security scope '{}' implemented by {}", scopeName, candidate.getName()); + scopeClasses.put(scopeName, (Class) candidate); } } } + LOGGER.debug("Detected {} security scope(s)", scopeClasses.size()); } @Override @@ -122,7 +149,7 @@ public Object nativeUnitModule() { securityConfigurer, scopeClasses, elAvailable, - securityProviders - ); + securityProviders, + crudActionResolvers); } } diff --git a/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/AbstractInterceptor.java b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/AbstractInterceptor.java new file mode 100644 index 000000000..af565998b --- /dev/null +++ b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/AbstractInterceptor.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.authorization; + +import org.apache.shiro.SecurityUtils; +import org.seedstack.seed.security.AuthorizationException; +import org.seedstack.seed.security.Logical; + +import java.util.Arrays; + + +class AbstractInterceptor { + + protected void checkPermission(String permission) { + try { + SecurityUtils.getSubject().checkPermission(permission); + } catch (org.apache.shiro.authz.AuthorizationException e) { + throw new AuthorizationException("Subject doesn't have permission " + permission, e); + } + } + + protected boolean hasPermissions(String[] permissions, Logical logic) { + switch (logic) { + case AND: + return hasAllPermissions(permissions); + case OR: + return hasAnyPermission(permissions); + default: + throw new AuthorizationException("Invalid logical operation specified"); + } + + } + + protected void checkRole(String role) { + try { + SecurityUtils.getSubject().checkRole(role); + } catch (org.apache.shiro.authz.AuthorizationException e) { + throw new AuthorizationException("Subject doesn't have role " + role, e); + } + } + + protected boolean hasRoles(String[] roles, Logical logic) { + switch (logic) { + case AND: + return hasAllRoles(roles); + case OR: + return hasAnyRole(roles); + default: + throw new AuthorizationException("Invalid logical operation specified"); + } + } + + protected boolean hasPermission(String permission) { + return SecurityUtils.getSubject().isPermitted(permission); + } + + protected boolean hasRole(String role) { + return SecurityUtils.getSubject().hasRole(role); + } + + private boolean hasAllPermissions(String[] permissions) { + return SecurityUtils.getSubject().isPermittedAll(permissions); + } + + private boolean hasAnyPermission(String[] permissions) { + return Arrays.stream(permissions).anyMatch(this::hasPermission); + } + + private boolean hasAllRoles(String[] roles) { + return SecurityUtils.getSubject().hasAllRoles(Arrays.asList(roles)); + } + + private boolean hasAnyRole(String[] roles) { + return Arrays.stream(roles).anyMatch(this::hasRole); + } + +} diff --git a/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/AbstractPermissionsInterceptor.java b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/AbstractPermissionsInterceptor.java new file mode 100644 index 000000000..9d9c9044f --- /dev/null +++ b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/AbstractPermissionsInterceptor.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.authorization; + +import org.aopalliance.intercept.MethodInterceptor; +import org.seedstack.seed.security.AuthorizationException; +import org.seedstack.seed.security.Logical; + +import java.lang.reflect.Method; +import java.util.Arrays; + +public abstract class AbstractPermissionsInterceptor extends AbstractInterceptor implements MethodInterceptor { + protected void checkPermissions(Method method, String[] perms, Logical logical) { + if (perms.length == 1) { + checkPermission(perms[0]); + } else { + boolean isAllowed = hasPermissions(perms, logical); + if (!isAllowed) { + if (Logical.OR.equals(logical)) { + throw new AuthorizationException("Subject does not have any of the permissions to access method " + method.toString()); + } else { + throw new AuthorizationException("Subject doesn't have permissions " + Arrays.toString(perms)); + } + } + } + } +} diff --git a/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresCrudPermissionsInterceptor.java b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresCrudPermissionsInterceptor.java new file mode 100644 index 000000000..cc6a98a4e --- /dev/null +++ b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresCrudPermissionsInterceptor.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.authorization; + +import org.aopalliance.intercept.MethodInvocation; +import org.seedstack.seed.core.internal.guice.ProxyUtils; +import org.seedstack.seed.security.AuthorizationException; +import org.seedstack.seed.security.CrudAction; +import org.seedstack.seed.security.RequiresCrudPermissions; +import org.seedstack.seed.security.spi.CrudActionResolver; + +import javax.inject.Inject; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; + +public class RequiresCrudPermissionsInterceptor extends AbstractPermissionsInterceptor { + @Inject + private Set resolvers; + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + findAnnotation(invocation).ifPresent(rcpAnnotation -> { + CrudAction action = findVerb(invocation).orElseThrow(() -> { + throw new AuthorizationException("Unable to determine CRUD action on method " + invocation.getMethod().toString()); + }); + checkPermissions( + invocation.getMethod(), + Arrays.stream(rcpAnnotation.value()) + .map(permission -> String.format("%s:%s", permission, action.getVerb())) + .toArray(String[]::new), + rcpAnnotation.logical()); + }); + return invocation.proceed(); + } + + private Optional findAnnotation(MethodInvocation invocation) { + RequiresCrudPermissions annotation = invocation.getMethod().getAnnotation(RequiresCrudPermissions.class); + if (annotation == null) { + annotation = ProxyUtils.cleanProxy(invocation.getThis().getClass()).getAnnotation(RequiresCrudPermissions.class); + } + return Optional.ofNullable(annotation); + } + + private Optional findVerb(MethodInvocation invocation) { + Method method = invocation.getMethod(); + // returns the result of the first resolver that gives a valid action + return resolvers.stream() + .map(x -> x.resolve(method)) + .filter(Optional::isPresent) + .findFirst() + .orElse(Optional.empty()); + } +} diff --git a/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresPermissionsInterceptor.java b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresPermissionsInterceptor.java index 77e1647a4..144898e3d 100644 --- a/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresPermissionsInterceptor.java +++ b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresPermissionsInterceptor.java @@ -7,66 +7,29 @@ */ package org.seedstack.seed.security.internal.authorization; -import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.seedstack.seed.security.AuthorizationException; -import org.seedstack.seed.security.Logical; +import org.seedstack.seed.core.internal.guice.ProxyUtils; import org.seedstack.seed.security.RequiresPermissions; -import org.seedstack.seed.security.SecuritySupport; -import java.lang.annotation.Annotation; +import java.util.Optional; /** * Interceptor for the annotation RequiresPermissions */ -public class RequiresPermissionsInterceptor implements MethodInterceptor { - - private SecuritySupport securitySupport; - - /** - * Constructor - * - * @param securitySupport - * the security support - */ - public RequiresPermissionsInterceptor(SecuritySupport securitySupport) { - this.securitySupport = securitySupport; - } - +public class RequiresPermissionsInterceptor extends AbstractPermissionsInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { - Annotation annotation = findAnnotation(invocation); - if (annotation == null) { - return invocation.proceed(); - } - RequiresPermissions rpAnnotation = (RequiresPermissions) annotation; - String[] perms = rpAnnotation.value(); - if (perms.length == 1) { - securitySupport.checkPermission(perms[0]); - return invocation.proceed(); - } else if (Logical.OR.equals(rpAnnotation.logical())) { - boolean hasAtLeastOnePermission = false; - for (String permission : perms) { - if (securitySupport.isPermitted(permission)) { - hasAtLeastOnePermission = true; - break; - } - } - if (!hasAtLeastOnePermission) { - throw new AuthorizationException("User does not have any of the permissions to access method " + invocation.getMethod().toString()); - } - } else { - // Otherwise rrAnnotation.logical() is by default considered as Logical.AND - securitySupport.checkPermissions(perms); - } + findAnnotation(invocation).ifPresent(rpAnnotation -> { + checkPermissions(invocation.getMethod(), rpAnnotation.value(), rpAnnotation.logical()); + }); return invocation.proceed(); } - private Annotation findAnnotation(MethodInvocation invocation) { - Annotation annotation = invocation.getMethod().getAnnotation(RequiresPermissions.class); + private Optional findAnnotation(MethodInvocation invocation) { + RequiresPermissions annotation = invocation.getMethod().getAnnotation(RequiresPermissions.class); if (annotation == null) { - annotation = invocation.getThis().getClass().getAnnotation(RequiresPermissions.class); + annotation = ProxyUtils.cleanProxy(invocation.getThis().getClass()).getAnnotation(RequiresPermissions.class); } - return annotation; + return Optional.ofNullable(annotation); } } diff --git a/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresRolesInterceptor.java b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresRolesInterceptor.java index 0fc50ef77..5b5deb981 100644 --- a/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresRolesInterceptor.java +++ b/security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresRolesInterceptor.java @@ -9,64 +9,45 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.seedstack.seed.core.internal.guice.ProxyUtils; import org.seedstack.seed.security.AuthorizationException; import org.seedstack.seed.security.Logical; import org.seedstack.seed.security.RequiresRoles; -import org.seedstack.seed.security.SecuritySupport; -import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Optional; /** * Interceptor for annotation RequiresRoles */ -public class RequiresRolesInterceptor implements MethodInterceptor { - - private SecuritySupport securitySupport; - - /** - * Constructor - * - * @param securitySupport - * the security support - */ - public RequiresRolesInterceptor(SecuritySupport securitySupport) { - this.securitySupport = securitySupport; - } - +public class RequiresRolesInterceptor extends AbstractInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { - Annotation annotation = findAnnotation(invocation); - if (annotation == null) { - return invocation.proceed(); - } - RequiresRoles rrAnnotation = (RequiresRoles) annotation; - String[] roles = rrAnnotation.value(); - if (roles.length == 1) { - securitySupport.checkRole(roles[0]); - return invocation.proceed(); - } else if (Logical.OR.equals(rrAnnotation.logical())) { - boolean hasAtLeastOneRole = false; - for (String role : roles) { - if (securitySupport.hasRole(role)) { - hasAtLeastOneRole = true; - break; + Optional annotation = findAnnotation(invocation); + if (annotation.isPresent()) { + RequiresRoles rrAnnotation = annotation.get(); + String[] roles = rrAnnotation.value(); + if (roles.length == 1) { + checkRole(roles[0]); + } else { + boolean isAllowed = hasRoles(roles, rrAnnotation.logical()); + if (!isAllowed) { + if (Logical.OR.equals(rrAnnotation.logical())) { + throw new AuthorizationException("User does not have any of the roles to access method " + invocation.getMethod().toString()); + } else { + throw new AuthorizationException("Subject doesn't have roles " + Arrays.toString(roles)); + } } } - if (!hasAtLeastOneRole) { - throw new AuthorizationException("User does not have any of the roles to access method " + invocation.getMethod().toString()); - } - } else { - // Otherwise rrAnnotation.logical() is by default considered as Logical.AND - securitySupport.checkRoles(roles); } return invocation.proceed(); } - private Annotation findAnnotation(MethodInvocation invocation) { - Annotation annotation = invocation.getMethod().getAnnotation(RequiresRoles.class); + private Optional findAnnotation(MethodInvocation invocation) { + RequiresRoles annotation = invocation.getMethod().getAnnotation(RequiresRoles.class); if (annotation == null) { - annotation = invocation.getThis().getClass().getAnnotation(RequiresRoles.class); + annotation = ProxyUtils.cleanProxy(invocation.getThis().getClass()).getAnnotation(RequiresRoles.class); } - return annotation; + return Optional.ofNullable(annotation); } } diff --git a/security/core/src/test/java/org/seedstack/seed/security/internal/SecurityAopModuleUniTest.java b/security/core/src/test/java/org/seedstack/seed/security/internal/SecurityAopModuleUniTest.java deleted file mode 100644 index 1094a8215..000000000 --- a/security/core/src/test/java/org/seedstack/seed/security/internal/SecurityAopModuleUniTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2013-2016, The SeedStack authors - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.seedstack.seed.security.internal; - -import com.google.inject.Binder; -import org.junit.Test; -import org.mockito.internal.util.reflection.Whitebox; - -import static org.mockito.Mockito.mock; - -public class SecurityAopModuleUniTest { - - @Test - public void testModule(){ - SecurityAopModule underTest = new SecurityAopModule(); - Binder b = mock(Binder.class); - Whitebox.setInternalState(underTest, "binder", b); - - underTest.configure(); - } -} diff --git a/security/core/src/test/java/org/seedstack/seed/security/internal/authorization/RequiresPermissionsInterceptorTest.java b/security/core/src/test/java/org/seedstack/seed/security/internal/authorization/RequiresPermissionsInterceptorTest.java index 0b4b58f74..5c020032c 100644 --- a/security/core/src/test/java/org/seedstack/seed/security/internal/authorization/RequiresPermissionsInterceptorTest.java +++ b/security/core/src/test/java/org/seedstack/seed/security/internal/authorization/RequiresPermissionsInterceptorTest.java @@ -7,102 +7,124 @@ */ package org.seedstack.seed.security.internal.authorization; +import static org.mockito.Mockito.when; + import org.aopalliance.intercept.MethodInvocation; +import org.apache.shiro.subject.Subject; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.seedstack.seed.security.AuthorizationException; import org.seedstack.seed.security.Logical; import org.seedstack.seed.security.RequiresPermissions; -import org.seedstack.seed.security.SecuritySupport; +import org.seedstack.seed.security.internal.shiro.AbstractShiroTest; -import static org.mockito.Mockito.when; +public class RequiresPermissionsInterceptorTest extends AbstractShiroTest { + + private RequiresPermissionsInterceptor underTest; + private Subject subjectUnderTest; + + @Before + public void setup() throws Exception { + subjectUnderTest = Mockito.mock(Subject.class); + setSubject(subjectUnderTest); + } + + @After + public void tearDownSubject() { + clearSubject(); + } + + @Test + public void test_one_permission_ok() throws Throwable { + + underTest = new RequiresPermissionsInterceptor(); + + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + when(methodInvocation.getMethod()) + .thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedMethod")); + + underTest.invoke(methodInvocation); + } + + @Test(expected = AuthorizationException.class) + public void test_one_permission_fail() throws Throwable { + Mockito.doThrow(new AuthorizationException()).when(subjectUnderTest).checkPermission("CODE"); -public class RequiresPermissionsInterceptorTest { + underTest = new RequiresPermissionsInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - private RequiresPermissionsInterceptor underTest; + when(methodInvocation.getMethod()) + .thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedMethod")); + underTest.invoke(methodInvocation); + } - @Test - public void test_one_permission_ok() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - underTest = new RequiresPermissionsInterceptor(securitySupport); + @Test + public void test_or_permission_ok() throws Throwable { - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - when(methodInvocation.getMethod()).thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedMethod")); + Mockito.when(subjectUnderTest.isPermitted("CODE")).thenReturn(true); + Mockito.when(subjectUnderTest.isPermitted("EAT")).thenReturn(false); - underTest.invoke(methodInvocation); - } + underTest = new RequiresPermissionsInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - @Test(expected = AuthorizationException.class) - public void test_one_permission_fail() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.doThrow(new AuthorizationException()).when(securitySupport).checkPermission("CODE"); + when(methodInvocation.getMethod()) + .thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedOrMethod")); + underTest.invoke(methodInvocation); + } - underTest = new RequiresPermissionsInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + @Test(expected = AuthorizationException.class) + public void test_or_permission_fail() throws Throwable { - when(methodInvocation.getMethod()).thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedMethod")); - underTest.invoke(methodInvocation); - } + Mockito.when(subjectUnderTest.isPermitted("CODE")).thenReturn(false); + Mockito.when(subjectUnderTest.isPermitted("EAT")).thenReturn(false); - @Test - public void test_or_permission_ok() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.when(securitySupport.isPermitted("CODE")).thenReturn(true); - Mockito.when(securitySupport.isPermitted("EAT")).thenReturn(false); + underTest = new RequiresPermissionsInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - underTest = new RequiresPermissionsInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + when(methodInvocation.getMethod()) + .thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedOrMethod")); + underTest.invoke(methodInvocation); + } - when(methodInvocation.getMethod()).thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedOrMethod")); - underTest.invoke(methodInvocation); - } + @Test + public void test_and_permission_ok() throws Throwable { - @Test(expected = AuthorizationException.class) - public void test_or_permission_fail() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.when(securitySupport.isPermitted("CODE")).thenReturn(false); - Mockito.when(securitySupport.isPermitted("EAT")).thenReturn(false); + Mockito.when(subjectUnderTest.isPermittedAll("CODE", "EAT")).thenReturn(true); - underTest = new RequiresPermissionsInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + underTest = new RequiresPermissionsInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - when(methodInvocation.getMethod()).thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedOrMethod")); - underTest.invoke(methodInvocation); - } + when(methodInvocation.getMethod()) + .thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedAndMethod")); + underTest.invoke(methodInvocation); + } - @Test - public void test_and_permission_ok() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.when(securitySupport.isPermitted("CODE")).thenReturn(true); - Mockito.when(securitySupport.isPermitted("EAT")).thenReturn(true); + @Test(expected = AuthorizationException.class) + public void test_and_permission_fail() throws Throwable { - underTest = new RequiresPermissionsInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + Mockito.doThrow(new AuthorizationException()).when(subjectUnderTest).checkPermissions("CODE", + "EAT"); - when(methodInvocation.getMethod()).thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedAndMethod")); - underTest.invoke(methodInvocation); - } + underTest = new RequiresPermissionsInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - @Test(expected = AuthorizationException.class) - public void test_and_permission_fail() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.doThrow(new AuthorizationException()).when(securitySupport).checkPermissions("CODE", "EAT"); + when(methodInvocation.getMethod()) + .thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedAndMethod")); + underTest.invoke(methodInvocation); + } - underTest = new RequiresPermissionsInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + @RequiresPermissions("CODE") + public void securedMethod() { + } - when(methodInvocation.getMethod()).thenReturn(RequiresPermissionsInterceptorTest.class.getMethod("securedAndMethod")); - underTest.invoke(methodInvocation); - } + @RequiresPermissions(value = { "CODE", "EAT" }, logical = Logical.OR) + public void securedOrMethod() { + } - @RequiresPermissions("CODE") - public void securedMethod() { - } - @RequiresPermissions(value = {"CODE", "EAT"}, logical = Logical.OR) - public void securedOrMethod() { - } - @RequiresPermissions(value = {"CODE", "EAT"}, logical = Logical.AND) - public void securedAndMethod() { - } + @RequiresPermissions(value = { "CODE", "EAT" }, logical = Logical.AND) + public void securedAndMethod() { + } } diff --git a/security/core/src/test/java/org/seedstack/seed/security/internal/authorization/RequiresRolesInterceptorTest.java b/security/core/src/test/java/org/seedstack/seed/security/internal/authorization/RequiresRolesInterceptorTest.java index 4afd5ee40..db3958fb8 100644 --- a/security/core/src/test/java/org/seedstack/seed/security/internal/authorization/RequiresRolesInterceptorTest.java +++ b/security/core/src/test/java/org/seedstack/seed/security/internal/authorization/RequiresRolesInterceptorTest.java @@ -7,102 +7,122 @@ */ package org.seedstack.seed.security.internal.authorization; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + import org.aopalliance.intercept.MethodInvocation; +import org.apache.shiro.subject.Subject; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.seedstack.seed.security.AuthorizationException; import org.seedstack.seed.security.Logical; import org.seedstack.seed.security.RequiresRoles; -import org.seedstack.seed.security.SecuritySupport; - -import static org.mockito.Mockito.when; - - -public class RequiresRolesInterceptorTest { - - private RequiresRolesInterceptor underTest; - - @Test - public void test_one_permission_ok() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - underTest = new RequiresRolesInterceptor(securitySupport); - - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - when(methodInvocation.getMethod()).thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedMethod")); - - underTest.invoke(methodInvocation); - } - - @Test(expected = AuthorizationException.class) - public void test_one_permission_fail() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.doThrow(new AuthorizationException()).when(securitySupport).checkRole("CODE"); - - underTest = new RequiresRolesInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - - when(methodInvocation.getMethod()).thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedMethod")); - underTest.invoke(methodInvocation); - } - - @Test - public void test_or_permission_ok() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.when(securitySupport.hasRole("CODE")).thenReturn(true); - Mockito.when(securitySupport.hasRole("EAT")).thenReturn(false); - - underTest = new RequiresRolesInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - - when(methodInvocation.getMethod()).thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedOrMethod")); - underTest.invoke(methodInvocation); - } - - @Test(expected = AuthorizationException.class) - public void test_or_permission_fail() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.when(securitySupport.hasRole("CODE")).thenReturn(false); - Mockito.when(securitySupport.hasRole("EAT")).thenReturn(false); - - underTest = new RequiresRolesInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - - when(methodInvocation.getMethod()).thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedOrMethod")); - underTest.invoke(methodInvocation); - } - - @Test - public void test_and_permission_ok() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.when(securitySupport.hasRole("CODE")).thenReturn(true); - Mockito.when(securitySupport.hasRole("EAT")).thenReturn(true); - - underTest = new RequiresRolesInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - - when(methodInvocation.getMethod()).thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedAndMethod")); - underTest.invoke(methodInvocation); - } - - @Test(expected = AuthorizationException.class) - public void test_and_permission_fail() throws Throwable { - SecuritySupport securitySupport = Mockito.mock(SecuritySupport.class); - Mockito.doThrow(new AuthorizationException()).when(securitySupport).checkRoles("CODE", "EAT"); - - underTest = new RequiresRolesInterceptor(securitySupport); - MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); - - when(methodInvocation.getMethod()).thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedAndMethod")); - underTest.invoke(methodInvocation); - } - - @RequiresRoles("CODE") - public void securedMethod() { - } - @RequiresRoles(value = {"CODE", "EAT"}, logical = Logical.OR) - public void securedOrMethod() { - } - @RequiresRoles(value = {"CODE", "EAT"}, logical = Logical.AND) - public void securedAndMethod() { - } +import org.seedstack.seed.security.internal.shiro.AbstractShiroTest; + +public class RequiresRolesInterceptorTest extends AbstractShiroTest { + + private RequiresRolesInterceptor underTest; + private Subject subjectUnderTest; + + @Before + public void setup() throws Exception { + subjectUnderTest = Mockito.mock(Subject.class); + setSubject(subjectUnderTest); + } + + @After + public void tearDownSubject() { + clearSubject(); + } + + @Test + public void test_one_permission_ok() throws Throwable { + underTest = new RequiresRolesInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + when(methodInvocation.getMethod()) + .thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedMethod")); + + underTest.invoke(methodInvocation); + } + + @Test(expected = AuthorizationException.class) + public void test_one_permission_fail() throws Throwable { + Mockito.doThrow(new AuthorizationException()).when(subjectUnderTest).checkRole("CODE"); + + underTest = new RequiresRolesInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + when(methodInvocation.getMethod()) + .thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedMethod")); + underTest.invoke(methodInvocation); + } + + @Test + public void test_or_permission_ok() throws Throwable { + Mockito.when(subjectUnderTest.hasRole("CODE")).thenReturn(true); + Mockito.when(subjectUnderTest.hasRole("EAT")).thenReturn(false); + + underTest = new RequiresRolesInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + + when(methodInvocation.getMethod()) + .thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedOrMethod")); + underTest.invoke(methodInvocation); + } + + @Test(expected = AuthorizationException.class) + public void test_or_permission_fail() throws Throwable { + Mockito.when(subjectUnderTest.hasRole("CODE")).thenReturn(false); + Mockito.when(subjectUnderTest.hasRole("EAT")).thenReturn(false); + + underTest = new RequiresRolesInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + + when(methodInvocation.getMethod()) + .thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedOrMethod")); + underTest.invoke(methodInvocation); + } + + @Test + public void test_and_permission_ok() throws Throwable { + + List roles = new ArrayList<>(); + roles.add("CODE"); + roles.add("EAT"); + Mockito.when(subjectUnderTest.hasAllRoles(roles)).thenReturn(true); + + underTest = new RequiresRolesInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + + when(methodInvocation.getMethod()) + .thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedAndMethod")); + underTest.invoke(methodInvocation); + } + + @Test(expected = AuthorizationException.class) + public void test_and_permission_fail() throws Throwable { + Mockito.doThrow(new AuthorizationException()).when(subjectUnderTest).checkRoles("CODE", "EAT"); + + underTest = new RequiresRolesInterceptor(); + MethodInvocation methodInvocation = Mockito.mock(MethodInvocation.class); + + when(methodInvocation.getMethod()) + .thenReturn(RequiresRolesInterceptorTest.class.getMethod("securedAndMethod")); + underTest.invoke(methodInvocation); + } + + @RequiresRoles("CODE") + public void securedMethod() { + } + + @RequiresRoles(value = { "CODE", "EAT" }, logical = Logical.OR) + public void securedOrMethod() { + } + + @RequiresRoles(value = { "CODE", "EAT" }, logical = Logical.AND) + public void securedAndMethod() { + } } diff --git a/security/core/src/test/java/org/seedstack/seed/security/internal/shiro/AbstractShiroTest.java b/security/core/src/test/java/org/seedstack/seed/security/internal/shiro/AbstractShiroTest.java new file mode 100644 index 000000000..8a1927fab --- /dev/null +++ b/security/core/src/test/java/org/seedstack/seed/security/internal/shiro/AbstractShiroTest.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.internal.shiro; + +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.UnavailableSecurityManagerException; +import org.apache.shiro.mgt.SecurityManager; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.LifecycleUtils; +import org.apache.shiro.util.ThreadState; +import org.junit.AfterClass; + +/** + * Abstract test case enabling Shiro in test environments. + */ +//TODO:find a suitable package for this helper class provided by shiro +public abstract class AbstractShiroTest { + + private static ThreadState subjectThreadState; + + public AbstractShiroTest() { + } + + /** + * Allows subclasses to set the currently executing {@link Subject} instance. + * + * @param subject + * the Subject instance + */ + protected void setSubject(Subject subject) { + clearSubject(); + subjectThreadState = createThreadState(subject); + subjectThreadState.bind(); + } + + protected Subject getSubject() { + return SecurityUtils.getSubject(); + } + + protected ThreadState createThreadState(Subject subject) { + return new SubjectThreadState(subject); + } + + /** + * Clears Shiro's thread state, ensuring the thread remains clean for future test execution. + */ + protected void clearSubject() { + doClearSubject(); + } + + private static void doClearSubject() { + if (subjectThreadState != null) { + subjectThreadState.clear(); + subjectThreadState = null; + } + } + + protected static void setSecurityManager(SecurityManager securityManager) { + SecurityUtils.setSecurityManager(securityManager); + } + + protected static SecurityManager getSecurityManager() { + return SecurityUtils.getSecurityManager(); + } + + @AfterClass + public static void tearDownShiro() { + doClearSubject(); + try { + SecurityManager securityManager = getSecurityManager(); + LifecycleUtils.destroy(securityManager); + } catch (UnavailableSecurityManagerException e) { + // we don't care about this when cleaning up the test environment + // (for example, maybe the subclass is a unit test and it didn't + // need a SecurityManager instance because it was using only + // mock Subject instances) + } + setSecurityManager(null); + } +} diff --git a/security/pom.xml b/security/pom.xml index ed69222a7..c3325dea9 100644 --- a/security/pom.xml +++ b/security/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-security diff --git a/security/specs/pom.xml b/security/specs/pom.xml index 51a8792e3..661dea75b 100644 --- a/security/specs/pom.xml +++ b/security/specs/pom.xml @@ -13,7 +13,7 @@ org.seedstack.seed seed-security - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-security-specs diff --git a/security/specs/src/main/java/org/seedstack/seed/security/CrudAction.java b/security/specs/src/main/java/org/seedstack/seed/security/CrudAction.java new file mode 100644 index 000000000..20ea3d9a3 --- /dev/null +++ b/security/specs/src/main/java/org/seedstack/seed/security/CrudAction.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security; + +/*** + * Possible CRUD actions that will be taken into account for CRUD interceptors + */ +public enum CrudAction { + CREATE("create"), UPDATE("update"), READ("read"), DELETE("delete"); + + private String verb; + + CrudAction(String verb) { + this.verb = verb; + } + + public String getVerb() { + return this.verb; + } +} diff --git a/security/specs/src/main/java/org/seedstack/seed/security/RequiresCrudPermissions.java b/security/specs/src/main/java/org/seedstack/seed/security/RequiresCrudPermissions.java new file mode 100644 index 000000000..d13479773 --- /dev/null +++ b/security/specs/src/main/java/org/seedstack/seed/security/RequiresCrudPermissions.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +/** + * Annotation that marks classes and methods which should be intercepted and checked for subject permissions associated with + * an automatically inferred {@link CrudAction}. The CRUD action is added at the end of the permission. For instance, checking + * for permission "admin:users" will effectively check for "admin:users:create" if the inferred CRUD action of the method is + * {@link CrudAction#CREATE}. + */ +@Retention(RUNTIME) +@Target({METHOD, TYPE}) +public @interface RequiresCrudPermissions { + + /** + * @return the permissions to check for. + */ + String[] value(); + + /** + * @return the logical operator to use between multiple permissions. + */ + Logical logical() default Logical.AND; + +} diff --git a/security/specs/src/main/java/org/seedstack/seed/security/RequiresPermissions.java b/security/specs/src/main/java/org/seedstack/seed/security/RequiresPermissions.java index d19c40f4c..9e2d458b3 100644 --- a/security/specs/src/main/java/org/seedstack/seed/security/RequiresPermissions.java +++ b/security/specs/src/main/java/org/seedstack/seed/security/RequiresPermissions.java @@ -21,13 +21,13 @@ @Retention(RUNTIME) public @interface RequiresPermissions { /** - * @return the list of permissions to check for. + * @return the permissions to check for. */ - String[] value(); + String[] value(); /** * @return the logical operator to use between multiple permissions. */ - Logical logical() default Logical.AND; + Logical logical() default Logical.AND; } diff --git a/security/specs/src/main/java/org/seedstack/seed/security/spi/CrudActionResolver.java b/security/specs/src/main/java/org/seedstack/seed/security/spi/CrudActionResolver.java new file mode 100644 index 000000000..3574719b5 --- /dev/null +++ b/security/specs/src/main/java/org/seedstack/seed/security/spi/CrudActionResolver.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.security.spi; + +import org.seedstack.seed.security.CrudAction; + +import java.lang.reflect.Method; +import java.util.Optional; + +/** + * A class implementing {@link CrudActionResolver} provides logic to resolve the {@link CrudAction} that is associated + * to a particular method. For instance, a JAX-RS resolver could use JAX-RS annotations to determine the ongoing CRUD + * action. Another example would be a Servlet resolver which can use the method signature of an HttpServlet to determine + * the corresponding action. + * + *

+ * Classes implementing this interface can be annotated with {@link javax.annotation.Priority} to define an absolute + * order among them. + *

+ */ +public interface CrudActionResolver { + /** + * Resolves a {@link CrudAction} from the specified method object. + * + * @param method the method object. + * @return an optionally resolved {@link CrudAction}. + */ + Optional resolve(Method method); +} \ No newline at end of file diff --git a/specs/pom.xml b/specs/pom.xml index d7f0fd3d7..967fa4060 100644 --- a/specs/pom.xml +++ b/specs/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-specs diff --git a/testing/pom.xml b/testing/pom.xml index 05cc3dabe..d59c2f3ef 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-testing diff --git a/web/core/pom.xml b/web/core/pom.xml index 9c0da8c0b..c4e790001 100644 --- a/web/core/pom.xml +++ b/web/core/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed-web - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-web-core diff --git a/web/core/src/main/java/org/seedstack/seed/web/internal/ServletCrudActionResolver.java b/web/core/src/main/java/org/seedstack/seed/web/internal/ServletCrudActionResolver.java new file mode 100644 index 000000000..ebc67be73 --- /dev/null +++ b/web/core/src/main/java/org/seedstack/seed/web/internal/ServletCrudActionResolver.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.web.internal; + +import org.seedstack.seed.security.CrudAction; +import org.seedstack.seed.security.spi.CrudActionResolver; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Optional; + +class ServletCrudActionResolver implements CrudActionResolver { + private static final Class[] PARAMETER_TYPES = {HttpServletRequest.class, HttpServletResponse.class}; + + @Override + public Optional resolve(Method method) { + if (HttpServlet.class.isAssignableFrom(method.getDeclaringClass()) && Arrays.equals(method.getParameterTypes(), PARAMETER_TYPES)) { + switch (method.getName()) { + case "doDelete": + return Optional.of(CrudAction.DELETE); + case "doGet": + return Optional.of(CrudAction.READ); + case "doHead": + return Optional.of(CrudAction.READ); + case "doOptions": + return Optional.of(CrudAction.READ); + case "doPost": + return Optional.of(CrudAction.CREATE); + case "doPut": + return Optional.of(CrudAction.UPDATE); + case "doTrace": + return Optional.of(CrudAction.READ); + } + } + return Optional.empty(); + } +} diff --git a/web/core/src/test/java/org/seedstack/seed/web/internal/ServletCrudActionResolverTest.java b/web/core/src/test/java/org/seedstack/seed/web/internal/ServletCrudActionResolverTest.java new file mode 100644 index 000000000..1334adcb2 --- /dev/null +++ b/web/core/src/test/java/org/seedstack/seed/web/internal/ServletCrudActionResolverTest.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.web.internal; + +import org.junit.Before; +import org.junit.Test; +import org.seedstack.seed.security.CrudAction; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ServletCrudActionResolverTest { + private ServletCrudActionResolver resolverUnderTest; + + @Before + public void setup() throws Exception { + resolverUnderTest = new ServletCrudActionResolver(); + } + + @Test + public void resolveRightVerb() throws Exception { + assertThat(resolverUnderTest.resolve(Fixture.class.getDeclaredMethod("doGet", HttpServletRequest.class, HttpServletResponse.class))).isEqualTo(Optional.of(CrudAction.READ)); + assertThat(resolverUnderTest.resolve(Fixture.class.getDeclaredMethod("doHead", HttpServletRequest.class, HttpServletResponse.class))).isEqualTo(Optional.of(CrudAction.READ)); + assertThat(resolverUnderTest.resolve(Fixture.class.getDeclaredMethod("doPost", HttpServletRequest.class, HttpServletResponse.class))).isEqualTo(Optional.of(CrudAction.CREATE)); + assertThat(resolverUnderTest.resolve(Fixture.class.getDeclaredMethod("doPut", HttpServletRequest.class, HttpServletResponse.class))).isEqualTo(Optional.of(CrudAction.UPDATE)); + assertThat(resolverUnderTest.resolve(Fixture.class.getDeclaredMethod("doDelete", HttpServletRequest.class, HttpServletResponse.class))).isEqualTo(Optional.of(CrudAction.DELETE)); + assertThat(resolverUnderTest.resolve(Fixture.class.getDeclaredMethod("doOptions", HttpServletRequest.class, HttpServletResponse.class))).isEqualTo(Optional.of(CrudAction.READ)); + assertThat(resolverUnderTest.resolve(Fixture.class.getDeclaredMethod("doTrace", HttpServletRequest.class, HttpServletResponse.class))).isEqualTo(Optional.of(CrudAction.READ)); + assertThat(resolverUnderTest.resolve(Fixture.class.getMethod("doGet"))).isNotPresent(); + } + + // Test Fixture + public static class Fixture extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + super.doGet(req, resp); + } + + @Override + protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + super.doHead(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + super.doPost(req, resp); + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + super.doPut(req, resp); + } + + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + super.doDelete(req, resp); + } + + @Override + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + super.doOptions(req, resp); + } + + @Override + protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + super.doTrace(req, resp); + } + + public void doGet() { + + } + } +} diff --git a/web/pom.xml b/web/pom.xml index dc8929ce9..85b1b2b53 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-web diff --git a/web/security/pom.xml b/web/security/pom.xml index 4ae41fbbb..ef7f8436a 100644 --- a/web/security/pom.xml +++ b/web/security/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed-web - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-web-security diff --git a/web/specs/pom.xml b/web/specs/pom.xml index 1233f17d8..cfc0b2643 100644 --- a/web/specs/pom.xml +++ b/web/specs/pom.xml @@ -13,7 +13,7 @@ org.seedstack.seed seed-web - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-web-specs diff --git a/web/undertow/pom.xml b/web/undertow/pom.xml index 45a367fab..8308d7b71 100644 --- a/web/undertow/pom.xml +++ b/web/undertow/pom.xml @@ -14,7 +14,7 @@ org.seedstack.seed seed-web - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT seed-web-undertow