Skip to content

ユーザ権限の確認とサイト権限継承の部分追加・除外 ​

第8版作成 最終更新 (日本時間)
確認バージョン1.5.3.01.5.8.1

サイトのアクセス権に関する調べ方と応用をまとめます。

  • あるユーザがあるサイトでどの権限を持っているかは、Sites.InheritPermission と Permissions テーブルを結合する SQL で調べられる。PermissionType はビットマスク
  • 権限を継承しているサイトツリーの一部だけにユーザを追加・除外する機能は標準にはない。拡張機能で同期する方法と、本体コードを改修する方法の 2 つの案を示す

ユーザの権限を調べる ​

INFO

SQL Server・PostgreSQL・MySQL のタブから使用中の DBMS を選んでください。@名前 はバインドするパラメータです。DB のコンソールで直接実行する場合は、対象の値に置き換えるか、実行ツールに合わせてパラメータを設定します。

サイトの管理の「アクセス権の管理」で設定した権限を取得するクエリです。@UserId と @SiteId を、調べたいユーザのユーザ ID と対象サイトのサイト ID に置き換えて実行します。組織経由・グループ経由(グループに含まれる組織経由を含む)の権限も対象です。

sql
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]
sql
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"
sql
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 もこの行で親グループの権限を解決しているためです(グループの入れ子)。

結果は次のような形で返ります。

json
{
    "SiteId":171783,
    "PermissionType":63
}

PermissionType の読み方 ​

PermissionType はビットマスクです。定義は Permissions.cs にあります。

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 です。特定の権限があるかどうかは、ビット演算で判定できます。

cs
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)。

json
{
    "Name": "SyncPermissionOverride",
    "Api": true
}

SQL ファイル名は SyncPermissionOverride.json.sql です。

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, ','));
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 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, ',')));
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 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)。上のコードはこれに合わせて書いています。

javascript
// 権限例外サイトの 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 を用意しておくと棚卸しが容易です。件数が同じならほぼ同期されていると見なせ、差が大きければ想定外の追加・除外がないか確認します。

json
{
    "Name": "ListPermissionOverride",
    "Api": true
}

SQL ファイル名はいずれも Implem.Pleasanter/App_Data/Parameters/ExtendedSqls/ListPermissionOverride.json.sql です。

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";
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" = @ipT
ORDER BY p."SiteId", c."SiteId";
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` = @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 に持たせます。

jsonc
// 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.csSQL 結果を 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. 例外定義クラスを追加する ​

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 にプロパティを追加します。null の場合はシリアライズ対象から外れるようにしておきます。

cs
public InheritPermissionOverrides InheritPermissionOverrides { get; set; }

2. 例外を適用するメソッドを追加する ​

cs
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 から権限を取り出した直後に呼び出します(ロックされたテーブルの制限より前に入れると、ロック時の制限も効きます)。

cs
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 アクセスは増えません。

cs
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 で運用するのが安全
APIAPI 経由のアクセスも Context.SetPermissions を通るため、同じように効く
SQL で権限を判定する処理一覧やサイトメニューの絞り込みなど、CanRead.sql などの SQL で Permissions テーブルを直接見る処理には効かない(CanReadSites.sql)。除外したユーザにもサイトがメニューに出るなどの差が残るため、必要ならその SQL 側も改修する

関連ページ ​

変更履歴

第8版記事の確認版を繰り返す表現を整理する
第7版権限の確認と部分追加・除外の SQL を3種類のDBMSに対応
第6版管理機能の権限・グループの入れ子・トップ画面とテナント・api/users の実行権限の解説と、権限グループ・特権ユーザ保護の改修・設計メモを追加
第5版本文から元記事や以前の版への言及を除き、正しい動作だけを書く形に整理
第4版「機能の仕様と使いこなし」を 1.5.8.1 のソースで検証して修正
第3版元記事への言及を整理し、必要なコードをページに収録。検索機能に一覧の検索と絞り込みを追加
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「機能の仕様と使いこなし」セクションの記事を追加