アクセス制御リストをグラフDBで構築 - Neo4j
柔軟性のあるアクセス制御を考えた際に、アクセス制御リスト(ACL)を有向グラフで表現すればどうだろうかと思い、グラフDBの Neo4j を使って試してみました。
概要
アクセス制御リストを有向グラフで表現し、アクセスの許可・拒否を以下のように判定する事にします。
- アクセスの主体からアクセス対象へのパス(経路)が存在すればそのアクセスを許可する
例えば、user1 が service1 へアクセスできる事(アクセス許可が与えられている)を以下のように表現する事にします。
(a)
ただ、これだと自由度が高すぎると思うのでルールを加えます。
- アクセスの主体となる全てのノードは Principals ノードへ所属させる
- アクセスの対象となる全てのノードは Resources ノードへ所属させる
- アクセスの主体同士と対象同士でエッジ(辺)の向きを調整
これに基づいてノードやエッジをいくつか追加してみた結果が以下です。
(b)
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
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
(3) アクセス許可の判定(経路の探索)
このデータに対して、どのような Cypher クエリを使って経路探索(= アクセス許可の確認)すればよいのか検証していきます。
単独エッジ
まずは、単純に user1 から service1 へ PERMIT で繋がっているかどうかは、以下のクエリで確認できます。
user1 から service1 へ PERMIT で直接繋がっているものを取得
MATCH (u:User{oid:"user1"})-[:PERMIT]->(s:Service{oid:"service1"}) RETURN u,s
このクエリでは PERMIT エッジで直接繋がっているケースにしか対応できないので汎用的には使えません。
複数エッジ(条件なし)
次に、PERMIT で繋がっているかどうかは気にしないで、user2 から service2.post へのパス(経路)を取得してみます。
エッジの条件を [*]
とする事で、複数のエッジで繋がっているパスを取得できます。
また、MATCH で指定したパスを変数(以下の path
)へ設定して、これを RETURN するようにすればパスの内容を取得できます。
user2 から service2.post へのパスを取得
MATCH path=(:User{oid:"user2"})-[*]->(:Operation{oid:"service2.post"}) RETURN path
複数エッジ(条件あり)
上記のクエリに対して、PERMIT のエッジを少なくとも 1つ含むという条件を加えるには where
と any
が使えます。
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
このクエリなら汎用的に使えそうです。
ノードのラベル(今回の User, Service 等)を特定する必要が無ければ、以下のようにプロパティの条件だけを指定する事も可能です。
admin から service1 へのパス(PERMIT を含む)を取得
MATCH path=({oid:"admin"})-[*]->({oid:"service1"}) WHERE any(r IN relationships(path) WHERE type(r) = "PERMIT") RETURN path
user1 と service1 もこの方法で取得できます。
user1 から service1 へのパス(PERMIT を含む)を取得
MATCH path=(:User{oid:"user1"})-[*]->({oid:"service1"}) WHERE any(r IN relationships(path) WHERE type(r) = "PERMIT") RETURN path
最後に、パスが繋がっていない場合も確認しておきます。 当然ながら結果は空となりました。
user2 から service1 へのパス(PERMIT を含む)を取得
MATCH path=(u:User{oid:"user2"})-[*]->({oid:"service1"}) WHERE any(r IN relationships(path) WHERE type(r) = "PERMIT") RETURN path
備考
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
と警告されましたので、使わない方が無難かもしれません。