4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この記事の目的

前回、前々回の記事では Open Policy Agent (OPA) と認可アーキテクチャの概要を話してきました。

本記事では、OPA を組み込んだ OSS である Aserto Topaz を動かしながら、アプリケーションレイヤでの認可の実装例を確認していきます。

11 月の OpenStandia メールマガジンでもご紹介させていただきました。
メルマガもどうぞよろしくお願いいたします。。。
https://openstandia.jp/mm/mm20241113.html

Aserto Topaz とは

image.png

Aserto Topaz は「Open Policy Agent によるポリシー判定アーキテクチャ」および「Google Zanzibar1 におけるデータモデル」の利点をかけ合わせ、さらに管理コンソールやディレクトリを付属させ、Open Container Initiative (OCI) 仕様のポリシーライフサイクル管理機能・ログ集約機能なども備えた、アプリケーションレイヤでの認可実装をより容易にする機能を豊富に持つ認可 OSS です。

image.png

Topaz はマイクロサービス、あるいはサイドカーとしてホストすることが想定されています2

OPA との最大の違いはディレクトリとして BoltDB が組み込まれており、よりリッチなデータ情報に基づく認可判定 (ReBAC) を実現できることです。
また、もちろん OPA と同様にポリシーを用いた判定も行うことができます。

アプリは Topaz と連携することで「ユーザが最低限の情報しか提供しなくても、アプリはユーザのデータを基に画面表示を適切に制御する」などの今まで実装が難しかった挙動の実現が期待できます。
連携できるアプリ向け言語も豊富で、公式から提供している SDK を用いて連携実装を行うようになっています。

ちなみに Aserto は Topaz を管理する企業であり、同社が提供する認可プラットフォームの名称でもあります。

image.png

Aserto のサービスラインナップとして見ると、認可プラットフォーム Aserto が利用する判定機能部分を Topaz として OSS 公開している形となります。
AuthZEN Working Group にも中核として参加しており、いわば認可のリーディングカンパニーと言える立場にある企業です。

さっそく起動してみる

公式 ドキュメントの Getting Started を見ながら動かしていきます。
Getting Started にて提供されているサンプルアプリを使って認可の動きを確かめます。

Getting Started 動作環境として Docker 環境が求められています。
その上 Rootless な Docker 環境であることが前提となっています。
※ 筆者はココで引っかかりました。
本動作確認に先立って Arch Linux にて Rootless Docker 環境を作成したうえで実施しました。

まずは topaz CLI のインストールから行います。
topaz CLI は Topaz インスタンスの管理を単純化するために提供されています。
今回は GitHub のリリースから最新版 (v0.32.38) を取得して利用しました。

# 取得
wget https://github.com/aserto-dev/topaz/releases/download/v0.32.38/topaz_linux_x86_64.zip
# 展開
7z x topaz_linux_x86_64.zip
# パスが通っている場所に移動する
sudo mv topaz /usr/local/bin/
sudo mv topazd /usr/local/bin/
# 確認
topaz version
> topaz 0.32.38 gdf63b9a linux-amd64 [2024-11-26T22:05:27Z]

次に Topaz で利用する証明書をストアに登録できるようにします。
※ 証明書を正しく読み込めるかどうかが Getting Started を完遂できるかの肝です。

topaz certs trust
> certs directory: /home/caunu-s/.local/share/topaz/certs
> Adding trust to dev cert was requested. 
> Trusting the certificate on Linux distributions automatically is not supported. 
> For instructions on how to manually trust the dev cert on your Linux distribution, 
> go to: https://www.topaz.sh/docs/command-line-interface/topaz-cli/certs

では Topaz の Docker イメージを取得します。

topaz install
> >>> installing ghcr.io/aserto-dev/topaz:0.32.38 (linux/amd64)...

続いて Topaz 立ち上げです。
今回 Topaz はローカルで独立したコンテナとして立ち上げます。
ポリシーなどのサンプルテンプレートが準備されているのでそのまま使います。

topaz templates install todo
> Installing this template will completely reset your topaz configuration.
> Do you want to continue? (y/N) y
> >>> stopping topaz...
> >>> topaz is not running
> >>> configure policy
> certs directory: /home/caunu-s/.local/share/topaz/certs
>   FILE            ACTION     
>   gateway-ca.crt  generated  
>   gateway.crt     generated  
>   gateway.key     generated  
>   grpc-ca.crt     generated  
>   grpc.crt        generated  
>   grpc.key        generated  
> policy name: todo
> Using configuration "todo"
> >>> starting topaz "todo"...
> 3be74303533f51ac9d0230a3f6d4e2149e6cdb72044f6a764e40f263ef684b69
> 
> WARNING: delete manifest resets all directory state, including relation and object data
> >>> delete manifest
> >>> set manifest to /home/caunu-s/.local/share/topaz/tmpl/todo/model/manifest.yaml
> >>> importing data from /home/caunu-s/.local/share/topaz/tmpl/todo/data
> objects   : 20 (set:20 delete:0 error:0)
> relations : 25 (set:25 delete:0 error:0)
> dial tcp: lookup localhost on 127.0.0.1:53: no such host

# 確認
docker ps -a
> CONTAINER ID   IMAGE                              COMMAND                  CREATED          STATUS          PORTS                                                                                                                                                                                                                                                                                                                             NAMES
> 3be74303533f   ghcr.io/aserto-dev/topaz:0.32.38   "./topazd run --conf…"   10 seconds ago   Up 10 seconds   0.0.0.0:8080-8081->8080-8081/tcp, :::8080-8081->8080-8081/tcp, 0.0.0.0:8282->8282/tcp, :::8282->8282/tcp, 0.0.0.0:8383->8383/tcp, :::8383->8383/tcp, 0.0.0.0:9292->9292/tcp, :::9292->9292/tcp, 0.0.0.0:9393->9393/tcp, :::9393->9393/tcp, 0.0.0.0:9494->9494/tcp, :::9494->9494/tcp, 0.0.0.0:9696->9696/tcp, :::9696->9696/tcp   topaz-todo

起動中に様々なものが入ったので確認していきます。

tree $HOME/.local/share/topaz
> /home/caunu-s/.local/share/topaz
> ├── certs
> │   ├── gateway-ca.crt
> │   ├── gateway.crt
> │   ├── gateway.key
> │   ├── grpc-ca.crt
> │   ├── grpc.crt
> │   └── grpc.key
> ├── db
> │   └── todo.db
> └── tmpl
>     └── todo
>         ├── data
>         │   ├── citadel_objects.json
>         │   ├── citadel_relations.json
>         │   ├── todo_objects.json
>         │   └── todo_relations.json
>         └── model
>             └── manifest.yaml

証明書がたくさん入っていますが、これらは設定ファイル todo.yaml の中で各サービスで利用するように設定されています。
今回 BoltDB の中身はローカルに保存するように設定したみたいですね。
/tmpl/todo 配下からはおいしい香りがするので動作確認するときに取っておきましょう。

~/.config/topaz/topaz.json
{
    "version": 1,
    "active": {
        "config": "todo",
        "config_file": "/home/caunu-s/.config/topaz/cfg/todo.yaml"
    },
    "running": {
        "config": "todo",
        "config_file": "/home/caunu-s/.config/topaz/cfg/todo.yaml",
        "container_name": "topaz-todo"
    },
    "defaults": {
        "no_check": false,
        "no_color": false,
        "container_registry": "",
        "container_image": "",
        "container_tag": "",
        "container_platform": ""
    }
}

設定ファイルに todo.yaml を使うことは topaz.json へ記載することがわかります。
このファイルが設定ファイル群の親玉ですね。

~/.config/topaz/cfg/todo.yaml
# yaml-language-server: $schema=https://topaz.sh/schema/config.json
---
# config schema version
version: 2

# logger settings.
logging:
  prod: true
  log_level: info
  grpc_log_level: info

# edge directory configuration.
directory:
  db_path: '${TOPAZ_DB_DIR}/todo.db'
  request_timeout: 5s # set as default, 5 secs.

# remote directory is used to resolve the identity for the authorizer.
remote_directory:
  address: "0.0.0.0:9292" # set as default, it should be the same as the reader as we resolve the identity from the local directory service.
  tenant_id: ""
...

# default jwt validation configuration
jwt:
  acceptable_time_skew_seconds: 5 # set as default, 5 secs

# authentication configuration
auth:
  keys:
    # - "<API key>"
    # - "<Password>"
  options:
    default:
      enable_api_key: false
      enable_anonymous: true
    overrides:
      paths:
        - /aserto.authorizer.v2.Authorizer/Info
        - /grpc.reflection.v1.ServerReflection/ServerReflectionInfo
        - /grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo
      override:
        enable_api_key: false
        enable_anonymous: true

api:
  health:
    listen_address: "0.0.0.0:9494"

  metrics:
    listen_address: "0.0.0.0:9696"
    zpages: true

  services:
    console:
      grpc:
        listen_address: "0.0.0.0:8081"
        fqdn: ""
        certs:
          tls_key_path: '${TOPAZ_CERTS_DIR}/grpc.key'
          tls_cert_path: '${TOPAZ_CERTS_DIR}/grpc.crt'
          tls_ca_cert_path: '${TOPAZ_CERTS_DIR}/grpc-ca.crt'
      gateway:
        listen_address: "0.0.0.0:8080"
...
        certs:
          tls_key_path: '${TOPAZ_CERTS_DIR}/gateway.key'
          tls_cert_path: '${TOPAZ_CERTS_DIR}/gateway.crt'
          tls_ca_cert_path: '${TOPAZ_CERTS_DIR}/gateway-ca.crt'
...
    model:
      grpc:
        listen_address: "0.0.0.0:9292"
...
      gateway:
        listen_address: "0.0.0.0:9393"
...
    reader:
      needs:
        - model
      grpc:
        listen_address: "0.0.0.0:9292"
...
      gateway:
        listen_address: "0.0.0.0:9393"
...
    writer:
      needs:
        - model
      grpc:
        listen_address: "0.0.0.0:9292"
...
      gateway:
        listen_address: "0.0.0.0:9393"
...
    exporter:
      grpc:
        listen_address: "0.0.0.0:9292"
...
    importer:
      needs:
        - model
      grpc:
        listen_address: "0.0.0.0:9292"
...
    authorizer:
      needs:
        - reader
      grpc:
        connection_timeout_seconds: 2
        listen_address: "0.0.0.0:8282"
...
      gateway:
        listen_address: "0.0.0.0:8383"
...
opa:
  instance_id: "-"
  graceful_shutdown_period_seconds: 2
  # max_plugin_wait_time_seconds: 30 set as default
  local_bundles:
    paths: []
    skip_verification: true
  config:
    services:
      ghcr:
        url: https://ghcr.io
        type: "oci"
        response_header_timeout_seconds: 5
    bundles:
      todo:
        service: ghcr
        resource: "ghcr.io/aserto-policies/policy-todo:3.0.0"
        persist: false
        config:
          polling:
            min_delay_seconds: 60
            max_delay_seconds: 120

todo.yaml には証明書や使用するポートの指定、各サービスの依存関係などが記載されています。
また、gRPC による実装も行っていることもわかります。
gRPC UI からリクエストをバッチリ送れちゃいますね。

grpcui --insecure localhost:8282
> gRPC Web UI available at http://127.0.0.1:44435/
> Opening in existing browser session.

grpcui --insecure localhost:9292
> gRPC Web UI available at http://127.0.0.1:43375/
> Opening in existing browser session.

picture-1.png

picture-2.png

OPA の設定も todo.yaml にありました。特に注目したいのが bundles 配下の resource です。

Topaz では policy CLI を利用することでポリシーを Docker イメージのように取り扱い管理します。
実際に今回つかうポリシーは Github Container Registry で管理されていることがわかります。

image.png

ちなみに policy CLI ワークフローを提供しているのが opcr です。
Topaz と似たかわいいアイコンがつけられています。

opcr.png

ここまでで Topaz の立ち上げをしてきたので、バックエンド (Go サーバ) とフロントエンド (React アプリ) のサンプルアプリも立ち上げていきます。

# バックエンド
git clone https://github.com/aserto-demo/todo-go-v2
cp .env.example .env
vim .env
> ASERTO_GRPC_CA_CERT_PATH='/$HOME/.local/share/topaz/certs/grpc-ca.crt'
go mod tidy
go run .
> 5:15PM INF options loaded authorizer=localhost:8282 directory=localhost:9292
> 5:15PM INF starting server listen_address=0.0.0.0:3001

# フロントエンド
git clone https://github.com/aserto-demo/todo-application.git
cd todo-application
yarn
yarn start
> Compiled successfully!
> You can now view client in the browser.
>   Local:            http://localhost:3000
> ...

localhost:3000 でアプリが立ち上がりました。

image.png

中身を見ていく

まずはチュートリアル通りに動かします。

  • rick@the-citadel.com
  • V@erySecre#t123!

でログインします。

picture-3.png

todo リストを更新します。

picture-4.png

問題なく行えますね。

次に

  • morty@the-citadel.com
  • V@erySecre#t123!

でログインします。

picture-6.png

morty@the-citadel.com さんは自分の todo を追加するのも完了状態にするのも問題なく行えます。

picture-8.png

では morty@the-citadel.com さんが rick@the-citadel.com さんの todo を削除してみます。

picture-9.png

できませんでした。
この仕組みを見ていこうと思います。

何はともあれ、Topaz の判定ロジックが気になります。
API を叩くことでどんな判定が行われるかわかります。

curl -k -X POST 'https://localhost:8383/api/v2/authz/is' \
-H 'Content-Type: application/json' \
-d '{
     "identity_context": {
          "type": "IDENTITY_TYPE_SUB",
          "identity": "rick@the-citadel.com"
     },
     "policy_context": {
          "path": "todoApp.GET.todos",
          "decisions": ["allowed"]
     }
}'

> {
>   "decisions":  [
>     {
>       "decision":  "allowed",
>       "is":  true
>     }
>   ]
> }

上記で rick@the-citadel.com さんは todoApp に GET できることがわかりますが、そもそもインタフェースがわからないと API へどのようにリクエストを送ったらいいかすらわかりません。
v2/policies API を叩いてもいいですが、大量に出てくるであろうドキュメントを捌くのは少し億劫です。

そこで Topaz Console (管理GUI) を見てみます。

picture-11.png

ポリシーはこの画面から管理できますね。
ちなみに管理 GUI では判定の確認や API 情報、データモデル情報なんかも参照できます。

picture-12.png

picture-13.png

ここでは削除に関するポリシーだけ見てみます。3

todoApp.common
package todoApp.common

is_member_of(user, group) := x {
	x := ds.check({
		"object_id": group,
		"object_type": "group",
		"relation": "member",
		"subject_id": user.id,
		"subject_type": "user",
	})
}

check(user, permission, todo) := x {
	x := ds.check({
		"object_id": todo,
		"object_type": "resource",
		"relation": permission,
		"subject_id": user.id,
		"subject_type": "user",
	})
}
todoApp.PUT.todos.__id
package todoApp.PUT.todos.__id

# This policy determines whether the user can complete a specific todo identified by input.resource.object_id

import data.todoApp.common.check
import data.todoApp.common.is_member_of
import future.keywords.in
import input.resource
import input.user

default allowed = false

# check if the user has the can_write permission on the resource
# (example of evaluating a permission on a specific resource)
allowed {
	check(user, "can_write", resource.object_id)
}

# check if the user is a member of the allowed groups
# (example of group-based RBAC)
allowed {
	allowedGroups := {"admin", "evil_genius"}
	some group in allowedGroups
	is_member_of(user, group)
}
todoApp.DELETE.todos.__id
package todoApp.DELETE.todos.__id

# This policy determines whether the user can delete the todo identified by input.resource.object_id

import data.todoApp.common.check
import data.todoApp.common.is_member_of
import input.resource
import input.user

default allowed = false

# check if the user has the can_delete permission on the resource
# (example of evaluating a permission on a specific resource)
allowed {
	check(user, "can_delete", resource.object_id)
}

# check if the user is a member of the admin group
# (example of group-based RBAC)
allowed {
	is_member_of(user, "admin")
}

まずパッケージの分け方ですが、共通関数をくくりだして一つのファイルとしています。
前回の記事で「テナントわけにも使えそう」とは述べたものの、(やはり) モジュールレベルでの管理として利用されています。

どうやら共通関数を用いて

  • 操作するユーザが権限を持っているか
  • 操作を許されたグループに所属しているか

を確認しているようです。

... これだけでは先ほどの動作 (他者のものは編集できない) は説明つきませんね。

ということで取っておいた /tmpl/todo 配下を確認してみます。

manifest.yaml
# yaml-language-server: $schema=https://www.topaz.sh/schema/manifest.json
---

# model
model:
  version: 3

# object type definitions
types:
  # user represents a user that can be granted role(s)
  user:
    relations:
      manager: user

    permissions:
      ### display_name: user#in_management_chain ###
      in_management_chain: manager | manager->in_management_chain


  # group represents a collection of users and/or (nested) groups
  group:
    relations:
      member: user | group#member


  # identity represents a collection of identities for users
  identity:
    relations:
      identifier: user


  # resource creator represents a user type that can create new resources
  resource-creator:
    relations:
      member: user | group#member

    permissions:
      can_create_resource: member


  # resource represents a protected resource
  resource:
    relations:
      owner: user
      writer: user | group#member
      reader: user | group#member

    permissions:
      can_read: reader | writer | owner
      can_write: writer | owner
      can_delete: owner

citadel_relation.json
{
  "relations": [
...
    {
      "object_type": "user",
      "object_id": "morty@the-citadel.com",
      "relation": "manager",
      "subject_type": "user",
      "subject_id": "rick@the-citadel.com"
    },
...
    {
      "object_type": "identity",
      "object_id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs",
      "relation": "identifier",
      "subject_type": "user",
      "subject_id": "rick@the-citadel.com"
    },
    {
      "object_type": "identity",
      "object_id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs",
      "relation": "identifier",
      "subject_type": "user",
      "subject_id": "morty@the-citadel.com"
    },
...
    {
      "object_type": "identity",
      "object_id": "morty@the-citadel.com",
      "relation": "identifier",
      "subject_type": "user",
      "subject_id": "morty@the-citadel.com"
    },
    {
      "object_type": "identity",
      "object_id": "rick@the-citadel.com",
      "relation": "identifier",
      "subject_type": "user",
      "subject_id": "rick@the-citadel.com"
    },
...
    {
      "object_type": "group",
      "object_id": "admin",
      "relation": "member",
      "subject_type": "user",
      "subject_id": "rick@the-citadel.com"
    },
    {
      "object_type": "group",
      "object_id": "editor",
      "relation": "member",
      "subject_type": "user",
      "subject_id": "morty@the-citadel.com"
    },
...
    {
      "object_type": "group",
      "object_id": "editor",
      "relation": "member",
      "subject_type": "group",
      "subject_id": "admin",
      "subject_relation": "member"
    },
    {
      "object_type": "group",
      "object_id": "evil_genius",
      "relation": "member",
      "subject_type": "user",
      "subject_id": "rick@the-citadel.com"
    },
...
    {
      "object_type": "group",
      "object_id": "viewer",
      "relation": "member",
      "subject_type": "group",
      "subject_id": "editor",
      "subject_relation": "member"
    }
  ]
}
citadel_objects.json
{
  "objects": [
...
    {
      "type": "group",
      "id": "evil_genius",
      "display_name": "evil_genius-group",
      "properties": {}
    },
...
    {
      "type": "group",
      "id": "admin",
      "display_name": "admin-group",
      "properties": {}
    },
...
    {
      "type": "group",
      "id": "viewer",
      "display_name": "viewer-group",
      "properties": {}
    },
...
    {
      "type": "user",
      "id": "rick@the-citadel.com",
      "display_name": "Rick Sanchez",
      "properties": {
        "email": "rick@the-citadel.com",
        "picture": "https://www.topaz.sh/assets/templates/citadel/img/Rick%20Sanchez.jpg",
        "roles": [
          "admin",
          "evil_genius"
        ],
        "status": "USER_STATUS_ACTIVE"
      }
    },
    {
      "type": "user",
      "id": "morty@the-citadel.com",
      "display_name": "Morty Smith",
      "properties": {
        "email": "morty@the-citadel.com",
        "picture": "https://www.topaz.sh/assets/templates/citadel/img/Morty%20Smith.jpg",
        "roles": [
          "editor"
        ],
        "status": "USER_STATUS_ACTIVE"
      }
    },
...
    {
      "type": "group",
      "id": "editor",
      "display_name": "editor-group",
      "properties": {}
    },
...
  ]
}
  • グループはサブグループとメンバーで構成される。
  • reader, writer, owner は can_read 権限をもつ。
  • writer, owner は can_write 権限をもつ。
  • owner は can_delete 権限をもつ。
  • rick@the-citadel.commorty@the-citadel.com のマネージャーである。
  • rick@the-citadel.com は admin, evil_genius メンバーに属する。
  • morty@the-citadel.com は editor メンバーに属する。
  • admin メンバーは editor メンバーにも属する。

等など、様々な関係データが掘り出せました。
ポリシーと照らし合わせると、

  • 誰でも誰のリソース (todo) を見れる。
  • 自分のものは変更できるし消せる。
  • 自分のものであるか admin 権限を持っていないと消せない。(完了は evil_genius 権限でよい)

ということが見えてきました。

冒頭に書いた

「Open Policy Agent によるポリシー判定アーキテクチャ」および「Google Zanzibar におけるデータモデル」の利点をかけ合わせ

が見えてきたような気がします。

まとめ

Aserto Topaz は OPA を拡張した認可サービスです。
二つ前の記事から読み進めていただいた方は、なんとなくでも理解が進められたのではないかなと思います。

認可アーキテクチャはまだまだ発展途上中で、いつ「本格的に導入せよ!」となるかわからないのが実情です。

こちらの記事を参考に、少しでも「最新の認可ってこうなってるんだ!」と感じていただけていたらうれしいです。

  1. 「誰が何に対してどのような権限(関係)を持つのか」「あれとこれはどのような関係をもつのか」を保持するデータモデルを元に柔軟な判定を実現する Google の認可判定基盤です。
    認可の道を行く者たちには ReBAC 認可モデルの実装例として知られています。
    グローバルに一貫性を保持しつつ、処理を高速化する仕組みを実現しているところがスゴイところです。

  2. バイナリでの配布も行われてはいますが、主流な使い方ではなさそうです。

  3. ポリシー全量を見たい場合は GitHub からご参照ください。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?