[JAVA] Spring Security Usage Memo Domain Object Security (ACL)

Basic and mechanical story Authentication / Authorization Story Remember-Me story CSRF story Session management story The story of the response header Method security story CORS story The story of Run-As Test story Talk about cooperation with MVC and Boot

Extra edition What Spring Security can and cannot do

What is domain object security?

Authorization processing using hasAuthority () etc. is basically controlled in units of functions. (A certain function (screen) needs to have the authority of XX, etc.)

However, when actually creating a system, it is rarely necessary to manage authority on a data-by-data basis. For example, make the data visible only to the person who belongs to the same person who created the data, or update the data only by the creator or the system administrator.

Spring Security provides a mechanism to realize such access control on a data-by-data basis.

It is called domain object security, or ACL (Access Control List).

Hello World

Implementation

Dependencies

build.gradle


dependencies {
    compile 'org.springframework.security:spring-security-web:4.2.1.RELEASE'
    compile 'org.springframework.security:spring-security-config:4.2.1.RELEASE'
    compile 'org.springframework.security:spring-security-acl:4.2.1.RELEASE'★ Addition
    compile 'org.springframework:spring-jdbc:4.3.7.RELEASE'
    compile 'com.h2database:h2:1.4.193'
}

DB

src/main/resources/sql/create_acl_tables.sql


CREATE TABLE ACL_SID (
    ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    PRINCIPAL BOOLEAN NOT NULL,
    SID VARCHAR_IGNORECASE(100) NOT NULL,
    CONSTRAINT UNIQUE_UK_1 UNIQUE(SID,PRINCIPAL)
);

CREATE TABLE ACL_CLASS(
    ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    CLASS VARCHAR_IGNORECASE(100) NOT NULL,
    CONSTRAINT UNIQUE_UK_2 UNIQUE(CLASS)
);

CREATE TABLE ACL_OBJECT_IDENTITY(
    ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    OBJECT_ID_CLASS BIGINT NOT NULL,
    OBJECT_ID_IDENTITY BIGINT NOT NULL,
    PARENT_OBJECT BIGINT,
    OWNER_SID BIGINT,
    ENTRIES_INHERITING BOOLEAN NOT NULL,
    CONSTRAINT UNIQUE_UK_3 UNIQUE(OBJECT_ID_CLASS, OBJECT_ID_IDENTITY),
    CONSTRAINT FOREIGN_FK_1 FOREIGN KEY(PARENT_OBJECT)REFERENCES ACL_OBJECT_IDENTITY(ID),
    CONSTRAINT FOREIGN_FK_2 FOREIGN KEY(OBJECT_ID_CLASS)REFERENCES ACL_CLASS(ID),
    CONSTRAINT FOREIGN_FK_3 FOREIGN KEY(OWNER_SID)REFERENCES ACL_SID(ID)
);

CREATE TABLE ACL_ENTRY(
    ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    ACL_OBJECT_IDENTITY BIGINT NOT NULL,
    ACE_ORDER INT NOT NULL,
    SID BIGINT NOT NULL,
    MASK INTEGER NOT NULL,
    GRANTING BOOLEAN NOT NULL,
    AUDIT_SUCCESS BOOLEAN NOT NULL,
    AUDIT_FAILURE BOOLEAN NOT NULL,
    CONSTRAINT UNIQUE_UK_4 UNIQUE(ACL_OBJECT_IDENTITY, ACE_ORDER),
    CONSTRAINT FOREIGN_FK_4 FOREIGN KEY(ACL_OBJECT_IDENTITY) REFERENCES ACL_OBJECT_IDENTITY(ID),
    CONSTRAINT FOREIGN_FK_5 FOREIGN KEY(SID) REFERENCES ACL_SID(ID)
);

src/main/resources/sql/insert_acl_tables.sql


INSERT INTO ACL_CLASS (ID, CLASS)
VALUES (100, 'sample.spring.security.domain.Foo');

INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (9, true, 'hoge');
INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (99, false, 'SAMPLE_AUTHORITY');

INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1000, 100, 44, NULL, 9, true);

INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (10, 1000, 0, 99, 1, true, false, false);

Common implementation for confirmation

Foo.java


package sample.spring.security.domain;

public class Foo {
    private final long id;

    public Foo(long id) {
        this.id = id;
    }

    public long getId() {
        return id;
    }

    @Override
    public String toString() {
        return "Foo{id=" + id + '}';
    }
}

--A simple class that just has ʻid`.

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;
import sample.spring.security.domain.Foo;

public class MyAclSampleService {
    
    @PreAuthorize("hasPermission(#foo, read)")
    public void logic(Foo foo) {
        System.out.println("foo=" + foo);
    }
}

--Standard output of foo received as an argument --Annotate the method with @ PreAuthorize and use thehasPermission ()function to check access to the domain object. --Here we are checking if the argument foo has read privileges.

MyAclServlet.java


package sample.spring.security.servlet;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import sample.spring.security.domain.Foo;
import sample.spring.security.service.MyAclSampleService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.stream.Collectors;

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.printPrincipal();
        this.callServiceLogic(new Foo(44L), req);
        this.callServiceLogic(new Foo(45L), req);
    }

    private void printPrincipal() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String name = auth.getName();
        System.out.println("name=" + name);
        System.out.println("authorities=" +
                auth.getAuthorities().stream()
                        .map(GrantedAuthority::getAuthority)
                        .collect(Collectors.joining(", "))
        );
    }
    
    private void callServiceLogic(Foo foo, HttpServletRequest req) {
        try {
            this.findServiceBean(req).logic(foo);
        } catch (AccessDeniedException e) {
            System.out.println("AccessDeniedException : " + e.getMessage());
        }
    }
    
    private MyAclSampleService findServiceBean(HttpServletRequest req) {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
        return context.getBean(MyAclSampleService.class);
    }
}

--After outputting the information of the current principal, instantiate Foo with ʻid = 44 and ʻid = 45 and execute thelogic ()method of MyAclSampleService. --If ʻAccessDeniedException` is thrown, it is output to the console.

Setting

namespace

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/security
         http://www.springframework.org/schema/security/spring-security.xsd
         http://www.springframework.org/schema/jdbc
         http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">

    <jdbc:embedded-database id="dataSource" type="H2">
        <jdbc:script location="classpath:/sql/create_acl_tables.sql" />
        <jdbc:script location="classpath:/sql/insert_acl_tables.sql" />
    </jdbc:embedded-database>
    
    <sec:global-method-security pre-post-annotations="enabled">
        <sec:expression-handler ref="expressionHandler" />
    </sec:global-method-security>
    
    <bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
        <property name="permissionEvaluator">
            <bean class="org.springframework.security.acls.AclPermissionEvaluator">
                <constructor-arg ref="aclService" />
            </bean>
        </property>
    </bean>

    <bean id="aclService" class="org.springframework.security.acls.jdbc.JdbcAclService">
        <constructor-arg ref="dataSource" />
        <constructor-arg ref="lookupStrategy" />
    </bean>

    <bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
        <constructor-arg ref="dataSource" />
        <constructor-arg ref="aclCache" />
        <constructor-arg ref="aclAuthorizationStrategy" />
        <constructor-arg ref="permissionGrantingStrategy" />
    </bean>

    <bean id="aclCache" class="org.springframework.security.acls.domain.SpringCacheBasedAclCache">
        <constructor-arg>
            <bean class="org.springframework.cache.support.NoOpCache">
                <constructor-arg value="myCache" />
            </bean>
        </constructor-arg>
        <constructor-arg ref="permissionGrantingStrategy" />
        <constructor-arg ref="aclAuthorizationStrategy" />
    </bean>

    <bean id="aclAuthorizationStrategy" class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                    <constructor-arg value="TEST"/>
                </bean>
            </list>
        </constructor-arg>
    </bean>

    <bean id="permissionGrantingStrategy" class="org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy">
        <constructor-arg>
            <bean class="org.springframework.security.acls.domain.ConsoleAuditLogger" />
        </constructor-arg>
    </bean>
    
    <bean class="sample.spring.security.service.MyAclSampleService" />

    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login />
        <sec:logout />
    </sec:http>

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="foo" password="foo" authorities="" />
                <sec:user name="bar" password="bar" authorities="SAMPLE_AUTHORITY" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans

Java Configuration

MySpringSecurityConfig.java


package sample.spring.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import sample.spring.security.service.MyAclSampleService;

import java.util.Collections;

@EnableWebSecurity
@Import(MyGlobalMethodSecurityConfig.class)
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin();
    }
    
    @Bean
    public MyAclSampleService myAclSampleService() {
        return new MyAclSampleService();
    }
    
    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("foo")
                .password("foo")
                .authorities(Collections.emptyList())
            .and()
                .withUser("bar")
                .password("bar")
                .authorities("SAMPLE_AUTHORITY");
    }
}

MyGlobalMethodSecurityConfig.java


package sample.spring.security;

import org.springframework.cache.support.NoOpCache;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.acls.AclPermissionEvaluator;
import org.springframework.security.acls.domain.AclAuthorizationStrategy;
import org.springframework.security.acls.domain.AclAuthorizationStrategyImpl;
import org.springframework.security.acls.domain.ConsoleAuditLogger;
import org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy;
import org.springframework.security.acls.domain.SpringCacheBasedAclCache;
import org.springframework.security.acls.jdbc.BasicLookupStrategy;
import org.springframework.security.acls.jdbc.JdbcAclService;
import org.springframework.security.acls.jdbc.LookupStrategy;
import org.springframework.security.acls.model.AclCache;
import org.springframework.security.acls.model.AclService;
import org.springframework.security.acls.model.PermissionGrantingStrategy;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import javax.sql.DataSource;

@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MyGlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(EmbeddedDatabaseType.H2)
                .setScriptEncoding("UTF-8")
                .addScripts("/sql/create_acl_tables.sql", "/sql/insert_acl_tables.sql")
                .build();
    }

    @Bean
    public PermissionEvaluator permissionEvaluator(AclService aclService) {
        return new AclPermissionEvaluator(aclService);
    }

    @Bean
    public AclService aclService(DataSource dataSource, LookupStrategy lookupStrategy) {
        return new JdbcAclService(dataSource, lookupStrategy);
    }

    @Bean
    public LookupStrategy lookupStrategy(DataSource dataSource, AclCache aclCache, AclAuthorizationStrategy aclAuthorizationStrategy, PermissionGrantingStrategy permissionGrantingStrategy) {
        return new BasicLookupStrategy(
                dataSource,
                aclCache,
                aclAuthorizationStrategy,
                permissionGrantingStrategy
        );
    }

    @Bean
    public AclCache aclCache(PermissionGrantingStrategy permissionGrantingStrategy, AclAuthorizationStrategy aclAuthorizationStrategy) {
        return new SpringCacheBasedAclCache(
                new NoOpCache("myCache"),
                permissionGrantingStrategy,
                aclAuthorizationStrategy
        );
    }
    
    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy() {
        return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("TEST"));
    }

    @Bean
    public PermissionGrantingStrategy permissionGrantingStrategy() {
        return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
    }
}

Operation check

Log in as the foo user and access / acl.

Server console output


name=foo
authorities=
AccessDeniedException : Access is denied
AccessDeniedException : Access is denied

Access was denied for both ʻid = 44 and ʻid = 45.

Then log in as the bar user and access / acl.

Server console output


name=bar
authorities=SAMPLE_AUTHORITY
foo=Foo{id=44}
AccessDeniedException : Access is denied

ʻId = 44 succeeded in executing the logic () method, and ʻid = 45 was denied access.

How it works

table

The following four will appear.

The table structure looks like the one below.

ACLテーブル構造.png

The arrow represents the FK, the source of the arrow is the reference table, and the tip of the arrow is the reference table.

The meaning of each table and column is as follows.

ACL_CLASS --A table that records the Java class name (FQCN) of domain objects

column meaning
CLASS Domain object Java class name (FQCN)

ACL_SID --Table that defines the target to which permission is granted --Register the authority (GrantedAuthority) or principal --SID stands for Security Identity

column meaning
PRINCIPAL A flag that distinguishes whether this record is a principal
true→ Principal
falseGrantedAuthority
SID A string that represents the SID.
If you are a principalusernameGrantedAuthorityThen the string representation is set

ACL_OBJECT_IDENTITY --Table that holds each instance of a domain object

column meaning
OBJECT_ID_CLASS A column that points to a class of domain objects.
ACL_CLASSExternally referenced to.
OBJECT_ID_IDENTITY ID that identifies the instance
PARENT_OBJECT Parent'sACL_OBJECT_IDENTITYID
OWNER_SID Created this instanceACL_SIDID
ENTRIES_INHERITING Flags for whether this instance has permission inheritance with other instances

ACL_ENTRY --A table that defines the permissions assigned to ʻACL_SID for each ʻACL_OBJECT_IDENTITY

column meaning
ACL_OBJECT_IDENTITY Apply this permission definitionACL_OBJECT_IDENTITYID
ACE_ORDER OneACL_OBJECT_IDENTITYTo multipleACL_ENTRTYOrder when is linked
SID Apply this permission definitionACL_SIDID
MASK Integer value representing permission definition (details below)
GRANTING "Grant" or "Reject" flag
AUDIT_SUCCESS Flag to output audit log when this permission definition is granted
AUDIT_FAILUER Flag to output audit log when this permission definition is denied

Hello World data

The data used in Hello World had a structure like ↓.

ACL_HelloWorld_データ.png

--ʻACL_SIDTwo records are registered in the table --One is a user (principal) namedhoge and the other represents an authority named SAMPLE_AUTHORITY. --In ʻACL_OBJECT_IDENTITY, a record representing the Foo object with ʻid = 44is registered. --You have sethoge as the owner --Owner is the principal who created the record --ʻACL_ENTRY defines the permissions to set on the Foo object with ʻid = 44 --MASKis the definition of permission --Details will be described later, but1 stands for" read ". --GRANTING indicates whether the permission should be "granted" or "denied". --trueis" grant " --SetSID to ʻACL_SID to apply permissions --Here, it points to SAMPLE_AUTHORITY --In other words, this ʻACL_ENTRY data means "If you have ** SAMPLE_AUTHORITYprivileges, you can read theFoo object with ʻid = 44 **".

Domain object identifier type

--A number is supposed to be stored in ʻOBJECT_ID_IDENTITYas the identifier of the domain object. --Corresponds to thelong` type in Java --What happens if there is a domain object that is identified by something other than a numeric type? The official reference states as follows.

We do not intend to support non-long identifiers in Spring Security’s ACL module, as longs are already compatible with all database sequences, the most common identifier data type, and are of sufficient length to accommodate all common usage scenarios.

(Translation) We do not intend to ** support non-long identifiers in Spring Security ACL modules **. long is already compatible with all database sequences, is the most common data type, and is long enough to store all common usage scenarios.

27.3 Getting Started

--So, it is assumed that the domain object identifier is long.

Bean settings to enable ACL

SecurityExpressionHandler extension

applicationContext.xml


    <sec:global-method-security pre-post-annotations="enabled">
        <sec:expression-handler ref="expressionHandler" />
    </sec:global-method-security>
    
    <bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
        <property name="permissionEvaluator">
            <bean class="org.springframework.security.acls.AclPermissionEvaluator">
                <constructor-arg ref="aclService" />
            </bean>
        </property>
    </bean>

--In order to be able to use hasPermission () in expressions such as @ PreAuthorize (), you need to extend SecurityExpressionHandler. --The hasPermission () expression is evaluated by PermissionEvaluator --However, the default PermissionEvaluator of DefaultMethodSecurityExpressionHandler is a class called DenyAllPermissionEvaluator, and it is implemented so that all evaluation results are false. --In other words, the hasPermission () expression does not work by default (it always evaluates to false). --For this reason, it is necessary to replace the PermissionEvaluator of the DefaultMethodSecurityExpressionHandler with the real implementation. --The above ʻAclPermissionEvaluator` is set for that.

Definition of AclService

applicationContext.xml


    <bean id="aclService" class="org.springframework.security.acls.jdbc.JdbcAclService">
        <constructor-arg ref="dataSource" />
        <constructor-arg ref="lookupStrategy" />
    </bean>

--ʻAclService provides ACL functionality --JdbcAclServiceuses JDBC to access the information defined in the database and implements ACL functionality. --Therefore, you need to passDataSource` as a constructor argument

LookupStrategy

applicationContext.xml


    <bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
        <constructor-arg ref="dataSource" />
        <constructor-arg ref="aclCache" />
        <constructor-arg ref="aclAuthorizationStrategy" />
        <constructor-arg ref="permissionGrantingStrategy" />
    </bean>

--The search process for ACL definition information (such as ʻACL_CLASS) is delegated to LookupStrategy instead of ʻAclService itself. --BasicLookupStrategy is an implementation that uses JDBC to retrieve ACL definition information. --Therefore, we are passing DataSource as a constructor argument

How the cache works

applicationContext.xml


    <bean id="aclCache" class="org.springframework.security.acls.domain.SpringCacheBasedAclCache">
        <constructor-arg>
            <bean class="org.springframework.cache.support.NoOpCache">
                <constructor-arg value="myCache" />
            </bean>
        </constructor-arg>
        <constructor-arg ref="permissionGrantingStrategy" />
        <constructor-arg ref="aclAuthorizationStrategy" />
    </bean>

--As you can imagine from the table definition above, the number of data in the table is quite large (because data is registered for each object). --Therefore, in order to speed up the search process, a cache mechanism is built in by default. --The cache is represented by the ʻAclCacheinterface --Two classes,SpringCacheBasedAclCache and ʻEhCacheBasedAclCache, are provided as standard as classes that implement this interface. --This time, the purpose is to verify the operation of ACL, so I used SpringCacheBasedAclCache. --Using NoOpCache for the actual cache implementation --This is an empty implementation that doesn't actually cache at all, as it says NoOp. --Actually, it seems that you will use ʻEhCacheBasedAclCache`

AclAuthorizationStrategy and PermissionGrantingStrategy

--SpringCacheBasedAclCache caches an object of class ʻAclImpl --Looking at the field definition of this ʻAclImpl, it looks like this:

AclImpl.java


	private Acl parentAcl;
	private transient AclAuthorizationStrategy aclAuthorizationStrategy;
	private transient PermissionGrantingStrategy permissionGrantingStrategy;
	private final List<AccessControlEntry> aces = new ArrayList<AccessControlEntry>();
	private ObjectIdentity objectIdentity;
	private Serializable id;
	private Sid owner; // OwnershipAcl
	private List<Sid> loadedSids = null; // includes all SIDs the WHERE clause covered,

--ʻAclImpl implements the ʻAcl interface, and ʻAcl inherits from Serializable. --Therefore, ʻAclImpl must be serializable, and all the above fields are basically serializable. --However, ʻaclAuthorizationStrategyandpermissionGrantingStrategy are qualified with transient, so they are excluded from serialization. --If the cache serializes and stores the object, these two fields will be lost when retrieving from the cache. --In other words, the cache needs to return these two fields to the same state as before the cache when deserializing. --Therefore, you need to pass instances of ʻAclAuthorizationStrategy and PermissionGrantingStrategy to the cache as constructor arguments. --The instance specified here is used to reconfigure when ʻAclImpl` is taken out of the cache.

By the way, BasicLookupStrategy also needs to receive instances of ʻAclAuthorizationStrategy and PermissionGrantingStrategy` as constructor arguments.

applicationContext.xml


    <bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
        ...
        <constructor-arg ref="aclAuthorizationStrategy" />
        <constructor-arg ref="permissionGrantingStrategy" />
    </bean>

This is used to set when BasicLookupStrategy reconstructs the ʻAclImpl` object from the database information.

AclAuthorizationStrategy

applicationContext.xml


    <bean id="aclAuthorizationStrategy" class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                    <constructor-arg value="TEST"/>
                </bean>
            </list>
        </constructor-arg>
    </bean>

--ʻAclAuthorizationStrategy` is responsible for checking if the current principal has that authority when changing the ACL definition. ――In other words, it is not actually used in this Hello World --Detailed explanation will be described later

PermissionGrantingStrategy

applicationContext.xml


    <bean id="permissionGrantingStrategy" class="org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy">
        <constructor-arg>
            <bean class="org.springframework.security.acls.domain.ConsoleAuditLogger" />
        </constructor-arg>
    </bean>

--PermissionGrantingStrategy provides a process to specifically determine if the current principal has the permissions specified byhasPermission ().

AuditLogger --ʻAuditLogger provides a process to log the permission judgment result. --ConsoleAuditLogger` is the only implementation class provided and outputs the result to the console. --Details will be described later

Class diagram summarizing the story so far

spring-security.png

Like this.

hasPermission()

MyAclSampleService.java


    @PreAuthorize("hasPermission(#foo, read)")
    public void logic(Foo foo) {

--Use the hasPermission () expression to determine the ACL --Specify the domain object in the first argument and the permissions in the second argument

How to get an ID from a domain object

--When a domain object is passed to hasPermission (), the ACL module looks for a method calledgetId ()in the domain object by reflection. --If there is a method, execute that method and use the returned value as the ID of the domain object. --An exception will be thrown if there is no such method or the returned value does not implement Serializable --The point is that you need to have a method called long getId () in your domain object.

Specify by ID only

MyAclSampleService.java


    @PreAuthorize("hasPermission(#id, 'sample.spring.security.domain.Foo', read)")
    public void logic(long id) {

--You can use hasPermission () to specify the ID when you haven't created an instance of the domain object yet. --The first argument is the domain object identifier --The second argument is the domain object FQCN --The third argument is the permission to verify

Specifying permissions

--The constant read is specified to specify the permission. --This is defined in SecurityExpressionRoot

SecurityExpressionRoot.java


	public final String read = "read";
	public final String write = "write";
	public final String create = "create";
	public final String delete = "delete";
	public final String admin = "administration";

Definition of permissions

Definitions of permissions (read, write, create, delete, ʻadministration) are expressed as integer values. On the database, it is stored in the MASK column of ʻACL_ENTRY.

Each permission is associated with each bit of the binary number as shown below.

5th bit 4th bit 3rd bit 2nd bit 1st bit
administration delete create write read

In other words

permission Binary representation Decimal representation
read 00001 1
write 00010 2
create 00100 4
delete 01000 8
administration 10000 16

You might wonder, "Then, if you want to have two permissions, read and write, it's 00011 ( 3 in decimal)? " To the last, one of the above five is entered in MASK of each ʻACL_ENTRY` record. When granting multiple permissions, the number of records will be registered. (Is this a mask?)

Verification

Input data


INSERT INTO ACL_CLASS (ID, CLASS)
VALUES (100, 'sample.spring.security.domain.Foo');

INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (9, true, 'hoge');
INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (98, false, 'AUTHORITY_10101');
INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (99, false, 'AUTHORITY_01010');

INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1000, 100, 44, NULL, 9, true);

-- AUTHORITY_10101
INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (10, 1000, 0, 98, 1, true, false, false); -- read(00001 = 1)
INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (11, 1000, 1, 98, 4, true, false, false); -- create(00100 = 4)
INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (12, 1000, 2, 98, 16, true, false, false); -- administration(10000 = 16)

-- AUTHORITY_01010
INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (13, 1000, 3, 99, 10, true, false, false); -- write, delete(01010 = 10) 

--Grant read (1), create (4), ʻadministration (16) permissions to ʻAUTHORITY_10101 individually --Add 01010 = 10 to ʻAUTHORITY_01010, which combines the permissions of write (2) and delete (8) `

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       ...>

    ...

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="foo" password="foo" authorities="AUTHORITY_10101" />
                <sec:user name="bar" password="bar" authorities="AUTHORITY_01010" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>

--The foo user has the authority of ʻAUTHORITY_10101, --Grant ʻAUTHORITY_01010 to the bar user

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;
import sample.spring.security.domain.Foo;

public class MyAclSampleService {
    
    @PreAuthorize("hasPermission(#foo, read)")
    public void read(Foo foo) {
        System.out.println("[read] foo=" + foo);
    }

    @PreAuthorize("hasPermission(#foo, write)")
    public void write(Foo foo) {
        System.out.println("[write] foo=" + foo);
    }

    @PreAuthorize("hasPermission(#foo, create)")
    public void create(Foo foo) {
        System.out.println("[create] foo=" + foo);
    }

    @PreAuthorize("hasPermission(#foo, delete)")
    public void delete(Foo foo) {
        System.out.println("[delete] foo=" + foo);
    }

    @PreAuthorize("hasPermission(#foo, admin)")
    public void admin(Foo foo) {
        System.out.println("[admin] foo=" + foo);
    }
}

--Specify the required permissions with hasPermission () for each method

MyAclServlet.java


package sample.spring.security.servlet;

...

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.printPrincipal();
        MyAclSampleService serviceBean = this.findServiceBean(req);
        Foo foo = new Foo(44L);
        this.callServiceLogic("read", () -> serviceBean.read(foo));
        this.callServiceLogic("write", () -> serviceBean.write(foo));
        this.callServiceLogic("create", () -> serviceBean.create(foo));
        this.callServiceLogic("delete", () -> serviceBean.delete(foo));
        this.callServiceLogic("admin", () -> serviceBean.admin(foo));
    }

    private void printPrincipal() {
        ...
    }
    
    private void callServiceLogic(String methodName, Runnable runnable) {
        try {
            System.out.println("* invoke " + methodName + "()");
            runnable.run();
        } catch (AccessDeniedException e) {
            System.out.println("AccessDeniedException : " + e.getMessage());
        }
    }
    
    private MyAclSampleService findServiceBean(HttpServletRequest req) {
        ...
    }
}

--Execute each method of MyAclSampleService and display the result on the standard output.

** Operation check **

Server output when logging in with foo


name=foo
authorities=AUTHORITY_10101

* invoke read()
[read] foo=Foo{id=44}

* invoke write()
AccessDeniedException : Access is denied

* invoke create()
[create] foo=Foo{id=44}

* invoke delete()
AccessDeniedException : Access is denied

* invoke admin()
[admin] foo=Foo{id=44}

--The method could be executed only with the permissions granted individually.

Server output when logging in with bar


name=bar
authorities=AUTHORITY_01010

* invoke read()
AccessDeniedException : Access is denied

* invoke write()
AccessDeniedException : Access is denied

* invoke create()
AccessDeniedException : Access is denied

* invoke delete()
AccessDeniedException : Access is denied

* invoke admin()
AccessDeniedException : Access is denied

--write and delete were also blocked

Why the mask is not a bit mask

The reason was described in This Issue on GitHub.

I don't know if it suits me because my English is low, but I feel like the following.

  1. When denying a parent's permission with a child's permission when using permission inheritance, it seems difficult to determine which permission is being denied with a bitmask.
  2. When searching with SQL, the bitmask makes the search difficult.

So, even though it says MASK, it actually feels like a division of integer values.

In the end, "I have prepared an extension point, so if you really want to judge with a bit mask, use that point." SEC-1166: Provide strategy interface for AclImpl isGranted() method.

ACL CRUD

Up to this point, data such as ʻACL_ENTRY` was registered by preparing an INSERT statement in SQL in advance.

However, it is difficult to edit these tables directly in actual development because you have to understand the specifications correctly.

Therefore, there is an API for maintaining these data.

Register a new instance ACL

Implementation

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.transaction.annotation.Transactional;
import sample.spring.security.domain.Foo;

@Transactional
public class MyAclSampleService {
    
    private MutableAclService aclService;

    public MyAclSampleService(MutableAclService aclService) {
        this.aclService = aclService;
    }

    public void createObjectIdentity() {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        MutableAcl acl = this.aclService.createAcl(objectIdentity);
        System.out.println("acl = " + acl);
    }
}

MyAclServlet.java


package sample.spring.security.servlet;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import sample.spring.security.service.MyAclSampleService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        MyAclSampleService service = this.findServiceBean(req, MyAclSampleService.class);
        service.createObjectIdentity();
        this.printTables(req);
    }
    
    private void printTables(HttpServletRequest req) {
        this.printTable(req, "ACL_SID");
        this.printTable(req, "ACL_CLASS");
        this.printTable(req, "ACL_OBJECT_IDENTITY");
        this.printTable(req, "ACL_ENTRY");
    }

    private void printTable(HttpServletRequest req, String table) {
        JdbcTemplate jdbcTemplate = this.findServiceBean(req, JdbcTemplate.class);
        List<Map<String, Object>> records = jdbcTemplate.queryForList("select * from " + table + " order by id asc");
        System.out.println("\n[" + table + "]");
        records.forEach(System.out::println);
    }

    private <T> T findServiceBean(HttpServletRequest req, Class<T> clazz) {
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());
        return context.getBean(clazz);
    }
}

namespace

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/security
         http://www.springframework.org/schema/security/spring-security.xsd
         http://www.springframework.org/schema/jdbc
         http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
         http://www.springframework.org/schema/tx
         http://www.springframework.org/schema/tx/spring-tx.xsd">

    <tx:annotation-driven />
    
    <jdbc:embedded-database id="dataSource" type="H2">
        <jdbc:script location="classpath:/sql/create_acl_tables.sql" />
    </jdbc:embedded-database>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource" />
    </bean>
    
    ...

    <bean id="aclService" class="org.springframework.security.acls.jdbc.JdbcMutableAclService">
        <constructor-arg ref="dataSource" />
        <constructor-arg ref="lookupStrategy" />
        <constructor-arg ref="aclCache" />
    </bean>

    ...
    
    <bean class="sample.spring.security.service.MyAclSampleService">
        <constructor-arg ref="aclService" />
    </bean>

    <bean class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg ref="dataSource" />
    </bean>

    ...
</beans>

Java Configuration

MySpringSecurityConfig.java


package sample.spring.security;

...
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import sample.spring.security.service.MyAclSampleService;

...

@EnableWebSecurity
@EnableTransactionManagement
@Import(MyGlobalMethodSecurityConfig.class)
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        ...
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
    @Bean
    public MyAclSampleService myAclSampleService(MutableAclService aclService) {
        return new MyAclSampleService(aclService);
    }
    
    ...
}

MyGlobalMethodSecurityConfig.java


package sample.spring.security;

...
import org.springframework.security.acls.jdbc.JdbcMutableAclService;
...


@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MyGlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    ...
    

    @Bean
    public AclService aclService(DataSource dataSource, LookupStrategy lookupStrategy, AclCache aclCache) {
        return new JdbcMutableAclService(dataSource, lookupStrategy, aclCache);
    }

    ...
}

Operation check

Access to run MyAclServlet.

Server console output


acl = AclImpl[
  id: 1;
  objectIdentity: org.springframework.security.acls.domain.ObjectIdentityImpl[
    Type: sample.spring.security.domain.Foo;
    Identifier: 10
  ];
  owner: PrincipalSid[
    foo
  ];
  no ACEs;
  inheriting: true;
  parent: Null;
  aclAuthorizationStrategy: org.springframework.security.acls.domain.AclAuthorizationStrategyImpl@2c7d9da4;
  permissionGrantingStrategy: org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy@634b81ac
]

[ACL_SID]
{ID=1, PRINCIPAL=true, SID=foo}

[ACL_CLASS]
{ID=1, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1, OBJECT_ID_CLASS=1, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=1, ENTRIES_INHERITING=true}

[ACL_ENTRY]

* The output of Acl is actually output in one line, but line breaks are added for easy viewing </ font>

Description

Transaction activation

--Annotation-based declarative transactions are enabled because transaction control is required when updating ACL tables.

MyAclSampleService.java


package sample.spring.security.service;

...
import org.springframework.transaction.annotation.Transactional;
...

@Transactional
public class MyAclSampleService {
...

--Annotate class with @Transactional

namespace

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans ...
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
         ...
         http://www.springframework.org/schema/tx
         http://www.springframework.org/schema/tx/spring-tx.xsd">

    <tx:annotation-driven />
    
    ...

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource" />
    </bean>

    ...

--Enable annotation-based transactions with <annotation-driven> --Bean registration of PlatformTransactionManager implementation (DataSourceTransactionManager) with the name transactionManager

Java Configuration

MySpringSecurityConfig.java


...
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
...
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

...

@EnableTransactionManagement
...
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    ...

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

--Enable transaction management with @EnableTransactionManagement --Register DataSourceTransactionManager, which is an implementation of PlatformTransactionManager, as a bean.

Definition of JdbcMutableAclService

namespace

applicationContext.xml


    <bean id="aclService" class="org.springframework.security.acls.jdbc.JdbcMutableAclService">
        <constructor-arg ref="dataSource" />
        <constructor-arg ref="lookupStrategy" />
        <constructor-arg ref="aclCache" />
    </bean>

Java Configuration

MyGlobalMethodSecurityConfig.java


import org.springframework.security.acls.jdbc.JdbcMutableAclService;

...

    @Bean
    public AclService aclService(DataSource dataSource, LookupStrategy lookupStrategy, AclCache aclCache) {
        return new JdbcMutableAclService(dataSource, lookupStrategy, aclCache);
    }

--There is a class called JdbcMutableAclService with a method added to update the ACL as a subclass of JdbcAclService. --Register an object of this class as a bean instead of JdbcAclService --You are passing ʻAclCachein the constructor argument, which is needed becauseJdbcMutableAclService` will also remove that information from the cache when you perform the process of removing the ACL

Registration of ObjectIdentity

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.transaction.annotation.Transactional;
import sample.spring.security.domain.Foo;

@Transactional
public class MyAclSampleService {
    
    private MutableAclService aclService;

    public MyAclSampleService(MutableAclService aclService) {
        this.aclService = aclService;
    }

    public void createObjectIdentity() {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        MutableAcl acl = this.aclService.createAcl(objectIdentity);
        System.out.println("acl = " + acl);
    }
}

--Create an object of ʻObjectIdentity (ʻObjectIdentityImpl) --ʻThe constructor of ObjectIdeneityImplhas the first domain object, theClass object. --The second is the domain object identifier (ʻID) --Pass the created ʻObjectIdentity to the createAcl (ObjectIdentity) method of MutableAclService --Information is saved in ʻACL_OBJECT_IDENTITY when createAcl () is executed. --ʻACL_CLASSis also created if the information does not exist --ReturnedMutableAcl`

Owner

Server console output


[ACL_SID]
{ID=1, PRINCIPAL=true, SID=foo}

[ACL_CLASS]
{ID=1, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1, OBJECT_ID_CLASS=1, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=1, ENTRIES_INHERITING=true}
{ID=2, OBJECT_ID_CLASS=1, OBJECT_ID_IDENTITY=11, PARENT_OBJECT=null, OWNER_SID=1, ENTRIES_INHERITING=true}

[ACL_ENTRY]

--If you look closely, the ʻOWNER_SID of the created ʻACL_OBJECT_IDENTITY is set to ʻACL_SID of SID = foo. --This is recorded with the login user (principal) information when createAcl ()ofMutableAclService` is executed.

Java class for ACL

ACL information is modeled in the class structure below.

ACLのクラス図.png

  • ObjectIdentity --An object that represents the identifier of a domain object --You can refer to the domain object identifier and class name.
  • Acl --The main object of ACL --Includes all permissions associated with ʻObjectIdentity`
  • AccessControlEntry --An object that represents the individual permissions assigned to Sid for ʻAcl`
  • Permission --Object with specific permission information
  • Sid --An object that represents the target to which permissions are assigned --Either Principal or GrantedAuthority

When createAcl () of MutableAclService is executed, ʻAcl, ʻAccessControlEntry, Permission, and Sid are constructed in the form linked to ʻObjectIdentity` specified by the argument.

Search for existing ACLs

Implementation

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.model.Acl;

...
public class MyAclSampleService {
    
    private MutableAclService aclService;

    ...

    public void createObjectIdentity() {
        ...
    }
    
    public void findAcl() {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        Acl acl = aclService.readAclById(objectIdentity);
        System.out.println("acl = " + acl);
    }
}

MyAclServlet.java


package sample.spring.security.servlet;

...

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        MyAclSampleService service = this.findServiceBean(req, MyAclSampleService.class);
        service.createObjectIdentity();
        this.printTables(req);

        service.findAcl();
    }
    
    ...
}

Operation check

Server console output (createObjectIdeneity)()Output is omitted)


acl = AclImpl[
  id: 1;
  objectIdentity: org.springframework.security.acls.domain.ObjectIdentityImpl[
    Type: sample.spring.security.domain.Foo;
    Identifier: 10
  ];
  owner: PrincipalSid[
    foo
  ];
  no ACEs;
  inheriting: true;
  parent: Null;
  aclAuthorizationStrategy: org.springframework.security.acls.domain.AclAuthorizationStrategyImpl@2c7d9da4;
  permissionGrantingStrategy: org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy@634b81ac
]

Description

MyAclSampleService.java


    public void findAcl() {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        Acl acl = aclService.readAclById(objectIdentity);
        System.out.println("acl = " + acl);
    }

--ʻAclService provides a method to search for ʻAcl --You can search for ʻAcl associated with ʻObjectIdentity withreadAclById (ObjectIdentity).

Add permissions

Implementation

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.AccessControlEntry;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.transaction.annotation.Transactional;
import sample.spring.security.domain.Foo;

import java.util.List;

@Transactional
public class MyAclSampleService {
    
    private MutableAclService aclService;

    ...

    public void createObjectIdentity() {
        ...
    }
    
    public void findAcl() {
        ...
    }
    
    public void addPermission() {
        //Search for the ACL to update and
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        MutableAcl acl = (MutableAcl) aclService.readAclById(objectIdentity);

        //Authority"HOGE_AUTHORITY"Grant CREATE permissions to
        List<AccessControlEntry> entries = acl.getEntries();
        GrantedAuthoritySid grantedAuthoritySid = new GrantedAuthoritySid(new SimpleGrantedAuthority("HOGE_AUTHORITY"));
        acl.insertAce(entries.size(), BasePermission.CREATE, grantedAuthoritySid, true);

        //Principal"test_user"Grant WRITE permission to
        PrincipalSid principalSid = new PrincipalSid("test_user");
        acl.insertAce(entries.size(), BasePermission.WRITE, principalSid, true);

        //Save ACL changes
        this.aclService.updateAcl(acl);
        
        System.out.println("acl = " + acl);
    }
}

MyAclServlet.java


package sample.spring.security.servlet;

...

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        MyAclSampleService service = this.findServiceBean(req, MyAclSampleService.class);
        service.createObjectIdentity();
        this.printTables(req);
        
        service.findAcl();
        
        service.addPermission();
        this.printTables(req);
    }
    
    ...
}

Operation check

Server console output


★ Before adding permissions
acl = AclImpl[
  id: 1;
  objectIdentity: org.springframework.security.acls.domain.ObjectIdentityImpl[
    Type: sample.spring.security.domain.Foo;
    Identifier: 10
  ];
  owner: PrincipalSid[
    foo
  ];
  no ACEs;
  inheriting: true;
  parent: Null;
  aclAuthorizationStrategy: org.springframework.security.acls.domain.AclAuthorizationStrategyImpl@162ff71c;
  permissionGrantingStrategy: org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy@742976d3
]

[ACL_SID]
{ID=1, PRINCIPAL=true, SID=foo}

[ACL_CLASS]
{ID=1, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1, OBJECT_ID_CLASS=1, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=1, ENTRIES_INHERITING=true}

[ACL_ENTRY]

...

★ After adding permissions
acl = AclImpl[
  id: 1;
  objectIdentity: org.springframework.security.acls.domain.ObjectIdentityImpl[
    Type: sample.spring.security.domain.Foo;
    Identifier: 10
  ];
  owner: PrincipalSid[
    foo
  ];
  AccessControlEntryImpl[
    id: null;
    granting: true;
    sid: PrincipalSid[
      test_user
    ];
    permission: BasePermission[
      ..............................W.=2
    ];
    auditSuccess: false;
    auditFailure: false
  ]
  AccessControlEntryImpl[
    id: null;
    granting: true;
    sid: GrantedAuthoritySid[
      HOGE_AUTHORITY
    ];
    permission: BasePermission[
      .............................C..=4
    ];
    auditSuccess: false;
    auditFailure: false
  ]
  inheriting: true;
  parent: Null;
  aclAuthorizationStrategy: org.springframework.security.acls.domain.AclAuthorizationStrategyImpl@162ff71c;
  permissionGrantingStrategy: org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy@742976d3
]

[ACL_SID]
{ID=1, PRINCIPAL=true, SID=foo}
{ID=2, PRINCIPAL=true, SID=test_user}
{ID=3, PRINCIPAL=false, SID=HOGE_AUTHORITY}

[ACL_CLASS]
{ID=1, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1, OBJECT_ID_CLASS=1, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=1, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=1, ACL_OBJECT_IDENTITY=1, ACE_ORDER=0, SID=2, MASK=2, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}
{ID=2, ACL_OBJECT_IDENTITY=1, ACE_ORDER=1, SID=3, MASK=4, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

Description

MyAclSampleService.java


    public void addPermission() {
        //Search for the ACL to update and
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        MutableAcl acl = (MutableAcl) aclService.readAclById(objectIdentity);

        //Authority"HOGE_AUTHORITY"Grant CREATE permissions to
        List<AccessControlEntry> entries = acl.getEntries();
        GrantedAuthoritySid grantedAuthoritySid = new GrantedAuthoritySid(new SimpleGrantedAuthority("HOGE_AUTHORITY"));
        acl.insertAce(entries.size(), BasePermission.CREATE, grantedAuthoritySid, true);

        //Principal"test_user"Grant WRITE permission to
        PrincipalSid principalSid = new PrincipalSid("test_user");
        acl.insertAce(entries.size(), BasePermission.WRITE, principalSid, true);

        //Save ACL changes
        this.aclService.updateAcl(acl);
        
        System.out.println("acl = " + acl);
    }

--Permissions can be added to ʻAcl by executing ʻinsertAce (int, Permission, Sid, boolean) of MutableAcl. --Internally, ʻAccessControlEntry is added to ʻAcl. --The first ʻintspecifies the permission insertion position by the index starting with0. --Permissions (ʻAccessControlEntry) are held internally in ʻAcl as List and are passed to the first argument of the ʻadd (int, E) method. --So, passing a value less than 0 or greater thansize ()will result in an error (if it is the same assize (), it will be added to the end). --In DB, it becomes ʻACE_ORDER of ʻACL_ENTRY --Permission can specify the constants defined in the implementation class BasePermission. --Sid specifies GrantedAuthoritySid or PrincipalSid --If you want to assign permissions to permissions, GrantedAuthoritySid --Use PrincipalSid to assign permissions to principals --The last boolean specifies whether the permission should be" granted "or" denied ". --true is granted, false is rejected

Deny permissions and ACE_ORDER

Implementation

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.acls.model.NotFoundException;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.transaction.annotation.Transactional;
import sample.spring.security.domain.Foo;

@Transactional
public class MyAclSampleService {
    
    private MutableAclService aclService;

    public MyAclSampleService(MutableAclService aclService) {
        this.aclService = aclService;
    }

    public void addPermission() {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        try {
            this.aclService.readAclById(objectIdentity);
        } catch (NotFoundException e) {
            MutableAcl acl = this.aclService.createAcl(objectIdentity);
            
            GrantedAuthoritySid deniedRead = new GrantedAuthoritySid(new SimpleGrantedAuthority("DENIED_READ"));
            acl.insertAce(0, BasePermission.READ, deniedRead, false);

            GrantedAuthoritySid permitRead = new GrantedAuthoritySid(new SimpleGrantedAuthority("PERMIT_READ"));
            acl.insertAce(1, BasePermission.READ, permitRead, true);
            
            this.aclService.updateAcl(acl);
        }
    }
    
    @PreAuthorize("hasPermission(#foo, read)")
    public void read(Foo foo) {
        System.out.println("read(" + foo + ")");
    }
}

--The first time, readAclById () cannot get the ACL and NotFoundException is thrown, so register the permission only at that time. --Register the read authority with" deny "in the DENIED_READ authority, --For the PERMIT_READ authority, the authority of read is registered by "grant". --In addition, the order is to set DENIED_READ to 0 so that it comes first. --Do nothing if ACL is already registered --Also, the read () method is annotated with @PreAuthorize, andhasPermission ()checks if you have read privileges.

package sample.spring.security.servlet;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import sample.spring.security.domain.Foo;
import sample.spring.security.service.MyAclSampleService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.printPrincipal();
        
        MyAclSampleService service = this.findServiceBean(req, MyAclSampleService.class);
        service.addPermission();

        this.printTables(req);
        
        try {
            service.read(new Foo(10L));
        } catch (AccessDeniedException e) {
            System.out.println(e.getMessage());
        }
    }
    
    ...
}

--Output the information of the current principal (printPrincipal ()) --Register ACL (ʻaddPermission () ) --Output the information registered in the table (printTables ()) --Execute the read () method and output a message if ʻAccessDeniedException is thrown.

Implementation

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    ...

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="foo" password="foo" authorities="PERMIT_READ" />
                <sec:user name="bar" password="bar" authorities="PERMIT_READ,DENIED_READ" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>

--Define two users, foo and bar --Set PERMIT_READ privileges on the foo user --Set both PERMIT_READ and DENIED_READ privileges on the bar user

Operation check

Log in as the foo user and access / acl.

Server console output


name=foo
authorities=PERMIT_READ

[ACL_SID]
{ID=1, PRINCIPAL=true, SID=foo}
{ID=2, PRINCIPAL=false, SID=DENIED_READ}
{ID=3, PRINCIPAL=false, SID=PERMIT_READ}

[ACL_CLASS]
{ID=1, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1, OBJECT_ID_CLASS=1, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=1, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=1, ACL_OBJECT_IDENTITY=1, ACE_ORDER=0, SID=2, MASK=1, GRANTING=false, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}
{ID=2, ACL_OBJECT_IDENTITY=1, ACE_ORDER=1, SID=3, MASK=1, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

read(Foo{id=10})

The read () method can be executed

Then log in as the bar user and access / acl.

Server console output


name=bar
authorities=DENIED_READ, PERMIT_READ

[ACL_SID]
{ID=1, PRINCIPAL=true, SID=foo}
{ID=2, PRINCIPAL=false, SID=DENIED_READ}
{ID=3, PRINCIPAL=false, SID=PERMIT_READ}

[ACL_CLASS]
{ID=1, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1, OBJECT_ID_CLASS=1, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=1, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=1, ACL_OBJECT_IDENTITY=1, ACE_ORDER=0, SID=2, MASK=1, GRANTING=false, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}
{ID=2, ACL_OBJECT_IDENTITY=1, ACE_ORDER=1, SID=3, MASK=1, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

Access is denied

Access was denied.

Description

MyAclSampleService.java


    MutableAcl acl = this.aclService.createAcl(objectIdentity);
    
    GrantedAuthoritySid deniedRead = new GrantedAuthoritySid(new SimpleGrantedAuthority("DENIED_READ"));
    acl.insertAce(0, BasePermission.READ, deniedRead, false);

    GrantedAuthoritySid permitRead = new GrantedAuthoritySid(new SimpleGrantedAuthority("PERMIT_READ"));
    acl.insertAce(1, BasePermission.READ, permitRead, true);
    
    this.aclService.updateAcl(acl);

applicationContext.xml


    <sec:user name="foo" password="foo" authorities="PERMIT_READ" />
    <sec:user name="bar" password="bar" authorities="PERMIT_READ,DENIED_READ" />

--ʻACE_ORDERseems to be applied with priority given to the order --Thebar user was also set to PERMIT_READ, but the permissions were determined to be "denied" because DENIED_READ preceded PERMIT_READ with ʻACE_ORDER.

Where to use it?

――When I first saw this mechanism (deny permission and ʻACE ordering), I couldn't think of much use. --If you just want to deny access, you don't have to give permission, so I didn't know when to give "deny" permission. ――The only thing I came up with was how to define the "completely deny" permission like ↑ --If you add the "grant" permission to the end and the "deny" permission to the beginning, the "deny" permission will always take precedence. ――And if you associate the permission of "deny" with the authority (GrantedAuthority`), it will behave like" basically you will not be able to access if you are given that authority ". ――I don't know if there is such a situation, but when you "want to basically deny access", I feel that you can control things like "you can disable access by setting this authority".

Permission checks performed on ACL updates

Not everyone can update the ACL.

Implementation

src/main/resources/sql/insert_acl_tables.sql


INSERT INTO ACL_CLASS (ID, CLASS)
VALUES (100, 'sample.spring.security.domain.Foo');

INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (9, true, 'hoge');
INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (19, true, 'admin');

INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1000, 100, 10, NULL, 9, true);

INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (10000, 1000, 0, 19, 16, true, false, false); --admin user admin(16)Grant permissions

--Create ʻACL_OBJECT_IDENTITY for Foo object with ʻID = 10 --Set the hoge principal as the owner --Grant ʻadministration permissions to ʻadmin principals

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       ...>

    ...
    
    <jdbc:embedded-database id="dataSource" type="H2">
        <jdbc:script location="classpath:/sql/create_acl_tables.sql" />
        <jdbc:script location="classpath:/sql/insert_acl_tables.sql" />
    </jdbc:embedded-database>

    ...

    <bean id="aclAuthorizationStrategy" class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                    <constructor-arg value="PERMISSION_MANAGER"/>
                </bean>
            </list>
        </constructor-arg>
    </bean>

    ...

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="hoge" password="hoge" authorities="" />
                <sec:user name="fuga" password="fuga" authorities="" />
                <sec:user name="piyo" password="piyo" authorities="PERMISSION_MANAGER" />
                <sec:user name="admin" password="admin" authorities="" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>

--ʻAclAuthorizationStrategyImplis passingPERMISSION_MANAGER as a constructor argument --hoge, fuga, ʻadmin User has no privileges --Grant PERMISSION_MANAGER privileges only to piyo users

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.AlreadyExistsException;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.transaction.annotation.Transactional;
import sample.spring.security.domain.Foo;

@Transactional
public class MyAclSampleService {
    
    private MutableAclService aclService;

    public MyAclSampleService(MutableAclService aclService) {
        this.aclService = aclService;
    }
    
    public void addPermission() {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        MutableAcl acl = (MutableAcl) this.aclService.readAclById(objectIdentity);
        
        acl.insertAce(
            acl.getEntries().size(),
            BasePermission.READ,
            new GrantedAuthoritySid(new SimpleGrantedAuthority("test")),
            true
        );
        
        this.aclService.updateAcl(acl);
    }
}

--Searching for a Foo object with ʻid = 10 and adding permissions with ʻinsertAce ()

MyAclServlet.java


package sample.spring.security.servlet;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.acls.model.NotFoundException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import sample.spring.security.service.MyAclSampleService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        MyAclSampleService service = this.findServiceBean(req, MyAclSampleService.class);

        this.printPrincipal();
        this.printTables(req);
        
        try {
            System.out.println("service.addPermission()");
            service.addPermission();
            this.printTables(req);
        } catch (AccessDeniedException | NotFoundException e) {
            System.out.println("e.class = " + e.getClass() + ", message = " + e.getMessage());
        }
    }
    
    ...
}

--Outputs the logged-in user information and database status before processing --After executing the ʻaddPermission ()method ofMyAclSampleService, output the database status again. --If an exception occurs in ʻaddPermission (), that information is output.

Operation check

** When accessed as a hoge user **

Server console output


★ User information
name=hoge
authorities=

★ State of the table before update
[ACL_SID]
{ID=9, PRINCIPAL=true, SID=hoge}
{ID=19, PRINCIPAL=true, SID=admin}

[ACL_CLASS]
{ID=100, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1000, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=9, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=10000, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=0, SID=19, MASK=16, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

★ Method execution
service.addPermission()

★ State of the table after update
[ACL_SID]
{ID=9, PRINCIPAL=true, SID=hoge}
{ID=19, PRINCIPAL=true, SID=admin}
{ID=20, PRINCIPAL=false, SID=test}

[ACL_CLASS]
{ID=100, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1000, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=9, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=10001, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=0, SID=19, MASK=16, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}
{ID=10002, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=1, SID=20, MASK=1, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

--Permissions have been added successfully

** When accessed as a fuga user **

Server console output


★ User information
name=fuga
authorities=

★ State of the table before update
[ACL_SID]
{ID=9, PRINCIPAL=true, SID=hoge}
{ID=19, PRINCIPAL=true, SID=admin}

[ACL_CLASS]
{ID=100, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1000, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=9, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=10000, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=0, SID=19, MASK=16, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

★ Method execution
service.addPermission()

★ Error information
e.class = class org.springframework.security.acls.model.NotFoundException, message = Unable to locate a matching ACE for passed permissions and SIDs

--Permissions could not be added and an exception (NotFoundException) was thrown

** When accessed as a piyo user **

Server console output


★ User information
name=piyo
authorities=PERMISSION_MANAGER

★ State of the table before update
[ACL_SID]
{ID=9, PRINCIPAL=true, SID=hoge}
{ID=19, PRINCIPAL=true, SID=admin}

[ACL_CLASS]
{ID=100, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1000, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=9, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=10000, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=0, SID=19, MASK=16, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

★ Method execution
service.addPermission()

★ State of the table after update
[ACL_SID]
{ID=9, PRINCIPAL=true, SID=hoge}
{ID=19, PRINCIPAL=true, SID=admin}
{ID=20, PRINCIPAL=false, SID=test}

[ACL_CLASS]
{ID=100, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1000, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=9, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=10001, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=0, SID=19, MASK=16, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}
{ID=10002, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=1, SID=20, MASK=1, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

--Permission update is successful

** When accessed as an admin user **

Server console output


★ User information
name=admin
authorities=

★ State of the table before update
[ACL_SID]
{ID=9, PRINCIPAL=true, SID=hoge}
{ID=19, PRINCIPAL=true, SID=admin}

[ACL_CLASS]
{ID=100, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1000, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=9, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=10000, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=0, SID=19, MASK=16, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

★ Method execution
service.addPermission()

★ State of the table after update
[ACL_SID]
{ID=9, PRINCIPAL=true, SID=hoge}
{ID=19, PRINCIPAL=true, SID=admin}
{ID=20, PRINCIPAL=false, SID=test}

[ACL_CLASS]
{ID=100, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1000, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=10, PARENT_OBJECT=null, OWNER_SID=9, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=10001, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=0, SID=19, MASK=16, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}
{ID=10002, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=1, SID=20, MASK=1, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

--Permission update is successful

Description

--The following factors are involved in updating permissions: --Whether you are the owner of the ACL you are trying to update --Whether the ACL you are trying to update has ʻadministration permissions. ――Whether you have the authority specified in the constructor argument of ʻAclAuthorizationStrategyImpl --ACL update type (general, owned, audited) --And it is judged whether it can be updated in the following order

  1. For the owner of the ACL that the current principal is trying to update, Allow </ font> if the ACL update type is "General" or "Owned"
  2. Otherwise, allow </ font> if you have the permissions specified in ʻAclAuthorizationStrategyImpl`
  3. Otherwise, allow </ font> if the ACL you are trying to update has ʻadministration` permissions.
  4. Otherwise not allowed </ font>

Update type

There are three types of ACL updates:

--General --Owned --Audit

And the methods to update the ACL are classified as follows.

Method Update type
insertAce() General
updateAce() General
deleteAce() General
setParent() General
setEntriesInheriting() General
setOwner() Owned
updateAuditing() audit

By the way, these three types are defined in the ʻAclAuthorizationStrategy` interface.

AclAuthorizationStrategy.java


public interface AclAuthorizationStrategy {

	int CHANGE_OWNERSHIP = 0;Owned
	int CHANGE_AUDITING = 1;audit
	int CHANGE_GENERAL = 2;General

Privileges specified in AclAuthorizationStrategyImpl

applicationContext.xml


    <bean id="aclAuthorizationStrategy" class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                    <constructor-arg value="PERMISSION_MANAGER"/>
                </bean>
            </list>
        </constructor-arg>
    </bean>

--ʻGrantedAuthority passed in the constructor of AclAuthorizationStrategyImpl is used when updating the ACL. --If the logged-in user who is trying to update has the authority specified here, the update is permitted.

Specify permissions for each update type

In the above example, only the authority PERMISSION_MANAGER is specified. In this case, all types of updates (general, owned, audited) are allowed if you have this permission.

If you want to control "●● is required for" general "update, ▲▲ is required for" owned "update, and ★★ is required for" audit "", as follows. Define a bean in.

applicationContext.xml


    <bean id="aclAuthorizationStrategy" class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
        <constructor-arg>
            <list>
                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                    <constructor-arg value="PERMISSION_MANAGER_OWNERSHIP"/>
                </bean>
                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                    <constructor-arg value="PERMISSION_MANAGER_AUDIT"/>
                </bean>
                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                    <constructor-arg value="PERMISSION_MANAGER_GENERAL"/>
                </bean>
            </list>
        </constructor-arg>
    </bean>

--Set the number of GrantedAuthoritys passed to the constructor argument to 3 (note that 2 or 4 will result in an error) --As shown below, the position of the specified GrantedAuthority determines which update type the authority is associated with.

  1. Specify the permissions required for "owned" updates
  2. Specify the permissions required for updates related to "Audit"
  3. Specify the permissions required for "general" updates

About the behavior when the update is denied

In the example above, a NotFoundException was thrown if the update was denied.

It's not true that NotFoundException is always thrown when disallowed. If the ACL to be updated has the permissions of ʻadministration set to" Deny ", ʻAccessDeniedException is thrown.

This is actually a ** bug **, and in both cases it is correct to throw ʻAccessDeniedException`.

A search for issues on GitHub reveals this bug has been raised. However, as of July 09, 2017, this issue remains OPEN and does not seem to be addressed. It seems that this issue was recognized in 2009, so it has been completely neglected (not limited to this, all ACL-related issues have been neglected for a long time and may not be much maintained. unknown).

In addition to this, the permission check for ACL update does not refer to the parent role when using role hierarchies [https://github.com/spring-projects/spring-security/issues Bugs such as / 4186) have been raised (also left unattended for a long time).

If you want to use this area in the correct operation, you need to copy the existing ʻAclAuthorizationStrategyImpl, create your own implementation class that fixes the problem, and use it instead of ʻAclAuthorizationStrategyImpl. (Or do you pull request the modified version to the head family)

Give permissions an inheritance relationship

ʻACL_OBJECT_IDENTITY` can have an inheritance relationship.

Hello World

Implementation

insert_acl_tables.sql


INSERT INTO ACL_CLASS (ID, CLASS)
VALUES (100, 'sample.spring.security.domain.Foo');

INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (9, true, 'hoge');
INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (98, false, 'READONLY');

INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1000, 100, 44, NULL, 9, false);
INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1001, 100, 45, NULL, 9, false);
INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1002, 100, 46, 1000, 9, false);
INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1003, 100, 47, 1000, 9, true);

INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (10, 1000, 0, 98, 1, true, false, false);

--Defining ʻACL_OBJECT_IDENTITY of ʻid = 44 ... 47 --Only ʻid = 44is grantedread permission to the READONLY privilege. --ʻId = 45 does not specify any parent object etc. --ʻId = 46specifies only the ID of the parent object withPARENT_OBJECT (ʻENTRIES_INHERITING is false) --ʻId = 47 sets ʻENTRIES_INHERITING to true after specifying the parent object.

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import sample.spring.security.domain.Foo;

@Transactional
public class MyAclSampleService {

    @PreAuthorize("hasPermission(#foo, read)")
    public void read(Foo foo) {
        System.out.println("read(" + foo + ")");
    }
}

--Check that you have read permissions on the Foo object you receive as an argument

MyAclServlet.java


package sample.spring.security.servlet;

...

import sample.spring.security.domain.Foo;
import sample.spring.security.service.MyAclSampleService;

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.printPrincipal();
        this.printTables(req);
        
        this.callServiceLogic(req, 44L);
        this.callServiceLogic(req, 45L);
        this.callServiceLogic(req, 46L);
        this.callServiceLogic(req, 47L);
    }

    private void callServiceLogic(HttpServletRequest req, long id) {
        try {
            System.out.println("id=" + id);
            MyAclSampleService service = this.findServiceBean(req);
            Foo foo = new Foo(id);
            service.read(foo);
        } catch (AccessDeniedException e) {
            System.out.println("AccessDeniedException : " + e.getMessage());
        }
    }
    
    ...
}

--Create a Foo object with ʻid = 44 ... 47 and execute the read () method of MyAclSampleService`

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    ...

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="foo" password="foo" authorities="READONLY" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>

--Grant READONLY privileges to the foo user

Operation check

Log in as the foo user and access / acl.

Server console output


name=foo
authorities=READONLY

[ACL_SID]
{ID=9, PRINCIPAL=true, SID=hoge}
{ID=98, PRINCIPAL=false, SID=READONLY}

[ACL_CLASS]
{ID=100, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1000, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=44, PARENT_OBJECT=null, OWNER_SID=9, ENTRIES_INHERITING=false}
{ID=1001, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=45, PARENT_OBJECT=null, OWNER_SID=9, ENTRIES_INHERITING=false}
{ID=1002, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=46, PARENT_OBJECT=1000, OWNER_SID=9, ENTRIES_INHERITING=false}
{ID=1003, OBJECT_ID_CLASS=100, OBJECT_ID_IDENTITY=47, PARENT_OBJECT=1000, OWNER_SID=9, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=10, ACL_OBJECT_IDENTITY=1000, ACE_ORDER=0, SID=98, MASK=1, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

id=44
read(Foo{id=44})
id=45
AccessDeniedException : Access is denied
id=46
AccessDeniedException : Access is denied
id=47
read(Foo{id=47})

--Only ʻid = 44, 47 can execute the method, ʻid = 45, 46 is rejected

Description

insert_acl_tables.sql


...

INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1000, 100, 44, NULL, 9, false);
INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1001, 100, 45, NULL, 9, false);
INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1002, 100, 46, 1000, 9, false);
INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (1003, 100, 47, 1000, 9, true);

--To give permissions inheritance, add the following two settings to ʻACL_OBJECT_IDENTITY --SetPARENT_OBJECT to ʻID of the parent ʻACL_OBJECT_IDENTITY --Settrue to ʻENTRIES_INHERITING --This will allow the permission to be determined retroactively to the parent when the permission is checked for the child's ʻACL_OBJECT_IDENTITY`.

Use as default permission

--By using permission inheritance, you can realize things like "default permissions" and manage definition information in one place. -** It is a mystery whether it is correct for usage **, but ʻOBJECT_IDENTITY of ʻACL_OBJECT_IDENTITY can be registered even if it is not an existing ʻid. --If you register ʻACL_OBJECT_IDENTITY with an impossible value such as ʻOBJECT_IDENTITY = -1 and make other normal ʻACL_OBJECT_IDENTITY inherit this default ʻACL_OBJECT_IDENTITY, the default permissions will be realizable --However, if inheritance is layered in multiple layers, I feel that it will be difficult to identify "all permissions set in that ʻACL_OBJECT_IDENTITY" using SQL alone (the maximum value of the layer is fixed). If so, is it okay if you do LEFT JOIN for that number?) ――I think it's safer to keep the hierarchy as small as possible.

Programmatically set parents

Implementation

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.transaction.annotation.Transactional;
import sample.spring.security.domain.Foo;

@Transactional
public class MyAclSampleService {
    
    private MutableAclService aclService;

    public MyAclSampleService(MutableAclService aclService) {
        this.aclService = aclService;
    }

    public void init() {
        ObjectIdentityImpl parentId = new ObjectIdentityImpl(Foo.class, 44L);
        MutableAcl parentAcl = this.aclService.createAcl(parentId);
        parentAcl.insertAce(
            parentAcl.getEntries().size(),
            BasePermission.READ,
            new GrantedAuthoritySid(new SimpleGrantedAuthority("READONLY")),
            true
        );
        this.aclService.updateAcl(parentAcl);

        ObjectIdentityImpl childId = new ObjectIdentityImpl(Foo.class, 45L);
        MutableAcl childAcl = this.aclService.createAcl(childId);
        childAcl.setParent(parentAcl);
        this.aclService.updateAcl(childAcl);
    }
}

python


package sample.spring.security.servlet;

...

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        MyAclSampleService service = this.findServiceBean(req);
        service.init();

        this.printTables(req);
    }
    
    ...
}

Operation check

Access / acl

Server log output


[ACL_SID]
{ID=1, PRINCIPAL=true, SID=foo}
{ID=2, PRINCIPAL=false, SID=READONLY}

[ACL_CLASS]
{ID=1, CLASS=sample.spring.security.domain.Foo}

[ACL_OBJECT_IDENTITY]
{ID=1, OBJECT_ID_CLASS=1, OBJECT_ID_IDENTITY=44, PARENT_OBJECT=null, OWNER_SID=1, ENTRIES_INHERITING=true}
{ID=2, OBJECT_ID_CLASS=1, OBJECT_ID_IDENTITY=45, PARENT_OBJECT=1, OWNER_SID=1, ENTRIES_INHERITING=true}

[ACL_ENTRY]
{ID=1, ACL_OBJECT_IDENTITY=1, ACE_ORDER=0, SID=2, MASK=1, GRANTING=true, AUDIT_SUCCESS=false, AUDIT_FAILURE=false}

Description

MyAclSampleService.java


    public void init() {
        ObjectIdentityImpl parentId = new ObjectIdentityImpl(Foo.class, 44L);
        MutableAcl parentAcl = this.aclService.createAcl(parentId);
        parentAcl.insertAce(
            parentAcl.getEntries().size(),
            BasePermission.READ,
            new GrantedAuthoritySid(new SimpleGrantedAuthority("READONLY")),
            true
        );
        this.aclService.updateAcl(parentAcl);

        ObjectIdentityImpl childId = new ObjectIdentityImpl(Foo.class, 45L);
        MutableAcl childAcl = this.aclService.createAcl(childId);
        childAcl.setParent(parentAcl); //★ Set parents here
        this.aclService.updateAcl(childAcl);
    }

--To set a parent ACL on an ACL, use the setParent () method of MutableAcl. --ʻENTRIES_INHERITINGdoes not need to be specified if you create a new ACL because the default istrue when you create a new ACL (it can be set by the setEntriesInheriting () `method)

Audit log

There is a mechanism to output the information as an audit log when the permission is granted or denied.

Hello World

Implementation

insert_acl_tables.sql


INSERT INTO ACL_CLASS (ID, CLASS)
VALUES (100, 'sample.spring.security.domain.Foo');

INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (9, true, 'hoge');
INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (98, false, 'READONLY');

INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (999, 100, 44, NULL, 9, false);

INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (10, 999, 0, 98, 1, true, true, false);
INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (11, 999, 1, 98, 2, false, false, true);

--For Foo ʻid = 44, set the permissions of" grant read" and "deny write "for the READONLY privilege. --ʻACL_ENTRY in "Grant read" changes ʻAUDIT_SUCCESS to true --ʻACL_ENTRY in "Reject write" sets ʻAUDIT_FAILURE to true`

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;
import sample.spring.security.domain.Foo;

public class MyAclSampleService {

    @PreAuthorize("hasPermission(#foo, read)")
    public void read(Foo foo) {
        System.out.println(foo);
    }
    
    @PreAuthorize("hasPermission(#foo, write)")
    public void write(Foo foo) {
        System.out.println(foo);
    }
}

MyAclServlet.java


package sample.spring.security.servlet;

...

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        MyAclSampleService service = this.findServiceBean(req);
        Foo foo = new Foo(44L);

        try {
            System.out.println("service.read()");
            service.read(foo);
            System.out.println("service.write()");
            service.write(foo);
        } catch (AccessDeniedException e) {
            System.out.println(e.getMessage());
        }
    }
    
    ...
}

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    ...

    <bean id="permissionGrantingStrategy" class="org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy">
        <constructor-arg>
            <bean class="org.springframework.security.acls.domain.ConsoleAuditLogger" />
        </constructor-arg>
    </bean>

    ...

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="foo" password="foo" authorities="READONLY" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>

Operation check

Log in as the foo user and access / acl

Server console output


service.read()
GRANTED due to ACE: AccessControlEntryImpl[id: 10; granting: true; sid: GrantedAuthoritySid[READONLY]; permission: BasePermission[...............................R=1]; auditSuccess: true; auditFailure: false]
Foo{id=44}

service.write()
DENIED due to ACE: AccessControlEntryImpl[id: 11; granting: false; sid: GrantedAuthoritySid[READONLY]; permission: BasePermission[..............................W.=2]; auditSuccess: false; auditFailure: true]
Access is denied

--The console prints whether permissions have been granted or denied before each method is executed.

Description

insert_acl_tables.sql


INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (10, 999, 0, 98, 1, true, true, false);
INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (11, 999, 1, 98, 2, false, false, true);

--To enable audit log output, set ʻAUDIT_SUCCESS or ʻAUDIT_FAILURE in ʻACL_ENTRY to true. --If ʻAUDIT_SUCCESS is true, a log is output when permission grant is determined. --If ʻAUDIT_FAILUREistrue`, a log is output when permission denial is determined.

applicationContext.xml


    <bean id="permissionGrantingStrategy" class="org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy">
        <constructor-arg>
            <bean class="org.springframework.security.acls.domain.ConsoleAuditLogger" />
        </constructor-arg>
    </bean>

--Audit log output is done by a class that implements the ʻAuditLogger interface. --ʻAuditLogger is set to DefaultPermissionGrantingStrategy --ʻThe ConsoleAuditLogger class, which is provided as standard as an implementation of AuditLogger`, outputs the log to the standard output.

Implementation of default audit log output

The implementation of ConsoleAuditLogger looks like this:

ConsoleAuditLogger.java


package org.springframework.security.acls.domain;

import org.springframework.security.acls.model.AccessControlEntry;
import org.springframework.security.acls.model.AuditableAccessControlEntry;

import org.springframework.util.Assert;

public class ConsoleAuditLogger implements AuditLogger {

    public void logIfNeeded(boolean granted, AccessControlEntry ace) {
        Assert.notNull(ace, "AccessControlEntry required");

        if (ace instanceof AuditableAccessControlEntry) {
            AuditableAccessControlEntry auditableAce = (AuditableAccessControlEntry) ace;

            if (granted && auditableAce.isAuditSuccess()) {
                System.out.println("GRANTED due to ACE: " + ace);
            }
            else if (!granted && auditableAce.isAuditFailure()) {
                System.out.println("DENIED due to ACE: " + ace);
            }
        }
    }
}

--ʻThe AuditLoggerinterface defines thelogIfNeeded (boolean, AccessControlEntry) method. --The first argument is passed a flag whether permissions have been granted or denied. --The second argument is the ʻAccessControlEntry object that holds the permission information. --To get the setting (ʻAUDIT_SUCCESS, ʻAUDIT_FAILURE) of whether to output the audit log, use the method of ʻisAuditSuccess () or ʻisAuditFailure () of ʻAuditableAccessControlEntry(Argument ʻAccessControlEntry". You need to cast) --Actually, I think that it is necessary to output to a log file, so I feel that I will implement my own ʻAuditLogger` class by referring to this implementation.

Set from the program

import org.springframework.security.acls.model.AuditableAcl;

...

    ObjectIdentityImpl objectIdentity = new ObjectIdentityImpl(Foo.class, 44L);
    AuditableAcl acl = (AuditableAcl) this.aclService.readAclById(objectIdentity);
    acl.updateAuditing(0, true, false);
    this.aclService.updateAcl(acl);

--To change ʻAUDIT_SUCCESS and ʻAUDIT_FAILURE programmatically, use the ʻupdateAuditing (int, boolean, boolean) method of ʻAuditableAcl. --The first argument is the index to specify ʻAccessControlEntry that you want to change. --The value to be set in ʻAUDIT_SUCCESS as the second argument --The value to be set in ʻAUDIT_FAILURE` as the third argument

Define your own permissions

There are only five permissions available by default: read, write, create, delete, and ʻadministration`, but you can define additional permissions of your own.

Try making 32 ( 100000 in binary notation).

Implementation

MyPermission.java


package sample.spring.security.acl;

import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.model.Permission;

public class MyPermission extends BasePermission {
    public static final Permission HOGE = new MyPermission(0b100000, 'H');

    private MyPermission(int mask, char code) {
        super(mask, code);
    }
}

--Create your own Permission class by inheriting BasePermission --Create a constant called HOGE, set the mask to 0b100000 ( 32 in decimal), and set the code to 'H' to set the instance.

insert_acl_tables.sql


INSERT INTO ACL_CLASS (ID, CLASS)
VALUES (100, 'sample.spring.security.domain.Foo');

INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (9, true, 'hoge');
INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (98, false, 'READONLY');
INSERT INTO ACL_SID (ID, PRINCIPAL, SID)
VALUES (99, false, 'HOGE');

INSERT INTO ACL_OBJECT_IDENTITY (ID, OBJECT_ID_CLASS, OBJECT_ID_IDENTITY, PARENT_OBJECT, OWNER_SID, ENTRIES_INHERITING)
VALUES (999, 100, 44, NULL, 9, false);

INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (10, 999, 0, 98, 1, true, false, false);
INSERT INTO ACL_ENTRY (ID, ACL_OBJECT_IDENTITY, ACE_ORDER, SID, MASK, GRANTING, AUDIT_SUCCESS, AUDIT_FAILURE)
VALUES (11, 999, 1, 99, 32, true, false, false);

--Grant read permissions to READONLY privileges --Grant 32 (HOGE defined in MyPermission) permission to HOGE authority

MyAclSampleService.java


package sample.spring.security.service;

import org.springframework.security.access.prepost.PreAuthorize;
import sample.spring.security.domain.Foo;

public class MyAclSampleService {

    @PreAuthorize("hasPermission(#foo, read)")
    public void read(Foo foo) {
        System.out.println(foo);
    }
    
    @PreAuthorize("hasPermission(#foo, 'hoge')")
    public void hoge(Foo foo) {
        System.out.println(foo);
    }
}

--Check permissions with read and hoge respectively

MyAclServlet.java


package sample.spring.security.servlet;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import sample.spring.security.domain.Foo;
import sample.spring.security.service.MyAclSampleService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@WebServlet("/acl")
public class MyAclServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        MyAclSampleService service = this.findServiceBean(req);
        Foo foo = new Foo(44L);

        this.callMethod("read", () -> service.read(foo));
        this.callMethod("hoge", () -> service.hoge(foo));
    }
        
    private void callMethod(String method, Runnable runnable) {
        try {
            System.out.println(method);
            runnable.run();
        } catch (AccessDeniedException e) {
            System.out.println(e.getMessage());
        }
    }
    
    ...
}

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    ...
    
    <bean id="permissionFactory" class="org.springframework.security.acls.domain.DefaultPermissionFactory">
        <constructor-arg value="sample.spring.security.acl.MyPermission" />
    </bean>
    
    <bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
        <property name="permissionEvaluator">
            <bean class="org.springframework.security.acls.AclPermissionEvaluator">
                <constructor-arg ref="aclService" />
                <property name="permissionFactory" ref="permissionFactory" /> ★
            </bean>
        </property>
    </bean>

    <bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
        <constructor-arg ref="dataSource" />
        <constructor-arg ref="aclCache" />
        <constructor-arg ref="aclAuthorizationStrategy" />
        <constructor-arg ref="permissionGrantingStrategy" />
        <property name="permissionFactory" ref="permissionFactory" /> ★
    </bean>
    
    ...

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="foo" password="foo" authorities="READONLY" />
                <sec:user name="bar" password="bar" authorities="HOGE" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>

--Define DefaultPermissionFactory as a bean --Specify the Class object of your ownPermission (MyPermission) in the constructor argument. --Set the Bean of DefaultPermissionFactory to ʻAclPermissionEvaluator and BasicLookupStrategy` as properties respectively.

Operation check

Log in as the foo user and access / acl

Console output


read
Foo{id=44}
hoge
Access is denied

Log in as the bar user and access / acl

Console output


read
Access is denied
hoge
Foo{id=44}

Description

Definition of own permission class

MyPermission.java


package sample.spring.security.acl;

import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.model.Permission;

public class MyPermission extends BasePermission {
    public static final Permission HOGE = new MyPermission(0b100000, 'H');

    private MyPermission(int mask, char code) {
        super(mask, code);
    }
}

--If you want to define your own permissions, create a class that inherits BasePermission and define the permissions you want to add with constants. --The first argument of the constructor is the bit value corresponding to the permission, and the second argument specifies the one-letter representation of the permission. --The constant name is important, and the constant name specified here becomes the name of the permission specified in the second argument of the hasPermission () expression.

Replace existing permission class

applicationContext.xml


    <bean id="permissionFactory" class="org.springframework.security.acls.domain.DefaultPermissionFactory">
        <constructor-arg value="sample.spring.security.acl.MyPermission" />
    </bean>
    
    <bean id="expressionHandler" class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
        <property name="permissionEvaluator">
            <bean class="org.springframework.security.acls.AclPermissionEvaluator">
                <constructor-arg ref="aclService" />
                <property name="permissionFactory" ref="permissionFactory" />
            </bean>
        </property>
    </bean>

    <bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
        <constructor-arg ref="dataSource" />
        <constructor-arg ref="aclCache" />
        <constructor-arg ref="aclAuthorizationStrategy" />
        <constructor-arg ref="permissionGrantingStrategy" />
        <property name="permissionFactory" ref="permissionFactory" />
    </bean>

--If you do nothing, the permission class will use BasePermission --You need to replace this with your own permission class MyPermission that you created earlier. --The permission class is directly used by DefaultPermissionFactory, which is an implementation class of the PermissionFactory interface. --Specify the Class object of the permission class you want to use in the constructor argument of this class --The constants defined in the permission class inside DefaultPermissionFactory are fetched by reflection. --Since PermissionFactory is used in the ʻAclPermissionEvaluator and BasicLookupStrategyclasses, replace theirpermissionFactory` properties.

Use in formula

MyAclSampleService.java


    @PreAuthorize("hasPermission(#foo, 'hoge')")
    public void hoge(Foo foo) {
        System.out.println(foo);
    }

--With the settings up to this point, you will be able to use your own permissions. --However, unlike other existing permissions, unique permissions do not have constants defined in SecurityExpressionRoot. --So, you need to enclose it in single quotes (') and specify it as a character string. --If the constant name defined in the permission class is all uppercase, there is no real case sensitivity and there is no problem. ――What this means is that, in reality, you first check in the same form (hoge), and if not, capitalize all letters (HOGE) and check.

reference