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()になっている。

参考文献

JEP 370: Foreign-Memory Access API (Incubator)をテキトーに訳した

https://openjdk.java.net/jeps/370

JEP 370: Foreign-Memory Access API (Incubator)

Owner    Maurizio Cimadamore
Type    Feature
Scope   JDK
Status  Proposed to Target
Release 14
Component   tools
Discussion  panama dash dev at openjdk dot java dot net
Created 2019/07/09 15:55
Updated 2019/12/05 01:19
Issue   8227446

Summary

javaヒープ外の外部メモリ(foreign memory)に安全かつ効率的なアクセスをするAPIを導入します。

Goals

外部メモリAPIは以下条件を満たす必要があります。

  • 汎用性(Generality): 同一APIで様々な外部メモリ(native memory, persistent memory, managed heap memoryなど)を操作可能であること。
  • 安全性(Safety): どのような種類のメモリであっても、JVMの安全性を損なわないAPIであること。
  • 決定論的(Determinism): メモリ割り当て解放操作はソースコード上から自明であること。

Success Metrics

外部メモリAPIは、今日使われているjava.nio.ByteBuffer, sun.misc.Unsafe、の代替を目指します。新規APIはそうした既存APIと比べてパフォーマンスの向上を目指します。

Motivation

現状、外部メモリにアクセスするライブラリは、Ignite, mapDB, memcached, Netty (ByteBuf)、など多数存在します。

ただ、Java APIは外部メモリアクセスに対しては不十分です。

Java 1.4で導入したByteBuffer APIは、off-heapに割り当てるダイレクトバイトバッファ(direct byte buffers)を生成可能で、Javaから直接off-heapメモリを操作出来ます。しかしダイレクトバッファには制限があります。たとえば、2GB以上のバッファを生成できず、これはByteBuffer APIがintベースのインデクシングスキーマなためです。また、ダイレクトバッファに関連するメモリの割り当て解除はGC任せなため、バッファ使用は面倒事になりがちです。これは、GCから到達不能と見なされたダイレクトバッファのみ解放可能なためです。長年、問題解決や制限解除のための機能拡張リクエストが多数提出されました(例:4496703, 6558368, 4837564, 5029431)。ByteBuffer APIは、off-heapメモリアクセスだけでなく、文字エンコーディング/デコーディングとパーシャルI/O操作に重要なバルクデータのプロデューサー/コンシューマ、のためにも設計されたもので、各種制限はここに由来しています。

もう一つのJavaから外部メモリアクセスする主要な方法はsun.misc.Unsafe APiです。Unsafeは多数のメモリアクセス操作(例:Unsafe::getIntputInt)を公開し、このAPIは良く出来た汎用アドレスモデルによりon-heap同様にoff-heapでも動作します。メモリアクセスにUnsafeを使用するのは極めて有効です。すべてのメモリアクセス操作はJVM intrinsicsとして定義してあるため、メモリアクセス操作は基本的にJITが最適化します。しかし、Unsafe APIはその定義上unsafeで、任意のメモリにアクセスできます(例:Unsafe::getIntlongアドレスを取る)。これにより、例えば解放済みメモリにアクセスするなどで、JavaプログラムでJVMをクラッシュ可能です。加えて、Unsafe APIはサポートされるJava APIでは無いので、その使用は強く非推奨です。

メモリアクセスにJNIを用いることも可能ですが、固有のコストがあるのであまり使う機会はありません。開発パイプライン全体が複雑で、これはJNIがCコードのメンテナンスを開発者に強いるためです。またJNIは本質的に低速で、これはJava-ネイティブ変換がアクセスのたびに発生するためです。

要約すると、外部メモリでは開発者はジレンマとして、安全だが制限(プラス基本的に非効率)のある方法を取るか、安全保障が無く非サポートで危険なUnsafe APIを使うべきか、に直面します。

本JEPでは、サポートが有る・安全・効率的な外部メモリアクセスAPI、を導入します。外部メモリアクセスの問題を解決することで、既存APIの制限や危険性から開発者を解放します。また、新規APIJIT最適化を念頭に作り直すため、パフォーマンスも向上します。

Description

外部メモリアクセスAPIは三つの抽象化、MemorySegment, MemoryAddress, MemoryLayout、を提供します。

MemorySegmentは指定の空間的・一時的境界で隣接メモリ空間のモデルを扱います。MemoryAddressはセグメント内のオフセットと見なせるものです。MemoryLayoutはメモリセグメントの中身のプログラム上の表現です。

メモリセグメントは各種ソース、ネイティブメモリバッファ・Java配列・バイトバッファ(ダイレクトかヒープベース)、から生成します。たとえば、ネイティブメモリセグメントは以下のように生成します。

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   ...
}

これは100 byteのネイティブメモリバッファに関連付けられたメモリセグメントを生成します。メモリセグメントは空間的な境界(spatially bounded)で、上限・下限があります。この境界外へのメモリアクセスにセグメントを使用しようとすると例外をスローします。try-with-resourceと共に使用する場合、セグメントは一時的な境界(temporally bounded)となり、生成・使用・クローズするとアクセス出来なくなります。セグメントのクローズは常に明示的な操作で、セグメントに関連付けられたメモリ解放など、副作用を伴います。クローズ済みメモリセグメントへのアクセスは例外をスローします。メモリアクセスAPIの安全性を保障するのに空間的・一時的メモリの安全性チェックは、JVMクラッシュを防止などで、重要です。

セグメントとメモリ間のデリファレンスmemory-access var handleで行います。この特殊なvar handleは少なくとも1つの必須アドレス、MemoryAddress型、を持ちます。ここでのMemoryAddressデリファレンス対象のアドレスです。これはMemoryHandlesのファクトリメソッドで取得します。例として、ネイティブセグメントのsetにはメモリアクセスのvar handleを以下のように使用します。

VarHandle intHandle = MemoryHandles.varHandle(int.class);

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   MemoryAddress base = segment.baseAddress();
   for (int i = 0 ; i < 25 ; i++) {
        intHandle.set(base.offset(i * 4), i);
   }
}

メモリアクセスvar handleは、longの、追加パラメータアドレスを使用可能で、多次元インデックスアクセスなど、複雑なアドレッシングスキーマのために使います。こうしたメモリアクセスvar handleは通常MemoryHandlesコンビネータメソッド呼び出しで取得します。たとえば、インデックスメモリアクセスハンドル経由でネイティブセグメントに直接setするには以下のようにします。

VarHandle intHandle = MemoryHandles.varHandle(int.class);
VarHandle intElemHandle = MemoryHandles.withStride(intHandle, 4);

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   MemoryAddress base = segment.baseAddress();
   for (int i = 0 ; i < 25 ; i++) {
        intElemHandle.set(base, (long)i);
   }
}

This effectively allows rich, multi-dimensional addressing of an otherwise flat memory buffer.

APIの表現力強化と、上記サンプルのような明示的な数値計算の必要を省略するために、MemoryLayoutはメモリセグメントをプログラム的に記述するのに使います。たとえば、上記サンプルで使用するネイティブメモリセグメントのレイアウトは以下のように記述します。

MemoryLayout intArrayLayout
    = MemoryLayout.ofSequence(25,
                              MemoryLayout.ofValueBits(32,
                                                       ByteOrder.nativeOrder()));

上記は指定(32-bit)のシーケンスメモリレイアウトを25回繰り返したものを生成します。メモリレイアウトを作ると、コードから手計算のコードを追い出して、必要なメモリアクセスvar handleを単に生成するだけになります。

MemoryLayout intArrayLayout
    = MemoryLayout.ofSequence(25,
                              MemoryLayout.ofValueBits(32,
                                                       ByteOrder.nativeOrder()));

VarHandle intElemHandle
    = intArrayLayout.varHandle(int.class,
                               PathElement.sequenceElement());

try (MemorySegment segment = MemorySegment.allocateNative(intArrayLayout)) {
   MemoryAddress base = segment.baseAddress();
   for (int i = 0 ; i < intArrayLayout.elementCount() ; i++) {
        intElemHandle.set(base, (long)i);
   }
}

上の例では、レイアウトパス(layout path)の生成経由でメモリアクセスvar handleを生成してレイアウトインスタンスを作成しています。複雑なレイアウト式からネストしたレイアウトを構築するのにレイアウトパスを使います。レイアウトインスタンスはネイティブメモリセグメントのアロケーションを、レイアウトのサイズとアライメントに基づいて行います。前述サンプルのループ定数はシーケンスレイアウトの要素数で置き換えています。

外部メモリアクセスAPIはまずインキュベートモジュールjdk.incubator.foreignとして提供します。

Alternatives

既存APIByteBuffer, Unsafe, 最悪JNIの使用を継続する。

Risks and Assumptions

安全性と有効性を兼ね備える外部メモリアクセスAPIの構築は困難なタスクです。前述セクションのような空間的・一時的チェックはアクセスのたびに実行の必要があり、そうしたチェックでJITが最適化可能なことは重要です e.g., hoisting them outside of hot loops. メモリアクセスAPIが有効かつ既存のByteBufferやUnsafeのような最適化可能なことを保障する機能がJIT実装に必要になると思われます。

Dependencies

本JEPのAPIProject Panamaの目的であるネイティブ相互運用機能の構築を補助するかもしれません。また、本APIJEP 352 (Non-Volatile Mapped Byte Buffers)のnon-volatileメモリアクセスにおいてより汎用で効果的な方法で使えるかもしれません。

spring-bootでspring-mvcの依存性が解決される仕組みを調べる

spring-bootでは、例えば以下のようにparentとdependencyを指定しておけば、spring-webとかspring-mvcとかの依存性をよしなに解決してくれる。以下のように書くとspring-webmvc-5.2.1.RELEASE.jarが解決されるが、このエントリではそれがどのようにして行われるのか? を勉強したメモを残しておく。

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

spring-boot-starter-webのdependencies

まず、spring-boot-starter-webpom.xmlを見るとspring-webmvcdependencyがあるのが分かる。ただしversionについては記載が無い。よって、parentであるspring-boot-starter-parentの親子階層を見ていく。

 <dependencies>
        <!--  省略 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>
    </dependencies>

spring-boot-parentの階層

次に、parentで指定するspring-boot-starter-parentの親子階層から見ていく。

spring-boot-starter-parentは上記のような階層になっている。また、spring-boot-starter-webspring-boot-starter-testは、spring-boot-parentの子のspring-boot-startersのmodule、という構造になっている。

spring-boot-buildでspring-framework-bomをimport

spring-webmvcのversionは親階層のどこかで指定しているハズだが、ここでには直接書かれてはいない。

まず、以下のようにspring-framework-bomdependencyManagementでimportしている。

 <properties>
        <!-- 省略 -->           
        <spring-framework.version>5.2.2.BUILD-SNAPSHOT</spring-framework.version>
        <!-- 省略 -->
    </properties>
    <dependencyManagement>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-framework-bom</artifactId>
                <version>${spring-framework.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>

spring-framework-bom-5.2.1.RELEASE*1をimportしている。で、ここで具体的なversionと共にspring-webmvcが書いてある。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.springframework</groupId>
  <artifactId>spring-framework-bom</artifactId>
  <version>5.2.1.RELEASE</version>
  <packaging>pom</packaging>
  <name>Spring Framework (Bill of Materials)</name>
 <!-- 省略 -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.2.1.RELEASE</version>
    </dependency>

基本的にはこのようなparent・importの関係によってdepedencyが解決されるのが分かる。

bomとdependencyManagementで必要なライブラリだけ使用する

spring-framework-bom-5.2.1.RELEASEには様々なspring関連ライブラリが並んでいるが、spring-boot-starter-webだけだとspring-webspring-mvcしか使われない。これはmavenのbomという仕組みを使用している。

bomとは、ライブラリ提供者が複数ライブラリのversionの意図した組み合わせ、を記述したもの。使用者はそこから必要なものだけを使用するが、これはmavanのdependencyManagementで実現する。これは名前の通りdependencyを管理するもので、dependencyManagementをparenで親子関係にしたりimportしたりしてもdependencyとして解決されず、<dependencies><dependency>...を記述すると依存性として解決される。ただし、versionは省略出来てdependencyManagementのversionが使用される。これによって、使用者は必要なライブラリだけ、かつ、開発者が意図したversionのものを使用できる。

なお、上記のbomやdependencyManagementの解説はあくまで自分の言葉で書いたものなので、詳細な定義が必要な場合は公式リファレンス等を参照してください。

まとめ

というわけで、

  1. spring関連ライブラリのbomをimport
  2. そのversionはparenの親子関係で指定

のようなタテ・ヨコの関係を駆使した構造になっている。

*1:上記xmlでは5.2.2だが何か無いので解説のため5.2.1を使用