1
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?

プリザンターでテナント管理者による特権ユーザの削除を拡張機能だけで制限してみる

1
Posted at

はじめに

プリザンターの「特権ユーザ」は、サイトやレコードのアクセス権限を無視して最上位の権限を取得できる特別なユーザです。この特権ユーザをテナント管理者が誤って削除してしまうと、システム運用に重大な影響を及ぼす可能性があります。

しかし、プリザンターの標準機能ではユーザ削除時に「削除対象が特権ユーザかどうか」を検証する仕組みがありません。そこで今回は、拡張SQL(ExtendedSQL)だけでこの制限を実装できるか調査し、実際に実装してみます。

プリザンターのユーザ削除の仕組み

まず、プリザンターのソースコードからユーザ削除の流れを確認します。

削除の処理フロー

ユーザ削除は UserUtilities.Delete メソッドで以下の順に処理されます。

1. UserModel の生成(対象ユーザの情報を取得)
2. UserValidators.OnDeleting による検証
3. 検証 OK なら userModel.Delete を実行
4. 削除結果に応じてレスポンスを返す

OnDeleting の検証内容

UserValidators.OnDeleting では以下の検証がおこなわれます。

検証項目 内容
API 検証 API 経由の場合、API アクセス権を確認
ShowProfiles 検証 ShowProfiles が無効かつ特権ユーザでない場合は拒否
削除権限 context.CanDelete(ss) で削除権限を確認
ReadOnly 対象ユーザが読み取り専用でないことを確認

ここで重要なのは、「対象ユーザが特権ユーザかどうか」の検証がおこなわれていないという点です。つまり、テナント管理者に削除権限があれば、特権ユーザであっても削除できてしまいます。

特権ユーザの判定ロジック

プリザンターの特権ユーザは App_Data/Parameters/Security.jsonPrivilegedUsersLoginId のリストとして定義されています。

Security.json(抜粋)
{
    "PrivilegedUsers": ["admin", "superuser"]
}

内部的には Permissions.PrivilegedUsers メソッドで判定しています。

public static bool PrivilegedUsers(string loginId)
{
    return loginId != null &&
        Parameters.Security.PrivilegedUsers?.Contains(loginId) == true;
}

しかし、サーバスクリプトからは Parameters.Security.PrivilegedUsers に直接アクセスできません。これが拡張機能だけで実装する際の課題となります。

サーバスクリプトと拡張SQLの対応状況

BeforeDelete サーバスクリプトは Users では動作しない

調査の結果、BeforeDelete 拡張サーバスクリプトはユーザ管理画面(Users コントローラ)では実行されないことが判明しました。

ソースコードを確認すると、SetByBeforeDeleteServerScript を呼び出しているのは以下のモデルのみです。

モデル BeforeDelete 対応
ResultModel(レコード)
IssueModel(レコード)
WikiModel(Wiki) 1
DashboardModel(ダッシュボード) 1
UserModel(ユーザ)
GroupModel(グループ)
DeptModel(組織)

UserModel.Delete メソッドは SetByBeforeDeleteServerScript を呼び出さず、直接 SQL 文を実行して削除をおこないます。そのため、BeforeDelete フックにスクリプトを配置しても Users コントローラでは実行されません

OnDeleting 拡張SQLも Users では動作しない

同様に、OnDeletingExtendedSqls(削除時に追加の SQL を実行する仕組み)も Users コントローラでは呼び出されません。コード生成テンプレートに "ItemOnly": "1" が設定されており、レコード系モデル(Results、Issues、Wikis、Dashboards)のみが対象です。

使用可能な拡張SQL: OnSelectingWhere

一方、OnSelectingWhereView.Where() メソッドで呼び出されるため、Users コントローラを含むすべてのコントローラで動作します。この仕組みを使って、特権ユーザをユーザ一覧に表示しないようにすることで、テナント管理者が特権ユーザを選択・削除できなくなります。

拡張機能による実装方針

方針の検討

調査結果をふまえると、Users コントローラで使用できる拡張機能は OnSelectingWhere のみです。

拡張機能 Users での動作 用途
BeforeDelete サーバスクリプト ❌ 動作しない
OnDeleting 拡張SQL ❌ 動作しない
OnSelectingWhere 拡張SQL ✅ 動作する ユーザ一覧から特権ユーザを除外

OnSelectingWhere で特権ユーザを一覧から除外することで、UI 上で特権ユーザを選択できなくなり、結果として削除操作を防ぐことができます。

特権ユーザのリストを指定する方法

OnSelectingWhereCommandText に特権ユーザの LoginId を指定する方法として、以下の選択肢が考えられます。

方式 メリット デメリット
NOT IN 句にハードコーディング 最もシンプル Security.json と二重管理になる
管理テーブルを参照 DB で一元管理できる テーブルの作成・メンテナンスが必要
OPENROWSET で Security.json を直接読み取り Security.json と完全に同期できる SQL Server 限定。同一サーバ構成が前提2

ハードコーディング方式では、Security.json の PrivilegedUsers を変更した場合に拡張SQL 内のリストも合わせて更新する必要があります。

Controllers の値は小文字で指定してください。プリザンターは内部的にコントローラ名を小文字に変換して比較するため、"Users" ではなく "users" と記述する必要があります。

OnSelectingWhere によるユーザ一覧からの非表示

拡張SQLの OnSelectingWhere を使うと、ユーザ一覧の取得クエリに WHERE 条件を追加できます。特権ユーザをユーザ一覧に表示しないようにすることで、そもそも削除対象として選択できなくする方式です。

仕組み

プリザンターのユーザ一覧画面は、内部的に View.Where() メソッドで WHERE 句を構築しています。OnSelectingWheretrue に設定された拡張SQLは、この WHERE 句に条件として自動的に追加されます。

SELECT ... FROM [Users]
WHERE [TenantId] = @_T
  AND(OnSelectingWhere で追加した条件)  ← ここに追加される

ファイル構成

App_Data/Parameters/ExtendedSqls/ に設定ファイルを配置します。

App_Data/Parameters/ExtendedSqls/
└── HidePrivilegedUsers.json    ← 設定ファイル(CommandText 内に SQL を記述)

ハードコーディング方式

NOT IN 句に特権ユーザの LoginId を直接記述するシンプルな方式です。

ExtendedSqls/HidePrivilegedUsers.json
{
    "Name": "HidePrivilegedUsers",
    "Description": "特権ユーザをユーザ一覧から非表示にする",
    "Disabled": false,
    "Controllers": ["users"],
    "OnSelectingWhere": true,
    "CommandText": "AND \"Users\".\"LoginId\" NOT IN ('admin')"
}
設定項目 説明
Controllers ["users"] ユーザ管理画面でのみ適用
OnSelectingWhere true SELECT の WHERE 句に条件を追加
CommandText AND ... 追加する WHERE 条件(先頭に AND が必要)

複数の特権ユーザを除外する場合は、NOT IN 句にカンマ区切りで列挙します。

"CommandText": "AND \"Users\".\"LoginId\" NOT IN ('admin', 'superuser')"

管理テーブル方式

データベースに特権ユーザの LoginId を管理するテーブルを作成し、OnSelectingWhereCommandText からサブクエリで参照する方式です。特権ユーザの追加・変更をデータベース上で一元管理できます。

テーブルの作成

-- SQL Server / PostgreSQL 共通
CREATE TABLE PrivilegedUserMaster (
    LoginId NVARCHAR(256) NOT NULL PRIMARY KEY
);

-- 特権ユーザを登録
INSERT INTO PrivilegedUserMaster (LoginId) VALUES ('admin');

設定ファイル

ExtendedSqls/HidePrivilegedUsers.json
{
    "Name": "HidePrivilegedUsers",
    "Description": "特権ユーザをユーザ一覧から非表示にする(管理テーブル参照)",
    "Disabled": false,
    "Controllers": ["users"],
    "OnSelectingWhere": true,
    "CommandText": "AND \"Users\".\"LoginId\" NOT IN (SELECT \"LoginId\" FROM \"PrivilegedUserMaster\")"
}

特権ユーザの追加・削除は PrivilegedUserMaster テーブルを更新するだけで反映されます。Security.json との二重管理は発生しますが、拡張SQL ファイル自体を変更する必要はありません。

OPENROWSET 方式(SQL Server 限定)

SQL Server の OPENROWSET(BULK ...)OPENJSON を組み合わせることで、Security.json ファイルを直接読み取り、PrivilegedUsers のリストを動的に取得できます。この方式では Security.json との二重管理が不要になります。

前提条件

OPENROWSET を使用するには、SQL Server で Ad Hoc Distributed Queries オプションを有効にする必要があります。

-- SQL Server の設定(管理者権限で実行)
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'Ad Hoc Distributed Queries', 1;
RECONFIGURE;

また、SQL Server のサービスアカウントが Security.json ファイルに対する読み取り権限を持っている必要があります。

設定ファイル

OnSelectingWhereCommandText にサブクエリとして OPENROWSET を記述します。

ExtendedSqls/HidePrivilegedUsers.json
{
    "Name": "HidePrivilegedUsers",
    "Description": "特権ユーザをユーザ一覧から非表示にする(Security.json参照)",
    "Disabled": false,
    "Controllers": ["users"],
    "OnSelectingWhere": true,
    "CommandText": "AND [Users].[LoginId] NOT IN (SELECT j.[value] FROM OPENROWSET(BULK 'C:\\inetpub\\pleasanter\\App_Data\\Parameters\\Security.json', SINGLE_CLOB) AS f CROSS APPLY OPENJSON(f.BulkColumn, '$.PrivilegedUsers') AS j)"
}

OPENROWSET(BULK ...) でファイルをテキストとして読み込み、OPENJSONPrivilegedUsers 配列の各要素を行として展開しています。ファイルパスは環境に合わせて変更してください。

OPENROWSET はセキュリティ上のリスクがあるため、本番環境での使用は慎重に検討してください。SQL Server のサービスアカウントの権限を最小限に保つことが重要です。また、この方式は SQL Server がプリザンターと同一サーバに配置されている(または UNC パスでファイルにアクセスできる)ことが前提です。

PostgreSQL の場合

PostgreSQL 環境では、ハードコーディング方式をそのまま使用できます。識別子の囲み文字はダブルクォートで動作します。

ExtendedSqls/HidePrivilegedUsers.json
{
    "Name": "HidePrivilegedUsers",
    "Description": "特権ユーザをユーザ一覧から非表示にする",
    "Disabled": false,
    "Controllers": ["users"],
    "OnSelectingWhere": true,
    "CommandText": "AND \"Users\".\"LoginId\" NOT IN ('admin')"
}

制約事項

API 経由の削除は防げない

OnSelectingWhere はユーザ一覧の SELECT クエリにのみ影響します。ユーザ ID を直接指定した API 経由の削除(DELETE /api/users/{id})には対応できません。

DELETE /api/users/1  ← OnSelectingWhere では防げない

API 経由の削除まで制限する必要がある場合は、プリザンター本体のソースコードを修正するか、リバースプロキシやファイアウォールなどのインフラレベルでの対策を検討してください。

特権ユーザの管理ができなくなる

OnSelectingWhere で特権ユーザを一覧から非表示にすると、テナント管理者が特権ユーザの情報を確認・編集することもできなくなります。特権ユーザの管理は、特権ユーザ自身がおこなうか、データベースを直接操作する必要があります。

動作確認

特権ユーザがユーザ一覧に表示されない

拡張SQLを配置してプリザンターを再起動(またはパラメータリロード3)すると、ユーザ一覧画面に特権ユーザが表示されなくなります。

一般ユーザの表示・削除は通常どおり

特権ユーザ以外のユーザは通常どおりユーザ一覧に表示され、削除操作もおこなえます。

まとめ

調査の結果、拡張SQLの OnSelectingWhere を使うことで、ユーザ一覧から特権ユーザを除外し、テナント管理者による削除を防ぐことが確認できました。

項目 結論
実現可能性 ✅ 拡張SQLだけで実現可能
使用する拡張機能 OnSelectingWhere(ユーザ一覧から特権ユーザを除外)
BeforeDelete サーバスクリプト ❌ Users コントローラでは動作しない
OnDeleting 拡張SQL ❌ Items(レコード系)のみ対応
標準機能の変更 不要
API 経由の削除 ⚠️ OnSelectingWhere では防げない
プリザンターの再起動 拡張SQLの配置後に必要(またはパラメータリロード3

BeforeDelete サーバスクリプトや OnDeleting 拡張SQLは Users コントローラでは実行されないため、ユーザ削除の制限に使える拡張機能は OnSelectingWhere のみです。API 経由の削除を含めた完全な制限が必要な場合は、プリザンター本体の UserValidators.OnDeleting に特権ユーザチェックを追加するなど、ソースコードレベルの対応が必要になります。

  1. WikiModelDashboardModelOnDeletingExtendedSqls には対応していますが、SetByBeforeDeleteServerScript は呼び出していません。 2

  2. OPENROWSET(BULK ...) は SQL Server のサービスアカウントがファイルシステム上のパスにアクセスできる必要があります。SQL Server とプリザンターが別サーバに配置されている場合は、UNC パス(\\server\share\...)で指定するか、ハードコーディング方式や管理テーブル方式を使用してください。

  3. パラメータリロードは /admins/reloadparameters で実行できます。 2

1
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
1
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?