From 1dcb3d703095f832309326c6a9d8a7ae9eb7c3a9 Mon Sep 17 00:00:00 2001 From: Vladimir Spasic Date: Mon, 2 Oct 2023 13:01:46 +0200 Subject: [PATCH 1/4] Spring Method Security using Authorization Manager --- build.gradle | 29 +++-- ...PermissionMethodSecurityConfiguration.java | 92 +++++++++++---- .../ebf/security/PermissionScanSelector.java | 3 +- .../guard/PermissionAccessDecisionVoter.java | 61 ---------- .../guard/PermissionAuthorizationManager.java | 95 +++++++++++++++ .../guard/PermissionSecurityAttribute.java | 32 ----- ... PermissionSecurityAttributeRegistry.java} | 44 ++++--- .../security/init/PermissionInitializer.java | 1 + .../{test => }/ConfigurationSpec.groovy | 26 ++++- .../PermissionAuthorizationManagerSpec.groovy | 110 ++++++++++++++++++ ...ssionSecurityAttributeRegistrySpec.groovy} | 41 +++---- .../guard/testcases/ProtectedClass.java | 2 +- .../guard/testcases/ProtectedInterface.java | 2 +- .../guard/testcases/PublicClass.java | 2 +- .../guard/testcases/PublicInterface.java | 2 +- .../CustomRepositoryMethodSecuritySpec.groovy | 2 +- .../InitPermissionsIntegrationSpec.groovy | 2 +- .../JpaRepositoryMethodSecuritySpec.groovy | 2 +- .../integration/MethodSecuritySpec.groovy | 2 +- .../integration/SecuritySpecification.groovy | 5 +- .../SpringMethodSecuritySpec.groovy | 63 ++++++++++ .../data/PermissionModelDefinitionSpec.groovy | 2 +- .../DefaultPermissionModelFinderSpec.groovy | 2 +- .../scanner/PermissionScannerSpec.groovy | 2 +- .../PermissionAccessDecisionVoterSpec.groovy | 102 ---------------- .../testapp/controllers/TestController.java | 4 + 26 files changed, 447 insertions(+), 283 deletions(-) delete mode 100644 src/main/java/com/ebf/security/guard/PermissionAccessDecisionVoter.java create mode 100644 src/main/java/com/ebf/security/guard/PermissionAuthorizationManager.java delete mode 100644 src/main/java/com/ebf/security/guard/PermissionSecurityAttribute.java rename src/main/java/com/ebf/security/guard/{PermissionMetadataSource.java => PermissionSecurityAttributeRegistry.java} (70%) rename src/test/groovy/com/ebf/security/{test => }/ConfigurationSpec.groovy (89%) create mode 100644 src/test/groovy/com/ebf/security/guard/PermissionAuthorizationManagerSpec.groovy rename src/test/groovy/com/ebf/security/{test/guard/PermissionMetadataSourceSpec.groovy => guard/PermissionSecurityAttributeRegistrySpec.groovy} (56%) rename src/test/groovy/com/ebf/security/{test => }/guard/testcases/ProtectedClass.java (95%) rename src/test/groovy/com/ebf/security/{test => }/guard/testcases/ProtectedInterface.java (94%) rename src/test/groovy/com/ebf/security/{test => }/guard/testcases/PublicClass.java (94%) rename src/test/groovy/com/ebf/security/{test => }/guard/testcases/PublicInterface.java (93%) rename src/test/groovy/com/ebf/security/{test => }/integration/CustomRepositoryMethodSecuritySpec.groovy (98%) rename src/test/groovy/com/ebf/security/{test => }/integration/InitPermissionsIntegrationSpec.groovy (97%) rename src/test/groovy/com/ebf/security/{test => }/integration/JpaRepositoryMethodSecuritySpec.groovy (98%) rename src/test/groovy/com/ebf/security/{test => }/integration/MethodSecuritySpec.groovy (97%) rename src/test/groovy/com/ebf/security/{test => }/integration/SecuritySpecification.groovy (93%) create mode 100644 src/test/groovy/com/ebf/security/integration/SpringMethodSecuritySpec.groovy rename src/test/groovy/com/ebf/security/{test => }/internal/data/PermissionModelDefinitionSpec.groovy (98%) rename src/test/groovy/com/ebf/security/{test => }/internal/services/DefaultPermissionModelFinderSpec.groovy (98%) rename src/test/groovy/com/ebf/security/{test => }/scanner/PermissionScannerSpec.groovy (97%) delete mode 100644 src/test/groovy/com/ebf/security/test/guard/PermissionAccessDecisionVoterSpec.groovy diff --git a/build.gradle b/build.gradle index 24b96a9..c60f80d 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ plugins { id 'com.github.hierynomus.license' version '0.16.1' id 'org.hibernate.build.maven-repo-auth' version '3.0.2' } + repositories { mavenLocal() mavenCentral() @@ -17,7 +18,7 @@ repositories { group = "com.ebf" archivesBaseName = "spring-granular-permissions" -version = "3.0.1" +version = "3.1.0" compileJava { sourceCompatibility = JavaVersion.VERSION_17 @@ -42,26 +43,30 @@ idea { } } +ext { + set('springVersion', '3.1.4') +} + dependencies { - compileOnly('org.springframework.boot:spring-boot-starter-data-jpa:3.1.0') - compileOnly('org.springframework.boot:spring-boot-starter-security:3.1.0') + compileOnly("org.springframework.boot:spring-boot-starter-data-jpa:${springVersion}") + compileOnly("org.springframework.boot:spring-boot-starter-security:${springVersion}") /* Test Database implementation */ testImplementation('com.h2database:h2:2.1.214') - /* Spring and Spock Test dependencies */ - testImplementation('org.springframework.boot:spring-boot-starter-test:3.1.0') + /* Spring Boot Test dependencies */ + testImplementation("org.springframework.boot:spring-boot-starter-test:${springVersion}") + testImplementation("org.springframework.boot:spring-boot-starter-web:${springVersion}") + testImplementation("org.springframework.boot:spring-boot-starter-data-rest:${springVersion}") + testImplementation("org.springframework.boot:spring-boot-starter-data-jpa:${springVersion}") + testImplementation("org.springframework.boot:spring-boot-starter-security:${springVersion}") + + /* Spock Test dependencies */ testImplementation('org.spockframework:spock-core:2.4-M1-groovy-3.0') testImplementation('org.spockframework:spock-spring:2.4-M1-groovy-3.0') /* Test utilities */ testImplementation('org.codehaus.groovy:groovy-json:3.0.17') - - /* Spring Boot Test dependencies */ - testImplementation('org.springframework.boot:spring-boot-starter-web:3.1.0') - testImplementation('org.springframework.boot:spring-boot-starter-data-rest:3.1.0') - testImplementation('org.springframework.boot:spring-boot-starter-data-jpa:3.1.0') - testImplementation('org.springframework.boot:spring-boot-starter-security:3.1.0') } license { @@ -158,4 +163,4 @@ publishing { url = 'https://repository.hosting.ebf.de/nexus/content/repositories/releases/' } } -} +} \ No newline at end of file diff --git a/src/main/java/com/ebf/security/PermissionMethodSecurityConfiguration.java b/src/main/java/com/ebf/security/PermissionMethodSecurityConfiguration.java index 9e5383a..37ba745 100644 --- a/src/main/java/com/ebf/security/PermissionMethodSecurityConfiguration.java +++ b/src/main/java/com/ebf/security/PermissionMethodSecurityConfiguration.java @@ -15,37 +15,81 @@ */ package com.ebf.security; -import com.ebf.security.guard.PermissionAccessDecisionVoter; -import com.ebf.security.guard.PermissionMetadataSource; -import org.springframework.security.access.AccessDecisionManager; -import org.springframework.security.access.AccessDecisionVoter; -import org.springframework.security.access.method.MethodSecurityMetadataSource; -import org.springframework.security.access.vote.AffirmativeBased; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; - -import java.util.ArrayList; -import java.util.List; +import com.ebf.security.annotations.Permission; +import com.ebf.security.guard.PermissionAuthorizationManager; +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInterceptor; +import org.springframework.aop.Advisor; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.Pointcuts; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.context.annotation.Role; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.authorization.AuthorizationEventPublisher; +import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.core.context.SecurityContextHolderStrategy; /** - * @author Nenad Nikolic - * + * Configuration class that is imported as a configuration candidate by the + * {@link com.ebf.security.annotations.PermissionScan} annotation. + *

+ * Primary goal of this configuration is to set up method security interceptor around the + * {@link Permission} annotation. Methods or classes that are using this annotation are + * subjected to introspection by the Spring AOP {@link AuthorizationManagerBeforeMethodInterceptor} + * that is using the {@link PermissionAuthorizationManager} to evauluate if the current + * {@link org.springframework.security.core.Authentication} has sufficient permissions. * + * @author Nenad Nikolic + * @author Vladimir Spasic */ -@EnableGlobalMethodSecurity -public class PermissionMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration { +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class PermissionMethodSecurityConfiguration { + + static final String ADVISOR_BEAN_NAME = "granularPermissionAuthorizationAdvisor"; + static final String INTERCEPTOR_BEAN_NAME = "granularPermissionAuthorizationMethodInterceptor"; + + @Bean(name = INTERCEPTOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static MethodInterceptor granularPermissionAuthorizationMethodInterceptor( + ObjectProvider strategyProvider, + ObjectProvider eventPublisherProvider, + ObjectProvider registryProvider) { - @Override - protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() { - return new PermissionMetadataSource(); + AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor( + createClassOrMethodPointcut(), PermissionAuthorizationManager.create(registryProvider) + ); + + interceptor.setOrder(AuthorizationInterceptorsOrder.FIRST.getOrder()); + strategyProvider.ifAvailable(interceptor::setSecurityContextHolderStrategy); + eventPublisherProvider.ifAvailable(interceptor::setAuthorizationEventPublisher); + return interceptor; + } + + private static Pointcut createClassOrMethodPointcut() { + return Pointcuts.union(new AnnotationMatchingPointcut(null, Permission.class, true), + new AnnotationMatchingPointcut(Permission.class, true)); } - @Override - protected AccessDecisionManager accessDecisionManager() { - final AffirmativeBased manager = (AffirmativeBased) super.accessDecisionManager(); - final List> voters = new ArrayList<>(manager.getDecisionVoters()); - voters.add(new PermissionAccessDecisionVoter()); + static class PermissionImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { - return new AffirmativeBased(voters); + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + final BeanDefinition definition = registry.getBeanDefinition(INTERCEPTOR_BEAN_NAME); + + if (definition instanceof RootBeanDefinition advisor) { + advisor.setTargetType(Advisor.class); + registry.registerBeanDefinition(ADVISOR_BEAN_NAME, advisor); + } + } } + } diff --git a/src/main/java/com/ebf/security/PermissionScanSelector.java b/src/main/java/com/ebf/security/PermissionScanSelector.java index 4f3afea..c29ae09 100644 --- a/src/main/java/com/ebf/security/PermissionScanSelector.java +++ b/src/main/java/com/ebf/security/PermissionScanSelector.java @@ -42,7 +42,8 @@ public class PermissionScanSelector implements ImportSelector { private static final String[] CONFIGURATION_IMPORTS = new String[] { PermissionScannerConfiguration.class.getName(), - PermissionMethodSecurityConfiguration.class.getName() + PermissionMethodSecurityConfiguration.class.getName(), + PermissionMethodSecurityConfiguration.PermissionImportBeanDefinitionRegistrar.class.getName() }; @NonNull diff --git a/src/main/java/com/ebf/security/guard/PermissionAccessDecisionVoter.java b/src/main/java/com/ebf/security/guard/PermissionAccessDecisionVoter.java deleted file mode 100644 index 4b1add2..0000000 --- a/src/main/java/com/ebf/security/guard/PermissionAccessDecisionVoter.java +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright 2009-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.ebf.security.guard; - -import org.springframework.security.access.AccessDecisionVoter; -import org.springframework.security.access.ConfigAttribute; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.util.CollectionUtils; - -import java.util.Collection; - -public class PermissionAccessDecisionVoter implements AccessDecisionVoter { - - @Override - public int vote(Authentication authentication, Object object, Collection attributes) { - // no security attributes to check, abstain... - if (CollectionUtils.isEmpty(attributes)) { - return ACCESS_ABSTAIN; - } - - final Collection authorities = authentication.getAuthorities(); - - // security attributes are set but the authentication has no granted authorities, deny access... - if (CollectionUtils.isEmpty(authorities)) { - return ACCESS_DENIED; - } - - boolean hasSufficientAuthority = attributes.stream().anyMatch(configAttr -> authentication - .getAuthorities().stream().filter(authority -> authority.getAuthority() - .equals(configAttr.getAttribute()) - ).count() == 1 - ); - - return hasSufficientAuthority ? ACCESS_GRANTED : ACCESS_DENIED; - } - - @Override - public boolean supports(ConfigAttribute attribute) { - return PermissionSecurityAttribute.class.isAssignableFrom(attribute.getClass()); - } - - @Override - public boolean supports(Class clazz) { - return true; - } - -} diff --git a/src/main/java/com/ebf/security/guard/PermissionAuthorizationManager.java b/src/main/java/com/ebf/security/guard/PermissionAuthorizationManager.java new file mode 100644 index 0000000..0a94be5 --- /dev/null +++ b/src/main/java/com/ebf/security/guard/PermissionAuthorizationManager.java @@ -0,0 +1,95 @@ +package com.ebf.security.guard; + +import io.micrometer.observation.ObservationRegistry; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.ObservationAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.CollectionUtils; +import org.springframework.util.function.SingletonSupplier; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * Implementation of the {@link AuthorizationManager} that would check if the {@link MethodInvocation}, + * decorated by the {@link com.ebf.security.annotations.Permission}, can proceed. + *

+ * The manager would extract the permissions which are required to perform the invocation via + * the {@link PermissionSecurityAttributeRegistry} and compare them with {@link GrantedAuthority authorities} + * that the current {@link Authentication} has. + *

+ * In case the method has multiple permissions set, the manager would proceed with the invocation when + * the {@link Authentication} contains at least one {@link GrantedAuthority} matching the permissions. + * + * @author : vladimir.spasic@ebf.com + * @since : 29.09.23, Fri + **/ +public class PermissionAuthorizationManager implements AuthorizationManager { + + private final PermissionSecurityAttributeRegistry registry; + + public static AuthorizationManager create() { + return new PermissionAuthorizationManager(); + } + + public static AuthorizationManager create(ObjectProvider provider) { + return new DeferringObservationAuthorizationManager(provider); + } + + private PermissionAuthorizationManager() { + registry = new PermissionSecurityAttributeRegistry(); + } + + @Override + public AuthorizationDecision check(Supplier authentication, MethodInvocation invocation) { + final Collection permissions = registry.get(invocation); + + // no security attributes to check, abstain... + if (CollectionUtils.isEmpty(permissions)) { + return null; + } + + final Collection authorities = authentication.get().getAuthorities(); + + // security attributes are set but the authentication has no granted authorities, deny access... + if (CollectionUtils.isEmpty(authorities)) { + return new AuthorizationDecision(false); + } + + boolean hasSufficientAuthority = permissions.stream().anyMatch(permission -> authorities + .stream() + .filter(authority -> authority.getAuthority().equals(permission)) + .count() == 1 + ); + + return new AuthorizationDecision(hasSufficientAuthority); + } + + private static class DeferringObservationAuthorizationManager implements AuthorizationManager { + private final Supplier> delegate; + + DeferringObservationAuthorizationManager(ObjectProvider provider) { + this(provider, new PermissionAuthorizationManager()); + } + + DeferringObservationAuthorizationManager(ObjectProvider provider, + AuthorizationManager delegate) { + this.delegate = SingletonSupplier.of(() -> { + ObservationRegistry registry = provider.getIfAvailable(() -> ObservationRegistry.NOOP); + if (registry.isNoop()) { + return delegate; + } + return new ObservationAuthorizationManager<>(registry, delegate); + }); + } + + @Override + public AuthorizationDecision check(Supplier authentication, MethodInvocation object) { + return this.delegate.get().check(authentication, object); + } + } +} diff --git a/src/main/java/com/ebf/security/guard/PermissionSecurityAttribute.java b/src/main/java/com/ebf/security/guard/PermissionSecurityAttribute.java deleted file mode 100644 index a24b9dc..0000000 --- a/src/main/java/com/ebf/security/guard/PermissionSecurityAttribute.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright 2009-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.ebf.security.guard; - -import lombok.Value; -import org.springframework.security.access.ConfigAttribute; - -@Value -public class PermissionSecurityAttribute implements ConfigAttribute { - private static final long serialVersionUID = 6648857928991476524L; - - String attribute; - - public PermissionSecurityAttribute(String attribute) { - super(); - this.attribute = attribute; - } - -} diff --git a/src/main/java/com/ebf/security/guard/PermissionMetadataSource.java b/src/main/java/com/ebf/security/guard/PermissionSecurityAttributeRegistry.java similarity index 70% rename from src/main/java/com/ebf/security/guard/PermissionMetadataSource.java rename to src/main/java/com/ebf/security/guard/PermissionSecurityAttributeRegistry.java index 0db9591..1657af8 100644 --- a/src/main/java/com/ebf/security/guard/PermissionMetadataSource.java +++ b/src/main/java/com/ebf/security/guard/PermissionSecurityAttributeRegistry.java @@ -16,9 +16,10 @@ package com.ebf.security.guard; import com.ebf.security.annotations.Permission; +import lombok.extern.slf4j.Slf4j; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.MethodClassKey; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.security.access.ConfigAttribute; -import org.springframework.security.access.method.AbstractMethodSecurityMetadataSource; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -27,26 +28,37 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.*; -import java.util.stream.Collectors; +import java.util.concurrent.ConcurrentHashMap; -public class PermissionMetadataSource extends AbstractMethodSecurityMetadataSource { +/** + * Registry that would resolve and cache the permission name from the {@link MethodInvocation} + * that is intercepted by the configured authorization method interceptor. + */ +@Slf4j +final class PermissionSecurityAttributeRegistry { + + private final Map> cache = new ConcurrentHashMap<>(); + + Collection get(MethodInvocation invocation) { + final Object target = invocation.getThis(); + final Class targetClass = (target != null) ? target.getClass() : null; - @Override - public Collection getAllConfigAttributes() { - return Collections.emptyList(); + return get(invocation.getMethod(), targetClass); } - @Override - public Collection getAttributes(Method method, Class targetClass) { + Collection get(Method method, Class targetClass) { + final MethodClassKey key = new MethodClassKey(method, targetClass); + return this.cache.computeIfAbsent(key, (k) -> resolve(method, targetClass)); + } + + private Collection resolve(Method method, Class targetClass) { final Set permissions = findPermission(method, targetClass); if (CollectionUtils.isEmpty(permissions)) { - return null; + return Collections.emptySet(); } - return permissions.stream() - .map(PermissionSecurityAttribute::new) - .collect(Collectors.toSet()); + return Collections.unmodifiableSet(permissions); } private Set findPermission(Method method, Class targetClass) { @@ -80,10 +92,10 @@ private Set extractValue(Permission annotation, AnnotatedElement element Set permissions = new HashSet<>(); final String[] values = annotation.value(); - if (logger.isDebugEnabled()) { - logger.debug(String.format("%s annotation found on element %s with value: '%s'", + if (log.isDebugEnabled()) { + log.debug("{} annotation found on element {} with value: '{}'", Permission.class.getSimpleName(), element.toString(), Arrays.toString(values) - )); + ); } Arrays.stream(values).forEach(permission -> { diff --git a/src/main/java/com/ebf/security/init/PermissionInitializer.java b/src/main/java/com/ebf/security/init/PermissionInitializer.java index 03f0999..2916608 100644 --- a/src/main/java/com/ebf/security/init/PermissionInitializer.java +++ b/src/main/java/com/ebf/security/init/PermissionInitializer.java @@ -25,6 +25,7 @@ * @author : vladimir.spasic@ebf.com * @since : 04.01.22, Tue **/ +@FunctionalInterface public interface PermissionInitializer { void initialize(@NonNull Set permissions) throws Exception; diff --git a/src/test/groovy/com/ebf/security/test/ConfigurationSpec.groovy b/src/test/groovy/com/ebf/security/ConfigurationSpec.groovy similarity index 89% rename from src/test/groovy/com/ebf/security/test/ConfigurationSpec.groovy rename to src/test/groovy/com/ebf/security/ConfigurationSpec.groovy index 423dade..826b424 100644 --- a/src/test/groovy/com/ebf/security/test/ConfigurationSpec.groovy +++ b/src/test/groovy/com/ebf/security/ConfigurationSpec.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test +package com.ebf.security import com.ebf.security.annotations.PermissionScan import com.ebf.security.init.DefaultPermissionInitializer @@ -29,10 +29,12 @@ import com.ebf.security.repository.PermissionModelRepository import com.ebf.security.scanner.DefaultPermissionScanner import com.ebf.security.scanner.PermissionScanner import org.assertj.core.api.InstanceOfAssertFactories +import org.springframework.aop.Advisor import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.context.runner.ApplicationContextRunner import org.springframework.context.annotation.ComponentScan +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor import spock.lang.Specification import static org.assertj.core.api.Assertions.assertThat @@ -132,6 +134,28 @@ class ConfigurationSpec extends Specification { 2 * scanner.scan() >> ["existing-permission", "to-be-created-permission"] } + def "should setup context security method interceptor and adviser"() { + setup: + def runner = new ApplicationContextRunner() + .withUserConfiguration(DefaultAnnotationConfiguration) + + expect: + runner.run { + assertThat(it) + .hasNotFailed() + .hasBean(PermissionMethodSecurityConfiguration.INTERCEPTOR_BEAN_NAME) + .hasBean(PermissionMethodSecurityConfiguration.ADVISOR_BEAN_NAME) + + assertThat(it.getBean(PermissionMethodSecurityConfiguration.INTERCEPTOR_BEAN_NAME)) + .isInstanceOf(AuthorizationManagerBeforeMethodInterceptor) + .isInstanceOf(Advisor) + + assertThat(it.getBean(PermissionMethodSecurityConfiguration.ADVISOR_BEAN_NAME)) + .isInstanceOf(AuthorizationManagerBeforeMethodInterceptor) + .isInstanceOf(Advisor) + } + } + def "should setup context with custom initializer bean"() { setup: def initializer = Mock(PermissionInitializer) diff --git a/src/test/groovy/com/ebf/security/guard/PermissionAuthorizationManagerSpec.groovy b/src/test/groovy/com/ebf/security/guard/PermissionAuthorizationManagerSpec.groovy new file mode 100644 index 0000000..944bb68 --- /dev/null +++ b/src/test/groovy/com/ebf/security/guard/PermissionAuthorizationManagerSpec.groovy @@ -0,0 +1,110 @@ +/* + * Copyright 2009-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ebf.security.guard + +import com.ebf.security.annotations.Permission +import com.ebf.security.guard.PermissionAuthorizationManager +import org.aopalliance.intercept.MethodInvocation +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.AuthorityUtils + +import spock.lang.Specification + +class PermissionAuthorizationManagerSpec extends Specification { + + def manager = PermissionAuthorizationManager.create() + + def authentication = Mock(Authentication) + + def invocation = Mock(MethodInvocation) + + def "should abstain if if no permissions are present"() { + setup: + scenario("allowed") + + when: + def result = manager.check(() -> authentication, invocation) + + then: + result == null + } + + def "should deny access if authentication holds no authorities"() { + setup: + scenario("user") + + when: + def result = manager.check(() -> authentication, invocation) + + then: + !result.granted + } + + def "should deny access if authentication holds insufficient authorities"() { + setup: + authentication.authorities >> AuthorityUtils.commaSeparatedStringToAuthorityList("user") + scenario("admin") + + when: + def result = manager.check(() -> authentication, invocation) + + then: + !result.granted + } + + def "should grant access if authentication holds sufficient authorities"() { + setup: + authentication.authorities >> AuthorityUtils.commaSeparatedStringToAuthorityList("user") + scenario("user") + + when: + def result = manager.check(() -> authentication, invocation) + + then: + result.granted + } + + def "should grant access if authentication holds at least one sufficient authority"() { + setup: + authentication.authorities >> AuthorityUtils.commaSeparatedStringToAuthorityList("user") + scenario("composite") + + when: + def result = manager.check(() -> authentication, invocation) + + then: + result.granted + } + + void scenario(String scenario) { + invocation.method >> Guarded.class.getMethod(scenario) + } + + static interface Guarded { + + @Permission("user") + void user(); + + @Permission("admin") + void admin(); + + @Permission(["admin", "user"]) + void composite(); + + void allowed(); + + } +} diff --git a/src/test/groovy/com/ebf/security/test/guard/PermissionMetadataSourceSpec.groovy b/src/test/groovy/com/ebf/security/guard/PermissionSecurityAttributeRegistrySpec.groovy similarity index 56% rename from src/test/groovy/com/ebf/security/test/guard/PermissionMetadataSourceSpec.groovy rename to src/test/groovy/com/ebf/security/guard/PermissionSecurityAttributeRegistrySpec.groovy index 1195892..4a80062 100644 --- a/src/test/groovy/com/ebf/security/test/guard/PermissionMetadataSourceSpec.groovy +++ b/src/test/groovy/com/ebf/security/guard/PermissionSecurityAttributeRegistrySpec.groovy @@ -13,62 +13,63 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.guard +package com.ebf.security.guard + +import com.ebf.security.guard.testcases.ProtectedClass +import com.ebf.security.guard.testcases.ProtectedInterface +import com.ebf.security.guard.testcases.PublicClass +import com.ebf.security.guard.testcases.PublicInterface import java.lang.reflect.Method import spock.lang.Specification -import com.ebf.security.guard.PermissionMetadataSource -import com.ebf.security.test.guard.testcases.ProtectedClass -import com.ebf.security.test.guard.testcases.ProtectedInterface -import com.ebf.security.test.guard.testcases.PublicClass -import com.ebf.security.test.guard.testcases.PublicInterface +class PermissionSecurityAttributeRegistrySpec extends Specification { + + PermissionSecurityAttributeRegistry registry; -class PermissionMetadataSourceSpec extends Specification { + def setup() { + registry = new PermissionSecurityAttributeRegistry() + } def "should return config attributes from the protected interface for protected interface public class" () { setup: - Method method = ProtectedInterface.class.getDeclaredMethod("protectedMethod") - def metadataSource = new PermissionMetadataSource() + Method method = PublicClass.class.getDeclaredMethod("protectedMethod") when: - def result = metadataSource.getAttributes(method, PublicClass) + def result = registry.get(method, PublicClass) then: - result[0].getAttribute() == "protectMe" + result[0] == "protectMe" } def "should return config attributes from the protected implementation for public interface protected class" () { setup: Method method = PublicInterface.class.getDeclaredMethod("publicMethod") - def metadataSource = new PermissionMetadataSource() when: - def result = metadataSource.getAttributes(method, ProtectedClass) + def result = registry.get(method, ProtectedClass) then: - result[0].getAttribute() == "protectedPublic" + result[0] == "protectedPublic" } def "should return null attributes for public interface public class" () { setup: Method method = PublicInterface.class.getDeclaredMethod("publicMethod") - def metadataSource = new PermissionMetadataSource() when: - def result = metadataSource.getAttributes(method, PublicClass) + def result = registry.get(method, PublicClass) then: - result== null + result.isEmpty() } def "should return config attributes from class for protected interface protected class" () { setup: Method method = ProtectedInterface.class.getDeclaredMethod("protectedMethod") - def metadataSource = new PermissionMetadataSource() when: - def result = metadataSource.getAttributes(method, ProtectedClass) + def result = registry.get(method, ProtectedClass) then: - result[0].getAttribute() == "overrideProtectMe" + result[0] == "overrideProtectMe" } } diff --git a/src/test/groovy/com/ebf/security/test/guard/testcases/ProtectedClass.java b/src/test/groovy/com/ebf/security/guard/testcases/ProtectedClass.java similarity index 95% rename from src/test/groovy/com/ebf/security/test/guard/testcases/ProtectedClass.java rename to src/test/groovy/com/ebf/security/guard/testcases/ProtectedClass.java index c3bddfd..ff1d905 100644 --- a/src/test/groovy/com/ebf/security/test/guard/testcases/ProtectedClass.java +++ b/src/test/groovy/com/ebf/security/guard/testcases/ProtectedClass.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.guard.testcases; +package com.ebf.security.guard.testcases; import com.ebf.security.annotations.Permission; import com.ebf.security.annotations.ProtectedResource; diff --git a/src/test/groovy/com/ebf/security/test/guard/testcases/ProtectedInterface.java b/src/test/groovy/com/ebf/security/guard/testcases/ProtectedInterface.java similarity index 94% rename from src/test/groovy/com/ebf/security/test/guard/testcases/ProtectedInterface.java rename to src/test/groovy/com/ebf/security/guard/testcases/ProtectedInterface.java index 6d98296..e343c4d 100644 --- a/src/test/groovy/com/ebf/security/test/guard/testcases/ProtectedInterface.java +++ b/src/test/groovy/com/ebf/security/guard/testcases/ProtectedInterface.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.guard.testcases; +package com.ebf.security.guard.testcases; import com.ebf.security.annotations.Permission; import com.ebf.security.annotations.ProtectedResource; diff --git a/src/test/groovy/com/ebf/security/test/guard/testcases/PublicClass.java b/src/test/groovy/com/ebf/security/guard/testcases/PublicClass.java similarity index 94% rename from src/test/groovy/com/ebf/security/test/guard/testcases/PublicClass.java rename to src/test/groovy/com/ebf/security/guard/testcases/PublicClass.java index c82eebd..de4fbbf 100644 --- a/src/test/groovy/com/ebf/security/test/guard/testcases/PublicClass.java +++ b/src/test/groovy/com/ebf/security/guard/testcases/PublicClass.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.guard.testcases; +package com.ebf.security.guard.testcases; public class PublicClass implements ProtectedInterface, PublicInterface { diff --git a/src/test/groovy/com/ebf/security/test/guard/testcases/PublicInterface.java b/src/test/groovy/com/ebf/security/guard/testcases/PublicInterface.java similarity index 93% rename from src/test/groovy/com/ebf/security/test/guard/testcases/PublicInterface.java rename to src/test/groovy/com/ebf/security/guard/testcases/PublicInterface.java index 5df18d7..5b5dcdc 100644 --- a/src/test/groovy/com/ebf/security/test/guard/testcases/PublicInterface.java +++ b/src/test/groovy/com/ebf/security/guard/testcases/PublicInterface.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.guard.testcases; +package com.ebf.security.guard.testcases; public interface PublicInterface { diff --git a/src/test/groovy/com/ebf/security/test/integration/CustomRepositoryMethodSecuritySpec.groovy b/src/test/groovy/com/ebf/security/integration/CustomRepositoryMethodSecuritySpec.groovy similarity index 98% rename from src/test/groovy/com/ebf/security/test/integration/CustomRepositoryMethodSecuritySpec.groovy rename to src/test/groovy/com/ebf/security/integration/CustomRepositoryMethodSecuritySpec.groovy index ec2caac..846e4ae 100644 --- a/src/test/groovy/com/ebf/security/test/integration/CustomRepositoryMethodSecuritySpec.groovy +++ b/src/test/groovy/com/ebf/security/integration/CustomRepositoryMethodSecuritySpec.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.integration +package com.ebf.security.integration import com.ebf.security.jwt.testapp.TestApplicationWithCustomRepository import com.ebf.security.jwt.testapp.models.Model diff --git a/src/test/groovy/com/ebf/security/test/integration/InitPermissionsIntegrationSpec.groovy b/src/test/groovy/com/ebf/security/integration/InitPermissionsIntegrationSpec.groovy similarity index 97% rename from src/test/groovy/com/ebf/security/test/integration/InitPermissionsIntegrationSpec.groovy rename to src/test/groovy/com/ebf/security/integration/InitPermissionsIntegrationSpec.groovy index bc8b20d..0599f1f 100644 --- a/src/test/groovy/com/ebf/security/test/integration/InitPermissionsIntegrationSpec.groovy +++ b/src/test/groovy/com/ebf/security/integration/InitPermissionsIntegrationSpec.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.integration +package com.ebf.security.integration import com.ebf.security.jwt.testapp.TestApplication import com.ebf.security.jwt.testapp.models.Model diff --git a/src/test/groovy/com/ebf/security/test/integration/JpaRepositoryMethodSecuritySpec.groovy b/src/test/groovy/com/ebf/security/integration/JpaRepositoryMethodSecuritySpec.groovy similarity index 98% rename from src/test/groovy/com/ebf/security/test/integration/JpaRepositoryMethodSecuritySpec.groovy rename to src/test/groovy/com/ebf/security/integration/JpaRepositoryMethodSecuritySpec.groovy index bcc2471..450cec4 100644 --- a/src/test/groovy/com/ebf/security/test/integration/JpaRepositoryMethodSecuritySpec.groovy +++ b/src/test/groovy/com/ebf/security/integration/JpaRepositoryMethodSecuritySpec.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.integration +package com.ebf.security.integration import com.ebf.security.jwt.testapp.models.Model diff --git a/src/test/groovy/com/ebf/security/test/integration/MethodSecuritySpec.groovy b/src/test/groovy/com/ebf/security/integration/MethodSecuritySpec.groovy similarity index 97% rename from src/test/groovy/com/ebf/security/test/integration/MethodSecuritySpec.groovy rename to src/test/groovy/com/ebf/security/integration/MethodSecuritySpec.groovy index a7984e6..5695d56 100644 --- a/src/test/groovy/com/ebf/security/test/integration/MethodSecuritySpec.groovy +++ b/src/test/groovy/com/ebf/security/integration/MethodSecuritySpec.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.integration +package com.ebf.security.integration import org.springframework.http.HttpStatus import org.springframework.test.context.ContextConfiguration diff --git a/src/test/groovy/com/ebf/security/test/integration/SecuritySpecification.groovy b/src/test/groovy/com/ebf/security/integration/SecuritySpecification.groovy similarity index 93% rename from src/test/groovy/com/ebf/security/test/integration/SecuritySpecification.groovy rename to src/test/groovy/com/ebf/security/integration/SecuritySpecification.groovy index e2cb568..97696f3 100644 --- a/src/test/groovy/com/ebf/security/test/integration/SecuritySpecification.groovy +++ b/src/test/groovy/com/ebf/security/integration/SecuritySpecification.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.integration +package com.ebf.security.integration import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -24,7 +24,6 @@ import org.springframework.core.ParameterizedTypeReference import org.springframework.http.HttpHeaders import org.springframework.http.RequestEntity import org.springframework.http.ResponseEntity -import org.springframework.util.Base64Utils import org.springframework.web.util.UriComponentsBuilder import spock.lang.Specification @@ -60,7 +59,7 @@ abstract class SecuritySpecification extends Specification { final def headers = new HttpHeaders() if (credentials != null) { - final def authorization = Base64Utils.encodeToString(credentials.bytes) + final def authorization = Base64.getEncoder().encodeToString(credentials.bytes) headers.setBasicAuth(authorization) } diff --git a/src/test/groovy/com/ebf/security/integration/SpringMethodSecuritySpec.groovy b/src/test/groovy/com/ebf/security/integration/SpringMethodSecuritySpec.groovy new file mode 100644 index 0000000..3bacf99 --- /dev/null +++ b/src/test/groovy/com/ebf/security/integration/SpringMethodSecuritySpec.groovy @@ -0,0 +1,63 @@ +/* + * Copyright 2009-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ebf.security.integration + +import com.ebf.security.jwt.testapp.TestApplicationWithAuthorizedUser +import org.springframework.http.HttpStatus +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity +import org.springframework.test.context.ContextConfiguration + +@EnableMethodSecurity +@ContextConfiguration(classes = TestApplicationWithAuthorizedUser) +class SpringMethodSecuritySpec extends SecuritySpecification { + + def "http request to / should result in 401 when no authentication is present" () { + + when: + def response = request(null, Object) + + then: + response.statusCode == HttpStatus.UNAUTHORIZED + } + + def "http request to / should result in 401 when invalid user credentials are sent" () { + + when: + def response = request("unknown:unknown", Object) + + then: + response.statusCode == HttpStatus.UNAUTHORIZED + } + + def "http request to / should result in 403 when the user has no spring method security permission" () { + + when: + def response = request("user:user", Object) + + then: + response.statusCode == HttpStatus.FORBIDDEN + } + + def "http request to / should result in 403 when the user does not have sufficient permissions" () { + + when: + def response = request("test:test", Object) + + then: + response.statusCode == HttpStatus.FORBIDDEN + } + +} diff --git a/src/test/groovy/com/ebf/security/test/internal/data/PermissionModelDefinitionSpec.groovy b/src/test/groovy/com/ebf/security/internal/data/PermissionModelDefinitionSpec.groovy similarity index 98% rename from src/test/groovy/com/ebf/security/test/internal/data/PermissionModelDefinitionSpec.groovy rename to src/test/groovy/com/ebf/security/internal/data/PermissionModelDefinitionSpec.groovy index 17d4e76..c53d179 100644 --- a/src/test/groovy/com/ebf/security/test/internal/data/PermissionModelDefinitionSpec.groovy +++ b/src/test/groovy/com/ebf/security/internal/data/PermissionModelDefinitionSpec.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.internal.data +package com.ebf.security.internal.data import com.ebf.security.exceptions.PermissionModelException import com.ebf.security.internal.data.PermissionModelDefinition diff --git a/src/test/groovy/com/ebf/security/test/internal/services/DefaultPermissionModelFinderSpec.groovy b/src/test/groovy/com/ebf/security/internal/services/DefaultPermissionModelFinderSpec.groovy similarity index 98% rename from src/test/groovy/com/ebf/security/test/internal/services/DefaultPermissionModelFinderSpec.groovy rename to src/test/groovy/com/ebf/security/internal/services/DefaultPermissionModelFinderSpec.groovy index 3a60a4f..cb046d5 100644 --- a/src/test/groovy/com/ebf/security/test/internal/services/DefaultPermissionModelFinderSpec.groovy +++ b/src/test/groovy/com/ebf/security/internal/services/DefaultPermissionModelFinderSpec.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.internal.services +package com.ebf.security.internal.services import com.ebf.security.exceptions.MoreThanOnePermissionModelFoundException import com.ebf.security.exceptions.NoPermissionModelFoundException diff --git a/src/test/groovy/com/ebf/security/test/scanner/PermissionScannerSpec.groovy b/src/test/groovy/com/ebf/security/scanner/PermissionScannerSpec.groovy similarity index 97% rename from src/test/groovy/com/ebf/security/test/scanner/PermissionScannerSpec.groovy rename to src/test/groovy/com/ebf/security/scanner/PermissionScannerSpec.groovy index 76f3929..c9ef3d6 100644 --- a/src/test/groovy/com/ebf/security/test/scanner/PermissionScannerSpec.groovy +++ b/src/test/groovy/com/ebf/security/scanner/PermissionScannerSpec.groovy @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.ebf.security.test.scanner +package com.ebf.security.scanner import com.ebf.security.jwt.testapp.TestApplication import com.ebf.security.scanner.PermissionScanner diff --git a/src/test/groovy/com/ebf/security/test/guard/PermissionAccessDecisionVoterSpec.groovy b/src/test/groovy/com/ebf/security/test/guard/PermissionAccessDecisionVoterSpec.groovy deleted file mode 100644 index eb3a791..0000000 --- a/src/test/groovy/com/ebf/security/test/guard/PermissionAccessDecisionVoterSpec.groovy +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2009-2017 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.ebf.security.test.guard - -import org.springframework.security.access.AccessDeniedException -import org.springframework.security.core.Authentication -import org.springframework.security.core.authority.SimpleGrantedAuthority - -import spock.lang.Specification -import com.ebf.security.guard.PermissionAccessDecisionVoter -import com.ebf.security.guard.PermissionSecurityAttribute - -class PermissionAccessDecisionVoterSpec extends Specification { - - def "should abstain if if no attributes are present"() { - setup: - Authentication authentication = Mock() - def voter = new PermissionAccessDecisionVoter() - - when: - def result = voter.vote(authentication, null, []) - - then: - result == PermissionAccessDecisionVoter.ACCESS_ABSTAIN - } - - def "should deny access if authentication holds no authorities"() { - setup: - Authentication authentication = Mock() - PermissionSecurityAttribute attribute = new PermissionSecurityAttribute("test") - def configAttributes = [attribute] - def voter = new PermissionAccessDecisionVoter() - - when: - def result = voter.vote(authentication, null, configAttributes) - - then: - result == PermissionAccessDecisionVoter.ACCESS_DENIED - } - - def "should deny access if authentication holds insufficient authorities"() { - setup: - Authentication authentication = Mock() - SimpleGrantedAuthority authority = new SimpleGrantedAuthority("whatever") - authentication.authorities >> [authority] - PermissionSecurityAttribute attribute = new PermissionSecurityAttribute("test") - def configAttributes = [attribute] - def voter = new PermissionAccessDecisionVoter() - - when: - def result = voter.vote(authentication, null, configAttributes) - - then: - result == PermissionAccessDecisionVoter.ACCESS_DENIED - } - - def "should grant access exceptions if authentication holds sufficient authorities"() { - setup: - Authentication authentication = Mock() - SimpleGrantedAuthority authority = new SimpleGrantedAuthority("test") - authentication.authorities >> [authority] - PermissionSecurityAttribute attribute = new PermissionSecurityAttribute("test") - def configAttributes = [attribute] - def voter = new PermissionAccessDecisionVoter() - - when: - def result = voter.vote(authentication, null, configAttributes) - - then: - result == PermissionAccessDecisionVoter.ACCESS_GRANTED - } - - def "should grant access exceptions if authentication holds at least one sufficient authority"() { - setup: - Authentication authentication = Mock() - SimpleGrantedAuthority authority = new SimpleGrantedAuthority("test") - authentication.authorities >> [authority] - PermissionSecurityAttribute attribute = new PermissionSecurityAttribute("test") - PermissionSecurityAttribute attribute2 = new PermissionSecurityAttribute("test-2") - def configAttributes = [attribute, attribute2] - def voter = new PermissionAccessDecisionVoter() - - when: - def result = voter.vote(authentication, null, configAttributes) - - then: - result == PermissionAccessDecisionVoter.ACCESS_GRANTED - } -} diff --git a/src/test/java/com/ebf/security/jwt/testapp/controllers/TestController.java b/src/test/java/com/ebf/security/jwt/testapp/controllers/TestController.java index 05b3850..6ef32a0 100644 --- a/src/test/java/com/ebf/security/jwt/testapp/controllers/TestController.java +++ b/src/test/java/com/ebf/security/jwt/testapp/controllers/TestController.java @@ -17,6 +17,8 @@ import com.ebf.security.annotations.Permission; import com.ebf.security.annotations.ProtectedResource; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -26,12 +28,14 @@ public class TestController { @RequestMapping(path = "/") @Permission(value = "test:request") + @PreAuthorize("hasAuthority('spring-security-method-permission')") public void testRequest() { } @RequestMapping(path = "/multiple-permissions") @Permission(value = { "test-multiple:request-1", "test-multiple:request-2" }) + @PostAuthorize("hasAuthority('spring-security-method-permission')") public void testMultiplePermissionsRequest() { } From 64b55a9f1593cbc155c89ec5d4b9d254e129556f Mon Sep 17 00:00:00 2001 From: Vladimir Spasic Date: Mon, 2 Oct 2023 13:16:09 +0200 Subject: [PATCH 2/4] Add licence --- .../guard/PermissionAuthorizationManager.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/ebf/security/guard/PermissionAuthorizationManager.java b/src/main/java/com/ebf/security/guard/PermissionAuthorizationManager.java index 0a94be5..220c4bf 100644 --- a/src/main/java/com/ebf/security/guard/PermissionAuthorizationManager.java +++ b/src/main/java/com/ebf/security/guard/PermissionAuthorizationManager.java @@ -1,3 +1,18 @@ +/** + * Copyright 2009-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.ebf.security.guard; import io.micrometer.observation.ObservationRegistry; From 5c59cdfab1e73a432b8f3680d4e9649ae9b03076 Mon Sep 17 00:00:00 2001 From: Vladimir Spasic Date: Tue, 20 Feb 2024 16:49:38 +0100 Subject: [PATCH 3/4] Use latest Spring, Gradle Versions, JDK 21 Support --- .github/workflows/ci.yml | 18 ++++++---- build.gradle | 43 +++++++++++++----------- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6114314..56d76b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,26 +11,32 @@ on: jobs: build: - runs-on: ubuntu-latest + strategy: + matrix: + java: [ '17', '21' ] steps: - name: Checkout uses: actions/checkout@v2 - - name: Set up JDK 17 - uses: actions/setup-java@v1 + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v3 with: - java-version: 17 + java-version: ${{ matrix.java }} + distribution: 'temurin' - name: Setup Gradle - run: chmod +x gradlew + uses: gradle/gradle-build-action@v3 - name: Build run: ./gradlew check --scan --stacktrace - name: Add coverage to PR - uses: madrapps/jacoco-report@v1.2 + uses: madrapps/jacoco-report@v1.6.1 with: paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml token: ${{ secrets.GITHUB_TOKEN }} diff --git a/build.gradle b/build.gradle index c60f80d..43fc6bd 100644 --- a/build.gradle +++ b/build.gradle @@ -5,9 +5,10 @@ plugins { id 'jacoco' id 'maven-publish' - id 'io.freefair.lombok' version '6.4.2' + id 'io.freefair.lombok' version '8.4' + id 'io.spring.dependency-management' version '1.1.4' id 'com.github.hierynomus.license' version '0.16.1' - id 'org.hibernate.build.maven-repo-auth' version '3.0.2' + id 'org.hibernate.build.maven-repo-auth' version '3.0.4' } repositories { @@ -20,11 +21,6 @@ group = "com.ebf" archivesBaseName = "spring-granular-permissions" version = "3.1.0" -compileJava { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - jar { manifest { attributes 'Implementation-Title': 'Spring Granular Permissions', @@ -44,29 +40,38 @@ idea { } ext { - set('springVersion', '3.1.4') + set('springVersion', '3.2.2') } dependencies { - compileOnly("org.springframework.boot:spring-boot-starter-data-jpa:${springVersion}") - compileOnly("org.springframework.boot:spring-boot-starter-security:${springVersion}") + compileOnly('org.springframework.boot:spring-boot-starter-data-jpa') + compileOnly('org.springframework.boot:spring-boot-starter-security') /* Test Database implementation */ - testImplementation('com.h2database:h2:2.1.214') + testImplementation('com.h2database:h2') /* Spring Boot Test dependencies */ - testImplementation("org.springframework.boot:spring-boot-starter-test:${springVersion}") - testImplementation("org.springframework.boot:spring-boot-starter-web:${springVersion}") - testImplementation("org.springframework.boot:spring-boot-starter-data-rest:${springVersion}") - testImplementation("org.springframework.boot:spring-boot-starter-data-jpa:${springVersion}") - testImplementation("org.springframework.boot:spring-boot-starter-security:${springVersion}") + testImplementation('org.springframework.boot:spring-boot-starter-test') + testImplementation('org.springframework.boot:spring-boot-starter-web') + testImplementation('org.springframework.boot:spring-boot-starter-data-rest') + testImplementation('org.springframework.boot:spring-boot-starter-data-jpa') + testImplementation('org.springframework.boot:spring-boot-starter-security') /* Spock Test dependencies */ - testImplementation('org.spockframework:spock-core:2.4-M1-groovy-3.0') - testImplementation('org.spockframework:spock-spring:2.4-M1-groovy-3.0') + testImplementation('org.spockframework:spock-spring:2.4-M1-groovy-4.0') /* Test utilities */ - testImplementation('org.codehaus.groovy:groovy-json:3.0.17') + testImplementation('org.apache.groovy:groovy-json:4.0.18') +} + +dependencyManagement { + generatedPomCustomization { + enabled = false + } + + imports { + mavenBom("org.springframework.boot:spring-boot-dependencies:${springVersion}") + } } license { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e589..a595206 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 0a3c37e72638aae08b87d1238ce0ef67493de9f7 Mon Sep 17 00:00:00 2001 From: Vladimir Spasic Date: Tue, 20 Feb 2024 16:51:46 +0100 Subject: [PATCH 4/4] Do not apply maven repo auth plugin on GH actions --- .github/workflows/ci.yml | 3 +++ build.gradle | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56d76b8..71c2e50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,9 @@ on: pull_request: branches: [ master ] +env: + CI: github + jobs: build: runs-on: ubuntu-latest diff --git a/build.gradle b/build.gradle index 43fc6bd..098f193 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,13 @@ plugins { id 'io.freefair.lombok' version '8.4' id 'io.spring.dependency-management' version '1.1.4' id 'com.github.hierynomus.license' version '0.16.1' - id 'org.hibernate.build.maven-repo-auth' version '3.0.4' + id 'org.hibernate.build.maven-repo-auth' version '3.0.4' apply(false) +} + +// do not apply the plugin when the build is running on Github actions as there we do not have +// an m2-settings.xml file. This plugin would throw an NPE when credentials could not be resolved +if (System.getenv('CI') != 'github') { + apply plugin: 'org.hibernate.build.maven-repo-auth' } repositories {