はじめに
前編:拡張機能編 では、Sites.InheritPermission を切ったうえで「親の権限+追加−除外」を バックグラウンドサーバスクリプトと拡張 SQL で同期する方法を紹介しました。
本体コードに触らずに済む反面、Permissions テーブルを物理的に書き換えるため、
- 反映タイミングが非同期になる
- 監査ログ上の操作者がスクリプト実行者になる
- 多段継承の同期順序を自前で意識する必要がある
といった制約がありました。
後編となる本記事では、本体コードを改修して継承の仕組み自体に「追加」と「除外」を組み込む アプローチを考えてみます。
継承は継承のまま、SiteSettings に保持した例外定義が権限解決時に評価される形を目指します。
バージョン 1.5.3.0 を対象にしています
設計方針
何を「追加」「除外」したいか
| 対象 | 例 |
|---|---|
| 追加するユーザ | 親サイトの権限はそのままに、特定の子サイトでだけ「Aさん」を閲覧可にしたい |
| 除外するユーザ | 親サイトの権限を引き継ぎつつ、特定の子サイトでは「Bさん」を見せたくない |
組織・グループ・ユーザの 3 種類すべてを対象にできるのが望ましいですが、本記事ではまず ユーザ単位 を最小スコープとして扱います。
どこに例外定義を持たせるか
Permissions テーブルは「ReferenceId 単位で完結する権限の集合」を表す既存テーブルなので、例外 という性質の情報を混ぜると意味が曖昧になります。
例外はサイト固有の振る舞い設定として SiteSettings(Sites.SiteSettings カラム、JSON)に持たせるのが自然です。
// Sites.SiteSettings に追加する例外設定(イメージ)
{
"ReferenceType": "Issues",
"InheritPermissionOverrides": {
"AddedUsers": [
{ "UserId": 12, "PermissionType": 1 } // 閲覧のみ追加
],
"ExcludedUsers": [
{ "UserId": 34 } // 完全除外
]
}
}
サイト設定 UI(アクセス権の管理画面)にもこの定義を編集する区画を追加することを前提に話を進めますが、本記事では実装の中核である 権限解決ロジック に絞って解説します。
どこのコードに手を入れるか
サイト権限の解決は次の経路でキャッシュされ、その後の処理で参照されます。
| レイヤ | 役割 |
|---|---|
Implem.SqlServer/SqlServerSqls.cs 等 |
GetPermissions SQL を返す |
Implem.Pleasanter/Libraries/Security/Permissions.cs |
SQL 結果を Dictionary<long, Types> に変換 |
Context.PermissionHash |
リクエスト中に参照されるキャッシュ |
例外を「結果のハッシュに対して後段で適用する」設計にすると、SQL 側を変えずに済みます。
本記事ではこの後段アプローチを採用します。
実装のステップ
ステップ 1: 例外定義クラスを追加する
Implem.Pleasanter/Libraries/Settings/SiteSettings.cs 配下に、シリアライズ可能なクラスを追加します。
namespace Implem.Pleasanter.Libraries.Settings
{
public class InheritPermissionOverrides
{
public List<PermissionOverride> AddedUsers { get; set; }
public List<PermissionOverride> ExcludedUsers { get; set; }
}
public class PermissionOverride
{
public int UserId { get; set; }
// 追加時のみ意味を持つ。除外時は無視
public long PermissionType { get; set; } = 1; // 既定: 閲覧
}
}
SiteSettings に以下のプロパティを追加します。
public InheritPermissionOverrides InheritPermissionOverrides { get; set; }
JSON 直列化のための既存仕組み(SiteSettings の RecordingJson/Init)にも乗るよう、null の場合はシリアライズ対象から外れる属性を付けておきます。
ステップ 2: 解決ロジックに例外を適用するメソッドを追加する
Permissions.cs に、ハッシュ生成後のフックを追加します。
public static void ApplyInheritOverrides(
Context context,
Dictionary<long, Types> hash)
{
// SiteId と SiteSettings のペアを取得するためのキャッシュ
var overrides = SiteInfo.TenantCaches
.Get(context.TenantId)?
.SiteMenu?
.Values
.Where(s => s.SiteSettings?.InheritPermissionOverrides != null)
.ToList();
if (overrides == null) return;
foreach (var site in overrides)
{
var o = site.SiteSettings.InheritPermissionOverrides;
// (1) 追加: 該当ユーザがログインユーザ本人なら、ハッシュにビットを OR
if (o.AddedUsers != null)
{
foreach (var add in o.AddedUsers)
{
if (add.UserId != context.UserId) continue;
if (!hash.ContainsKey(site.SiteId))
{
hash[site.SiteId] = Types.NotSet;
}
hash[site.SiteId] |= (Types)add.PermissionType;
}
}
// (2) 除外: 該当ユーザがログインユーザ本人なら、当該サイトのキーを除去
if (o.ExcludedUsers != null
&& o.ExcludedUsers.Any(e => e.UserId == context.UserId))
{
hash.Remove(site.SiteId);
}
}
}
ポイントは以下のとおりです。
| 観点 | 実装上の扱い |
|---|---|
| 評価対象ユーザ | リクエストを行ったログインユーザ(context.UserId)のみ |
| 「追加」の意味 | 既存ハッシュに対し PermissionType を OR でマージ
|
| 「除外」の意味 | 当該 SiteId のキーを 完全に削除(権限なし扱い) |
| 「追加」と「除外」の競合 | 同一ユーザが両方に含まれる場合は 除外を優先 |
ステップ 3: 既存の呼び出し箇所からフックする
Context.PermissionHash を構築している箇所(Context の初期化、または Permissions.Get 系の呼び出し直後)で ApplyInheritOverrides を呼びます。
PermissionHash = Permissions.Get(context: this);
Permissions.ApplyInheritOverrides(context: this, hash: PermissionHash);
PermissionHash は Dictionary<long, Types> の参照を直接保持しているため、ApplyInheritOverrides の中で書き換えれば全リクエスト処理に反映されます。
ステップ 4: グループ・組織への拡張
ユーザ単位だけで運用可能なケースは多いですが、組織・グループにも対応させる場合は PermissionOverride に DeptId / GroupId を増やし、判定式を以下のように差し替えます。
private static bool MatchesContext(PermissionOverride o, Context context)
{
if (o.UserId != 0 && o.UserId == context.UserId) return true;
if (o.DeptId != 0 && o.DeptId == context.DeptId) return true;
if (o.GroupId != 0
&& SiteInfo.TenantCaches.Get(context.TenantId)?.GroupHash
.Get(o.GroupId)?.Members.Any(m => m.UserId == context.UserId) == true)
{
return true;
}
return false;
}
組織やグループの所属解決は SiteInfo.TenantCaches 経由でメモリ内のキャッシュにアクセスでき、DB アクセスを増やさずに判定できます。
ステップ 5: UI 連携(概要)
「テーブルの管理 → アクセス権の管理」画面に、「このサイトだけの追加・除外」 タブを増やし、InheritPermissionOverrides を編集できるようにします。
編集 UI は既存の PermissionListBox(左右に振り分けるリストボックス)を流用すると違和感がありません。
保存は通常の SiteSettings 更新フローに乗るため、永続化用の追加処理は不要です。
SQL 側でやる選択肢との比較
「ハッシュ生成後の後段でフィルタ」ではなく、GetPermissions の SQL 自体を UNION で拡張する案も考えられます。
-- 追加: ⑥ サイト固有の追加ユーザ
SELECT "SiteId" AS "ReferenceId",
<SiteSettings JSON から取り出した PermissionType>
FROM "Sites"
WHERE "TenantId" = @_T
AND JSON_VALUE("SiteSettings", '$.InheritPermissionOverrides.AddedUsers[*].UserId') = @_U
この方式は強力ですが、
| 観点 | 評価 |
|---|---|
| SQL Server / PostgreSQL の JSON 関数差異 | 両 DB ぶんメンテが必要 |
| パフォーマンス |
Sites 全件に対する JSON パースが発生 |
| 除外の表現 |
MINUS/EXCEPT を組む必要があり可読性が落ちる |
といった負担があり、「結果ハッシュに対する後段フィルタ」のほうが影響範囲も小さく、テストも書きやすい という判断になります。
テストの考え方
Permissions.ApplyInheritOverrides は 純粋関数に近い ため、Dictionary<long, Types> と InheritPermissionOverrides を入力に取るユニットテストを置きやすい構造です。
最低限カバーしておきたいケースを挙げておきます。
| ケース | 期待される結果 |
|---|---|
AddedUsers に自分が含まれる |
該当 SiteId の権限ビットが OR で増える |
既に同等以上の権限を持つユーザに AddedUsers が当たる |
ビットマスクが冪等なため変化なし |
ExcludedUsers に自分が含まれる |
該当 SiteId のキーが Remove される |
AddedUsers と ExcludedUsers の両方に自分が含まれる |
除外が優先される |
| 自分が含まれないオーバーライドのみ | ハッシュは変化しない |
SiteId が hash に存在しない状態で AddedUsers が当たる |
キーが新規作成され、指定ビットがセットされる |
影響範囲と注意点
| 観点 | 内容 |
|---|---|
| パフォーマンス |
SiteInfo.TenantCaches のメモリキャッシュを使うので追加 DB アクセスはない |
| 既存サイトへの影響 |
InheritPermissionOverrides が null のサイトは従来どおりの挙動 |
| 監査 |
Permissions テーブルは触らないため、サイト権限の標準監査ログには現れない(オーバーライドの監査は SiteSettings 履歴で追う) |
| ManageSite / ManagePermission | 強い権限を「追加」で付与する設計は避け、サイト管理用の権限はあくまで Permissions で運用するのが安全 |
| API | API 経由のアクセスでも同じ Context.PermissionHash を使うため、自動的に効く |
まとめ
| 項目 | 内容 |
|---|---|
| 例外定義の場所 |
Sites.SiteSettings の InheritPermissionOverrides(JSON) |
| 評価タイミング |
Context.PermissionHash 構築直後 |
| 「追加」 | 既存ハッシュにビットマスクを OR でマージ |
| 「除外」 | 該当 SiteId のキーを Hash から削除 |
| 競合 | 除外を優先 |
| 影響範囲 |
Permissions.cs、SiteSettings.cs、Context.cs、UI 1 タブの追加 |
前編で扱った拡張機能ベースの方式は 本体に手を入れずに運用回避 できる利点があり、後編のコード改修は 継承を維持したまま例外をネイティブに表現できる 利点があります。
要件と運用体制に合わせて、どちらを採るかを選んでみてください。