Help us understand the problem. What is going on with this article?

Spring Security 使い方メモ ドメインオブジェクトセキュリティ(ACL)

More than 1 year has passed since last update.

基礎・仕組み的な話
認証・認可の話
Remember-Me の話
CSRF の話
セッション管理の話
レスポンスヘッダーの話
メソッドセキュリティの話
CORS の話
Run-As の話
テストの話
MVC, Boot との連携の話

番外編
Spring Security にできること・できないこと

ドメインオブジェクトセキュリティとは

hasAuthority() などを使った認可処理は、基本的に機能くらいの単位の制御になる。
(ある機能(画面)は ○○ という権限を持っている必要がある、とか)

しかし、実際にシステムを作っているとデータ単位で権限の管理がしたくなることがまれによくある。
例えば、データを作成した人と同じ所属の人しかそのデータを見れないようにする、とか、データの更新は作成者かシステム管理者しかできないようにする、とか。

Spring Security には、このようなデータ単位でのアクセス制御を実現する仕組みが用意されている。

それがドメインオブジェクトセキュリティ、もしくは ACL (Access Control List) と呼ばれている。

Hello World

実装

依存関係

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' ★追加
    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);

確認用の共通実装

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 + '}';
    }
}
  • 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);
    }
}
  • 引数で受け取った foo を標準出力する
  • @PreAuthorize でメソッドをアノテートし、 hasPermission() 関数を使ってドメインオブジェクトへのアクセス権限をチェックしている
  • ここでは引数 foo に対して read の権限があるかどうかをチェックしている
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);
    }
}
  • 現在のプリンシパルの情報を出力したあと、 id=44id=45Foo のインスタンスを生成し、 MyAclSampleServicelogic() メソッドを実行している
  • AccessDeniedException がスローされた場合は、そのことをコンソールに出力している。

設定

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());
    }
}

動作確認

foo ユーザでログインして /acl にアクセスする。

サーバーコンソール出力
name=foo
authorities=
AccessDeniedException : Access is denied
AccessDeniedException : Access is denied

id=44, id=45 どちらもアクセスが拒否された。

続いて bar ユーザでログインして /acl にアクセスする。

サーバーコンソール出力
name=bar
authorities=SAMPLE_AUTHORITY
foo=Foo{id=44}
AccessDeniedException : Access is denied

id=44logic() メソッドの実行に成功し、 id=45 はアクセスが拒否された。

仕組み

テーブル

登場するのは次の4つ。

  • ACL_SID
  • ACL_CLASS
  • ACL_OBJECT_IDENTITY
  • ACL_ENTRY

テーブル構造は下のような感じ。
※クラス図を利用した ER 図もどきです。雰囲気で読んでください。

ACLテーブル構造.png

矢印は FK を表していて、矢印の元が参照元テーブル、矢印の先が参照先テーブル。

それぞれのテーブルとカラムの意味は以下のような感じ。
ID の説明は省略

ACL_CLASS

  • ドメインオブジェクトの Java クラス名(FQCN)を記録するテーブル
カラム 意味
CLASS ドメインオブジェクトの Java クラス名(FQCN)

ACL_SID

  • パーミッションを付与する対象を定義するテーブル
  • 権限(GrantedAuthority)かプリンシパルを登録する
  • SID は Security Identity の略
カラム 意味
PRINCIPAL このレコードがプリンシパルかどうかを区別するフラグ
true → プリンシパル
falseGrantedAuthority
SID SID を表現する文字列。
プリンシパルなら usernameGrantedAuthority ならその文字列表現が設定される

ACL_OBJECT_IDENTITY

  • ドメインオブジェクトの各インスタンスを保持するテーブル
カラム 意味
OBJECT_ID_CLASS ドメインオブジェクトのクラスを指すカラム。
ACL_CLASS に外部参照している。
OBJECT_ID_IDENTITY インスタンスを識別するID
PARENT_OBJECT 親の ACL_OBJECT_IDENTITY の ID
OWNER_SID このインスタンスを生成した ACL_SID の ID
ENTRIES_INHERITING このインスタンスが他のインスタンスと権限の継承関係を持つかどうかのフラグ

ACL_ENTRY

  • ACL_OBJECT_IDENTITY ごとに ACL_SID に割り当てるパーミッションを定義したテーブル
カラム 意味
ACL_OBJECT_IDENTITY このパーミッション定義を適用する ACL_OBJECT_IDENTITY の ID
ACE_ORDER 1つの ACL_OBJECT_IDENTITY に複数の ACL_ENTRTY が紐づく場合の順序
SID このパーミッション定義を適用する ACL_SID の ID
MASK パーミッション定義を表す整数値(詳細後述)
GRANTING 「付与」か「拒否」かのフラグ
AUDIT_SUCCESS このパーミッション定義が付与されたときに監査ログを出力するかのフラグ
AUDIT_FAILUER このパーミッション定義が拒否されたときに監査ログを出力するかのフラグ

Hello World のデータ

Hello World で使用したデータは、↓のような構造になっていた。

ACL_HelloWorld_データ.png

  • ACL_SID テーブルに2つのレコードを登録している
    • 1つは hoge という名前のユーザ(プリンシパル)で、もう1つは SAMPLE_AUTHORITY という名前の権限を表している
  • ACL_OBJECT_IDENTITY には id=44Foo オブジェクトを表すレコードを登録している
    • hoge をオーナーとして設定している
    • オーナーとは、そのレコードを作成したプリンシパルを指している
  • id=44Foo オブジェクトに設定するパーミッションを定義しているのが、 ACL_ENTRY になる
    • MASK がパーミッションの定義になっている
      • 詳細は後述するが、 1 は「read(読み取り)」を表している
    • GRANTING が、パーミッションを「付与」にするのか「拒否」にするのかを表している
      • true は「付与」
    • SID に、パーミッションを適用する ACL_SID を設定している
      • ここでは SAMPLE_AUTHORITY を指すようにしている
    • つまり、この ACL_ENTRY のデータは「SAMPLE_AUTHORITY 権限を持っていれば id=44Foo オブジェクトの読み取りを許可する」という意味になる

ドメインオブジェクトの識別子の型

  • ドメインオブジェクトの識別子として、 OBJECT_ID_IDENTITY には数値が格納されることになっている
  • Java でいうと long 型に対応する
  • 数値型以外で識別しているドメインオブジェクトがある場合はどうなるかというと、公式リファレンスには以下のように記載されている

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.

(訳)
我々は、 Spring Security の ACL モジュールで long でない識別子をサポートするつもりはありません
long は全てのデータベースシーケンスで既に互換性があり、最も一般的なデータ型であり、全ての一般的な利用シナリオを格納するのに十分な長さを持ちます。

27.3 Getting Started

  • ということで、ドメインオブジェクトの識別子が long であることが前提となっている

ACL を有効にするための Bean 設定

SecurityExpressionHandler の拡張

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>
  • hasPermission()@PreAuthorize() などの式の中で使えるようにするためには、 SecurityExpressionHandler を拡張する必要がある
  • hasPermission() 式は、 PermissionEvaluator によって評価される
  • しかし、 DefaultMethodSecurityExpressionHandler が持つデフォルトの PermissionEvaluatorDenyAllPermissionEvaluator というクラスで、全ての評価結果を false とする実装になっている
    • つまり、デフォルトだと hasPermission() 式は動作しない(常に false と評価される)
  • このため、 DefaultMethodSecurityExpressionHandler が持つ PermissionEvaluator を本物の実装に差し替える必要がある
  • そのために設定するのが、上の AclPermissionEvaluator になる

AclService の定義

applicationContext.xml
    <bean id="aclService" class="org.springframework.security.acls.jdbc.JdbcAclService">
        <constructor-arg ref="dataSource" />
        <constructor-arg ref="lookupStrategy" />
    </bean>
  • AclService は ACL の機能を提供する
  • JdbcAclService は、 JDBC を使ってデータベースに定義された情報にアクセスし、 ACL の機能を実現する
  • そのため、コンストラクタ引数に DataSource を渡す必要がある

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>
  • ACL の定義情報(ACL_CLASS とか)の検索処理は、 AclService 自身は行わず LookupStrategy に委譲するようになっている
  • BasicLookupStrategy は JDBC を使って ACL の定義情報を検索する実装になっている
    • そのため、コンストラクタ引数に DataSource を渡している

キャッシュの仕組み

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>
  • 前述のテーブル定義から想像できる通り、テーブルのデータ件数はかなり大きくなる(オブジェクトごとにデータを登録するので)
  • そのため、検索処理を高速化するため、デフォルトでキャッシュの仕組みが組み込まれている
  • キャッシュは AclCache インターフェースによって表現される
  • このインターフェースを実装したクラスとして、 SpringCacheBasedAclCacheEhCacheBasedAclCache の2つが標準で用意されている
    • 今回は ACL の動作検証が目的なので、 SpringCacheBasedAclCache を利用した
    • 実際のキャッシュの実装には NoOpCache を使用している
      • これは NoOp とあるように実際はキャッシュを一切しない空実装になっている
    • 実際は EhCacheBasedAclCache を利用したりすることになりそう

AclAuthorizationStrategy と PermissionGrantingStrategy

  • SpringCacheBasedAclCache がキャッシュするのは AclImpl というクラスのオブジェクトになる
  • この AclImpl のフィールド定義を見ると、次のようになっている
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,
  • AclImplAcl インターフェースを実装しており、 AclSerializable を継承している
  • よって AclImpl はシリアライズ可能でなければならず、上のフィールドはどれも基本的にシリアライズ可能となっている
  • ただし、 aclAuthorizationStrategypermissionGrantingStrategytransient で修飾されているため、シリアライズの対象からは除外される
  • キャッシュがオブジェクトをシリアライズして格納する場合、キャッシュから取り出すときにこの2つのフィールドは失われることになる
  • つまり、キャッシュはデシリアライズ時にこの2つのフィールドをキャッシュ前と同じ状態に戻す必要がある
  • そのため、キャッシュには AclAuthorizationStrategyPermissionGrantingStrategy のインスタンスをコンストラクタ引数で渡す必要がある
  • ここで指定したインスタンスは、 AclImpl がキャッシュから取り出された時に再設定するために利用されている

ちなみに BasicLookupStrategyAclAuthorizationStrategyPermissionGrantingStrategy のインスタンスをコンストラクタ引数で受け取る必要がある。

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

これは、 BasicLookupStrategy がデータベースの情報から AclImpl オブジェクトを再構築したときに設定するために利用されている。

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 は、 ACL の定義を変更するときに、現在のプリンシパルがその権限を持つかどうかをチェックする役割を持つ
  • つまり、今回の Hello World では実質利用されていない
  • 詳細な説明は後述

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 は、 hasPermission() で指定したパーミッションを、現在のプリンシパルが持つかどうか具体的に判定する処理を提供する
AuditLogger
  • AuditLogger は、パーミッションの判定結果をログに残す処理を提供する
  • ConsoleAuditLogger は、唯一提供されている実装クラスで、結果をコンソールに出力する
  • 詳細は後述

ここまでの話をまとめたクラス図

spring-security.png

こんな感じ。

hasPermission()

MyAclSampleService.java
    @PreAuthorize("hasPermission(#foo, read)")
    public void logic(Foo foo) {
  • ACL の判定には hasPermission() 式を使用する
  • 第一引数にはドメインオブジェクトを、第二引数にはパーミッションを指定する

ドメインオブジェクトから ID が取得される方法

  • hasPermission() にドメインオブジェクトが渡されると、 ACL モジュールはドメインオブジェクトに getId() というメソッドがないかリフレクションで検索する
  • メソッドがあれば、そのメソッドを実行し、戻された値をドメインオブジェクトの ID として使用する
  • 該当するメソッドがないか、戻された値が Serializable を実装していない場合は例外がスローされる
  • 要は、ドメインオブジェクトには long getId() というメソッドを用意しておく必要がある、ということ

ID だけで指定する

MyAclSampleService.java
    @PreAuthorize("hasPermission(#id, 'sample.spring.security.domain.Foo', read)")
    public void logic(long id) {
  • まだドメインオブジェクトのインスタンスができていない場面などでは、 ID を指定する hasPermission() が利用できる
  • 第一引数が、ドメインオブジェクトの識別子
  • 第二引数が、ドメインオブジェクトの FQCN
  • 第三引数が、検証するパーミッション

パーミッションの指定

  • パーミッションの指定には read という定数を指定している
  • これは 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";

パーミッションの定義

パーミッション(read, write, create, delete, administration)の定義は、整数値で表現する。
データベース上は ACL_ENTRYMASK カラムに保存される。

各パーミッションは、以下のように2進数の各ビットと紐づいている

5bit目 4bit目 3bit目 2bit目 1bit目
administration delete create write read

つまり、

パーミッション 2進数表現 10進数表現
read 00001 1
write 00010 2
create 00100 4
delete 01000 8
administration 10000 16

「じゃあ、 readwrite の2つのパーミッションを持たせるなら 00011 (10進数で 3)なのかな?」と思うかもしれないがそうではない。
あくまで、各 ACL_ENTRY のレコードの MASK には上記5つのどれかが入る。
複数のパーミッションを付与する場合は、その数だけレコードを登録することになる。
(これはマスクというのか?)

検証

投入データ
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) 
  • AUTHORITY_10101read(1), create(4), administration(16) のパーミッションをそれぞれ個別に付与
  • AUTHORITY_01010write(2), delete(8) のパーミッションを合体させた 01010 = 10 を付与
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>
  • foo ユーザに AUTHORITY_10101 の権限を、
  • bar ユーザには AUTHORITY_01010 の権限を付与
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);
    }
}
  • メソッドごとに hasPermission() で必要なパーミッションを指定
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) {
        ...
    }
}
  • MyAclSampleService の各メソッドを実行して結果を標準出力に表示する

動作確認

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}
  • 個別で付与したパーミッションだけメソッドが実行できた
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
  • writedelete もブロックされた

マスクがビットマスクでない理由

GitHub のこの Issue に理由が書いてあった。

英語力低いので合っているか怪しいが、だいたい以下のような感じっぽい気がする気もしなくもない感じ。

  1. パーミッションの継承を利用したときに親のパーミッションを子のパーミッションで拒否するときに、ビットマスクだとどの権限を拒否しているのかが判別しづらくなる(らしい)
  2. SQL で検索するときに、ビットマスクだと検索が大変になる

ということで、 MASK と言いつつも、実際は整数値の区分のような感じになっている。

結局、「拡張ポイントを用意したので、どうしてもビットマスクで判定したい場合はそいつを利用してくれ」ということになっている。
SEC-1166: Provide strategy interface for AclImpl isGranted() method.

ACL の CRUD

ここまでは、 ACL_ENTRY などのデータはあらかじめ SQL で INSERT 文を用意して登録していた。

しかし、実際の開発でこれらのテーブルを直接編集するのは仕様などを正しく理解しなければならず大変。

なので、これらのデータをメンテナンスするための API が用意されている。

新たにインスタンスの ACL を登録する

実装

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);
    }

    ...
}

動作確認

MyAclServlet を実行するようにアクセスする。

サーバーコンソール出力
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]

※Acl の出力は実際は1行で出力されますが、見やすいように改行をいれています

説明

トランザクションの有効化

  • ACL のテーブルを更新する場合はトランザクションの制御が必要になるので、アノテーションベースの宣言的トランザクションを有効にしている
MyAclSampleService.java
package sample.spring.security.service;

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

@Transactional
public class MyAclSampleService {
...
  • クラスを @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>

    ...
  • <annotation-driven> でアノテーションベースのトランザクションを有効にする
  • transactionManager という名前で PlatformTransactionManager の実装(DataSourceTransactionManager)を Bean 登録

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);
    }
  • @EnableTransactionManagement でトランザクション管理を有効に
  • PlatformTransactionManager の実装である DataSourceTransactionManager を Bean 登録

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);
    }
  • JdbcAclService のサブクラスとして ACL を更新するためのメソッドが追加された JdbcMutableAclService というクラスがある
  • JdbcAclService の代わりにこのクラスのオブジェクトを Bean として登録する
  • コンストラクタ引数で AclCache を渡しているが、これは ACL を削除する処理を実行したときに JdbcMutableAclService がキャッシュからもその情報を削除するため必要になる

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);
    }
}
  • ObjectIdentity のオブジェクトを作成する(ObjectIdentityImpl
    • ObjectIdeneityImpl のコンストラクタは、1つ目がドメインオブジェクトの Class オブジェクト、
    • 2つ目がドメインオブジェクトの識別子(ID
  • MutableAclServicecreateAcl(ObjectIdentity) メソッドに作成した ObjectIdentity を渡す
  • createAcl() を実行した時点で ACL_OBJECT_IDENTITY に情報が保存される
    • ACL_CLASS も、情報が存在しない場合は一緒に作成される
  • 戻り値として、 MutableAcl が返される
オーナー
サーバーコンソール出力
[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]
  • よく見ると、作成された ACL_OBJECT_IDENTITYOWNER_SID には SID=fooACL_SID が設定されている
  • これは、 MutableAclServicecreateAcl() を実行したときのログインユーザ(プリンシパル)の情報で記録されている

ACL の Java クラス

ACL の情報は下のようなクラス構造でモデル化されている。

ACLのクラス図.png

  • ObjectIdentity
    • ドメインオブジェクトの識別子を表すオブジェクト
    • ドメインオブジェクトの識別子とクラス名を参照できる
  • Acl
    • ACL のメインとなるオブジェクト
    • ObjectIdentity に対して関連付けられた全てのパーミッションをまとめあげている
  • AccessControlEntry
    • Acl に対して Sid に割り当てられた個々のパーミッションを表すオブジェクト
  • Permission
    • 具体的なパーミッション情報を持つオブジェクト
  • Sid
    • パーミッションを割り当てる対象を表すオブジェクト
    • プリンシパルか GrantedAuthority のいずれか

MutableAclServicecreateAcl() を実行すると、引数で指定した ObjectIdentity に紐づく形で Acl, AccessControlEntry, Permission, Sid がそれぞれ構築されている。

既存の ACL を検索する

実装

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();
    }

    ...
}

動作確認

サーバーコンソール出力(createObjectIdeneity()での出力は省略)
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
]

説明

MyAclSampleService.java
    public void findAcl() {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        Acl acl = aclService.readAclById(objectIdentity);
        System.out.println("acl = " + acl);
    }
  • AclServiceAcl を検索するためのメソッドが用意されている
  • readAclById(ObjectIdentity) で、 ObjectIdentity に紐づく Acl を検索できる

パーミッションを追加する

実装

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() {
        // 更新する ACL を検索して、
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        MutableAcl acl = (MutableAcl) aclService.readAclById(objectIdentity);

        // 権限 "HOGE_AUTHORITY" に CREATE のパーミッションを付与
        List<AccessControlEntry> entries = acl.getEntries();
        GrantedAuthoritySid grantedAuthoritySid = new GrantedAuthoritySid(new SimpleGrantedAuthority("HOGE_AUTHORITY"));
        acl.insertAce(entries.size(), BasePermission.CREATE, grantedAuthoritySid, true);

        // プリンシパル "test_user" に対して WRITE のパーミッションを付与
        PrincipalSid principalSid = new PrincipalSid("test_user");
        acl.insertAce(entries.size(), BasePermission.WRITE, principalSid, true);

        // ACL の変更を保存する
        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);
    }

    ...
}

動作確認

サーバーコンソール出力
★パーミッション追加前
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]

...

★パーミッション追加後
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}

説明

MyAclSampleService.java
    public void addPermission() {
        // 更新する ACL を検索して、
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Foo.class, 10L);
        MutableAcl acl = (MutableAcl) aclService.readAclById(objectIdentity);

        // 権限 "HOGE_AUTHORITY" に CREATE のパーミッションを付与
        List<AccessControlEntry> entries = acl.getEntries();
        GrantedAuthoritySid grantedAuthoritySid = new GrantedAuthoritySid(new SimpleGrantedAuthority("HOGE_AUTHORITY"));
        acl.insertAce(entries.size(), BasePermission.CREATE, grantedAuthoritySid, true);

        // プリンシパル "test_user" に対して WRITE のパーミッションを付与
        PrincipalSid principalSid = new PrincipalSid("test_user");
        acl.insertAce(entries.size(), BasePermission.WRITE, principalSid, true);

        // ACL の変更を保存する
        this.aclService.updateAcl(acl);

        System.out.println("acl = " + acl);
    }
  • MutableAclinsertAce(int, Permission, Sid, boolean) を実行することで Acl にパーミッションを追加できる
    • 内部的には AclAccessControlEntry が追加される
  • 最初の int は、パーミッションの挿入位置を 0 始まりのインデックスで指定する
    • パーミッション(AccessControlEntry)は Acl 内部で List で保持されており、 add(int, E) メソッドの第一引数に渡される
    • なので、 0 より小さい値や size() より大きい値を渡すとエラーになる(size() と同じなら末尾に追加される)
    • DB だと ACL_ENTRYACE_ORDER になる
  • Permission は、実装クラスである BasePermission に定義された定数を指定できる
  • SidGrantedAuthoritySidPrincipalSid を指定する
    • 権限に対してパーミッションを割り当てるなら GrantedAuthoritySid
    • プリンシパルに対してパーミッションを割り当てるなら PrincipalSid を使用する
  • 最後の boolean は、パーミッションを「付与」にするのか「拒否」にするのかを指定している
    • true は付与、 false は拒否

拒否のパーミッションと ACE_ORDER

実装

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 + ")");
    }
}
  • 初回は readAclById() で ACL が取得できず NotFoundException がスローされるので、そのときだけパーミッションを登録
    • DENIED_READ 権限には read の権限を「拒否」で登録し、
    • PERMIT_READ 権限には read の権限を「付与」で登録している
    • また、順番は DENIED_READ0 にして先にくるようにしている
  • 既に ACL が登録されている場合は何もしない
  • また read() メソッドを @PreAuthorize でアノテートし、 hasPermission()read の権限があるかチェックしている
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());
        }
    }

    ...
}
  • 現在のプリンシパルの情報を出力し(printPrincipal()
  • ACL を登録(addPermission()
  • テーブルに登録された情報を出力(printTables()
  • read() メソッドを実行し、 AccessDeniedException がスローされた場合はメッセージ出力

という実装

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>
  • foobar の2つのユーザを定義
  • foo ユーザには PERMIT_READ 権限を設定
  • bar ユーザには PERMIT_READDENIED_READ の両方の権限を設定

動作確認

foo ユーザでログインして /acl にアクセスする。

サーバーコンソール出力
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})

read() メソッドが実行できている

続いて bar ユーザでログインして /acl にアクセスする。

サーバーコンソール出力
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

アクセスは拒否された。

説明

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_ORDER は順序が先の方が優先して適用されるもよう
  • bar ユーザーには PERMIT_READ も設定されていたが、 DENIED_READ のほうが PERMIT_READ よりも ACE_ORDER が先だったため、パーミッションは「拒否」と判定された

使いどころは?

  • 最初この仕組み(拒否のパーミッションと ACE の順序設定)を見たとき、あまり使いどころが思いつかなかった
    • アクセスを拒否したいだけなら、単純にパーミッションを与えなければいいだけなので、あえて「拒否」のパーミッションを与える場面が分からなかった
  • 唯一思いついたのが、↑のような「完全に拒否する」権限を定義する使い方
  • 「付与」系のパーミッションは末尾に追加し、「拒否」系のパーミッションは先頭に追加するようにしていれば、「拒否」のパーミッションが常に優先されるようになる
  • そして「拒否」のパーミッションを権限(GrantedAuthority)と紐づけておけば、「その権限を与えられると基本的にアクセスができなくなる」みたいな振る舞いになる
  • そんな場面があるのかは分からないが、「アクセスを基本拒否したい」ときに「この権限を設定すればアクセス不可にできる」みたいな制御ができる気がする

ACL の更新で行われる権限のチェック

ACL の更新は誰でもできるわけではない。

実装

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 ユーザに administration(16) のパーミッションを付与
  • ID=10Foo オブジェクトについての ACL_OBJECT_IDENTITY を作成
  • オーナーに hoge プリンシパルを設定
  • admin プリンシパルには administration のパーミッションを付与
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>
  • AclAuthorizationStrategyImpl のコンストラクタ引数で PERMISSION_MANAGER を渡している
  • hoge, fuga, admin ユーザは権限なし
  • piyo ユーザにだけ PERMISSION_MANAGER 権限を付与
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);
    }
}
  • id=10Foo オブジェクトを検索し、 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());
        }
    }

    ...
}
  • 処理の前にログインしているユーザの情報とデータベースの状態を出力
  • MyAclSampleServiceaddPermission() メソッドを実行したのち、データーベースの状態を再度出力
  • addPermission() で例外が発生した場合は、その情報を出力

動作確認

hoge ユーザでアクセスした場合

サーバーコンソール出力
★ユーザーの情報
name=hoge
authorities=

★更新前のテーブルの状態
[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}

★メソッド実行
service.addPermission()

★更新後のテーブルの状態
[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}
  • パーミッションの追加が成功している

fuga ユーザでアクセスした場合

サーバーコンソール出力
★ユーザーの情報
name=fuga
authorities=

★更新前のテーブルの状態
[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}

★メソッド実行
service.addPermission()

★エラーの情報
e.class = class org.springframework.security.acls.model.NotFoundException, message = Unable to locate a matching ACE for passed permissions and SIDs
  • パーミッションの追加はできず、例外(NotFoundException)がスローされた

piyo ユーザでアクセスした場合

サーバーコンソール出力
★ユーザーの情報
name=piyo
authorities=PERMISSION_MANAGER

★更新前のテーブルの状態
[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}

★メソッド実行
service.addPermission()

★更新後のテーブルの状態
[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}
  • パーミッションの更新が成功している

admin ユーザでアクセスした場合

サーバーコンソール出力
★ユーザーの情報
name=admin
authorities=

★更新前のテーブルの状態
[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}

★メソッド実行
service.addPermission()

★更新後のテーブルの状態
[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}
  • パーミッションの更新が成功している

説明

  • パーミッションの更新には、次の要素が関わってくる
    • 更新しようとしている ACL のオーナーかどうか
    • 更新しようとしている ACL に対して administration のパーミッションが付与されているかどうか
    • AclAuthorizationStrategyImpl のコンストラクタ引数で指定した権限を持っているかどうか
    • ACL の更新種別(一般、所有、監査)
  • そして、次のような順序で更新可能かどうかが判断されている
    1. 現在のプリンシパルが更新しようとしている ACL のオーナーの場合、 ACL の更新種別が「一般」 or 「所有」の場合は、許可
    2. そうでない場合、 AclAuthorizationStrategyImpl で指定した権限を持っていれば許可
    3. そうでない場合、更新しようとしている ACL に対して administration のパーミッションが付与されていれば許可
    4. そうでない場合は 不許可

更新の種類

ACL の更新の種類には、次の3つがある。

  • 一般
  • 所有
  • 監査

そして、 ACL を更新するメソッドは、それぞれ以下のように分類されている。

メソッド 更新の種類
insertAce() 一般
updateAce() 一般
deleteAce() 一般
setParent() 一般
setEntriesInheriting() 一般
setOwner() 所有
updateAuditing() 監査

ちなみに、この3つの種類は AclAuthorizationStrategy インターフェースに定義されている。

AclAuthorizationStrategy.java
public interface AclAuthorizationStrategy {

    int CHANGE_OWNERSHIP = 0; 所有
    int CHANGE_AUDITING = 1;  監査
    int CHANGE_GENERAL = 2;   一般

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>
  • AclAuthorizationStrategyImpl のコンストラクタで渡していた GrantedAuthority は、 ACL を更新するときに利用されている
  • 更新しようとしているログインユーザがここで指定した権限を持っていれば、更新が許可されるようになっている。
更新の種類ごとに権限を指定する

上記例では PERMISSION_MANAGER という権限だけを指定してる。
この場合、この権限を持っていれば全ての更新の種類(一般、所有、監査)が許可されることになる。

もし「"一般" の更新の場合は●●という権限が必要で、 "所有" の更新のときは▲▲、 "監査" のときは★★が必要」といった制御がしたい場合は、次のように Bean を定義する。

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>
  • コンストラクタ引数に渡す GrantedAuthority の数を3つにする(2つや4つだとエラーになるので注意)
  • 以下のように、指定した GrantedAuthority の位置により、どの更新種別に権限が紐づけられるかが決まる
    1. 「所有」に関する更新の場合に必要になる権限を指定する
    2. 「監査」に関する更新の場合に必要になる権限を指定する
    3. 「一般」に関する更新の場合に必要になる権限を指定する

更新が不許可となった場合の挙動について

前述の例では、更新が不許可となった場合 NotFoundException がスローされていた。

不許可の場合、常に NotFoundException がスローされるかというと、実は違う。
もし更新対象の ACL に administration のパーミッションが「拒否」で設定されていた場合、 AccessDeniedException がスローされる。

これは実はバグで、本当はどちらの場合でも AccessDeniedException がスローされるのが正しい。

GitHub の issue を検索するとこのバグが起票されている
しかし、 2017/07/09 現在この issue は OPEN のままで、対応されている様子はない。
この問題が認識されたのは、 2009 年のことらしいので、完全に放置されている(これに限らず、 ACL 関係の issue はどれも長期間放置されていて、あまりメンテする気はないのかもしれない)。

これ以外にも、 ACL の更新に関する権限チェックには ロールの階層化を利用していた場合に親のロールが参照されない といったバグが上げられている(こちらも長期間放置されている)。

この辺を正しい動作で利用したい場合は、既存の AclAuthorizationStrategyImpl をコピーして、問題箇所を修正した独自実装クラスを作り、 AclAuthorizationStrategyImpl の代わりに利用するといった対応が必要になると思われる。
(もしくは修正版を本家に Pull Request するか)

パーミッションに継承関係を持たせる

ACL_OBJECT_IDENTITY には、継承関係を持たせることができる。

Hello World

実装

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);
  • id=44...47ACL_OBJECT_IDENTITY を定義している
  • id=44 にだけ READONLY 権限に対して read のパーミッションを付与している
  • id=45 は、親オブジェクトなどを一切指定していない
  • id=46 は、親オブジェクトのID だけ PARENT_OBJECT で指定している(ENTRIES_INHERITINGfalse
  • id=47 は、親オブジェクトを指定したうえで ENTRIES_INHERITINGtrue にしている
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 + ")");
    }
}
  • 引数で受け取る Foo オブジェクトに対して read のパーミッションを持っていることをチェックする
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());
        }
    }

    ...
}
  • id=44...47Foo オブジェクトを生成し、 MyAclSampleServiceread() メソッドを実行する
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>
  • foo ユーザに READONLY 権限を付与

動作確認

foo ユーザでログインして /acl にアクセスする。

サーバーコンソール出力
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})
  • id=44, 47 のみメソッドが実行できて、 id=45, 46 は拒否された

説明

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);
  • パーミッションに継承関係を持たせるには、 ACL_OBJECT_IDENTITY に次の2つの設定を追加する
    • PARENT_OBJECT に、親となる ACL_OBJECT_IDENTITYID を設定する
    • ENTRIES_INHERITINGtrue を設定する
  • これにより、子供の ACL_OBJECT_IDENTITY に関するパーミッションのチェックが行われるときに、親までさかのぼってパーミッションの判定をするようになる

デフォルトのパーミッションとしての利用

  • パーミッションの継承を利用すれば、例えば「デフォルトのパーミッション」みたいなことが実現でき、定義情報を一か所で管理することができる
  • 使い方として正しいかは謎だがACL_OBJECT_IDENTITYOBJECT_IDENTITY は別に実在する id でなくても登録はできる
  • 仮に OBJECT_IDENTITY=-1 などのあり得ない値で ACL_OBJECT_IDENTITY を登録しておいて、他の通常の ACL_OBJECT_IDENTITY はこのデフォルトの ACL_OBJECT_IDENTITY を継承するようにすれば、デフォルトパーミッションが実現できる
  • ただ、継承を何層にも重ねてしまうと、 SQL だけで「その ACL_OBJECT_IDENTITY に設定されている全てのパーミッション」を特定することが困難になる気がする(階層の最大値が決まっているなら、その数だけ LEFT JOIN すればイケる?)
  • 階層はなるべく少なくしておいたほうが無難な気がする

プログラムで親を設定する

実装

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);
    }
}
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);
    }

    ...
}

動作確認

/acl にアクセス

サーバーログ出力
[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}

説明

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); // ★ここで親を設定
        this.aclService.updateAcl(childAcl);
    }
  • ある ACL に親の ACL を設定するには、 MutableAclsetParent() メソッドを使用する
  • ENTRIES_INHERITING は、 ACL を新規に作成するとデフォルトが true なので、新規作成なら明示は不要(一応 setEntriesInheriting() メソッドで設定が可能)

監査ログ

パーミッションを付与したか拒否したかのタイミングで、その情報を監査ログとして出力する仕組みが用意されている。

Hello World

実装

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);
  • Fooid=44 について、 READONLY 権限に対して「read の付与」と「write の拒否」のパーミッションを設定
  • read の付与」の ACL_ENTRY は、 AUDIT_SUCCESStrue
  • write の拒否」の ACL_ENTRY は、 AUDIT_FAILUREtrue に設定
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>

動作確認

foo ユーザでログインして /acl にアクセス

サーバーコンソール出力
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
  • 各メソッドの実行前に、パーミッションが付与されたか拒否されたかがコンソールに出力されている

説明

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);
  • 監査ログの出力を有効にするには、 ACL_ENTRYAUDIT_SUCCESS または AUDIT_FAILUREtrue を設定する
  • AUDIT_SUCCESStrue の場合、パーミッションの付与が判定されたときにログが出力される
  • AUDIT_FAILUREtrue の場合は、パーミッションの拒否が判定されたときにログが出力される
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>
  • 監査ログの出力は、 AuditLogger インターフェースを実装したクラスによって行われる
  • AuditLogger は、 DefaultPermissionGrantingStrategy にセットする
  • AuditLogger の実装として標準で用意されている ConsoleAuditLogger クラスは、ログを標準出力に出力する

デフォルトの監査ログ出力の実装

ConsoleAuditLogger の実装は次のようになっている

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);
            }
        }
    }
}
  • AuditLogger インターフェースには logIfNeeded(boolean, AccessControlEntry) メソッドが定義されている
    • 第一引数には、パーミッションが付与されたのか拒否されたのかのフラグが渡される
    • 第二引数には、そのパーミッションの情報を保持する AccessControlEntry オブジェクトが渡される
  • 監査ログを出力するかどうかの設定(AUDIT_SUCCESS, AUDIT_FAILURE)を取得するには、 AuditableAccessControlEntryisAuditSuccess()isAuditFailure() のメソッドを使用する(引数の AccessControlEntry をキャストする必要がある)
  • 実際はログファイルとかに出力する必要があると思うので、この実装を参考にして独自の AuditLogger クラスを実装することになる気がする

プログラムから設定する

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);
  • AUDIT_SUCCESSAUDIT_FAILURE をプログラムから変更するには、 AuditableAclupdateAuditing(int, boolean, boolean) メソッドを使用する
    • 第一引数が、変更したい AccessControlEntry を指定するためのインデックス
    • 第二引数が AUDIT_SUCCESS に設定する値
    • 第三引数が AUDIT_FAILURE に設定する値

独自のパーミッションを定義する

デフォルトで使用できるパーミッションは read, write, create, delete, administration の 5 つだけだが、独自のパーミッションを追加で定義することもできる。

32 (二進数表現で 100000)を作ってみる。

実装

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);
    }
}
  • BasePermission を継承して独自の Permission クラスを作成する
  • HOGE という定数を作成し、マスクを 0b100000 (十進数で 32)、コードを 'H' にしてインスタンスを設定している
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);
  • READONLY 権限に対して read のパーミッションを付与
  • HOGE 権限に対して 32MyPermission で定義した HOGE)のパーミッションを付与
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);
    }
}
  • readhoge で、それぞれパーミッションをチェック
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>
  • DefaultPermissionFactory を Bean として定義
    • コンストラクタ引数で、自作した PermissionMyPermission)の Class オブジェクトを指定
  • DefaultPermissionFactory の Bean を、 AclPermissionEvaluatorBasicLookupStrategy にそれぞれプロパティとして設定する

動作確認

foo ユーザーでログインして /acl にアクセス

コンソール出力
read
Foo{id=44}
hoge
Access is denied

bar ユーザーでログインして /acl にアクセス

コンソール出力
read
Access is denied
hoge
Foo{id=44}

説明

独自パーミッションクラスの定義

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);
    }
}
  • 独自のパーミッションを定義する場合は、 BasePermission を継承したクラスを作成し、そこに定数で追加したいパーミッションを定義する
  • コンストラクタの第一引数がそのパーミッションに対応するビット値になり、第二引数がパーミッションを一文字であらわした表現を指定する
  • 定数名が重要で、ここで指定した定数名が hasPermission() 式の第二引数で指定するパーミッションの名前になる

既存のパーミッションクラスと置き換える

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>
  • 何もしないと、パーミッションクラスは BasePermission が使用される
  • これを、先ほど作成した自作のパーミッションクラス MyPermission に差し替える必要がある
  • パーミッションクラスを直接利用しているのは PermissionFactory インターフェースの実装クラスである DefaultPermissionFactory になっている
  • このクラスのコンストラクタ引数で、使用したいパーミッションクラスの Class オブジェクトを指定する
    • DefaultPermissionFactory 内部でパーミッションクラスに定義されている定数をリフレクションで取得するようになっている
  • PermissionFactoryAclPermissionEvaluatorBasicLookupStrategy クラスで利用されているので、それらの permissionFactory プロパティを差し替える

式で利用する

MyAclSampleService.java
    @PreAuthorize("hasPermission(#foo, 'hoge')")
    public void hoge(Foo foo) {
        System.out.println(foo);
    }
  • ここまでの設定で、独自のパーミッションが利用できるようになる
  • ただし、他の既存のパーミッションとは違い、独自のパーミッションは SecurityExpressionRoot に定数が定義されていない
  • なので、シングルクォーテーション(')で囲って文字列として指定してあげる必要がある
  • パーミッションクラスに定義した定数名が全て大文字になっていれば、実質大文字小文字の区別はなくて問題ない
    • どういうことかというと、実際はまずそのままの形(hoge)でチェックし、なければ全て大文字にして(HOGE)チェックする、となっている

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした