本記事は Qiita Dify Advent Calendar 2024 の 15日目の記事です。
前置き
会社で Dify Communitiy版(セルフホスト環境)を AWS で動かしています。
構成は Dify on AWS with CDK に限りなく近いですが
- Terraform で構築
- ElastiCache Valkey を利用(Redis から移行しました)
が大きな違いでしょうか。
あとは、社員限定アクセスとするため ALB で Google 認証いれてます。
と、こんな感じの環境で社内で活発に利用され始めました。
みなさんにどんどんアプリを作ってもらいたいので基本的に エディター
権限を付与しています。
そうすると、一部の部署から(若干)機密性のある資料をナレッジにいれて利用したいという要望がありました。
Dify上で、ナレッジへのアクセスはナレッジ単位で制御できる設定はあるので良いのですが、会話ログも他の人に見られたくないとのこと。
現状の Dify では エディター
権限あれば、他の人が作ったアプリの会話ログも見えてしまいます。
この記事では、この会話ログへのアクセスを
- Difyのソースは変更せずに、オーナー・管理者・アプリ作成者に制限する仕組み
について書きます。
なお、一旦画面を使ってのアクセスのみについて対処するものです。
事前注意事項
本記事での方法は v0.13.2 の時点で動作できているものですが、将来にわたりその動作が保証されるものではありません。
また、本記事を参考に同等なものを作成し、それによって生じるいかなる結果も当方では責任はとれません。
仕組み
おおまかな仕組みは以下の通りです。
- API の前段にリーバースプロクシとして Nginx(実際は OpenResty)を配置
/console/api/apps/<uuid>/chat-conversations
へのアクセス時に Lua を動かす- Lua でDBにアクセスし、閲覧して良い人かどうかを判断する
- OKなら API コンテナにリバースプロクシする
- NGなら 403 Forbidden にする
以下で、もう少し詳しく書いていきます。
リバースプロクシ
今回、Lua を利用するのでOpenRestyコンテナを利用しています。 そこで、以下のような設定をしてあります。
location ~ /console/api/apps/(?<app_id>[^/]+)/chat-conversations { access_by_lua_file '/usr/local/openresty/lua-scripts/auth.lua'; proxy_pass http://localhost:5001; include proxy.conf; }
localhost:5001
が API コンテナのアクセス先になります。
ECSにおいて API コンテナと OpenResty コンテナを同一タスク内で定義しているので、このようなアクセス方法となっています。
また、ログ&アナウンス
は
/app/<uuid>/logs
というパスですが、その中の処理で実際の会話ログが取得されるようになっており、その際のアクセス先が
/console/api/apps/<uuid>/chat-conversations
というようなものなので、そこへのアクセスをきっかけに Lua が実行されるようになっています
Lua
Lua でやっていることはおおよそ以下の通りです(ソース公開は控えさせていただきますが以下を愚直に実装しているだけです)
Authorization
ヘッダーをデコードして、user_id を得る。- リクエストURIに含まれるアプリケーションIDをキーにDBに問い合わせ、アプリケーションの作者の id を得る。
- アプリケーション作者の場合は
許可
となる
- アプリケーション作者の場合は
- user_id をキーにしてDBに問い合わせ、そのユーザーのロールを取得する。
- ロールが
owner
oradmin
であれば、許可
となる
- ロールが
- 上記に該当しない場合は 403 Forbidden となる
これらのポイントとなる情報を後は説明して終わりにします
user_id の取得
エンドポイントへのアクセス時に、
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNDRmNTVlOTUtMDA3OC00YTJhLWIzMjMtYWQzNDZmNDYzMjkyIiwiZXhwIjoxNzMwMzQ2MzkzLCJpc3MiOiJTRUxGX0hPU1RFRCIsInN1YiI6IkNvbnNvbGUgQVBJIFBhc3Nwb3J0In0.ymnvkzc96HnWX26CZ7i3vCAl9h8knLZNmTB6lMwGro0
のようなリクエストヘッダーが付与されます。
このトークンは https://github.com/langgenius/dify/blob/0.13.2/api/extensions/ext_login.py#L23-L37 にあるように
decoded = PassportService().verify(auth_token)
user_id = decoded.get("user_id")
にてデコードされて user_id を取得できるものです
そしてデコード自体は https://github.com/langgenius/dify/blob/0.13.2/api/libs/passport.py#L16 にあるように
jwt.decode(token, self.sk, algorithms=["HS256"])
JWTライブラリを使ってデコードされています
self.sk
は環境変数で与えている SECRET_KEY
なので、同等なデコードの処理を Lua で行うことにより user-id を取得できるということです
アプリケーション作成者
アプリケーションの情報は apps
テーブルにあります
Table "public.apps" Column | Type | Collation | Nullable | Default -------------------------+-----------------------------+-----------+----------+----------------------------- id | uuid | | not null | uuid_generate_v4() tenant_id | uuid | | not null | name | character varying(255) | | not null | mode | character varying(255) | | not null | icon | character varying(255) | | | icon_background | character varying(255) | | | app_model_config_id | uuid | | | status | character varying(255) | | not null | 'normal'::character varying enable_site | boolean | | not null | enable_api | boolean | | not null | api_rpm | integer | | not null | 0 api_rph | integer | | not null | 0 is_demo | boolean | | not null | false is_public | boolean | | not null | false created_at | timestamp without time zone | | not null | CURRENT_TIMESTAMP(0) updated_at | timestamp without time zone | | not null | CURRENT_TIMESTAMP(0) is_universal | boolean | | not null | false workflow_id | uuid | | | description | text | | not null | ''::character varying tracing | text | | | max_active_requests | integer | | | icon_type | character varying(255) | | | created_by | uuid | | | updated_by | uuid | | | use_icon_as_answer_icon | boolean | | not null | false Indexes: "app_pkey" PRIMARY KEY, btree (id) "app_tenant_id_idx" btree (tenant_id) Referenced by: TABLE "tool_published_apps" CONSTRAINT "tool_published_apps_app_id_fkey" FOREIGN KEY (app_id) REFERENCES apps(id)
URLのpathに含まれる UUID がこのテーブルの id
の値なので、そこから create_by
カラムを見れば作者の user_id がわかります。
そして、認証情報に含まれる user_id と比較すれば、アクセスしている人がアプリケーションの作者かどうか判断できるということです。
オーナー、管理者
ロールは tenant_account_joins
で設定されているようです
Table "public.tenant_account_joins" Column | Type | Collation | Nullable | Default ------------+-----------------------------+-----------+----------+----------------------------- id | uuid | | not null | uuid_generate_v4() tenant_id | uuid | | not null | account_id | uuid | | not null | role | character varying(16) | | not null | 'normal'::character varying invited_by | uuid | | | created_at | timestamp without time zone | | not null | CURRENT_TIMESTAMP(0) updated_at | timestamp without time zone | | not null | CURRENT_TIMESTAMP(0) current | boolean | | not null | false Indexes: "tenant_account_join_pkey" PRIMARY KEY, btree (id) "tenant_account_join_account_id_idx" btree (account_id) "tenant_account_join_tenant_id_idx" btree (tenant_id) "unique_tenant_account_join" UNIQUE CONSTRAINT, btree (tenant_id, account_id)
account_id
が user_id なので、これからアクセスしている人のロールを取得できます
まとめ
Dify Community版において、Dify本体を修正せずに、会話ログの閲覧を制限する仕組みを作ってみました
という仕組みでした
以上、最後までご覧いただきありがとうございました