Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring Method Security using Authorization Manager #22

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,37 @@ on:
pull_request:
branches: [ master ]

env:
CI: github

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 }}
Expand Down
60 changes: 38 additions & 22 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ 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' 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 {
mavenLocal()
mavenCentral()
Expand All @@ -17,12 +25,7 @@ repositories {

group = "com.ebf"
archivesBaseName = "spring-granular-permissions"
version = "3.0.1"

compileJava {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
version = "3.1.0"

jar {
manifest {
Expand All @@ -42,26 +45,39 @@ idea {
}
}

ext {
set('springVersion', '3.2.2')
}

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')
compileOnly('org.springframework.boot:spring-boot-starter-security')

/* Test Database implementation */
testImplementation('com.h2database:h2:2.1.214')
testImplementation('com.h2database:h2')

/* Spring and Spock Test dependencies */
testImplementation('org.springframework.boot:spring-boot-starter-test:3.1.0')
testImplementation('org.spockframework:spock-core:2.4-M1-groovy-3.0')
testImplementation('org.spockframework:spock-spring:2.4-M1-groovy-3.0')
/* Spring Boot Test dependencies */
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-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')
}

/* 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')
dependencyManagement {
generatedPomCustomization {
enabled = false
}

imports {
mavenBom("org.springframework.boot:spring-boot-dependencies:${springVersion}")
}
}

license {
Expand Down Expand Up @@ -158,4 +174,4 @@ publishing {
url = 'https://repository.hosting.ebf.de/nexus/content/repositories/releases/'
}
}
}
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>
*
* Configuration class that is imported as a configuration candidate by the
* {@link com.ebf.security.annotations.PermissionScan} annotation.
* <p>
* 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 <[email protected]>
* @author Vladimir Spasic <[email protected]>
*/
@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<SecurityContextHolderStrategy> strategyProvider,
ObjectProvider<AuthorizationEventPublisher> eventPublisherProvider,
ObjectProvider<ObservationRegistry> 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<AccessDecisionVoter<?>> 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);
}
}
}

}
3 changes: 2 additions & 1 deletion src/main/java/com/ebf/security/PermissionScanSelector.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

This file was deleted.

Loading
Loading