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

参考文献

アトラス作品ファンのオフ会 新年号に開催する眼鏡祭(2019/12/14)に行ってきた

12/14(土) スーパー眼鏡祭R(eiwa)35 - TwiPlaに行ってきました。

f:id:kagamihoge:20191218192422j:plain

当日の肌寒さはそこまででもなく、冬にしては過ごしやすい一日となりました。今回も参加者は200人を超えての盛況振りで、発売まもないP5R - ペルソナ5 ザ・ロイヤルの話題などで盛り上がりました。会場はキリストンカフェ東京をワンフロア貸し切っての開催です。当日の様子は#眼鏡祭1214 - Twitterから追えます。

ペルソナはじめアトラス作品ファンの大規模オフ会

眼鏡祭はペルソナはじめアトラス作品ファン同士の交流を目的としたオフ会です。コスプレが可能なこと、参加者約200人と大規模なこと、など目を引く特徴はありますが、そうした派手な要素を抜きにすれば単なるペルソナなどアトラス作品好きの集いです。コスプレをして、お酒を飲んで、アニメ・ゲーム・マンガなどオタクな話題で一日中盛り上がる。それが眼鏡祭です。

コスプレ参加者の楽しみ方は、コスプレしてお酒を飲んでおしゃべりするのが最高、という方から、アトラス作品のコスプレがたくさん居ることを活かした多人数の写真撮影まで様々です。今回はP5R発売間もない事もあり、同作品のコスプレ参加者は多数で、それを活かした写真を撮りあう光景がそこかしこで見られました。また、眼鏡祭ならではの光景として、懐かしい作品のコスプレも一定数存在します。そうした作品のキャラクター同士、会場で声を掛け合うのも良く見かけました。

コスプレをしていなくとも眼鏡祭の楽しみ方は変わりません。P5RネタバレOKスペースでは感想や考察で盛り上がっていたし、十三騎兵防衛圏プレイ済参加者はひたすらに熱く布教する。ちなみに、私はアズールレーンにじさんじの話をしていました。

はじめましてに優しいオフ会

200人のオフ会にはじめて飛び込むのは並々ならぬ「度胸」が必要です。そこは運営側も良く理解しており、様々な工夫をしています。参加者は6-7人のテーブルに割り振られ、最初はその少人数グループで自己紹介などをします。なお、不安なので友達同士同じテーブルになりたい、のであれば配慮してくれます。また、1-2回目の参加者は名札の色が他と異なり、ベテラン参加者はそうした初参加者が困ってそうであれば積極的に話しかける文化が眼鏡祭にはあります。ちなみに、1-2回目参加者の割合は200人中40人ほどです。

また、眼鏡祭では「仮装」がオススメです。いわゆる「コスプレ」を眼鏡祭では「仮装」と呼びますが、本格的なコスプレでなくともデモニカジャケットを着るとか、普段は付けづらいキャラクターグッズを身につけるとか、ちょっとした工夫で十分との想いが込められています。テーマパークやハロウィンのイメージに近く、何かしらのグッズを身にまとってテンションを上げた方が楽しみやすい背景事情があるためです。

ただし、いわゆるコスプレイベントではないので、撮影メインにしたい方には不向きです。たとえば、更衣室は会場の1部屋を仕切っただけなので着替えやメイクはやりにくく、会場はカフェのため照明はやや薄暗いです。

実際のところ、初参加者でも楽しめるのでしょうか。ステマ気味ですが初参加者の感想をピックアップしてみます。

眼鏡勢のイベントに足を運ぶ

眼鏡祭当日だけで知り合いを作るのは相当に困難です。コスプレを解けば誰か分からなくなりがちだし、大人数の中で個人を識別するのは中々に難易度が高いです。そこで、眼鏡祭常連いわゆる眼鏡勢が開催する、より人数の少ないイベントに足を運ぶのがオススメです。

他にもtwitterを見ていると、ボドゲ会・サバゲなど、眼鏡勢は様々なイベントを開いています。COOPを深めるにはとりあえず行ってみるのが良いでしょう。もちろん、とりあえず知り合った人はtwitterでフォローしてオンライン上で交流するのも有効です。

次回の眼鏡祭予定は以下です。

みんなで作る眼鏡祭

眼鏡祭はイベント会社ではなく有志のスタッフで運営しています。200人を1日中動かすイベントなのでその仕事は多岐に渡ります。企画・運営などの目には見えにくいものから、会場のレイアウト変更・復元作業や更衣室管理など当日に目にするものまで。本当に毎回頭が上がらない思いでいっぱいです。この有志スタッフは随時募集しています。これに参加する人の動機はそれぞれですが、眼鏡祭の魅力に惹かれたのは確かでしょう。何が出来るか分からないという人でも、会場を立食レイアウトに変更する作業は人手が多いほど助かるため、まずはこれに手を上げるのがオススメなようです。

おわりに

主宰のマソーさんはじめ運営スタッフのみなさん、会場で私の相手をしてくれた方々、ならびにこの日記でtweetを引用*1させていただいた方々、今回の眼鏡祭も大変たのしく過ごせました。今後ともtwtterやら他イベントやらでお会いしたらよろしくお願いします。

リンク一覧

*1:不都合があればお手数ですが https://twitter.com/kagamihoge へ連絡願います。

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メモリアクセスにおいてより汎用で効果的な方法で使えるかもしれません。