アクセス制御リストをグラフDBで構築 - Neo4j

柔軟性のあるアクセス制御を考えた際に、アクセス制御リスト(ACL)を有向グラフで表現すればどうだろうかと思い、グラフDBの Neo4j を使って試してみました。

概要

アクセス制御リストを有向グラフで表現し、アクセスの許可・拒否を以下のように判定する事にします。

  • アクセスの主体からアクセス対象へのパス(経路)が存在すればそのアクセスを許可する

例えば、user1 が service1 へアクセスできる事(アクセス許可が与えられている)を以下のように表現する事にします。

(a)

f:id:fits:20170703224725p:plain

ただ、これだと自由度が高すぎると思うのでルールを加えます。

  • アクセスの主体となる全てのノードは Principals ノードへ所属させる
  • アクセスの対象となる全てのノードは Resources ノードへ所属させる
  • アクセスの主体同士と対象同士でエッジ(辺)の向きを調整

これに基づいてノードやエッジをいくつか追加してみた結果が以下です。

(b)

f:id:fits:20170703224809p:plain

admin ユーザーは Resources に属する全てのリソース(service1, service2, service2 の post や get)へアクセスでき、group1 へ所属する user2 は service2.post へアクセスできる事を表現しています。

Neo4j で検証

それでは実際に Neo4j を使って検証してみます。

Neo4j ではエッジ(辺)に該当する部分をリレーションシップと呼ぶようですが、以降もエッジと呼ぶことにします。

(1) Neo4j サーバー起動

まずは Neo4j のサーバーを起動しておきます。 Neo4j の bin ディレクトリに neo4j コマンドがあるので、これを使って起動します。

今回は neo4j console で Neo4j サーバーを起動しますが、neo4j start で実行する方法もあります。

Neo4j サーバー起動
> neo4j console

サーバーが起動した後、http://localhost:7474/ へ Web ブラウザでアクセスすると管理画面が表示されるので、初期パスワードに neo4j と入力して初回のログイン処理を行っておきます。

管理画面では Cypher と呼ばれるクエリ言語を使ってデータを操作できるようになっているので、これより Cypher でデータ操作していきます。

(2) グラフデータの作成

先ほどの (a) のグラフ(user1 と service1 ノードを PERMIT エッジで繋ぐ)は以下のような Cypher クエリで作成できます。

1. (a) のデータ作成 Cypher クエリ
CREATE (u:User{oid:"user1",name:"user1"})-[r:PERMIT]->(s:Service{oid:"service1",name:"service1"})
RETURN u,r,s

f:id:fits:20170703224725p:plain

CREATE でノードやエッジを作成でき、ノードの内容は (変数名:ラベル{プロパティ名:値, ・・・})、エッジの内容は [変数名:関連名{プロパティ名:値, ・・・}] のように指定できるようです。

A から B ノードへ向いたエッジを作成する際は (A)-[エッジ]->(B) のようにします

RETURN を使うと指定した変数の内容が返され、管理画面では RETURN の内容を SVG で表示してくれます。※

 ※ Cypher クエリ実行毎の左メニューの "Graph" を選択すると SVG のグラフ表示で、
    "Code" を選択すると Response の JSON を確認できます

ノードやエッジには一意の id が付与されますが、ノードを指定するのに不便なので、今回は oid という独自プロパティを設定するようにしました。

管理画面では name プロパティの値を表示するようなので name も設定しています。

次に、(a) の状態に対してノードやエッジを追加して (b) の状態にしていきます。

今度はいくつかのステップに分けてデータを追加します。

まずは principals, resources, admin を作成し、user2 と PART_OF エッジで繋いだ group1 を作成しました。

2. principals, resources, admin, user2, group1 の追加
CREATE (:Principals{name:"principals"}),(:Resources{name:"resources"})
CREATE (:User{oid:"admin",name:"admin"})
CREATE (:User{oid:"user2",name:"user2"})-[:PART_OF]->(:Group{oid:"group1",name:"group1"})

次は get と post というメソッドを持った service2 を追加します。 ここでは service2 を作った後で、METHOD エッジで繋がった get と post を作成するようにしてみました。

3. get と post を持つ service2 の追加
CREATE (s2:Service{oid:"service2",name:"service2"})
MERGE (s2)-[:METHOD]->(:Operation{oid:"service2.get",name:"get"})
MERGE (s2)-[:METHOD]->(:Operation{oid:"service2.post",name:"post"})

次に PERMIT エッジを作成していくつかのノードを繋ぎます。

4. group1 から service2.post への PERMIT を設定
MATCH (g1:Group{oid:"group1"}),(m1:Operation{oid:"service2.post"})
MERGE (g1)-[:PERMIT]->(m1)
5. admin から resources への PERMIT を設定
MATCH (au:User{oid:"admin"}),(r:Resources)
MERGE (au)-[:PERMIT]->(r)

これまでに作成した全ての User と Group が principals へ所属するように、全ての Service が resources へ所属するようにエッジで繋ぎます。

6. principals と resources へのエッジをそれぞれ設定
MATCH (u:User),(g:Group),(s:Service),(p:Principals),(r:Resources)
MERGE (u)-[:PART_OF]->(p)
MERGE (g)-[:PART_OF]->(p)
MERGE (r)-[:RESOURCE]->(s)

最後に全てのデータを表示します。

全表示

MATCH (a) RETURN a

f:id:fits:20170703224809p:plain

(3) アクセス許可の判定(経路の探索)

このデータに対して、どのような Cypher クエリを使って経路探索(= アクセス許可の確認)すればよいのか検証していきます。

単独エッジ

まずは、単純に user1 から service1 へ PERMIT で繋がっているかどうかは、以下のクエリで確認できます。

user1 から service1 へ PERMIT で直接繋がっているものを取得
MATCH (u:User{oid:"user1"})-[:PERMIT]->(s:Service{oid:"service1"})
RETURN u,s

f:id:fits:20170703224957p:plain

このクエリでは PERMIT エッジで直接繋がっているケースにしか対応できないので汎用的には使えません。

複数エッジ(条件なし)

次に、PERMIT で繋がっているかどうかは気にしないで、user2 から service2.post へのパス(経路)を取得してみます。

エッジの条件を [*] とする事で、複数のエッジで繋がっているパスを取得できます。

また、MATCH で指定したパスを変数(以下の path)へ設定して、これを RETURN するようにすればパスの内容を取得できます。

user2 から service2.post へのパスを取得
MATCH path=(:User{oid:"user2"})-[*]->(:Operation{oid:"service2.post"})
RETURN path

f:id:fits:20170703225057p:plain

複数エッジ(条件あり)

上記のクエリに対して、PERMIT のエッジを少なくとも 1つ含むという条件を加えるには whereany が使えます。

relationships(変数) を使って変数の中からエッジのみを抽出する事が可能です。 (nodes(変数) を使えばノードだけを抽出できる)

type(変数) を使えばラベル名を取得できますので、これを使えば PERMIT かどうかの判定が可能です。

user2 から service2.post へのパス(PERMIT を含む)を取得
MATCH path=(:User{oid:"user2"})-[*]->(:Operation{oid:"service2.post"})
WHERE any(r IN relationships(path) WHERE type(r) = "PERMIT")
RETURN path

f:id:fits:20170703225057p:plain

このクエリなら汎用的に使えそうです。

ノードのラベル(今回の User, Service 等)を特定する必要が無ければ、以下のようにプロパティの条件だけを指定する事も可能です。

admin から service1 へのパス(PERMIT を含む)を取得
MATCH path=({oid:"admin"})-[*]->({oid:"service1"})
WHERE any(r IN relationships(path) WHERE type(r) = "PERMIT")
RETURN path

f:id:fits:20170703225200p:plain

user1 と service1 もこの方法で取得できます。

user1 から service1 へのパス(PERMIT を含む)を取得
MATCH path=(:User{oid:"user1"})-[*]->({oid:"service1"})
WHERE any(r IN relationships(path) WHERE type(r) = "PERMIT")
RETURN path

f:id:fits:20170703225231p:plain

最後に、パスが繋がっていない場合も確認しておきます。 当然ながら結果は空となりました。

user2 から service1 へのパス(PERMIT を含む)を取得
MATCH path=(u:User{oid:"user2"})-[*]->({oid:"service1"})
WHERE any(r IN relationships(path) WHERE type(r) = "PERMIT")
RETURN path

f:id:fits:20170703225256p:plain

備考

relationships を使う以外に [rs *] のように変数を使う方法でも同じ結果が得られます。

user2 から service2.post へのパス(PERMIT を含む)を取得 - deprecated
MATCH path=(:User{oid:"user2"})-[rs *]->(:Operation{oid:"service2.post"})
WHERE any(r IN rs WHERE type(r) = "PERMIT")
RETURN path

こちらの方がシンプルなように思うのですが、このクエリでは Binding relationships to a list in a variable length pattern is deprecated と警告されましたので、使わない方が無難かもしれません。