kagamihogeの日記

kagamihogeの日記です。

spring-securityのaclをうごかす

spring-securityのDomain Object Security (ACLs)のhelllo world的なとりあえず動くところまでをやる。

ソースコード

build.gradle

plugins {
    id 'org.springframework.boot' version '2.2.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'java'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-devtools'
    
    implementation 'org.springframework.security:spring-security-acl'
    implementation 'org.springframework.security:spring-security-config'
}

DBにテーブルなど作成

この機能はRDBACLのデータを保存するため、あらかじめテーブルなどを作っておく必要がある。作成用のクエリはRDBごとに異なり Spring Security Reference - 20.1.3 ACL Schemaもしくはspring-security-acl-x.x.x.RELEASE.jarの中にあるので、それを単に実行すれば良い。

application.properties

前述の通りDBを使用するためDataSourceの設定が必要。今回はOracleを使用するため、以下のようなDB接続設定を作成。

spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:11521/XEPDB1
spring.datasource.username=xxxx
spring.datasource.password=xxxx

GlobalMethodSecurityConfigurationを拡張してACL設定

詳細はここでは触れない。かなり良くまとめられている https://qiita.com/opengl-8080/items/24a3118ef36bcf55ba71 を参照。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.support.NoOpCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
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.JdbcMutableAclService;
import org.springframework.security.acls.jdbc.LookupStrategy;
import org.springframework.security.acls.model.AclCache;
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;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {

    @Bean
    public JdbcMutableAclService aclService(DataSource dataSource, LookupStrategy lookupStrategy, AclCache aclCache) {
        JdbcMutableAclService s = new JdbcMutableAclService(dataSource, lookupStrategy, aclCache);
        s.setClassIdentityQuery("select acl_class_sequence.CURRVAL from dual");
        s.setSidIdentityQuery("select acl_sid_sequence.CURRVAL from dual");
        return s;
    }

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

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

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

    @Autowired
    MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return defaultMethodSecurityExpressionHandler;
    }

    @Bean
    public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler(DataSource dataSource,
            LookupStrategy lookupStrategy, AclCache aclCache) {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(
                aclService(dataSource, lookupStrategy, aclCache));
        expressionHandler.setPermissionEvaluator(permissionEvaluator);
        return expressionHandler;

    }
}

RDBごとにシーケンス取得SQLは異なるためsetClassIdentityQuerysetSidIdentityQueryで指定する。上はOracleの場合。

AclAuthorizationStrategyは今回は使わないので適当なauthorityを指定するに留めている。

ACL追加&チェック

以上で最低限の設定が完了したので、ACLの追加とそれを使用したチェックを実行してみる。

まず、サンプル動作のために、ACLと直接関係は無いがprincipalをkagami固定でログインするようにしておく。

@Component
public class MyAuthenticationManager implements AuthenticationManager {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        return new UsernamePasswordAuthenticationToken("kagami", "hoge");
    }
}

以下はACL対象となるデータを持つクラス。詳細は省略するがlong型のidを持つ必要がある。

public class User {
    private long id;

    public User() {
        super();
    }

    public User(long id) {
        super();
        this.id = id;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }
}

以下はエントリーポイントと、ACLを作成する/create-aclおよびACLでチェックを行う/userを持つクラス。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.security.acls.domain.BasePermission;
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.MutableAcl;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;

@SpringBootApplication
@Controller
public class App {
    public static void main(String[] args) {
        new SpringApplicationBuilder(App.class).web(WebApplicationType.SERVLET).run(args);
    }

    @Autowired
    SampleService sample;

    @Autowired
    MutableAclService aclService;

    @Transactional
    @GetMapping("/create-acl")
    public String createAcl() {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(User.class, 7L);
        MutableAcl createAcl = aclService.createAcl(objectIdentity);

        List<AccessControlEntry> entries = createAcl.getEntries();
        PrincipalSid principalSid = new PrincipalSid("kagami");
        createAcl.insertAce(entries.size(), BasePermission.READ, principalSid, true);

        aclService.updateAcl(createAcl);

        return "index";
    }

    @GetMapping("/user")
    public String user() {
        sample.getUser();
        return "index";
    }
}

サンプルなので、ACLのIDは7・principalはkagamiで固定しREAD権限を付与している。

次に、@PostAuthorize(value = "hasPermission(returnObject, 'READ')")で、サービスメソッド実行後の戻り値オブジェクト(IDが7)に対してREAD権限を持つかどうか、をチェックしている。

以上で、spring-securityのACL設定とその実行の最低限を作ることが出来た。

ハマりどころ

ORA-06576: ファンクション名またはプロシージャ名が無効です。

StatementCallback; bad SQL grammar [call identity()]; nested exception is java.sql.SQLException: ORA-06576: ファンクション名またはプロシージャ名が無効です。
org.springframework.jdbc.BadSqlGrammarException: StatementCallback; bad SQL grammar [call identity()]; nested exception is java.sql.SQLException: ORA-06576: ファンクション名またはプロシージャ名が無効です。

前述の通り、シーケンスから値を取得するSQLRDBごとに異なるため、使用するRDBに合わせたクエリを指定する必要がある。デフォルトはHSQLDBcall identity()になっている。

参考文献