はじめに
プリザンターの「特権ユーザ」は、サイトやレコードのアクセス権限を無視して最上位の権限を取得できる特別なユーザです。この特権ユーザをテナント管理者が誤って削除してしまうと、システム運用に重大な影響を及ぼす可能性があります。
しかし、プリザンターの標準機能ではユーザ削除時に「削除対象が特権ユーザかどうか」を検証する仕組みがありません。そこで今回は、拡張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.json の PrivilegedUsers に LoginId のリストとして定義されています。
{
"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
一方、OnSelectingWhere は View.Where() メソッドで呼び出されるため、Users コントローラを含むすべてのコントローラで動作します。この仕組みを使って、特権ユーザをユーザ一覧に表示しないようにすることで、テナント管理者が特権ユーザを選択・削除できなくなります。
拡張機能による実装方針
方針の検討
調査結果をふまえると、Users コントローラで使用できる拡張機能は OnSelectingWhere のみです。
| 拡張機能 | Users での動作 | 用途 |
|---|---|---|
BeforeDelete サーバスクリプト |
❌ 動作しない | — |
OnDeleting 拡張SQL |
❌ 動作しない | — |
OnSelectingWhere 拡張SQL |
✅ 動作する | ユーザ一覧から特権ユーザを除外 |
OnSelectingWhere で特権ユーザを一覧から除外することで、UI 上で特権ユーザを選択できなくなり、結果として削除操作を防ぐことができます。
特権ユーザのリストを指定する方法
OnSelectingWhere の CommandText に特権ユーザの 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 句を構築しています。OnSelectingWhere が true に設定された拡張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 を直接記述するシンプルな方式です。
{
"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 を管理するテーブルを作成し、OnSelectingWhere の CommandText からサブクエリで参照する方式です。特権ユーザの追加・変更をデータベース上で一元管理できます。
テーブルの作成
-- SQL Server / PostgreSQL 共通
CREATE TABLE PrivilegedUserMaster (
LoginId NVARCHAR(256) NOT NULL PRIMARY KEY
);
-- 特権ユーザを登録
INSERT INTO PrivilegedUserMaster (LoginId) VALUES ('admin');
設定ファイル
{
"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 ファイルに対する読み取り権限を持っている必要があります。
設定ファイル
OnSelectingWhere の CommandText にサブクエリとして OPENROWSET を記述します。
{
"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 ...) でファイルをテキストとして読み込み、OPENJSON で PrivilegedUsers 配列の各要素を行として展開しています。ファイルパスは環境に合わせて変更してください。
OPENROWSET はセキュリティ上のリスクがあるため、本番環境での使用は慎重に検討してください。SQL Server のサービスアカウントの権限を最小限に保つことが重要です。また、この方式は SQL Server がプリザンターと同一サーバに配置されている(または UNC パスでファイルにアクセスできる)ことが前提です。
PostgreSQL の場合
PostgreSQL 環境では、ハードコーディング方式をそのまま使用できます。識別子の囲み文字はダブルクォートで動作します。
{
"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 に特権ユーザチェックを追加するなど、ソースコードレベルの対応が必要になります。