From 6d6464cef9072b88c2d4bcd798426400712556d1 Mon Sep 17 00:00:00 2001 From: xiabou Date: Mon, 4 Sep 2017 15:33:49 +0200 Subject: [PATCH] Added @RequiresCrudPermission annotation to spec Added AbstractInterceptor as a base class for the interceptors, as much of their logic were pretty much the same Added SPI functionality to define custom verb resolvers Implementation improvements on CRUD security Simplify CrudActionResolver --- CHANGELOG.md | 12 ++ cli/pom.xml | 2 +- core/pom.xml | 2 +- .../org/seedstack/seed/core/CorePluginIT.java | 4 +- .../core/fixtures/TestSeedInitializer.java | 30 ++- .../java/org/seedstack/seed/core/Seed.java | 201 ++++++++++------- .../configuration/tool/ConfigTool.java | 12 +- .../internal/configuration/tool/Node.java | 114 ++++++---- .../configuration/tool/PropertyInfo.java | 13 ++ .../configuration/tool/TreePrinter.java | 32 ++- pom.xml | 2 +- rest/core/pom.xml | 2 +- .../rest/internal/RestCrudActionResolver.java | 44 ++++ .../internal/RestCrudActionResolverTest.java | 86 ++++++++ rest/jersey2/pom.xml | 2 +- rest/pom.xml | 2 +- rest/specs/pom.xml | 2 +- security/core/pom.xml | 3 +- .../security/internal/CrudSecurityIT.java | 117 ++++++++++ .../seed/security/internal/SecurityIT.java | 12 -- .../fixtures/AnnotatedClass4Security.java | 23 +- .../fixtures/AnnotatedCrudClass4Security.java | 46 ++++ .../AnnotatedCrudMethods4Security.java | 49 +++++ .../internal/fixtures/TestActionResolver.java | 39 ++++ .../internal/fixtures/annotations/CREATE.java | 23 ++ .../internal/fixtures/annotations/DELETE.java | 23 ++ .../internal/fixtures/annotations/READ.java | 24 +++ .../internal/fixtures/annotations/UPDATE.java | 23 ++ .../core/src/it/resources/application.yaml | 10 + .../security/internal/SecurityAopModule.java | 31 ++- .../security/internal/SecurityModule.java | 8 +- .../security/internal/SecurityPlugin.java | 65 ++++-- .../authorization/AbstractInterceptor.java | 82 +++++++ .../AbstractPermissionsInterceptor.java | 32 +++ .../RequiresCrudPermissionsInterceptor.java | 60 ++++++ .../RequiresPermissionsInterceptor.java | 57 +---- .../RequiresRolesInterceptor.java | 63 ++---- .../internal/SecurityAopModuleUniTest.java | 26 --- .../RequiresPermissionsInterceptorTest.java | 160 ++++++++------ .../RequiresRolesInterceptorTest.java | 204 ++++++++++-------- .../internal/shiro/AbstractShiroTest.java | 86 ++++++++ security/pom.xml | 2 +- security/specs/pom.xml | 2 +- .../seedstack/seed/security/CrudAction.java | 25 +++ .../security/RequiresCrudPermissions.java | 38 ++++ .../seed/security/RequiresPermissions.java | 6 +- .../seed/security/spi/CrudActionResolver.java | 34 +++ .../seed/security/SecurityConfig.properties | 9 + specs/pom.xml | 2 +- .../seedstack/seed/spi/SeedInitializer.java | 24 ++- .../seed/transaction/TransactionConfig.java | 3 +- .../seed/ApplicationConfig.properties | 4 +- .../seedstack/seed/LoggingConfig.properties | 4 +- .../seed/crypto/CryptoConfig.properties | 12 +- testing/pom.xml | 2 +- web/core/pom.xml | 2 +- .../internal/ServletCrudActionResolver.java | 45 ++++ .../ServletCrudActionResolverTest.java | 84 ++++++++ web/pom.xml | 2 +- web/security/pom.xml | 2 +- .../web/security/WebSecurityConfig.properties | 4 +- web/specs/pom.xml | 2 +- .../seedstack/seed/web/WebConfig.properties | 1 + web/undertow/pom.xml | 2 +- 64 files changed, 1663 insertions(+), 476 deletions(-) create mode 100644 rest/core/src/main/java/org/seedstack/seed/rest/internal/RestCrudActionResolver.java create mode 100644 rest/core/src/test/java/org/seedstack/seed/rest/internal/RestCrudActionResolverTest.java create mode 100644 security/core/src/it/java/org/seedstack/seed/security/internal/CrudSecurityIT.java create mode 100644 security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedCrudClass4Security.java create mode 100644 security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/AnnotatedCrudMethods4Security.java create mode 100644 security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/TestActionResolver.java create mode 100644 security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/CREATE.java create mode 100644 security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/DELETE.java create mode 100644 security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/READ.java create mode 100644 security/core/src/it/java/org/seedstack/seed/security/internal/fixtures/annotations/UPDATE.java create mode 100644 security/core/src/main/java/org/seedstack/seed/security/internal/authorization/AbstractInterceptor.java create mode 100644 security/core/src/main/java/org/seedstack/seed/security/internal/authorization/AbstractPermissionsInterceptor.java create mode 100644 security/core/src/main/java/org/seedstack/seed/security/internal/authorization/RequiresCrudPermissionsInterceptor.java delete mode 100644 security/core/src/test/java/org/seedstack/seed/security/internal/SecurityAopModuleUniTest.java create mode 100644 security/core/src/test/java/org/seedstack/seed/security/internal/shiro/AbstractShiroTest.java create mode 100644 security/specs/src/main/java/org/seedstack/seed/security/CrudAction.java create mode 100644 security/specs/src/main/java/org/seedstack/seed/security/RequiresCrudPermissions.java create mode 100644 security/specs/src/main/java/org/seedstack/seed/security/spi/CrudActionResolver.java create mode 100644 web/core/src/main/java/org/seedstack/seed/web/internal/ServletCrudActionResolver.java create mode 100644 web/core/src/test/java/org/seedstack/seed/web/internal/ServletCrudActionResolverTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b4c0d29..d4b0773e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 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. +* [new] Add `beforeInitialization()` and `afterInitialization()` methods on `SeedInitializer` interface. +* [new] Add `isRemembered()` on `SecuritySupport` interface. + # Version 3.3.0 (2017-07-31) * [new] Print a default banner at startup in case of missing custom `banner.txt`. 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/core/src/it/java/org/seedstack/seed/core/CorePluginIT.java b/core/src/it/java/org/seedstack/seed/core/CorePluginIT.java index 202374835..44167b7a9 100644 --- a/core/src/it/java/org/seedstack/seed/core/CorePluginIT.java +++ b/core/src/it/java/org/seedstack/seed/core/CorePluginIT.java @@ -108,7 +108,9 @@ public void before() { @Test public void initializers_are_called() { - assertThat(TestSeedInitializer.getCallCount()).isEqualTo(1); + assertThat(TestSeedInitializer.getBeforeCallCount()).isEqualTo(1); + assertThat(TestSeedInitializer.getOnCallCount()).isEqualTo(1); + assertThat(TestSeedInitializer.getAfterCallCount()).isEqualTo(1); } @Test diff --git a/core/src/it/java/org/seedstack/seed/core/fixtures/TestSeedInitializer.java b/core/src/it/java/org/seedstack/seed/core/fixtures/TestSeedInitializer.java index cc06b2755..781381cce 100644 --- a/core/src/it/java/org/seedstack/seed/core/fixtures/TestSeedInitializer.java +++ b/core/src/it/java/org/seedstack/seed/core/fixtures/TestSeedInitializer.java @@ -15,19 +15,39 @@ import static org.assertj.core.api.Assertions.assertThat; public class TestSeedInitializer implements SeedInitializer { - private static AtomicInteger callCount = new AtomicInteger(0); + private static AtomicInteger beforeCallCount = new AtomicInteger(0); + private static AtomicInteger onCallCount = new AtomicInteger(0); + private static AtomicInteger afterCallCount = new AtomicInteger(0); + + @Override + public void beforeInitialization() { + assertThat(beforeCallCount.getAndIncrement()).isEqualTo(0); + } @Override public void onInitialization(Coffig configuration) { - assertThat(callCount.getAndIncrement()).isEqualTo(0); + assertThat(onCallCount.getAndIncrement()).isEqualTo(0); + } + + @Override + public void afterInitialization() { + assertThat(afterCallCount.getAndIncrement()).isEqualTo(0); } @Override public void onClose() { - assertThat(callCount.getAndDecrement()).isEqualTo(1); + throw new IllegalStateException("Should not be called from tests"); + } + + public static int getBeforeCallCount() { + return beforeCallCount.get(); + } + + public static int getOnCallCount() { + return onCallCount.get(); } - public static int getCallCount() { - return callCount.get(); + public static int getAfterCallCount() { + return afterCallCount.get(); } } diff --git a/core/src/main/java/org/seedstack/seed/core/Seed.java b/core/src/main/java/org/seedstack/seed/core/Seed.java index dfb8e057b..66d38570c 100644 --- a/core/src/main/java/org/seedstack/seed/core/Seed.java +++ b/core/src/main/java/org/seedstack/seed/core/Seed.java @@ -44,18 +44,17 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Scanner; import java.util.ServiceLoader; -import java.util.Set; +import static com.google.common.base.Preconditions.checkState; import static org.seedstack.shed.misc.PriorityUtils.sortByPriority; /** - * This class is the Seed framework entry point, which is used create and dispose kernels. + * This class is the SeedStack framework entry point, which is used create and dispose kernels. * It handles global initialization and cleanup. */ public class Seed { @@ -68,10 +67,11 @@ public class Seed { private static volatile boolean initialized = false; private static volatile boolean disposed = false; private static volatile boolean noLogs = false; + private static final List seedInitializers; private static final List exceptionTranslators; private static final DiagnosticManager diagnosticManager; - private final String seedVersion; - private final String businessVersion; + private static final String seedVersion; + private static final String businessVersion; private final ApplicationConfig applicationConfig; private final Coffig configuration; private final ConsoleManager consoleManager; @@ -79,12 +79,24 @@ public class Seed { private final ValidatorFactory validatorFactory; private final ProxyManager proxyManager; private final KernelManager kernelManager; - private final Set seedInitializers = new HashSet<>(); static { + diagnosticManager = new DiagnosticManagerImpl(); + + seedInitializers = Lists.newArrayList(ServiceLoader.load(SeedInitializer.class)); + sortByPriority(seedInitializers, PriorityUtils::priorityOfClassOf); + exceptionTranslators = Lists.newArrayList(ServiceLoader.load(SeedExceptionTranslator.class)); sortByPriority(exceptionTranslators, PriorityUtils::priorityOfClassOf); - diagnosticManager = new DiagnosticManagerImpl(); + + seedVersion = Optional.ofNullable(Seed.class.getPackage()) + .map(Package::getImplementationVersion) + .orElse(null); + + businessVersion = Classes.optional("org.seedstack.business.internal.BusinessSpecifications") + .map(Class::getPackage) + .map(Package::getImplementationVersion) + .orElse(null); } private static class Holder { @@ -126,11 +138,52 @@ public static SeedLauncher getToolLauncher(String toolName) { return new ToolLauncher(toolName); } + /** + * Provides the default {@link DiagnosticManager} instance to dump diagnostics outside a running kernel. + * + * @return the default diagnostic manager. + */ + public static DiagnosticManager diagnostic() { + return diagnosticManager; + } + + /** + * Translates any exception that occurred in the application using an extensible exception mechanism. + * + * @param exception the exception to handle. + * @return the translated exception. + */ + public static BaseException translateException(Exception exception) { + if (exception instanceof BaseException) { + return (BaseException) exception; + } else { + for (SeedExceptionTranslator exceptionTranslator : exceptionTranslators) { + if (exceptionTranslator.canTranslate(exception)) { + return exceptionTranslator.translate(exception); + } + } + return SeedException.wrap(exception, CoreErrorCode.UNEXPECTED_EXCEPTION); + } + } + + /** + * Provides the application base configuration (i.e. not including configuration sources discovered after kernel + * startup). + * + *

Cannot be called from {@link SeedInitializer} methods.

+ * + * @return the {@link Coffig} object for application base configuration. + */ + public static Coffig baseConfiguration() { + return getInstance().configuration; + } /** * Create and start a basic kernel without specifying a runtime context, nor a configuration. Seed JVM-global * state is automatically initialized before the first time a kernel is created. * + *

Cannot be called from {@link SeedInitializer} methods.

+ * * @return the {@link Kernel} instance. */ public static Kernel createKernel() { @@ -141,6 +194,8 @@ public static Kernel createKernel() { * Create, initialize and optionally start a kernel with the specified runtime context and configuration. Seed JVM-global * state is automatically initialized before the first time a kernel is created. * + *

Cannot be called from {@link SeedInitializer} methods.

+ * * @param runtimeContext the runtime context object, which will be accessible from plugins. * @param kernelConfiguration the kernel configuration. * @param autoStart if true, the kernel is started automatically. @@ -155,8 +210,8 @@ public static Kernel createKernel(@Nullable Object runtimeContext, @Nullable Ker .configuration(instance.configuration.fork()) .validatorFactory(instance.validatorFactory) .applicationConfig(instance.applicationConfig) - .version(instance.seedVersion) - .businessVersion(instance.businessVersion) + .version(seedVersion) + .businessVersion(businessVersion) .build(), kernelConfiguration, autoStart); @@ -165,53 +220,20 @@ public static Kernel createKernel(@Nullable Object runtimeContext, @Nullable Ker /** * Stops and dispose a running {@link Kernel} instance. * + *

Cannot be called from {@link SeedInitializer} methods.

+ * * @param kernel the kernel to dispose. */ public static void disposeKernel(Kernel kernel) { - KernelManager.get().disposeKernel(kernel); + getInstance().kernelManager.disposeKernel(kernel); } /** - * Provides the default {@link DiagnosticManager} instance to dump diagnostics outside a running kernel. + * Explicitly cleanup SeedStack global state. After calling this method, SeedStack is no longer usable in the current + * classloader and cannot be reinitialized. Only call this method in standalone JVM environments, just before exiting, + * typically in a shutdown hook. * - * @return the default diagnostic manager. - */ - public static DiagnosticManager diagnostic() { - return diagnosticManager; - } - - /** - * Provides the application base configuration (i.e. not including configuration sources discovered after kernel - * startup). - * - * @return the {@link Coffig} object for application base configuration. - */ - public static Coffig baseConfiguration() { - return getInstance().configuration; - } - - /** - * Translates any exception that occurred in the application using an extensible exception mechanism. - * - * @param exception the exception to handle. - * @return the translated exception. - */ - public static BaseException translateException(Exception exception) { - if (exception instanceof BaseException) { - return (BaseException) exception; - } else { - for (SeedExceptionTranslator exceptionTranslator : exceptionTranslators) { - if (exceptionTranslator.canTranslate(exception)) { - return exceptionTranslator.translate(exception); - } - } - return SeedException.wrap(exception, CoreErrorCode.UNEXPECTED_EXCEPTION); - } - } - - /** - * Cleanup Seed JVM-global state explicitly. Should be done before exiting the JVM. After calling this method - * Seed is no longer usable in the current JVM. + *

Has no effect if called from {@link SeedInitializer} methods or after the first call.

*/ public static void close() { if (initialized && !disposed) { @@ -220,11 +242,11 @@ public static void close() { } private static Seed getInstance() { - if (disposed) { - throw new IllegalStateException("Seed is no longer usable after close() has been called"); - } + checkState(!disposed, "SeedStack cannot be used during or after shutdown"); try { - return Holder.INSTANCE; + Seed instance = Holder.INSTANCE; + checkState(initialized, "SeedStack cannot be used before or during initialization"); + return instance; } catch (Throwable t) { if (t instanceof ExceptionInInitializerError) { t = t.getCause(); @@ -234,14 +256,17 @@ private static Seed getInstance() { } private Seed() { - seedVersion = Optional.ofNullable(Seed.class.getPackage()) - .map(Package::getImplementationVersion) - .orElse(null); - businessVersion = Classes.optional("org.seedstack.business.internal.BusinessSpecifications") - .map(Class::getPackage) - .map(Package::getImplementationVersion) - .orElse(null); + // Trigger beforeInitialization() in custom initializers + for (SeedInitializer seedInitializer : seedInitializers) { + try { + seedInitializer.beforeInitialization(); + } catch (Exception e) { + throw SeedException.wrap(e, CoreErrorCode.ERROR_IN_INITIALIZER) + .put("initializerClass", seedInitializer.getClass().getName()); + } + } + // Setup a default exception handler that translates exceptions Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { Throwable translated; if (throwable instanceof Exception) { @@ -253,21 +278,33 @@ private Seed() { translated.printStackTrace(System.err); }); - // Logging initialization (should silence logs until logging activation later in the initialization) + // Initialize logging subsystem (should silence logs until logging activation later in the initialization) logManager = AutodetectLogManager.get(); - // Validation + // Create global validator factory validatorFactory = GlobalValidatorFactory.get(); - // Configuration + // Create base configuration configuration = BaseConfiguration.get(); + + // Trigger onInitialization() in custom initializers + for (SeedInitializer seedInitializer : seedInitializers) { + try { + seedInitializer.onInitialization(configuration); + } catch (Exception e) { + throw SeedException.wrap(e, CoreErrorCode.ERROR_IN_INITIALIZER) + .put("initializerClass", seedInitializer.getClass().getName()); + } + } + + // Access application configuration applicationConfig = configuration.get(ApplicationConfig.class); - // Console + // Install console enhancements consoleManager = ConsoleManager.get(); consoleManager.install(applicationConfig.getColorOutput()); - // Banner + // Print banner if (!noLogs && applicationConfig.isPrintBanner()) { System.out.println(buildBannerMessage(applicationConfig).orElseGet(this::buildWelcomeMessage)); } @@ -277,18 +314,17 @@ private Seed() { logManager.configure(configuration.get(LoggingConfig.class)); } - // Proxy + // Install global proxy handling proxyManager = ProxyManager.get(); proxyManager.install(configuration.get(ProxyConfig.class)); - // Nuun + // Create kernel manager kernelManager = KernelManager.get(); - // Custom initializers - for (SeedInitializer seedInitializer : ServiceLoader.load(SeedInitializer.class)) { + // Trigger afterInitialization() in custom initializers + for (SeedInitializer seedInitializer : seedInitializers) { try { - seedInitializer.onInitialization(configuration); - seedInitializers.add(seedInitializer); + seedInitializer.afterInitialization(); } catch (Exception e) { throw SeedException.wrap(e, CoreErrorCode.ERROR_IN_INITIALIZER) .put("initializerClass", seedInitializer.getClass().getName()); @@ -346,13 +382,32 @@ private String getBanner() { } private void dispose() { - seedInitializers.forEach(SeedInitializer::onClose); + markDisposed(); + + // Trigger onClose() in custom initializers + for (SeedInitializer seedInitializer : seedInitializers) { + try { + seedInitializer.onClose(); + } catch (Exception e) { + throw SeedException.wrap(e, CoreErrorCode.ERROR_IN_INITIALIZER) + .put("initializerClass", seedInitializer.getClass().getName()); + } + } + + // Uninstall global proxy handling proxyManager.uninstall(); + + // Close validator factory validatorFactory.close(); + + // Uninstall console enhancements consoleManager.uninstall(); + + // Close logging subsystem logManager.close(); + + // Remove default uncaught exception handler Thread.setDefaultUncaughtExceptionHandler(null); - markDisposed(); } private static void markDisposed() { diff --git a/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/ConfigTool.java b/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/ConfigTool.java index 7825f9d1f..5b4b8ea77 100644 --- a/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/ConfigTool.java +++ b/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/ConfigTool.java @@ -10,9 +10,11 @@ import io.nuun.kernel.api.plugin.InitState; import io.nuun.kernel.api.plugin.context.InitContext; import io.nuun.kernel.api.plugin.request.ClasspathScanRequest; +import org.seedstack.coffig.Coffig; import org.seedstack.coffig.Config; import org.seedstack.seed.SeedException; import org.seedstack.seed.cli.CliArgs; +import org.seedstack.seed.core.SeedRuntime; import org.seedstack.seed.core.internal.AbstractSeedTool; import org.seedstack.seed.core.internal.CoreErrorCode; @@ -26,12 +28,18 @@ public class ConfigTool extends AbstractSeedTool { private final Node root = new Node(); @CliArgs private String[] args; + private Coffig configuration; @Override public String toolName() { return "config"; } + @Override + protected void setup(SeedRuntime seedRuntime) { + configuration = seedRuntime.getConfiguration(); + } + @Override public Collection classpathScanRequests() { return classpathScanRequestBuilder() @@ -42,7 +50,7 @@ public Collection classpathScanRequests() { @Override protected InitState initialize(InitContext initContext) { List nodes = new ArrayList<>(); - initContext.scannedClassesByAnnotationClass().get(Config.class).stream().map(Node::new).forEach(nodes::add); + initContext.scannedClassesByAnnotationClass().get(Config.class).stream().map(configClass -> new Node(configClass, configuration)).forEach(nodes::add); Collections.sort(nodes); nodes.forEach(this::buildTree); return InitState.INITIALIZED; @@ -67,7 +75,7 @@ public Integer call() throws Exception { private void info(String[] path) { Node node = root.find(Arrays.copyOfRange(path, 0, path.length - 1)); if (node == null) { - throw SeedException.createNew(CoreErrorCode.INVALID_CONFIG_PATH).put("path", path); + throw SeedException.createNew(CoreErrorCode.INVALID_CONFIG_PATH).put("path", String.join(".", path)); } else { PropertyInfo propertyInfo = node.getPropertyInfo(path[path.length - 1]); if (propertyInfo == null) { diff --git a/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/Node.java b/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/Node.java index e839c8042..a208ef02c 100644 --- a/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/Node.java +++ b/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/Node.java @@ -7,13 +7,19 @@ */ package org.seedstack.seed.core.internal.configuration.tool; +import org.seedstack.coffig.Coffig; import org.seedstack.coffig.Config; import org.seedstack.coffig.SingleValue; import org.seedstack.shed.reflect.Annotations; +import org.seedstack.shed.reflect.ReflectUtils; +import org.seedstack.shed.reflect.Types; +import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -24,7 +30,6 @@ import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; -import java.util.SortedMap; import java.util.TreeMap; import static org.seedstack.shed.reflect.Classes.instantiateDefault; @@ -32,29 +37,33 @@ import static org.seedstack.shed.reflect.Types.simpleNameOf; class Node implements Comparable { - private final String name; private final Class configClass; + private final Coffig coffig; + private final String name; private final Class outermostClass; private final int outermostLevel; private final String[] path; + private final ResourceBundle bundle; private final Map propertyInfo; - private final SortedMap children = new TreeMap<>(); + private final Map children = new TreeMap<>(); Node() { - this.name = ""; this.configClass = null; + this.coffig = null; + this.name = ""; this.outermostClass = null; this.outermostLevel = 0; this.path = new String[0]; + this.bundle = null; this.propertyInfo = new HashMap<>(); } - Node(Class configClass) { + Node(Class configClass, Coffig coffig) { this.configClass = configClass; + this.coffig = coffig; List path = new ArrayList<>(); Class previousClass = configClass; - int nestingLevel = -1; do { Config annotation = configClass.getAnnotation(Config.class); if (annotation == null) { @@ -64,7 +73,6 @@ class Node implements Comparable { Collections.reverse(splitPath); path.addAll(splitPath); previousClass = configClass; - nestingLevel++; } while ((configClass = configClass.getDeclaringClass()) != null); Collections.reverse(path); @@ -72,7 +80,8 @@ class Node implements Comparable { this.outermostLevel = this.outermostClass.getAnnotation(Config.class).value().split("\\.").length; this.path = path.toArray(new String[path.size()]); this.name = this.path[this.path.length - 1]; - this.propertyInfo = buildPropertyInfo(); + this.bundle = getResourceBundle(); + this.propertyInfo = buildPropertyInfo(this.configClass, "", null); } String getName() { @@ -149,35 +158,31 @@ public int compareTo(Node that) { return 0; } - private Map buildPropertyInfo() { + private Map buildPropertyInfo(Class configClass, String parentPropertyName, Object defaultInstance) { Map result = new LinkedHashMap<>(); - ResourceBundle bundle = null; - try { - bundle = ResourceBundle.getBundle(outermostClass.getName()); - } catch (MissingResourceException e) { - // ignore - } - - Object defaultInstance; - try { - defaultInstance = instantiateDefault(configClass); - } catch (Exception e) { - defaultInstance = null; + if (defaultInstance == null) { + try { + defaultInstance = instantiateDefault(configClass); + } catch (Exception e) { + defaultInstance = null; + } } for (Field field : configClass.getDeclaredFields()) { if (Modifier.isStatic(field.getModifiers())) { + // Skip static fields (not used for configuration) continue; } if (field.getType().isAnnotationPresent(Config.class)) { + // Skip fields of type annotated with @Config as they are already detected continue; } makeAccessible(field); - PropertyInfo propertyInfo = new PropertyInfo(); Config configAnnotation = field.getAnnotation(Config.class); + Type genericType = field.getGenericType(); String name; if (configAnnotation != null) { name = configAnnotation.value(); @@ -185,19 +190,37 @@ private Map buildPropertyInfo() { name = field.getName(); } + PropertyInfo propertyInfo = new PropertyInfo(); propertyInfo.setName(name); - propertyInfo.setShortDescription(getMessage(bundle, "No description.", buildKey(name))); - propertyInfo.setLongDescription(getMessage(bundle, null, buildKey(name, "long"))); - propertyInfo.setType(simpleNameOf(field.getGenericType())); + propertyInfo.setShortDescription(getMessage("", buildKey(parentPropertyName, name))); + propertyInfo.setLongDescription(getMessage(null, buildKey(parentPropertyName, name, "long"))); + propertyInfo.setType(simpleNameOf(genericType)); propertyInfo.setSingleValue(field.isAnnotationPresent(SingleValue.class)); + propertyInfo.setMandatory(isNotNull(field)); if (defaultInstance != null) { - try { - propertyInfo.setDefaultValue(field.get(defaultInstance)); - } catch (IllegalAccessException e) { - // ignore - } + propertyInfo.setDefaultValue(ReflectUtils.getValue(field, defaultInstance)); + } + + Class rawClass = Types.rawClassOf(genericType); + Type itemType; + if (Collection.class.isAssignableFrom(rawClass) && genericType instanceof ParameterizedType) { + itemType = Types.rawClassOf(((ParameterizedType) genericType).getActualTypeArguments()[0]); + } else if (Map.class.isAssignableFrom(rawClass) && genericType instanceof ParameterizedType) { + itemType = Types.rawClassOf(((ParameterizedType) genericType).getActualTypeArguments()[1]); + } else if (genericType instanceof Class && ((Class) genericType).isArray()) { + itemType = ((Class) genericType).getComponentType(); + } else { + itemType = genericType; + } + if (!coffig.getMapper().canHandle(itemType)) { + propertyInfo.addInnerPropertyInfo( + buildPropertyInfo( + Types.rawClassOf(itemType), + parentPropertyName.isEmpty() ? name : parentPropertyName + "." + name, + itemType.equals(genericType) ? ReflectUtils.getValue(field, defaultInstance) : null + ) + ); } - propertyInfo.setMandatory(propertyInfo.getDefaultValue() == null && Annotations.on(field).includingMetaAnnotations().find(NotNull.class).isPresent()); result.put(name, propertyInfo); } @@ -205,12 +228,16 @@ private Map buildPropertyInfo() { return result; } - private String getMessage(ResourceBundle resourceBundle, String defaultMessage, String key) { - if (resourceBundle == null) { + private boolean isNotNull(Field field) { + return Annotations.on(field).includingMetaAnnotations().find(NotNull.class).isPresent(); + } + + private String getMessage(String defaultMessage, String key) { + if (bundle == null) { return defaultMessage; } try { - return resourceBundle.getString(key); + return bundle.getString(key); } catch (MissingResourceException e) { return defaultMessage; } @@ -227,11 +254,24 @@ private String buildKey(String... parts) { } } for (int i = 0; i < parts.length; i++) { - sb.append(parts[i]); - if (i < parts.length - 1) { - sb.append("."); + if (!parts[i].isEmpty()) { + sb.append(parts[i]); + if (i < parts.length - 1) { + sb.append("."); + } } } return sb.toString(); } + + @Nullable + private ResourceBundle getResourceBundle() { + ResourceBundle bundle; + try { + bundle = ResourceBundle.getBundle(outermostClass.getName()); + } catch (MissingResourceException e) { + bundle = null; + } + return bundle; + } } diff --git a/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/PropertyInfo.java b/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/PropertyInfo.java index 7fefe0b3d..8aa8d5b53 100644 --- a/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/PropertyInfo.java +++ b/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/PropertyInfo.java @@ -7,6 +7,10 @@ */ package org.seedstack.seed.core.internal.configuration.tool; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + class PropertyInfo { private String name; private String type; @@ -15,6 +19,7 @@ class PropertyInfo { private boolean singleValue; private boolean mandatory; private Object defaultValue; + private Map innerPropertyInfo = new TreeMap<>(); String getName() { return name; @@ -71,4 +76,12 @@ Object getDefaultValue() { void setDefaultValue(Object defaultValue) { this.defaultValue = defaultValue; } + + Map getInnerPropertyInfo() { + return Collections.unmodifiableMap(innerPropertyInfo); + } + + void addInnerPropertyInfo(Map innerPropertyInfo) { + this.innerPropertyInfo.putAll(innerPropertyInfo); + } } diff --git a/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/TreePrinter.java b/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/TreePrinter.java index 409ab7737..568f08824 100644 --- a/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/TreePrinter.java +++ b/core/src/main/java/org/seedstack/seed/core/internal/configuration/tool/TreePrinter.java @@ -8,6 +8,7 @@ package org.seedstack.seed.core.internal.configuration.tool; import org.fusesource.jansi.Ansi; +import org.fusesource.jansi.AnsiRenderer; import java.io.PrintStream; @@ -65,13 +66,36 @@ private void printProperty(PropertyInfo propertyInfo, String leftPadding, Ansi a .a(propertyInfo.isSingleValue() ? "~" : "") .a(propertyInfo.isMandatory() ? "*" : "") .a(propertyInfo.getName()) - .reset() - .a(": ") + .reset(); + + Object defaultValue = propertyInfo.getDefaultValue(); + if (defaultValue != null) { + String stringDefaultValue = String.valueOf(defaultValue); + if (!defaultToString(defaultValue).equals(stringDefaultValue)) { + ansi + .a(" = ") + .fgBright(Ansi.Color.GREEN) + .a(defaultValue instanceof String ? String.format("\"%s\"", stringDefaultValue) : stringDefaultValue) + .reset(); + } + } + + ansi .fgBright(Ansi.Color.MAGENTA) + .a(" (") .a(propertyInfo.getType()) + .a(")") .reset() - .a(". ") - .a(propertyInfo.getShortDescription()) + .a(": ") + .a(AnsiRenderer.render(propertyInfo.getShortDescription())) .newline(); + + for (PropertyInfo child : propertyInfo.getInnerPropertyInfo().values()) { + printProperty(child, leftPadding + INDENTATION, ansi); + } + } + + private static String defaultToString(Object o) { + return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); } } 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/security/specs/src/main/resources/org/seedstack/seed/security/SecurityConfig.properties b/security/specs/src/main/resources/org/seedstack/seed/security/SecurityConfig.properties index d41deaaeb..5f43ba352 100644 --- a/security/specs/src/main/resources/org/seedstack/seed/security/SecurityConfig.properties +++ b/security/specs/src/main/resources/org/seedstack/seed/security/SecurityConfig.properties @@ -7,11 +7,20 @@ # realms=The security realms used to authenticate and authorize users. +realms.name=Name of the security realm. +realms.permissionResolver=Name of the permission resolver used for this realm (optional). +realms.roleMapper=Name of the role mapper used for this realm (optional). users=Users valid in the application (key: user id, value: user config). +users.password=The password of the user. +users.roles=Set of roles granted to the user. roles=Application roles (key: app role name, value: corresponding realm role(s)). permissions=Application permissions (key: app role name, value: permission(s) granted for this role). cache.authorization=Configuration of the authorization cache. +cache.authorization.enabled=If true, authorization information is cached by the security subsystem. +cache.authorization.name=The name of the authorization cache if any. cache.authentication=Configuration of the authorization cache. +cache.authentication.enabled=If true, authentication information is cached by the security subsystem. +cache.authentication.name=The name of the authentication cache if any. cache.enabled=If true, security caching is enabled, otherwise realms are queried for each security operation. sessions.enabled=If true, security sessions are enabled, otherwise security is session-less. sessions.timeout=Session inactivity timeout, in seconds. 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/specs/src/main/java/org/seedstack/seed/spi/SeedInitializer.java b/specs/src/main/java/org/seedstack/seed/spi/SeedInitializer.java index a95206e64..9a1ed8f7a 100644 --- a/specs/src/main/java/org/seedstack/seed/spi/SeedInitializer.java +++ b/specs/src/main/java/org/seedstack/seed/spi/SeedInitializer.java @@ -10,19 +10,35 @@ import org.seedstack.coffig.Coffig; /** - * This interface defines two methods that are called at Seed JVM initialization and close. - * Implementations must be declared as a {@link java.util.ServiceLoader} service in META-INF/services to be detected. + *

This interface defines methods that are called at various stages of the SeedStack JVM initialization and shutdown + * process. Implementations must be declared as a {@link java.util.ServiceLoader} service in META-INF/services to be + * detected.

+ * + *

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

+ * + *

A single instance of each implementation is created and using throughout the whole lifecycle.

*/ public interface SeedInitializer { /** - * Called at Seed JVM-wide initialization. + * Called before SeedStack initialization. + */ + void beforeInitialization(); + + /** + * Called during Seed initialization, just after base configuration has been made available. * * @param configuration the base configuration. */ void onInitialization(Coffig configuration); /** - * Called at Seed JVM-wide close. + * Called after Seed initialization has been completed. + */ + void afterInitialization(); + + /** + * Called at explicit Seed global state cleanup. */ void onClose(); } diff --git a/specs/src/main/java/org/seedstack/seed/transaction/TransactionConfig.java b/specs/src/main/java/org/seedstack/seed/transaction/TransactionConfig.java index a8edc02c8..d7efca5f3 100644 --- a/specs/src/main/java/org/seedstack/seed/transaction/TransactionConfig.java +++ b/specs/src/main/java/org/seedstack/seed/transaction/TransactionConfig.java @@ -43,8 +43,7 @@ public JtaConfig jta() { @Config("jta") public static class JtaConfig { - private static final String DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction"; - + private static final String DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction"; @SingleValue private String txManagerName; private String userTxName = DEFAULT_USER_TRANSACTION_NAME; diff --git a/specs/src/main/resources/org/seedstack/seed/ApplicationConfig.properties b/specs/src/main/resources/org/seedstack/seed/ApplicationConfig.properties index f0f6ee419..36a50c9fb 100644 --- a/specs/src/main/resources/org/seedstack/seed/ApplicationConfig.properties +++ b/specs/src/main/resources/org/seedstack/seed/ApplicationConfig.properties @@ -6,7 +6,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. # -basePackages=Base packages that will be scanned by Seed in addition to 'org.seedstack.*'. +basePackages=Base packages that will be scanned in addition to 'org.seedstack.*'. basePackages.long=By default, SeedStack only scans the 'org.seedstack.*' packages and will not discover any class outside these. To make your classes discoverable by SeedStack, add your application or organization base package to this property. id=Identifier of the application. name=Human-friendly name of the application. @@ -14,4 +14,4 @@ version=Version of the application. storage=Path to a directory that will be used for application local storage. packageScanWarning=If true, a warning will be logged when no custom base package is configured. printBanner=If true, a banner will be printed on standard output at startup. -colorOutput=Application color output mode (AUTODETECT, PASSTHROUGH, ENABLE, DISABLE). \ No newline at end of file +colorOutput=Application color output mode (AUTODETECT|PASSTHROUGH|ENABLE|DISABLE). \ No newline at end of file diff --git a/specs/src/main/resources/org/seedstack/seed/LoggingConfig.properties b/specs/src/main/resources/org/seedstack/seed/LoggingConfig.properties index 9b6a537c4..4f4c2d630 100644 --- a/specs/src/main/resources/org/seedstack/seed/LoggingConfig.properties +++ b/specs/src/main/resources/org/seedstack/seed/LoggingConfig.properties @@ -8,4 +8,6 @@ level=Logging level of the root logger. loggers=The configured loggers (key: the logger name, value: the logger config). -pattern=The pattern for the default console appender. \ No newline at end of file +loggers.additive=Additivity flag for a specific logger. +loggers.level=Logging level for a specific logger. +pattern=The pattern for the default console appender (if not specified an implementation-dependent default pattern will be used). diff --git a/specs/src/main/resources/org/seedstack/seed/crypto/CryptoConfig.properties b/specs/src/main/resources/org/seedstack/seed/crypto/CryptoConfig.properties index 38c84e3b2..24fb3c2ce 100644 --- a/specs/src/main/resources/org/seedstack/seed/crypto/CryptoConfig.properties +++ b/specs/src/main/resources/org/seedstack/seed/crypto/CryptoConfig.properties @@ -7,9 +7,19 @@ # keystores=Key stores configured and usable in the application (key: key store name, value: key store configuration). +keystores.aliases=Aliases of the key store configured and usable in the application (key: alias name, value: alias configuration). +keystores.aliases.password=Password of the alias. +keystores.aliases.qualifier=String qualifier to use for injecting the cryptography services (defaults to the name of the alias if not specified). +keystores.password=Password of the key store. +keystores.path=Path of the key store. +keystores.provider=The security provider to use (will default to the registered list if not provided). +keystores.type=Type of the key store (use the default key store type if not specified). certificates=Certificates configured and usable in the application (key: certificate name, value: certificate configuration). +certificates.file=File location of the certificate (exclusive with resource). +certificates.resource=Classpath location of the certificate (exclusive with file). ssl.protocol=The protocol to use for SSL communication. -ssl.keyStore=The name of the key store used to load the SSL key managers. +ssl.keystore=The name of the key store used to load the SSL key managers. +ssl.truststore=The name of the key store used as a trust store for SSL. ssl.alias=The name of the alias in the SSL key store used to access the key store. ssl.trustStore=The name of the key store used as a trust store for SSL. ssl.clientAuthMode=The client authentication mode. 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/security/src/main/resources/org/seedstack/seed/web/security/WebSecurityConfig.properties b/web/security/src/main/resources/org/seedstack/seed/web/security/WebSecurityConfig.properties index 1a6a87bf3..5f7164e92 100644 --- a/web/security/src/main/resources/org/seedstack/seed/web/security/WebSecurityConfig.properties +++ b/web/security/src/main/resources/org/seedstack/seed/web/security/WebSecurityConfig.properties @@ -7,11 +7,13 @@ # urls=The list of secured URL patterns in the application. +urls.filters=The list of filters to apply in sequence to any URL matching the associated pattern. +urls.pattern=The ant-style pattern against which an URL will be matched. If this pattern matches, the associated filters are applied. successUrl=The URL to redirect to when the user has logged in successfully. logoutUrl=The URL to redirect to after logout. xsrf.cookieName=The name of the cookie used for XSRF protection. xsrf.headerName=The name of the HTTP header used for XSRF protection. -xsrf.algorithm=The name of the SecureRandom algorithm for generated the XSRF random token. +xsrf.algorithm=The name of the SecureRandom algorithm for generating the XSRF random token. xsrf.length=The length of the random XSRF token. form.loginUrl=The URL of the authentication form to redirect to when the user needs to be logged in. form.usernameParameter=The name of the parameter carrying the user name in the authentication form. 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/specs/src/main/resources/org/seedstack/seed/web/WebConfig.properties b/web/specs/src/main/resources/org/seedstack/seed/web/WebConfig.properties index 7d1eacb55..21b588b01 100644 --- a/web/specs/src/main/resources/org/seedstack/seed/web/WebConfig.properties +++ b/web/specs/src/main/resources/org/seedstack/seed/web/WebConfig.properties @@ -7,6 +7,7 @@ # requestDiagnostic=If true, a diagnostic report will be dumped for every exception occurring during request processing. +sessionTrackingMode=The session tracking mode used for the application (COOKIE|SSL|URL). cors.enabled=If true, Cross-Origin-Resource-Sharing (CORS) will be enabled. cors.path=The servlet path mapping on which CORS will be active. cors.properties=Allows to specify custom properties to the CORS filter. 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