ユーザ権限の確認とサイト権限継承の部分追加・除外
サイトのアクセス権に関する調べ方と応用をまとめます。
- あるユーザがあるサイトでどの権限を持っているかは、
Sites.InheritPermissionとPermissionsテーブルを結合する SQL で調べられる。PermissionTypeはビットマスク - 権限を継承しているサイトツリーの一部だけにユーザを追加・除外する機能は標準にはない。拡張機能で同期する方法と、本体コードを改修する方法の 2 つの案を示す
ユーザの権限を調べる
INFO
SQL Server・PostgreSQL・MySQL のタブから使用中の DBMS を選んでください。@名前 はバインドするパラメータです。DB のコンソールで直接実行する場合は、対象の値に置き換えるか、実行ツールに合わせてパラメータを設定します。
サイトの管理の「アクセス権の管理」で設定した権限を取得するクエリです。@UserId と @SiteId を、調べたいユーザのユーザ ID と対象サイトのサイト ID に置き換えて実行します。組織経由・グループ経由(グループに含まれる組織経由を含む)の権限も対象です。
SELECT
[SiteId],
[PermissionType]
FROM
(
SELECT
[Sites].[SiteId],
[Permissions].[PermissionType]
FROM
[Sites]
LEFT JOIN [Permissions]
ON [Sites].[InheritPermission] = [Permissions].[ReferenceId]
LEFT JOIN [Depts] AS [Depts]
ON [Permissions].[DeptId] = [Depts].[DeptId]
LEFT JOIN [Users] AS [DeptsUsers]
ON [Depts].[DeptId] = [DeptsUsers].[DeptId]
LEFT JOIN [GroupMembers] AS [Groups]
ON [Permissions].[GroupId] = [Groups].[GroupId]
LEFT JOIN [Depts] AS [GroupsDepts]
ON [Groups].[DeptId] = [GroupsDepts].[DeptId]
LEFT JOIN [Users] AS [GroupsDeptsUsers]
ON [GroupsDepts].[DeptId] = [GroupsDeptsUsers].[DeptId]
WHERE
(
@UserId IN ([Permissions].[UserId], [DeptsUsers].[UserId], [GroupsDeptsUsers].[UserId], [Groups].[UserId])
)
AND [Sites].[SiteId] = @SiteId
) AS Query
GROUP BY
[SiteId],
[PermissionType]SELECT
"SiteId",
"PermissionType"
FROM
(
SELECT
"Sites"."SiteId",
"Permissions"."PermissionType"
FROM
"Sites"
LEFT JOIN "Permissions"
ON "Sites"."InheritPermission" = "Permissions"."ReferenceId"
LEFT JOIN "Depts" AS "Depts"
ON "Permissions"."DeptId" = "Depts"."DeptId"
LEFT JOIN "Users" AS "DeptsUsers"
ON "Depts"."DeptId" = "DeptsUsers"."DeptId"
LEFT JOIN "GroupMembers" AS "Groups"
ON "Permissions"."GroupId" = "Groups"."GroupId"
LEFT JOIN "Depts" AS "GroupsDepts"
ON "Groups"."DeptId" = "GroupsDepts"."DeptId"
LEFT JOIN "Users" AS "GroupsDeptsUsers"
ON "GroupsDepts"."DeptId" = "GroupsDeptsUsers"."DeptId"
WHERE
(
@UserId IN ("Permissions"."UserId", "DeptsUsers"."UserId", "GroupsDeptsUsers"."UserId", "Groups"."UserId")
)
AND "Sites"."SiteId" = @SiteId
) AS Query
GROUP BY
"SiteId",
"PermissionType"SELECT
`SiteId`,
`PermissionType`
FROM
(
SELECT
`Sites`.`SiteId`,
`Permissions`.`PermissionType`
FROM
`Sites`
LEFT JOIN `Permissions`
ON `Sites`.`InheritPermission` = `Permissions`.`ReferenceId`
LEFT JOIN `Depts` AS `Depts`
ON `Permissions`.`DeptId` = `Depts`.`DeptId`
LEFT JOIN `Users` AS `DeptsUsers`
ON `Depts`.`DeptId` = `DeptsUsers`.`DeptId`
LEFT JOIN `GroupMembers` AS `Groups`
ON `Permissions`.`GroupId` = `Groups`.`GroupId`
LEFT JOIN `Depts` AS `GroupsDepts`
ON `Groups`.`DeptId` = `GroupsDepts`.`DeptId`
LEFT JOIN `Users` AS `GroupsDeptsUsers`
ON `GroupsDepts`.`DeptId` = `GroupsDeptsUsers`.`DeptId`
WHERE
(
@UserId IN (`Permissions`.`UserId`, `DeptsUsers`.`UserId`, `GroupsDeptsUsers`.`UserId`, `Groups`.`UserId`)
)
AND `Sites`.`SiteId` = @SiteId
) AS Query
GROUP BY
`SiteId`,
`PermissionType`対象外のもの
- 「レコードのアクセス制御」や「項目のアクセス制御」による、サイトより細かい粒度のアクセス権
子グループ(グループ in グループ)の扱い
このクエリは GroupChildren を再帰的にたどりませんが、子グループ経由の権限も結果に含まれます。1.5.8.1 では、子グループの直接メンバーが親グループの GroupMembers に ChildGroup = 1 の行としてコピーされており、プリザンター本体の権限取得 SQL もこの行で親グループの権限を解決しているためです(グループの入れ子)。
結果は次のような形で返ります。
{
"SiteId":171783,
"PermissionType":63
}PermissionType の読み方
PermissionType はビットマスクです。定義は Permissions.cs にあります。
public enum Types : long
{
NotSet = 0, // 00000000000000000000000000000000
Read = 1, // 00000000000000000000000000000001
Create = 2, // 00000000000000000000000000000010
Update = 4, // 00000000000000000000000000000100
Delete = 8, // 00000000000000000000000000001000
SendMail = 16, // 00000000000000000000000000010000
Export = 32, // 00000000000000000000000000100000
Import = 64, // 00000000000000000000000001000000
ManageSite = 128, // 00000000000000000000000010000000
ManagePermission = 256, // 00000000000000000000000100000000
ManageTenant = 1073741824, // 01000000000000000000000000000000
ManageService = 2147483648, // 10000000000000000000000000000000
}たとえば 63 は 2 進数で 0011 1111 なので、1, 2, 4, 8, 16, 32、つまり Read, Create, Update, Delete, SendMail, Export です。特定の権限があるかどうかは、ビット演算で判定できます。
var permissionType = 63;
var delete = 8;
if ((permissionType & delete) == delete)
{
Console.WriteLine("Delete権限あり");
}
else
{
Console.WriteLine("Delete権限なし");
}このクエリを拡張 SQL にしてスクリプトと組み合わせれば、サイトアイコンに読み取り専用マークを付けるといった UI の工夫にも使えます。
サイト権限継承で一部サイトだけ追加・除外する
課題
サイト権限は Permissions テーブルに 1 行ずつ保持され、Sites.InheritPermission がそのサイトの権限解決時に参照する ReferenceId を指します。子サイトが親サイトの権限を継承している場合、親子は同じ ReferenceId の Permissions 行を共有します。
図を読み込み中…
このため「子サイト β だけ D さんを追加したい」「子サイト α だけ A さんを外したい」と思っても、片方だけを変更できません。子サイトの継承を切って(InheritPermission = SiteId)独自の Permissions 行を作れば例外を表現できますが、今度は親の権限変更が反映されなくなります。
「継承は続けたい、でも一部だけ例外を入れたい」を両立する方法として、次の 2 つの案があります。いずれも設計・実装例で、プリザンターの標準機能ではありません。
| 案 | 概要 | 長所 | 短所 |
|---|---|---|---|
| A. 拡張機能で同期 | 子サイトの継承を切り、「親の権限+追加−除外」をバックグラウンドサーバースクリプトで定期的に書き込む | 本体コードに手を入れない | 反映が非同期。Permissions を直接書き換えるため監査面の配慮が必要 |
| B. 本体コード改修 | 例外定義を SiteSettings に持たせ、権限解決の後段で評価する | 継承を維持したまま例外をネイティブに表現できる | 本体の改修が必要 |
案 A: 拡張機能で同期する
子サイトの継承は切りますが、親の権限変更を自動で追従させることで、実質的に同じ結果を得ます。
| 役割 | 使う仕組み |
|---|---|
| 例外を定義する場所 | 専用の管理テーブル(記録テーブル)「権限例外」 |
| 親の権限を取り込む | バックグラウンドサーバースクリプトで Permissions テーブルを同期 |
| 例外を反映する | 同スクリプトから「追加ユーザ」を INSERT、「除外ユーザ」を取り込まない |
| 例外サイトの確認 | 拡張 SQL で「継承を切っているサイト」を一覧化 |
1. 例外定義の管理テーブルを作る
例外の保管場所には「サイト概要」や SiteSettings の拡張プロパティなども考えられますが、ここでは管理テーブルを用意します。例外設定そのものを検索・履歴管理・通知の対象にできるためです。
| 項目 | 種類 | 用途 |
|---|---|---|
ClassA | 文字列 | 対象サイトの SiteId |
ClassB | 文字列 | 親サイト(継承元)の SiteId |
ClassC | 文字列 | 動作モード(Add / Exclude) |
ClassD | 文字列 | 対象ユーザの LoginId(カンマ区切り) |
NumA | 数値 | 付与する PermissionType(例: 1=閲覧、31=編集) |
2. 例外サイトの継承を切る
対象の子サイトの「テーブルの管理 → アクセス権の管理」で「上位サイトから継承する」を解除します。内部的には Sites.InheritPermission = SiteId となり、そのサイト専用の Permissions 行が作成されます。
3. 拡張 SQL を用意する
子サイトの権限をいったん削除し、親サイトの権限をコピー(除外ユーザを除く)してから、追加ユーザを INSERT します。カンマ区切りの LoginId は、SQL Server では STRING_SPLIT、PostgreSQL では string_to_array と unnest、MySQL では FIND_IN_SET で照合します。LoginId 自体にカンマを含まないことを前提とします。
サーバースクリプトの extendedSql から呼び出せるのは "Api": true の拡張 SQL だけです(1.5.8.1 の ExtensionUtilities.cs)。定義ファイル(.json)と、同じ名前に .sql を付けた SQL ファイル(.json.sql)を並べて置くと、SQL ファイルの内容が CommandText として読み込まれます(Initializer.cs)。
Permissions テーブルの実際の列は ReferenceId・DeptId・GroupId・UserId・PermissionType と共通列(Ver・Creator・Updator・CreatedTime・UpdatedTime など)です。DeptName・GroupName・Name は Depts・Groups・Users を結合して表示するための項目で、Permissions テーブルには列がないため、INSERT の列から外しています(Permissions_Name.json)。
{
"Name": "SyncPermissionOverride",
"Api": true
}SQL ファイル名は SyncPermissionOverride.json.sql です。
-- @ChildSiteId, @ParentSiteId, @AddLoginIds, @ExcludeLoginIds, @PermissionType, @CreatorId
-- 1) 子サイトの権限をいったん削除
DELETE FROM "Permissions" WHERE "ReferenceId" = @ChildSiteId;
-- 2) 親サイトの権限をコピー
INSERT INTO "Permissions"
("ReferenceId", "DeptId", "GroupId", "UserId",
"PermissionType",
"Creator", "Updator", "CreatedTime", "UpdatedTime")
SELECT
@ChildSiteId, "DeptId", "GroupId", "UserId",
"PermissionType",
"Creator", "Updator", "CreatedTime", "UpdatedTime"
FROM "Permissions"
WHERE "ReferenceId" = @ParentSiteId
-- 除外指定された LoginId のユーザを取り込まない
AND "UserId" NOT IN (
SELECT "UserId" FROM "Users"
WHERE "LoginId" IN (SELECT value FROM STRING_SPLIT(@ExcludeLoginIds, ','))
);
-- 3) 追加指定された LoginId のユーザを追加
INSERT INTO "Permissions"
("ReferenceId", "DeptId", "GroupId", "UserId",
"PermissionType",
"Creator", "Updator", "CreatedTime", "UpdatedTime")
SELECT
@ChildSiteId, 0, 0, "UserId",
@PermissionType,
@CreatorId, @CreatorId, GETDATE(), GETDATE()
FROM "Users"
WHERE "LoginId" IN (SELECT value FROM STRING_SPLIT(@AddLoginIds, ','));-- @ChildSiteId, @ParentSiteId, @AddLoginIds, @ExcludeLoginIds, @PermissionType, @CreatorId
-- 1) 子サイトの権限をいったん削除
DELETE FROM "Permissions" WHERE "ReferenceId" = @ChildSiteId;
-- 2) 親サイトの権限をコピー
INSERT INTO "Permissions"
("ReferenceId", "DeptId", "GroupId", "UserId",
"PermissionType",
"Creator", "Updator", "CreatedTime", "UpdatedTime")
SELECT
@ChildSiteId, "DeptId", "GroupId", "UserId",
"PermissionType",
"Creator", "Updator", "CreatedTime", "UpdatedTime"
FROM "Permissions"
WHERE "ReferenceId" = @ParentSiteId
-- 除外指定された LoginId のユーザを取り込まない
AND "UserId" NOT IN (
SELECT "UserId" FROM "Users"
WHERE "LoginId" IN (SELECT unnest(string_to_array(@ExcludeLoginIds, ',')))
);
-- 3) 追加指定された LoginId のユーザを追加
INSERT INTO "Permissions"
("ReferenceId", "DeptId", "GroupId", "UserId",
"PermissionType",
"Creator", "Updator", "CreatedTime", "UpdatedTime")
SELECT
@ChildSiteId, 0, 0, "UserId",
@PermissionType,
@CreatorId, @CreatorId, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM "Users"
WHERE "LoginId" IN (SELECT unnest(string_to_array(@AddLoginIds, ',')));-- @ChildSiteId, @ParentSiteId, @AddLoginIds, @ExcludeLoginIds, @PermissionType, @CreatorId
-- 1) 子サイトの権限をいったん削除
DELETE FROM `Permissions` WHERE `ReferenceId` = @ChildSiteId;
-- 2) 親サイトの権限をコピー
INSERT INTO `Permissions`
(`ReferenceId`, `DeptId`, `GroupId`, `UserId`,
`PermissionType`,
`Creator`, `Updator`, `CreatedTime`, `UpdatedTime`)
SELECT
@ChildSiteId, `DeptId`, `GroupId`, `UserId`,
`PermissionType`,
`Creator`, `Updator`, `CreatedTime`, `UpdatedTime`
FROM `Permissions`
WHERE `ReferenceId` = @ParentSiteId
-- 除外指定された LoginId のユーザを取り込まない
AND `UserId` NOT IN (
SELECT `UserId` FROM `Users`
WHERE FIND_IN_SET(`LoginId`, @ExcludeLoginIds) > 0
);
-- 3) 追加指定された LoginId のユーザを追加
INSERT INTO `Permissions`
(`ReferenceId`, `DeptId`, `GroupId`, `UserId`,
`PermissionType`,
`Creator`, `Updator`, `CreatedTime`, `UpdatedTime`)
SELECT
@ChildSiteId, 0, 0, `UserId`,
@PermissionType,
@CreatorId, @CreatorId, CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3)
FROM `Users`
WHERE FIND_IN_SET(`LoginId`, @AddLoginIds) > 0;4. バックグラウンドサーバースクリプトで同期する
管理テーブルの行を対象サイト・親サイト・権限ごとにまとめ、サイトごとに拡張 SQL を実行します。
確認したソースでは、items.Get の第 2 引数は取得する列ではなくビュー(JSON 文字列)です(ServerScriptModelApiItems.cs)。また extendedSql は関数ではなく、ExecuteNonQuery(name, params) などのメソッドを持つオブジェクトで、params は JSON 文字列として受け取ります(ServerScriptModelExtendedSql.cs)。上のコードはこれに合わせて書いています。
// 権限例外サイトの SiteId
var overrideSiteId = 12345;
// 管理テーブルから例外定義を取得
var rows = items.Get(overrideSiteId);
// ClassA (対象子サイト) と ClassB (親サイト) で集約する
var bySite = {};
for (const r of rows) {
var key = r.ClassA + ":" + r.ClassB + ":" + r.NumA;
if (!bySite[key]) {
bySite[key] = {
ChildSiteId: parseInt(r.ClassA, 10),
ParentSiteId: parseInt(r.ClassB, 10),
PermissionType: parseInt(r.NumA, 10) || 31,
Adds: [],
Excludes: []
};
}
var ids = (r.ClassD || "")
.split(",")
.map(function (s) { return s.trim(); })
.filter(function (s) { return s !== ""; });
if (r.ClassC === "Add") {
bySite[key].Adds = bySite[key].Adds.concat(ids);
} else if (r.ClassC === "Exclude") {
bySite[key].Excludes = bySite[key].Excludes.concat(ids);
}
}
// サイトごとに拡張 SQL を実行
Object.keys(bySite).forEach(function (key) {
var s = bySite[key];
extendedSql.ExecuteNonQuery("SyncPermissionOverride", JSON.stringify({
ChildSiteId: s.ChildSiteId,
ParentSiteId: s.ParentSiteId,
AddLoginIds: s.Adds.join(","),
ExcludeLoginIds: s.Excludes.join(","),
PermissionType: s.PermissionType,
CreatorId: context.UserId
}));
});バックグラウンドサーバースクリプトとして登録し、実行間隔(たとえば 10 分)を設定すれば、親サイトの権限が変わっても次回の実行時に追従します。すぐに反映したい場合は、次の方法で補います。
| 方法 | 内容 |
|---|---|
| 親サイトの拡張サーバースクリプト(更新後) | サイト保存後に、同じロジックを子サイトに対して実行 |
| 「権限例外」テーブルの更新後スクリプト | 管理テーブルの更新時に、関連サイトの同期だけを実行 |
| 実行間隔の短縮 | 厳密性より運用の単純さを優先したい場合 |
5. 例外サイトを可視化する
例外が増えると、どのサイトが親と違う権限を持っているのか分からなくなります。継承を切っているサイトについて、親と子の Permissions の件数を比較する拡張 SQL を用意しておくと棚卸しが容易です。件数が同じならほぼ同期されていると見なせ、差が大きければ想定外の追加・除外がないか確認します。
{
"Name": "ListPermissionOverride",
"Api": true
}SQL ファイル名はいずれも Implem.Pleasanter/App_Data/Parameters/ExtendedSqls/ListPermissionOverride.json.sql です。
SELECT
c."SiteId" AS "ChildSiteId",
c."Title" AS "ChildTitle",
p."SiteId" AS "ParentSiteId",
p."Title" AS "ParentTitle",
(SELECT COUNT(*) FROM "Permissions" WHERE "ReferenceId" = c."SiteId") AS "ChildPermCount",
(SELECT COUNT(*) FROM "Permissions" WHERE "ReferenceId" = p."SiteId") AS "ParentPermCount"
FROM "Sites" c
INNER JOIN "Sites" p ON p."SiteId" = c."ParentId"
WHERE c."InheritPermission" = c."SiteId" -- 継承を切っているサイト
AND c."TenantId" = @_T
ORDER BY p."SiteId", c."SiteId";SELECT
c."SiteId" AS "ChildSiteId",
c."Title" AS "ChildTitle",
p."SiteId" AS "ParentSiteId",
p."Title" AS "ParentTitle",
(SELECT COUNT(*) FROM "Permissions" WHERE "ReferenceId" = c."SiteId") AS "ChildPermCount",
(SELECT COUNT(*) FROM "Permissions" WHERE "ReferenceId" = p."SiteId") AS "ParentPermCount"
FROM "Sites" c
INNER JOIN "Sites" p ON p."SiteId" = c."ParentId"
WHERE c."InheritPermission" = c."SiteId" -- 継承を切っているサイト
AND c."TenantId" = @ipT
ORDER BY p."SiteId", c."SiteId";SELECT
c.`SiteId` AS `ChildSiteId`,
c.`Title` AS `ChildTitle`,
p.`SiteId` AS `ParentSiteId`,
p.`Title` AS `ParentTitle`,
(SELECT COUNT(*) FROM `Permissions` WHERE `ReferenceId` = c.`SiteId`) AS `ChildPermCount`,
(SELECT COUNT(*) FROM `Permissions` WHERE `ReferenceId` = p.`SiteId`) AS `ParentPermCount`
FROM `Sites` c
INNER JOIN `Sites` p ON p.`SiteId` = c.`ParentId`
WHERE c.`InheritPermission` = c.`SiteId` -- 継承を切っているサイト
AND c.`TenantId` = @ipT
ORDER BY p.`SiteId`, c.`SiteId`;@_T(PostgreSQL・MySQL では @ipT)は拡張 SQL に自動で渡されるログインユーザーのテナント ID です。接頭辞は DBMS で変わり、Parameter.json の SqlParameterPrefix が空の既定なら SQL Server は @_、PostgreSQL・MySQL は @ip です(Parameter.cs、SqlIo.cs)。
案 A の限界
| 限界 | 内容 |
|---|---|
| 即時性 | 実行間隔のラグが発生する(即時反映には別途トリガーが必要) |
| 整合性 | 親側の細かな変更(例: DeptId 経由のグループ追加)も同期対象になる |
| 監査 | Permissions の更新者がスクリプト実行者になり、本来の操作者と一致しない |
| 例外の入れ子 | 親→子→孫と多段で継承を切る場合、同期順序を ParentSiteId 順で制御する必要がある |
案 B: 本体コードを改修する
継承の仕組みそのものに「追加」と「除外」を組み込む改修案です。継承は継承のまま、SiteSettings に持たせた例外定義を権限解決時に評価します。ここではユーザ単位を最小スコープとします。
例外定義の置き場所
Permissions テーブルは「ReferenceId 単位で完結する権限の集合」を表すため、例外の情報を混ぜると意味が曖昧になります。例外はサイト固有の設定として Sites.SiteSettings に持たせます。
// Sites.SiteSettings に追加する例外設定(イメージ)
{
"ReferenceType": "Issues",
"InheritPermissionOverrides": {
"AddedUsers": [
{ "UserId": 12, "PermissionType": 1 } // 閲覧のみ追加
],
"ExcludedUsers": [
{ "UserId": 34 } // 完全除外
]
}
}手を入れる場所
サイト権限は次の経路で解決されます(1.5.8.1 のソースで確認)。
| レイヤ | 役割 |
|---|---|
Rds/Implem.SqlServer/SqlServerSqls.cs 等 | GetPermissions SQL を返す |
Implem.Pleasanter/Libraries/Security/Permissions.cs | SQL 結果を ReferenceId をキーにした Dictionary<long, Types> に変換(Permissions.cs) |
Context.PermissionHash | リクエスト中に参照されるキャッシュ(Context.cs) |
Context.SetPermissions(ss) | サイトの InheritPermission をキーに PermissionHash を引き、そのサイトの権限を決める(Context.cs) |
PermissionHash のキーはサイト ID ではなく ReferenceId(継承元の InheritPermission)です。継承している子サイトは親と同じキーを引くため、PermissionHash を書き換えると同じ継承元を使う全サイトに効いてしまい、子サイト 1 つだけの例外は表現できません。そこで、サイトごとの権限が決まる Context.SetPermissions(ss) の後段で、そのサイトの SiteSettings(ss)に持たせた例外を適用します。ss は引数で渡されるので、キャッシュから探し直す必要もありません。
図を読み込み中…
INFO
PermissionHash をサイト ID で書き換えても、1.5.8.1 のソースではキーが ReferenceId のため、子サイト単位の例外にはなりません。そのため、ここでは SetPermissions の後段で適用しています。
1. 例外定義クラスを追加する
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 にプロパティを追加します。null の場合はシリアライズ対象から外れるようにしておきます。
public InheritPermissionOverrides InheritPermissionOverrides { get; set; }2. 例外を適用するメソッドを追加する
private void ApplyInheritOverrides(SiteSettings ss, ContextPermission cp)
{
var o = ss.InheritPermissionOverrides;
if (o == null) return;
// (1) 追加: 該当ユーザがログインユーザ本人なら、権限ビットを OR
if (o.AddedUsers?.Any(add => add.UserId == UserId) == true)
{
var added = o.AddedUsers
.Where(add => add.UserId == UserId)
.Aggregate(Permissions.Types.NotSet,
(types, add) => types | (Permissions.Types)add.PermissionType);
cp.PermissionType = (cp.PermissionType ?? Permissions.Types.NotSet) | added;
}
// (2) 除外: 該当ユーザがログインユーザ本人なら、このサイトの権限をなくす
if (o.ExcludedUsers?.Any(e => e.UserId == UserId) == true)
{
cp.PermissionType = null;
}
}| 観点 | 実装上の扱い |
|---|---|
| 評価対象ユーザ | リクエストを行ったログインユーザ(context.UserId)のみ |
| 「追加」 | そのサイトの権限に PermissionType を OR でマージ |
| 「除外」 | そのサイトの権限を null(権限なし扱い)にする |
| 追加と除外の競合 | 同一ユーザが両方に含まれる場合は除外を優先 |
3. 呼び出し箇所に組み込む
Context.SetPermissions(ss) の中で、PermissionHash から権限を取り出した直後に呼び出します(ロックされたテーブルの制限より前に入れると、ロック時の制限も効きます)。
if (PermissionHash?.ContainsKey(ss.InheritPermission) == true)
{
cp.PermissionType = PermissionHash[ss.InheritPermission];
}
ApplyInheritOverrides(ss: ss, cp: cp);4. 組織・グループへの拡張
PermissionOverride に DeptId / GroupId を増やし、判定を次のように差し替えます。ログインユーザの組織は Context.DeptId、所属グループの ID は Context.Groups(SetPermissions() で PermissionHash と一緒に設定される)で判定でき、DB アクセスは増えません。
private bool MatchesContext(PermissionOverride o)
{
if (o.UserId != 0 && o.UserId == UserId) return true;
if (o.DeptId != 0 && o.DeptId == DeptId) return true;
if (o.GroupId != 0 && Groups?.Contains(o.GroupId) == true) return true;
return false;
}5. UI
「テーブルの管理 → アクセス権の管理」に「このサイトだけの追加・除外」タブを増やし、InheritPermissionOverrides を編集できるようにします。既存の PermissionListBox(左右に振り分けるリストボックス)を流用でき、保存は通常の SiteSettings 更新フローに乗るため、永続化の追加処理は不要です。
SQL 側で拡張しない理由
GetPermissions の SQL 自体を UNION で拡張する案もありますが、SQL Server / PostgreSQL の JSON 関数の差異で両 DB 分のメンテナンスが必要になること、Sites 全件に対する JSON パースが発生すること、除外に EXCEPT などが必要で可読性が落ちることから、サイトごとの権限を決める箇所での後段適用のほうが影響範囲が小さく、テストも書きやすいと判断しています。
テストで確認するケース
| ケース | 期待される結果 |
|---|---|
AddedUsers に自分が含まれる | そのサイトの権限ビットが OR で増える |
既に同等以上の権限を持つユーザに AddedUsers が当たる | 変化なし(ビットマスクは冪等) |
ExcludedUsers に自分が含まれる | そのサイトの権限がなくなる |
AddedUsers と ExcludedUsers の両方に自分が含まれる | 除外が優先される |
| 自分が含まれないオーバーライドのみ | 権限は変化しない |
継承元に権限がない状態で AddedUsers が当たる | 指定ビットだけがセットされる |
| 同じ親を継承する兄弟サイト | 例外を設定していないサイトの権限は変化しない |
影響範囲と注意点
| 観点 | 内容 |
|---|---|
| パフォーマンス | 読み込み済みの SiteSettings を使うので追加の DB アクセスはない |
| 既存サイト | InheritPermissionOverrides が null のサイトは従来どおり |
| 監査 | Permissions テーブルは触らないため、サイト権限の標準の監査ログには現れない(SiteSettings の履歴で追う) |
| ManageSite / ManagePermission | 強い権限を「追加」で付与する設計は避け、サイト管理用の権限は Permissions で運用するのが安全 |
| API | API 経由のアクセスも Context.SetPermissions を通るため、同じように効く |
| SQL で権限を判定する処理 | 一覧やサイトメニューの絞り込みなど、CanRead.sql などの SQL で Permissions テーブルを直接見る処理には効かない(CanReadSites.sql)。除外したユーザにもサイトがメニューに出るなどの差が残るため、必要ならその SQL 側も改修する |