Skip to content

アクセス権限の実装(サイト・レコード・項目) ​

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

プリザンターはサイト単位・レコード単位・項目単位の 3 つの粒度でアクセス権限を設定できます。このページでは、それぞれの権限がどこに保持され、どのように判定され、CRUD の SQL にどう影響するかを内部実装から説明します。

  • サイト権限とレコード権限は同じ Permissions テーブルに保持され、ReferenceId にサイト ID が入るかレコード ID が入るかで区別される。権限の値はビットマスク
  • 権限の判定は「サイト権限 OR レコード権限」。複数の経路(組織・グループ・ユーザー・全員)から得た権限も OR でマージされ、最も強い値が効く
  • 項目のアクセス制御は SiteSettings の JSON に保持され、SQL ではなくアプリケーション層(値のセット・レスポンス生成・画面描画)で効く

特定ユーザーのサイト権限を SQL で調べる方法や、権限継承の一部だけを追加・除外する方法は ユーザ権限の確認とサイト権限継承の部分追加・除外 を参照してください。

SQL 例の読み方

DBMS 名のない SQL は、3 DBMS で共通の構文を使った説明用の抜粋です。プリザンターは MySQL 接続でも ansi_quotes を設定するため、識別子の二重引用符が使えます。DB のコンソールで直接実行するときの引用符・パラメータの扱いは DBMS ごとの SQL の書き方 を参照してください。... を含む例は実行用 SQL ではありません。

3 層の全体像 ​

図を読み込み中…

粒度設定する場所保持場所主に効く場所
サイトアクセス権の管理Permissions(ReferenceId = Sites.InheritPermission)Can* の事前チェック、一覧 SQL の WHERE 句
レコードレコードのアクセス制御Permissions(ReferenceId = Items.ReferenceId)同上(サイト権限と OR)
項目項目のアクセス制御Sites.SiteSettings の CreateColumnAccessControls / ReadColumnAccessControls / UpdateColumnAccessControls値のセット、レスポンス、画面描画

サイトのアクセス権限 ​

Permissions テーブル ​

サイトのアクセス権限は Permissions テーブルに保持されています。

カラム名型説明
ReferenceIdbigint権限の付与対象 ID(Sites.InheritPermission と紐づく)
DeptIdint組織 ID(組織への付与時に使用、それ以外は 0)
GroupIdintグループ ID(グループへの付与時に使用、それ以外は 0)
UserIdintユーザー ID(ユーザーへの付与時に使用、それ以外は 0)。-1 は「全員」を意味する特殊値
DeptNamenvarchar組織名(表示用の非正規化キャッシュ)
GroupNamenvarcharグループ名(表示用の非正規化キャッシュ)
Namenvarcharユーザー名(表示用の非正規化キャッシュ)
PermissionTypebigint権限のビットマスク値
Creatorint登録者ユーザー ID
Updatorint更新者ユーザー ID
CreatedTimedatetime登録日時
UpdatedTimedatetime更新日時

1 レコードにつき DeptId・GroupId・UserId のいずれか 1 つだけが非ゼロになります。

モデルクラスの定義は次のとおりです。

cs
public class PermissionModel : BaseModel
{
    public long ReferenceId = 0;
    public int DeptId = 0;
    public int GroupId = 0;
    public int UserId = 0;
    public string DeptName = string.Empty;
    public string GroupName = string.Empty;
    public string Name = string.Empty;
    public Permissions.Types PermissionType = (Permissions.Types)31;
    // ...
}

デフォルト値の 31 は Read(1) + Create(2) + Update(4) + Delete(8) + SendMail(16) です。

PermissionType のビットマスク ​

PermissionType は long 型のビットフィールドで、各ビットが独立した権限に対応しています。

cs
public enum Types : long
{
    NotSet           = 0,           // 0000 0000 0000
    Read             = 1,           // 0000 0000 0001
    Create           = 2,           // 0000 0000 0010
    Update           = 4,           // 0000 0000 0100
    Delete           = 8,           // 0000 0000 1000
    SendMail         = 16,          // 0000 0001 0000
    Export           = 32,          // 0000 0010 0000
    Import           = 64,          // 0000 0100 0000
    ManageSite       = 128,         // 0000 1000 0000
    ManagePermission = 256,         // 0001 0000 0000
    ManageTenant     = 1073741824,  // 0100 0000 0000 0000 0000 0000 0000 0000
    ManageService    = 2147483648,  // 1000 0000 0000 0000 0000 0000 0000 0000
}

Permissions.cs#L16-L30

「アクセス権の管理」で選べる標準パターンは、Parameters/Permissions.json の Pattern に定義された 4 つです(1.5.8.1 のソースでは Permissions.json#L5-L10)。

値パターン名(画面の表示)内訳
1ReadOnly(読取専用)Read
31ReadWrite(書き込み)Read + Create + Update + Delete + SendMail
255Leader(リーダー)Read 〜 ManageSite
511Manager(管理者)Read 〜 ManagePermission

63 や 127 のような値には、パターン名はありません。標準パターンに個別の権限(エクスポート・インポートなど)を足したときの値で、次のような内訳です。

値内訳
63Read + Create + Update + Delete + SendMail + Export
127Read 〜 Import

権限の有無はビット演算で確認できます。

cs
var permissionType = 63L;
var deleteFlag = (long)Permissions.Types.Delete; // 8

if ((permissionType & deleteFlag) == deleteFlag)
{
    // Delete 権限あり
}

Sites.InheritPermission と権限の継承 ​

サイトがどの権限を使うかは、Sites テーブルの InheritPermission カラムで決まります。

text
-- Sites テーブルの関連カラム(型名は SQL Server)
SiteId            bigint   -- サイト ID(主キー)
TenantId          int      -- テナント ID
ParentId          bigint   -- 親サイト ID(階層構造)
InheritPermission bigint   -- 権限の継承元サイト ID
ReferenceType     nvarchar -- 'Results' / 'Issues' / 'Wikis' / 'Sites'

InheritPermission には「このサイトの権限として参照する Permissions.ReferenceId」が入ります。

  • 自サイトに独自の権限を設定する場合: InheritPermission = SiteId
  • 親サイトから継承する場合: InheritPermission は親の SiteId(親がさらに継承していれば、その継承先の SiteId)

図を読み込み中…

この例では子サイト(SiteId=300)が親サイト(SiteId=100)の権限を継承しているため、両方のサイトが Permissions.ReferenceId=100 の行を参照します。

権限の一括取得(GetPermissions) ​

ログイン時やリクエスト処理の開始時に、プリザンターはそのユーザーが持つ全サイトの権限を一括で取得し、Context.PermissionHash(Dictionary<long, Types>)にキャッシュします。取得 SQL は次の 5 系統の UNION ALL です。

系統経路
①組織(Dept)経由
②グループ → 組織経由
③グループ → ユーザー経由
④ユーザー直接指定
⑤全員(UserId = -1)
sql
-- Rds/Implem.SqlServer/SqlServerSqls.cs GetPermissions プロパティ(一部)
-- ① 組織(Dept)経由の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
    INNER JOIN "Depts"
        ON "Permissions"."DeptId" = "Depts"."DeptId"
WHERE "Sites"."TenantId" = @_T
    AND "Depts"."DeptId" = @_D
    AND "Depts"."Disabled" = 'false'

UNION ALL

-- ② グループ→組織経由の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
    INNER JOIN "Groups"
        ON "Permissions"."GroupId" = "Groups"."GroupId"
    INNER JOIN "GroupMembers"
        ON "Groups"."GroupId" = "GroupMembers"."GroupId"
    INNER JOIN "Depts"
        ON "GroupMembers"."DeptId" = "Depts"."DeptId"
WHERE "Sites"."TenantId" = @_T
    AND "Groups"."Disabled" = 'false'
    AND "Depts"."DeptId" = @_D
    AND "Depts"."Disabled" = 'false'

UNION ALL

-- ③ グループ→ユーザー経由の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
    INNER JOIN "Groups"
        ON "Permissions"."GroupId" = "Groups"."GroupId"
    INNER JOIN "GroupMembers"
        ON "Groups"."GroupId" = "GroupMembers"."GroupId"
    INNER JOIN "Users"
        ON "GroupMembers"."UserId" = "Users"."UserId"
WHERE "Sites"."TenantId" = @_T
    AND "Groups"."Disabled" = 'false'
    AND "Users"."UserId" = @_U
    AND "Users"."Disabled" = 'false'

UNION ALL

-- ④ ユーザー直接指定の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
WHERE "Sites"."TenantId" = @_T
    AND "Permissions"."UserId" > 0
    AND "Permissions"."UserId" = @_U

UNION ALL

-- ⑤ 全員(UserId = -1)への権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
WHERE "Sites"."TenantId" = @_T
    AND "Permissions"."UserId" = -1;
sql
-- Rds/Implem.PostgreSql/PostgreSqlSqls.cs GetPermissions プロパティ(一部)
-- ① 組織(Dept)経由の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
    INNER JOIN "Depts"
        ON "Permissions"."DeptId" = "Depts"."DeptId"
WHERE "Sites"."TenantId" = @ipT
    AND "Depts"."DeptId" = @ipD
    AND "Depts"."Disabled" = 'false'

UNION ALL

-- ② グループ→組織経由の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
    INNER JOIN "Groups"
        ON "Permissions"."GroupId" = "Groups"."GroupId"
    INNER JOIN "GroupMembers"
        ON "Groups"."GroupId" = "GroupMembers"."GroupId"
    INNER JOIN "Depts"
        ON "GroupMembers"."DeptId" = "Depts"."DeptId"
WHERE "Sites"."TenantId" = @ipT
    AND "Groups"."Disabled" = 'false'
    AND "Depts"."DeptId" = @ipD
    AND "Depts"."Disabled" = 'false'

UNION ALL

-- ③ グループ→ユーザー経由の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
    INNER JOIN "Groups"
        ON "Permissions"."GroupId" = "Groups"."GroupId"
    INNER JOIN "GroupMembers"
        ON "Groups"."GroupId" = "GroupMembers"."GroupId"
    INNER JOIN "Users"
        ON "GroupMembers"."UserId" = "Users"."UserId"
WHERE "Sites"."TenantId" = @ipT
    AND "Groups"."Disabled" = 'false'
    AND "Users"."UserId" = @ipU
    AND "Users"."Disabled" = 'false'

UNION ALL

-- ④ ユーザー直接指定の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
WHERE "Sites"."TenantId" = @ipT
    AND "Permissions"."UserId" > 0
    AND "Permissions"."UserId" = @ipU

UNION ALL

-- ⑤ 全員(UserId = -1)への権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
WHERE "Sites"."TenantId" = @ipT
    AND "Permissions"."UserId" = -1;
sql
-- Rds/Implem.MySql/MySqlSqls.cs GetPermissions プロパティ(一部)
-- ① 組織(Dept)経由の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
    INNER JOIN "Depts"
        ON "Permissions"."DeptId" = "Depts"."DeptId"
WHERE "Sites"."TenantId" = @ipT
    AND "Depts"."DeptId" = @ipD
    AND "Depts"."Disabled" = 0

UNION ALL

-- ② グループ→組織経由の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
    INNER JOIN "Groups"
        ON "Permissions"."GroupId" = "Groups"."GroupId"
    INNER JOIN "GroupMembers"
        ON "Groups"."GroupId" = "GroupMembers"."GroupId"
    INNER JOIN "Depts"
        ON "GroupMembers"."DeptId" = "Depts"."DeptId"
WHERE "Sites"."TenantId" = @ipT
    AND "Groups"."Disabled" = 0
    AND "Depts"."DeptId" = @ipD
    AND "Depts"."Disabled" = 0

UNION ALL

-- ③ グループ→ユーザー経由の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
    INNER JOIN "Groups"
        ON "Permissions"."GroupId" = "Groups"."GroupId"
    INNER JOIN "GroupMembers"
        ON "Groups"."GroupId" = "GroupMembers"."GroupId"
    INNER JOIN "Users"
        ON "GroupMembers"."UserId" = "Users"."UserId"
WHERE "Sites"."TenantId" = @ipT
    AND "Groups"."Disabled" = 0
    AND "Users"."UserId" = @ipU
    AND "Users"."Disabled" = 0

UNION ALL

-- ④ ユーザー直接指定の権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
WHERE "Sites"."TenantId" = @ipT
    AND "Permissions"."UserId" > 0
    AND "Permissions"."UserId" = @ipU

UNION ALL

-- ⑤ 全員(UserId = -1)への権限
SELECT DISTINCT
    "Sites"."SiteId" AS "ReferenceId",
    "Permissions"."PermissionType"
FROM "Sites"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Sites"."InheritPermission"
WHERE "Sites"."TenantId" = @ipT
    AND "Permissions"."UserId" = -1;
SQL ServerPostgreSQL・MySQL意味
@_T@ipTTenantId(テナント ID)
@_D@ipDDeptId(ログインユーザーの組織 ID)
@_U@ipUUserId(ログインユーザーの ID)

パラメータの接頭辞は DBMS で変わり、Parameter.json の SqlParameterPrefix が空の既定なら SQL Server は @_、PostgreSQL・MySQL は @ip です(Parameter.cs、SqlIo.cs)。SQL は DBMS ごとに SqlServerSqls.cs・PostgreSqlSqls.cs・MySqlSqls.cs に分かれています。MySQL だけ Disabled の比較が 'false' ではなく 0 です。MySQL でも識別子が二重引用符のままなのは、プリザンターが SQL の実行前に sql_mode へ ansi_quotes を設定しているためです(MySqlCommandText.cs)。このページの以降の SQL も同じです。

同じサイトに対して複数の結果が返った場合(例: 組織経由で 63、ユーザー直接で 255)は、Dictionary に OR ビット演算でマージされます。

cs
private static Dictionary<long, Types> Hash(
    EnumerableRowCollection<DataRow> dataRows)
{
    var hash = dataRows
        .Select(o => o.Long("ReferenceId"))
        .Distinct()
        .ToDictionary(o => o, o => Types.NotSet);
    dataRows.ForEach(dataRow =>
    {
        var key = dataRow.Long("ReferenceId");
        hash[key] |= (Types)dataRow.Long("PermissionType"); // OR でマージ
    });
    return hash;
}

つまり、複数の経路で同じサイトにアクセスできるユーザーには、すべての経路の権限を合わせたもの(最も強い権限)が適用されます。組織・グループ・ユーザーの間に優先順位はなく、権限は足し算だけで、特定の経路で権限を打ち消す(拒否する)仕組みはありません。

グループの入れ子 ​

グループには、ユーザー・組織に加えて別のグループを「子グループ」として登録できます。親子関係は GroupChildren テーブル(GroupId = 親、ChildId = 子)に入りますが、上の GetPermissions も一覧の権限条件も GroupChildren は見ず、GroupMembers だけを結合しています。

子グループのメンバーが親グループの権限を得られるのは、子グループの直接メンバーが親グループの GroupMembers に ChildGroup = 1 の行として物理的にコピーされるためです。グループを更新すると GroupMemberUtilities.SyncGroupMembers が呼ばれ、RefreshAllChildMembers.sql が再帰 CTE で子孫グループをたどり、MERGE で親グループの ChildGroup = 1 の行を差分同期します(GroupMemberUtilities.cs#L33-L91、RefreshAllChildMembers.sql)。

図を読み込み中…

項目1.5.8.1 の実装
コピー元子孫グループの ChildGroup = 0 の行(直接メンバー)だけ。無効(Disabled)のグループはたどらない
深さの上限General.json の GroupsDepthMax(既定 30)。再帰 CTE の Lv の上限にも使われる(General.json#L103)
循環参照子グループの設定時に深さ優先探索で検査し、循環なら CircularGroupChild、深さ超過なら GroupDepthMax のエラー(GroupChildUtilities.cs#L81-L159)

ログインユーザーが属するグループの一覧(Context.Groups)は、SetPermissions() で PermissionHash と一緒に GetGroup SQL から取得されます(Context.cs#L909-L913)。この SQL も GroupMembers を組織経由・ユーザー直接の 2 通りで引くだけなので(SqlServerSqls.cs#L203-L231)、ChildGroup = 1 の行を通じて親グループも含まれます。項目のアクセス制御の「グループ」指定(GroupContains)はこの Context.Groups と照合されます。

Context への権限のセット ​

リクエスト処理時に Context.SetPermissions() が呼ばれ、PermissionHash から対象サイトの権限が ContextPermission にセットされます。

cs
public void SetPermissions(SiteSettings ss, long referenceId = 0)
{
    var cp = new ContextPermission();
    if (Publish)
    {
        cp.PermissionType = Permissions.Types.Read;
        cp.ItemPermissionType = Permissions.Types.Read;
    }
    else if (Controller != "publishes")
    {
        if (PermissionHash?.ContainsKey(ss.InheritPermission) == true)
        {
            cp.PermissionType = PermissionHash[ss.InheritPermission];
        }
        if (referenceId != 0 && PermissionHash?.ContainsKey(referenceId) == true)
        {
            cp.ItemPermissionType = PermissionHash[referenceId];
        }
        // テーブルロック時は Read/Export/SendMail のみに制限
        if (ss.LockedTable())
        {
            var lockedPermissionType = Permissions.Types.Read
                | Permissions.Types.Export
                | Permissions.Types.SendMail;
            cp.PermissionType &= lockedPermissionType;
            cp.ItemPermissionType &= lockedPermissionType;
        }
    }
    ContextPermissions[$"{ss.SiteId}_{ss.ReferenceType}"] = cp;
}

Context.cs#L921-L950

読み取れるポイントは次のとおりです。

状況結果
公開(Publish)サイト権限・レコード権限とも Read
通常PermissionHash[ss.InheritPermission] がサイト権限、PermissionHash[referenceId] がレコード権限
テーブルロック中(ss.LockedTable())どちらも Read / Export / SendMail だけに絞られる

ContextPermission は PermissionType(サイトレベルの権限)と ItemPermissionType(レコード個別の権限)だけを持つ小さなクラスです。

cs
public class ContextPermission
{
    public Permissions.Types? PermissionType { get; set; }
    public Permissions.Types? ItemPermissionType { get; set; }
}

1.5.6.0 での変更

1.5.6.0 で、権限の保持場所が SiteSettings.PermissionType / SiteSettings.ItemPermissionType から Context.ContextPermissions(Dictionary<string, ContextPermission>)へ移されました。キーは "{SiteId}_{ReferenceType}" で、同一リクエスト内で複数サイトの権限を取り違えないようになっています。1.5.5.0 以前のコードを読む場合は読み替えてください。

取り出すときは Context.GetContextPermission() を使います。未設定なら SetPermissions() で解決してから返します。

cs
public ContextPermission GetContextPermission(SiteSettings ss)
{
    var cp = ContextPermissions.Get($"{ss.SiteId}_{ss.ReferenceType}");
    if (cp == null)
    {
        SetPermissions(
            ss: ss,
            referenceId: Id);
        cp = ContextPermissions.Get($"{ss.SiteId}_{ss.ReferenceType}");
    }
    return cp;
}

Context.cs#L966-L977

権限確認メソッド ​

コントローラーやモデルからの権限確認には、主に次のメソッドが使われます(Implem.Pleasanter/Libraries/Security/Permissions.cs)。

メソッドtrue になる条件
context.HasPermission(ss: ss)権限が 1 つでもある
context.CanRead(ss: ss)Read 権限がある
context.CanCreate(ss: ss)Create 権限がある
context.CanUpdate(ss: ss)Update 権限がある
context.CanDelete(ss: ss)Delete 権限がある
context.CanManageSite(ss: ss)ManageSite 権限がある
context.CanManagePermission(ss: ss)ManagePermission 権限がある

これらは内部で ItemsCan() を呼んでいます。

cs
private static bool ItemsCan(
    this Context context,
    SiteSettings ss,
    Types type,
    bool site,
    bool checkLocked = true)
{
    // ロック状態のチェック
    if (checkLocked && ss.Locked())
    {
        if ((type & Types.Update) == Types.Update) return false;
        if ((type & Types.Delete) == Types.Delete) return false;
    }
    if (ss.LockedTable())
    {
        if ((type & Types.Create) == Types.Create) return false;
        if ((type & Types.Import) == Types.Import) return false;
    }
    return (context.GetPermissionType(
        ss: ss,
        site: site) & type) == type
            || context.HasPrivilege; // 特権ユーザーは常に true
}

Permissions.cs#L830-L850

HasPrivilege は Security.json の PrivilegedUsers に含まれるログイン ID のユーザーが該当し、すべての権限チェックをバイパスします(ほかにバイパスされるものは管理機能の権限を参照)。

権限の更新(全削除 → 再登録) ​

「アクセス権の管理」で権限を変更すると、PermissionUtilities.UpdatePermissions() が呼ばれます。対象 ReferenceId の行をいったん全削除し、再登録する REPLACE パターンです。

cs
public static void UpdatePermissions(
    this List<SqlStatement> statements,
    Context context,
    SiteSettings ss,
    long referenceId,
    IEnumerable<string> permissions,
    bool site = false)
{
    // いったん全削除して再登録(REPLACE パターン)
    statements.Add(Rds.PhysicalDeletePermissions(
        where: Rds.PermissionsWhere().ReferenceId(referenceId)));

    // 継承元が自分自身(独立した権限設定)の場合のみ再挿入
    if (!site || site && ss.InheritPermission == ss.SiteId)
    {
        new PermissionCollection(
            context: context,
            referenceId: referenceId,
            permissions: permissions)
                .ForEach(permissionModel =>
                    statements.Add(Insert(permissionModel)));
    }
    // ...
}

挿入は 1 行ずつ行われます。

sql
INSERT INTO "Permissions" (
    "ReferenceId", "PermissionType",
    "DeptId", "GroupId", "UserId"
)
VALUES (
    @ReferenceId, @PermissionType,
    @DeptId, @GroupId, @UserId
);

レコードのアクセス制御 ​

「サイトのアクセス権の管理」で「レコードのアクセス制御を使用する」を有効にすると、各レコードにサイト権限とは別の独自の権限を設定できます。

格納先はサイト権限と同じ Permissions テーブル ​

レコード個別の権限も Permissions テーブルに保持されます。違いは ReferenceId に入る値だけです。

ケースReferenceId の値
サイト権限Sites.InheritPermission(サイト ID)
レコード権限Items.ReferenceId(レコード ID)
sql
-- サイト権限の例(ReferenceId がサイト ID)
SELECT * FROM "Permissions"
WHERE "ReferenceId" = 100;   -- SiteId = 100 のサイト権限

-- レコード権限の例(ReferenceId がレコード ID)
SELECT * FROM "Permissions"
WHERE "ReferenceId" = 12345; -- ItemId = 12345 のレコード権限

サイト権限とレコード権限の OR 合成 ​

ContextPermission の PermissionType(サイト)と ItemPermissionType(レコード)は、Context.GetPermissionType() で OR 合成されます。

cs
public Permissions.Types GetPermissionType(SiteSettings ss, bool site = false)
{
    var cp = GetContextPermission(ss: ss);
    var permission = Permissions.Types.NotSet;
    if (cp.PermissionType != null)
    {
        permission |= (Permissions.Types)cp.PermissionType;
    }
    if (cp.ItemPermissionType != null && !site)
    {
        permission |= (Permissions.Types)cp.ItemPermissionType; // OR で合成
    }
    return permission;
}

Context.cs#L951-L964

CanRead などはこの値を使うため、サイト権限とレコード権限のどちらかを持っていれば許可されます。site: true で呼ばれた場合はサイト権限だけで判定します。

開いているレコードの権限の取得(GetPermissionsById) ​

レコードを開くときは、ログイン時と同じ GetPermissions に加えて、そのレコード固有の権限を取る GetPermissionsById が発行されます。

cs
public static Dictionary<long, Types> Get(Context context)
{
    if (context.Authenticated)
    {
        var statements = new List<SqlStatement>()
        {
            // ① 全サイトの権限を取得(ログイン時と同じ)
            new SqlStatement(
                context.Sqls.GetPermissions,
                new SqlParamCollection())
        };
        if (context.Id > 0 && context.Id != context.SiteId)
        {
            // ② 現在開いているレコードの固有権限を追加取得
            statements.Add(new SqlStatement(
                context.Sqls.GetPermissionsById
                    .Replace("@ReferenceId", context.Id.ToStr()),
                new SqlParamCollection()));
        }
        return Hash(
            dataRows: Repository.ExecuteTable(
                context: context,
                statements: statements.ToArray())
                    .AsEnumerable());
    }
    else
    {
        return new Dictionary<long, Types>();
    }
}

GetPermissionsById は GetPermissions の後ろに UNION ALL で追加される形で、GetPermissions と同じ 5 系統で構成されています。違いは JOIN の起点が Sites.InheritPermission ではなく Items.ReferenceId になっている点です。①の組織経由の部分だけを示します(②〜⑤も同じパターンです)。

sql
-- Rds/Implem.SqlServer/SqlServerSqls.cs GetPermissionsById プロパティ(①のみ抜粋)
UNION ALL

SELECT DISTINCT
    "Items"."ReferenceId",
    "Permissions"."PermissionType"
FROM "Items"
    INNER JOIN "Sites"
        ON "Items"."SiteId" = "Sites"."SiteId"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Items"."ReferenceId"
    INNER JOIN "Depts"
        ON "Permissions"."DeptId" = "Depts"."DeptId"
WHERE "Items"."ReferenceId" = @ReferenceId
    AND "Sites"."TenantId" = @_T
    AND "Depts"."DeptId" = @_D
    AND "Depts"."Disabled" = 'false'
sql
-- Rds/Implem.PostgreSql/PostgreSqlSqls.cs GetPermissionsById プロパティ(①のみ抜粋)
UNION ALL

SELECT DISTINCT
    "Items"."ReferenceId",
    "Permissions"."PermissionType"
FROM "Items"
    INNER JOIN "Sites"
        ON "Items"."SiteId" = "Sites"."SiteId"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Items"."ReferenceId"
    INNER JOIN "Depts"
        ON "Permissions"."DeptId" = "Depts"."DeptId"
WHERE "Items"."ReferenceId" = @ReferenceId
    AND "Sites"."TenantId" = @ipT
    AND "Depts"."DeptId" = @ipD
    AND "Depts"."Disabled" = 'false'
sql
-- Rds/Implem.MySql/MySqlSqls.cs GetPermissionsById プロパティ(①のみ抜粋)
UNION ALL

SELECT DISTINCT
    "Items"."ReferenceId",
    "Permissions"."PermissionType"
FROM "Items"
    INNER JOIN "Sites"
        ON "Items"."SiteId" = "Sites"."SiteId"
    INNER JOIN "Permissions"
        ON "Permissions"."ReferenceId" = "Items"."ReferenceId"
    INNER JOIN "Depts"
        ON "Permissions"."DeptId" = "Depts"."DeptId"
WHERE "Items"."ReferenceId" = @ReferenceId
    AND "Sites"."TenantId" = @ipT
    AND "Depts"."DeptId" = @ipD
    AND "Depts"."Disabled" = 0

結果は PermissionHash に OR マージされ、SetPermissions() の referenceId の分岐で ContextPermission.ItemPermissionType にセットされます。

一覧の SELECT に付く権限条件(CanRead) ​

レコードの一覧取得 SQL では、WHERE 句に権限チェックのサブクエリが自動で付きます(CanRead_Body.txt から生成)。

sql
"Sites"."TenantId" = @_T
AND (
    EXISTS (
        SELECT TOP 1 "Permissions"."PermissionType"
        FROM "Permissions"
        WHERE
            -- InheritPermission と Items.ReferenceId の両方を IN 句で参照
            "Permissions"."ReferenceId"
                IN ("InheritPermission", "Items"."ReferenceId")
            AND "Permissions"."PermissionType" & 1 = 1  -- Read フラグ確認
            AND (
                ("Permissions"."DeptId" = @_D AND @_D <> 0)
                OR ("Permissions"."UserId" = @_U AND @_U <> 0)
                OR ("Permissions"."UserId" = -1)
                OR (
                    EXISTS (
                        SELECT *
                        FROM "GroupMembers"
                            INNER JOIN "Groups"
                                ON "GroupMembers"."GroupId" = "Groups"."GroupId"
                        WHERE "Groups"."TenantId" = @_T
                            AND "Permissions"."GroupId" = "GroupMembers"."GroupId"
                            AND (
                                ("GroupMembers"."DeptId" = @_D AND @_D <> 0)
                                OR ("GroupMembers"."UserId" = @_U AND @_U <> 0)
                            )
                    )
                )
            )
    )
)

この SQL は SQL Server 用の定義です

CanRead_Body.txt は SQL Server の書き方(@_T・TOP 1)で書かれた定義で、PostgreSQL・MySQL 版はありません。確認したソースでは、実行時の Def.Sql.CanRead には Definition_Sql のこのファイルではなく、App_Data/Definitions/Sqls/ の下の DBMS ごとのフォルダ(SQLServer・PostgreSQL・MySQL)にある CanRead.sql の内容が入ります(Def.cs、Directories.cs)。DBMS ごとの CanRead.sql の違い(接頭辞 @_ / @ip と、MySQL だけ Disabled の比較が 0)は 検索機能の内部実装 の「権限フィルタ(CanRead.sql)」にまとめています。

"Permissions"."ReferenceId" IN ("InheritPermission", "Items"."ReferenceId") により、サイト権限とレコード権限のどちらかに Read があればそのレコードが返ります。サイト権限がなくてもレコード権限があれば一覧に表示される、という動作はこの条件によるものです。

さらに SetPermissionsWhere() では、サイト権限だけでは要求された権限を満たせない場合に、CheckRecordPermission の EXISTS サブクエリを追加します。

cs
public static SqlWhereCollection SetPermissionsWhere(
    Context context,
    SiteSettings ss,
    SqlWhereCollection where,
    Types permissionType,
    bool checkPermission = true)
{
    // ...省略...
    if (!CheckSitePermission(
        context: context,
        ss: ss,
        permissionType: permissionType) && checkPermission)
    {
        where.CheckRecordPermission(
            context: context,
            ss: ss,
            permissionType: permissionType ^ ((context.GetContextPermission(ss: ss).PermissionType ?? Types.NotSet) & permissionType));
    }
    // ...
}
cs
public static SqlExists CheckRecordPermission(
    Context context,
    string idColumnBracket,
    Types permissionType = Types.Read,
    List<long> siteIdList = null)
{
    var type = permissionType.ToInt().ToString();
    return Rds.ExistsPermissions(
        where: Rds.PermissionsWhere()
            .ReferenceId(raw: idColumnBracket)
            // ...
            .PermissionType(_operator: $" & {type}={type}") // 対象権限のビットチェック
            .PermissionsWhere(context: context));            // ユーザー所属チェック
}

作成時権限(PermissionForCreating) ​

作成時権限は、レコードを作成したときに作成者(User)・作成者の組織(Dept)・作成者のグループ(Group)、または特定の列に入力されたユーザー・組織に自動で権限を付与する設定です。SiteSettings.PermissionForCreating に Dictionary<string, Permissions.Types> として保持されます。

json
// SiteSettings の JSON の一例
{
  "PermissionForCreating": {
    "User": 511,   // 作成者自身に ManagePermission 以下の権限を付与
    "Dept": 63,    // 作成者の組織に付与
    "Group": 31    // 作成者のグループに付与
  }
}

ResultModel / IssueModel の CreateStatements() で、Items と Results(Issues)への INSERT、リンク情報の挿入に続いて PermissionUtilities.InsertStatements() が呼ばれます。

cs
public List<SqlStatement> CreateStatements(
    Context context,
    SiteSettings ss,
    // ...
)
{
    var statements = new List<SqlStatement>();
    // ...
    statements.AddRange(new List<SqlStatement>
    {
        // ① Items テーブルへ挿入
        Rds.InsertItems(selectIdentity: true, /* ... */),

        // ② Results テーブルへ挿入
        Rds.InsertResults(/* ... */),

        // ③ リンク情報の挿入
        InsertLinks(context, ss, setIdentity: true),
    });
    // ④ レコード作成時権限の挿入
    statements.AddRange(PermissionUtilities.InsertStatements(
        context: context,
        ss: ss,
        columns: ss.Columns
            .Where(o => o.Type != Column.Types.Normal)
            .ToDictionary(
                o => $"{o.ColumnName},{o.Type}",
                o => PropertyValue(context, o)?.ToInt().ToSingleList() ?? new List<int>()),
        permissions: ss.PermissionForCreating));
    return statements;
}

InsertStatements() はキーに応じて Permissions に行を INSERT します。

キー付与先
Dept作成者の組織(context.DeptId)
Group作成者が属する全グループ
User作成者(context.UserId)
列名(Manager、Owner など)その列に入力された ID のユーザー/組織
cs
// PermissionUtilities.InsertStatements() の主要ロジック(抜粋)
permissions?.ForEach(data =>
{
    switch (data.Key)
    {
        case "Dept":
            insertSet.Add(new PermissionModel(
                referenceId: referenceId,       // レコード ID
                deptId: context.DeptId,         // 作成者の組織 ID
                userId: 0,
                permissionType: data.Value));   // 付与する権限
            break;
        case "Group":
            Groups(context).ForEach(groupId =>
                insertSet.Add(new PermissionModel(
                    referenceId: referenceId,
                    groupId: groupId,            // 作成者が属する全グループ
                    permissionType: data.Value)));
            break;
        case "User":
            insertSet.Add(new PermissionModel(
                referenceId: referenceId,
                userId: context.UserId,          // 作成者のユーザー ID
                permissionType: data.Value));
            break;
        default:
            // 列名(Manager, Owner など)の場合
            // その列に入力された ID のユーザー/組織に権限を付与
            var columnData = columns.FirstOrDefault(
                o => o.Key.StartsWith(data.Key + ","));
            // ...
    }
});

発行される SQL は次のようになります(トランザクション内)。

sql
-- ① Items テーブルへ INSERT
INSERT INTO "Items" ("ReferenceType", "SiteId", "Title", ...)
VALUES ('Results', @SiteId, @Title, ...);

-- ② Results テーブルへ INSERT
INSERT INTO "Results" ("SiteId", "ResultId", "Ver", "Title", "Body", ...)
VALUES (@SiteId, @_I, 1, @Title, @Body, ...);

-- ③ Permissions テーブルへ INSERT(作成者ユーザーに権限付与)
INSERT INTO "Permissions" ("Creator", "Updator", "ReferenceId", "PermissionType", "DeptId", "GroupId", "UserId")
VALUES (@_U, @_U, @_I, 511, 0, 0, 5);  -- 5 は作成者のユーザー ID の例

-- ④ Permissions テーブルへ INSERT(作成者組織に権限付与、Dept 指定の場合)
INSERT INTO "Permissions" ("Creator", "Updator", "ReferenceId", "PermissionType", "DeptId", "GroupId", "UserId")
VALUES (@_U, @_U, @_I, 63, 3, 0, 0);  -- 3 は作成者の組織 ID の例
sql
-- ① Items テーブルへ INSERT
INSERT INTO "Items" ("ReferenceType", "SiteId", "Title", ...)
VALUES ('Results', @SiteId, @Title, ...);

-- ② Results テーブルへ INSERT
INSERT INTO "Results" ("SiteId", "ResultId", "Ver", "Title", "Body", ...)
VALUES (@SiteId, @ipI, 1, @Title, @Body, ...);

-- ③ Permissions テーブルへ INSERT(作成者ユーザーに権限付与)
INSERT INTO "Permissions" ("Creator", "Updator", "ReferenceId", "PermissionType", "DeptId", "GroupId", "UserId")
VALUES (@ipU, @ipU, @ipI, 511, 0, 0, 5);  -- 5 は作成者のユーザー ID の例

-- ④ Permissions テーブルへ INSERT(作成者組織に権限付与、Dept 指定の場合)
INSERT INTO "Permissions" ("Creator", "Updator", "ReferenceId", "PermissionType", "DeptId", "GroupId", "UserId")
VALUES (@ipU, @ipU, @ipI, 63, 3, 0, 0);  -- 3 は作成者の組織 ID の例
sql
-- ① Items テーブルへ INSERT
INSERT INTO "Items" ("ReferenceType", "SiteId", "Title", ...)
VALUES ('Results', @SiteId, @Title, ...);

-- ② Results テーブルへ INSERT
INSERT INTO "Results" ("SiteId", "ResultId", "Ver", "Title", "Body", ...)
VALUES (@SiteId, @ipI, 1, @Title, @Body, ...);

-- ③ Permissions テーブルへ INSERT(作成者ユーザーに権限付与)
INSERT INTO "Permissions" ("Creator", "Updator", "ReferenceId", "PermissionType", "DeptId", "GroupId", "UserId")
VALUES (@ipU, @ipU, @ipI, 511, 0, 0, 5);  -- 5 は作成者のユーザー ID の例

-- ④ Permissions テーブルへ INSERT(作成者組織に権限付与、Dept 指定の場合)
INSERT INTO "Permissions" ("Creator", "Updator", "ReferenceId", "PermissionType", "DeptId", "GroupId", "UserId")
VALUES (@ipU, @ipU, @ipI, 63, 3, 0, 0);  -- 3 は作成者の組織 ID の例

@_I(PostgreSQL・MySQL では @ipI)は、①の INSERT で採番された ID をプリザンターが後続の文に渡すパラメータです(SQLServer/Identity.sql、PostgreSQL/Identity.sql、Rds.cs、Repository.cs)。③④で権限を付ける相手("UserId"・"DeptId")は、ログインユーザーのパラメータ(@_U・@_D)ではなく、数値がそのまま SQL に書き込まれます(PermissionUtilities.cs)。③④の "Creator"・"Updator" のように、INSERT には作成者・更新者の列が自動で付き、@_U(PostgreSQL・MySQL では @ipU)が入ります(①②では省略しています。Rds.cs、SqlInsert.cs)。

作成時権限の設定がなければ、レコード作成の SQL に Permissions への INSERT は含まれません。

更新時権限(PermissionForUpdating) ​

更新時権限は、レコード更新時に権限を再設定する仕組みで、PermissionForCreating と同じ構造です。PermissionUtilities.UpdateStatements() が既存のレコード権限を全削除してから再設定します。

cs
// PermissionUtilities.UpdateStatements()
public static List<SqlStatement> UpdateStatements(
    Context context,
    SiteSettings ss,
    long referenceId,
    Dictionary<string, List<int>> columns,
    Dictionary<string, Permissions.Types> permissions)
{
    var statements = new List<SqlStatement>();
    if (permissions?.Any() == true)
    {
        // 既存のレコード権限を全削除
        statements.Add(Rds.PhysicalDeletePermissions(
            where: Rds.PermissionsWhere().ReferenceId(referenceId)));
    }
    // 新しいレコード権限を設定
    statements.AddRange(InsertStatements(
        context: context,
        ss: ss,
        columns: columns,
        permissions: permissions,
        referenceId: referenceId));
    return statements;
}
sql
-- ① 既存のレコード権限を全削除
DELETE FROM "Permissions"
WHERE "ReferenceId" = @ReferenceId;

-- ② 新しい権限を挿入(Manager 列に入力された値のユーザーに対して)
INSERT INTO "Permissions" ("ReferenceId", "PermissionType", "DeptId", "GroupId", "UserId")
VALUES (@ReferenceId, 511, 0, 0, @ManagerUserId);

典型的な用途は、管理者(Manager)や担当者(Owner)が変わったときに権限も自動で付け替える運用です。

レコードの「権限」タブからの手動変更 ​

レコードの「権限」タブから手動で変更した場合は、RecordPermissions(List<string>)経由で statements.UpdatePermissions() が呼ばれます。サイト権限の更新と同じ「全削除 → 再登録」です。更新時権限が設定されている場合はそちらが優先されます。

cs
// ResultModel.cs / IssueModel.cs UpdateStatements() の一部
if (ss.PermissionForUpdating?.Any() == true)
{
    // 自動更新時権限
    statements.AddRange(PermissionUtilities.UpdateStatements(...));
}
else if (RecordPermissions != null)
{
    // UI から手動変更された場合
    statements.UpdatePermissions(
        context: context,
        ss: ss,
        referenceId: ResultId,
        permissions: RecordPermissions);
}

Mine():レコード上の自分の立場 ​

Mine() は「このレコードに関して自分はどの立場か」を文字列リストで返します。項目のアクセス制御の RecordUsers 判定に使われます。

cs
public override List<string> Mine(Context context)
{
    if (MineCache == null)
    {
        var mine = new List<string>();
        var userId = context.UserId;
        if (SavedManager == userId) mine.Add("Manager");  // 管理者
        if (SavedOwner   == userId) mine.Add("Owner");    // 担当者
        if (SavedCreator == userId) mine.Add("Creator");  // 作成者
        if (SavedUpdator == userId) mine.Add("Updator");  // 更新者
        MineCache = mine;
    }
    return MineCache;
}

項目のアクセス制御 ​

「項目のアクセス制御」では、特定の列を「読み取り専用にする」「特定のユーザーだけが変更できる」「担当者以外には非表示にする」といった制御ができます。

ColumnAccessControl と 3 つのリスト ​

中心になるのは ColumnAccessControl クラスで、1 列・1 操作分の権限設定を持ちます。

cs
[Serializable()]
public class ColumnAccessControl
{
    [NonSerialized]
    public int? No;               // 列の順序番号(非シリアル化)
    public string ColumnName;     // 対象列名(例:"ClassA", "Manager")
    public List<int> Depts;       // 許可する組織 ID リスト
    public List<int> Groups;      // 許可するグループ ID リスト
    public List<int> Users;       // 許可するユーザー ID リスト(-1 = 全員)
    public List<string> RecordUsers; // 許可するレコード上の役割("Manager", "Owner" など)
    public Permissions.Types? Type;  // 最低必要なサイト権限レベル
    public List<string> AllowedUsers; // 旧フィールド(v1.014 以前、現在は RecordUsers に統合)
}

SiteSettings には操作ごとに 3 種類のリストがあります。

リスト制御する操作許可されない場合
CreateColumnAccessControlsレコード新規作成その項目への入力が無視される
ReadColumnAccessControlsレコード表示・一覧その項目が空白で返される
UpdateColumnAccessControlsレコード更新その項目への変更が無視される

これらは Sites.SiteSettings カラムに JSON で保存されます。

json
// Sites.SiteSettings の JSON 例
{
  "ReadColumnAccessControls": [
    {
      "ColumnName": "ClassA",
      "Depts": null,
      "Groups": null,
      "Users": [101, 102],
      "RecordUsers": ["Manager"],
      "Type": 128
    }
  ],
  "UpdateColumnAccessControls": [
    {
      "ColumnName": "ClassA",
      "Users": [101],
      "Type": null
    }
  ]
}

保存時は RecordingData() が空のリストや NotSet を null にして最小化します。

cs
public ColumnAccessControl RecordingData()
{
    return new ColumnAccessControl()
    {
        No = No,
        ColumnName = ColumnName,
        Depts  = Depts?.Any()  == true ? Depts  : null,
        Groups = Groups?.Any() == true ? Groups : null,
        Users  = Users?.Any()  == true ? Users  : null,
        RecordUsers = RecordUsers?.Any() == true ? RecordUsers : null,
        Type = Type != Permissions.Types.NotSet ? Type : null
    };
}

許可判定(Allowed) ​

許可の判定は ColumnAccessControl.Allowed() で行われます。条件が 1 つも設定されていなければ全員許可です。

cs
public bool Allowed(Context context, SiteSettings ss, List<string> mine)
{
    // ① 何も設定されていない場合は全員許可
    if (Depts?.Any() != true
        && Groups?.Any() != true
        && Users?.Any() != true
        && RecordUsers?.Any() != true
        && (Type ?? Permissions.Types.NotSet) == Permissions.Types.NotSet)
    {
        return true;
    }
    // ② 組織チェック
    else if (DeptContains(context: context))
    {
        return true;
    }
    // ③ グループチェック
    else if (GroupContains(context: context))
    {
        return true;
    }
    // ④ ユーザーチェック
    else if (UserContains(context: context))
    {
        return true;
    }
    // ⑤ サイト権限レベルチェック
    else if (Type != null
        && Type != Permissions.Types.NotSet
        && (Type & context.GetContextPermission(ss: ss).PermissionType) == Type)
    {
        return true;
    }
    // ⑥ サイト権限レベルが不足 & RecordUsers も未指定の場合は拒否
    else if (Type != null
        && Type != Permissions.Types.NotSet
        && RecordUsers?.Any() != true)
    {
        return false;
    }
    // ⑦ RecordUsers チェック(レコード内の役割との照合)
    else if (RecordUsers?.Any(o => mine == null || mine?.Contains(o) == true) == true)
    {
        return true;
    }
    else
    {
        return false;
    }
}

図を読み込み中…

Type:サイト権限レベルによる制御 ​

Type は「このサイト権限レベルを持つユーザーに許可する」という設定です。

json
// UpdateColumnAccessControls の設定例
{
  "ColumnName": "ClassA",
  "Type": 128  // サイト権限に ManageSite のビットを含むユーザーのみ更新可
}

この設定では、ManageSite(128)を含む権限を持つユーザーだけが ClassA を更新できます。一般的な編集権限(31 = Read + Create + Update + Delete + SendMail)のユーザーは更新できません。

判定式は (Type & PermissionType) == Type で、値の大小ではなく Type のビットをすべて含むか を見ます。Type に 63 のような複数ビットの値を入れると、その全ビットを持つユーザーだけが許可されます。判定に使うのは ContextPermission.PermissionType、つまりサイトレベルの権限で、レコード権限(ItemPermissionType)は見ません。確認したソースでも同じ判定です(ColumnAccessControl.cs#L72-L117)。なお mine が null で渡されたとき(レコードが特定されない場面)は、⑦の RecordUsers の条件は満たされたものとして扱われます。

Type と RecordUsers を組み合わせると「ManageSite 権限 または 担当者」という条件も表現できます(⑥で拒否されずに⑦へ進むため)。

RecordUsers と Mine() ​

RecordUsers は ["Manager", "Owner", "Creator", "Updator"] などの文字列リストで、レコードのアクセス制御の節で説明した Mine() の戻り値と照合されます。列側の CanRead(CanCreate / CanUpdate も同じ構造)は、該当列の設定がなければ列定義由来のデフォルト設定を使います。

cs
// Column.cs の CanRead(CanCreate/CanUpdate も同様の構造)
public bool CanRead(Context context, SiteSettings ss, List<string> mine, ...)
{
    var columnAccessControl = ss.ReadColumnAccessControls?
        .FirstOrDefault(o => o.ColumnName == ColumnName)
        ?? new ColumnAccessControl(ss, this, "Read"); // デフォルト設定

    return columnAccessControl.Allowed(
        context: context,
        ss: ss,
        mine: mine);   // ← Mine() の戻り値を渡す
}

たとえば RecordUsers = ["Manager"] なら、管理者(Manager)列に自分のユーザー ID が入っているレコードでだけ許可されます。

画面表示の 3 値(ColumnPermissionTypes) ​

エディタ画面や一覧では、各列の権限を 3 値で表します。

cs
public enum ColumnPermissionTypes
{
    Deny,   // 非表示(列ごと見えない)
    Read,   // 読み取り専用(入力不可)
    Update  // 編集可能
}

Permissions.ColumnPermissionType() が次のように決めます。

cs
public static ColumnPermissionTypes ColumnPermissionType(
    Context context,
    SiteSettings ss,
    Column column,
    BaseModel baseModel)
{
    var canEdit = column.CanEdit(
        context: context,
        ss: ss,
        baseModel: baseModel);

    if (context.IsNew)
    {
        // 新規作成画面
        return column.CanCreate(context, ss, baseModel?.Mine(context))
            && canEdit
                ? ColumnPermissionTypes.Update
                : column.CanRead(context, ss, baseModel?.Mine(context))
                    ? ColumnPermissionTypes.Read
                    : ColumnPermissionTypes.Deny;
    }
    else
    {
        // 編集画面
        return column.CanRead(context, ss, baseModel?.Mine(context))
            && canEdit
                ? ColumnPermissionTypes.Update
                : column.CanRead(context, ss, baseModel?.Mine(context))
                    ? ColumnPermissionTypes.Read
                    : ColumnPermissionTypes.Deny;
    }
}
値新規作成時編集時
UpdateCanCreate かつ CanEditCanRead かつ CanEdit
Read上記以外で CanRead上記以外で CanRead
DenyCanRead でないCanRead でない

CanEdit は、編集時は CanRead かつ CanUpdate(UpdateColumnAccessControls の判定)、新規作成時は CanCreate です。サーバースクリプトで columns.{列名}.ReadOnly を設定している場合は、その値が CanEdit の結果より優先されます。1.5.8.1 のソースで確認しました(Permissions.cs#L722-L762、Column.cs#L1433-L1465、ServerScriptUtilities.cs#L1374-L1399)。

Deny の場合は HTML 要素そのものが生成されず、Read の場合は readOnly 属性が設定されます。

デフォルトのアクセス制御 ​

ColumnAccessControl の初期値は、列定義ファイル(Definition_Column/*.json)の CreateAccessControl・ReadAccessControl・UpdateAccessControl から決まります。ほとんどの列は NotSet(= 全員許可)です。

cs
private Permissions.Types DefaultType(SiteSettings ss, string type)
{
    switch (type)
    {
        case "Create":
            return Permissions.Get(
                ss.ColumnDefinitionHash.Get(ColumnName)?.CreateAccessControl);
        case "Read":
            return Permissions.Get(
                ss.ColumnDefinitionHash.Get(ColumnName)?.ReadAccessControl);
        case "Update":
            return Permissions.Get(
                ss.ColumnDefinitionHash.Get(ColumnName)?.UpdateAccessControl);
        default:
            return Permissions.Types.NotSet;
    }
}

キャッシュ ​

Column には CanReadCache・CanCreateCache・CanUpdateCache という列ごとのキャッシュがあり、同一リクエスト内での重複判定を防ぎます。キャッシュは SiteSettings.ClearColumnAccessControlCaches() で無効化されます(レコードを切り替えるときなど、mine の値が変わる場合に呼ばれます)。

cs
// Column.cs
public bool? CanReadCache;
public bool? CanCreateCache;
public bool? CanUpdateCache;

// SiteSettings.cs
public Dictionary<string, Column> ColumnAccessControlCaches =
    new Dictionary<string, Column>();

CRUD の SQL への影響 ​

3 層それぞれが CRUD のどこで効くかをまとめます。

操作サイト権限レコード権限項目のアクセス制御
SELECT(一覧)WHERE 句のサブクエリ(InheritPermission)同じサブクエリ(Items.ReferenceId)、必要に応じて CheckRecordPermissionSQL は全列取得。レスポンス生成時に読み取り不可の列は値を返さない
CREATECanCreate の事前チェック作成時権限があれば Permissions へ INSERTCanCreate = false の列は値のセットから除外
UPDATECanUpdate の事前チェック更新時権限・手動変更があれば全削除 → 再登録CanUpdate = false の列は SET 句に含まれない
DELETECanDelete の事前チェックゴミ箱移動では消えず、完全削除時に削除—

SELECT ​

一覧の WHERE 句は、レコードのアクセス制御の節の「一覧の SELECT に付く権限条件」のとおりです。項目のアクセス制御は SQL には現れず、Column.CanRead() の判定でレスポンスに値を含めるかが決まります。

cs
// Column.cs
public bool CanRead(Context context, SiteSettings ss, List<string> mine, ...)
{
    var columnAccessControl = ss.ReadColumnAccessControls?
        .FirstOrDefault(o => o.ColumnName == ColumnName)
        ?? new ColumnAccessControl(ss, this, "Read");
    CanReadCache = columnAccessControl.Allowed(context, ss, mine);
    return CanReadCache == true;
}

履歴表示や一覧では、AllowedColumns() で読み取りが許可された列だけを SELECT 列に含めます。

cs
public static IEnumerable<Column> AllowedColumns(
    this IEnumerable<Column> columns,
    Context context,
    SiteSettings ss,
    bool checkPermission)
{
    return columns.Where(o => !checkPermission
        || o.CanRead(context: context, ss: ss, mine: null));
}

CREATE ​

CanCreate のチェック後に実行されるレコード作成の SQL そのものには、権限テーブルへの JOIN はありません。インポートや API でレコードを作成する場合、SetByModel() で CreateColumnAccessControls が評価されます。

cs
// ResultModel.cs SetByModel()(API・インポート共通)
columnHash
    .Where(column =>
        column.Value.Column.CanCreate(
            context: context,
            ss: ss,
            mine: Mine(context: context))
                && ResultId == 0)  // 新規作成時のみ
    .ForEach(column => { /* 値をセット */ });

CanCreate = false の列はループから除外されるため、リクエストに値が含まれていても無視され、SQL にも現れません。

UPDATE ​

CanUpdate のチェックはアプリケーション層で行われ、更新 SQL 自体には権限条件は付きません。更新の流れの概略は次のとおりです。

sql
-- ① 現行レコードを履歴テーブルにコピー
INSERT INTO "Results_history" SELECT * FROM "Results"
WHERE "ResultId" = @ResultId;

-- ② バージョンをインクリメントして更新
UPDATE "Results"
SET "Title" = @Title,
    "Body"  = @Body,
    "Ver"   = "Ver" + 1,
    "Updator" = @_U,
    "UpdatedTime" = getdate()
WHERE "SiteId" = @SiteId
    AND "ResultId" = @ResultId
    AND "UpdatedTime" = @UpdatedTime;  -- 画面を開いた時点の更新日時(競合チェック)

-- ③ Items テーブルの全文検索インデックスを更新
UPDATE "Items"
SET "Title" = @Title,
    "FullText" = @FullText,
    "SearchIndexCreatedTime" = CURRENT_TIMESTAMP
WHERE "ReferenceId" = @ResultId;
sql
-- ① 現行レコードを履歴テーブルにコピー
INSERT INTO "Results_history" SELECT * FROM "Results"
WHERE "ResultId" = @ResultId;

-- ② バージョンをインクリメントして更新
UPDATE "Results"
SET "Title" = @Title,
    "Body"  = @Body,
    "Ver"   = "Ver" + 1,
    "Updator" = @ipU,
    "UpdatedTime" = CURRENT_TIMESTAMP
WHERE "SiteId" = @SiteId
    AND "ResultId" = @ResultId
    AND "UpdatedTime" = @UpdatedTime;  -- 画面を開いた時点の更新日時(競合チェック)

-- ③ Items テーブルの全文検索インデックスを更新
UPDATE "Items"
SET "Title" = @Title,
    "FullText" = @FullText,
    "SearchIndexCreatedTime" = CURRENT_TIMESTAMP
WHERE "ReferenceId" = @ResultId;
sql
-- ① 現行レコードを履歴テーブルにコピー
INSERT INTO "Results_history" SELECT * FROM "Results"
WHERE "ResultId" = @ResultId;

-- ② バージョンをインクリメントして更新
UPDATE "Results"
SET "Title" = @Title,
    "Body"  = @Body,
    "Ver"   = "Ver" + 1,
    "Updator" = @ipU,
    "UpdatedTime" = CURRENT_TIMESTAMP(3)
WHERE "SiteId" = @SiteId
    AND "ResultId" = @ResultId
    AND "UpdatedTime" = @UpdatedTime;  -- 画面を開いた時点の更新日時(競合チェック)

-- ③ Items テーブルの全文検索インデックスを更新
UPDATE "Items"
SET "Title" = @Title,
    "FullText" = @FullText,
    "SearchIndexCreatedTime" = CURRENT_TIMESTAMP
WHERE "ReferenceId" = @ResultId;

②の "Updator" と "UpdatedTime" はプリザンターが自動で付ける列で、更新者はログインユーザーのパラメータ(SQL Server は @_U、PostgreSQL・MySQL は @ipU)、更新日時は DBMS ごとの現在日時(SQL Server は getdate()、PostgreSQL は CURRENT_TIMESTAMP、MySQL は CURRENT_TIMESTAMP(3))です(SqlUpdate.cs、SqlServerSqls.cs、PostgreSqlSqls.cs、MySqlSqls.cs)。WHERE は SiteId・ResultId と、競合チェックをするときは画面を開いた時点の更新日時です(ResultModel.cs、Rds.cs)。

項目のアクセス制御は SetByModel() で UpdateColumnAccessControls として評価されます。

cs
// ResultModel.cs SetByModel()
columnHash
    .Where(column =>
        column.Value.Column.CanUpdate(
            context: context,
            ss: ss,
            mine: Mine(context: context))
                && ResultId > 0)  // 既存レコード更新時
    .ForEach(column => { /* 値をセット */ });

CanUpdate = false の列は SET 句に含まれません。たとえば ClassA の更新を禁止されたユーザーが API で ClassA を送っても、値は変更されません。

sql
-- UpdateColumnAccessControls で ClassA が禁止されているユーザーの更新
UPDATE "Results"
SET
    "Title"       = @Title,
    "Body"        = @Body,
    "Ver"         = "Ver" + 1,
    "Updator"     = @_U,
    "UpdatedTime" = getdate()
    -- ClassA は含まれない
WHERE "ResultId" = @ResultId;
sql
-- UpdateColumnAccessControls で ClassA が禁止されているユーザーの更新
UPDATE "Results"
SET
    "Title"       = @Title,
    "Body"        = @Body,
    "Ver"         = "Ver" + 1,
    "Updator"     = @ipU,
    "UpdatedTime" = CURRENT_TIMESTAMP
    -- ClassA は含まれない
WHERE "ResultId" = @ResultId;
sql
-- UpdateColumnAccessControls で ClassA が禁止されているユーザーの更新
UPDATE "Results"
SET
    "Title"       = @Title,
    "Body"        = @Body,
    "Ver"         = "Ver" + 1,
    "Updator"     = @ipU,
    "UpdatedTime" = CURRENT_TIMESTAMP(3)
    -- ClassA は含まれない
WHERE "ResultId" = @ResultId;

DELETE ​

CanDelete のチェック後に論理削除(ゴミ箱への移動)が行われます。

sql
-- ゴミ箱への移動(論理削除)
INSERT INTO "Results_deleted"
SELECT * FROM "Results"
WHERE "ResultId" = @ResultId;

DELETE FROM "Results" WHERE "ResultId" = @ResultId;

-- Items テーブルのレコードも更新
UPDATE "Items"
SET "ReferenceType" = 'Results_Deleted'
WHERE "ReferenceId" = @ResultId;

ゴミ箱への移動では、Permissions のレコード権限は削除されません。ゴミ箱から完全に削除したときに初めて削除されます。

sql
-- 完全削除時のクリーンアップ
DELETE FROM "Results_deleted" WHERE "ResultId" = @ResultId;
DELETE FROM "Permissions"     WHERE "ReferenceId" = @ResultId;
DELETE FROM "Items"           WHERE "ReferenceId" = @ResultId;

サイト自体を削除する場合(ManageSite 以上の権限が必要)は、Permissions の関連行も合わせて削除されます。

sql
DELETE FROM "Permissions" WHERE "ReferenceId" = @SiteId;
DELETE FROM "Sites" WHERE "SiteId" = @SiteId;

コピー時の扱い ​

レコードのコピー時には SetCopyDefault() が呼ばれ、読み取りできない列(CanRead = false)はデフォルト値にリセットされます。コピーボタン・参照コピーボタンの動作全体は コピーボタンと参照コピーボタン を参照してください。

cs
public void SetCopyDefault(Context context, SiteSettings ss)
{
    ss.Columns
        .Where(column =>
            column.CopyByDefault == true          // コピー時にデフォルト値
            || column.TypeCs == "Attachments"
            || !column.CanRead(                   // 読み取り不可の列もリセット
                context: context,
                ss: ss,
                mine: Mine(context: context)))
        .ForEach(column => SetDefault(context, ss, column));
}

管理機能の権限 ​

ここまでのサイト・レコード・項目の権限とは別に、ユーザー管理・組織管理・グループ管理・テナント管理などの管理機能は、ユーザー単位のフラグや設定で許可されます。

種別設定する場所判定
特権ユーザApp_Data/Parameters/Security.json の PrivilegedUsers(LoginId のリスト)Context.HasPrivilege。ログイン ID がリストに含まれれば true(Permissions.cs#L883-L887、Context.cs#L215)
テナント管理者ユーザーの「テナント管理者」(Users.TenantManager)CanManageTenant() = TenantManager または HasPrivilege(Permissions.cs#L770-L774)
サービス管理者Users.ServiceManager管理画面用の SiteSettings に ManageService ビットが付くだけ(後述)
委任管理ユーザーの UserSettings.EnableManageTenantコントローラごとの判定に「または EnableManageTenant」として加わる

特権ユーザは、ユーザー情報を読み込むときに TenantManager も自動で true になります(User.cs#L153-L154)。

管理画面の SiteSettings に付く権限(Admins) ​

ユーザー・組織・テナントなどの管理画面では、サイトの Permissions ではなく Permissions.Admins() の値が SiteSettings の権限としてセットされます。TenantManager なら ManageTenant(1073741824)、ServiceManager なら ManageService(2147483648)のビットが立ちます(Permissions.cs#L876-L881、SiteSettingsUtilities.cs#L536-L597)。

このビットは、ユーザーの列定義(Definition_Column/Users_*.json)の ReadAccessControl などと項目のアクセス制御の Type として照合されます。たとえば「テナント管理者」「無効」「ロックアウト」などの列は ManageTenant、「姓」「名」「生年月日」「性別」などの列は ManageService が既定の条件です(詳しくは api/users の実行権限)。1.5.8.1 のソースで権限の判定に ServiceManager を使っているのはこの Admins() だけで、HasPrivilege のようにすべての権限チェックを通す仕組みではありません。

特権ユーザがバイパスするもの ​

確認したソースで HasPrivilege(または PrivilegedUsers())が条件になっている主な箇所です。

対象動作
ItemsCan()・Can()・CanRead(siteId)・HasPermission()常に許可(Permissions.cs#L352-L365、#L434-L441)
一覧・サイトメニューの SQL権限条件(CanReadSites など)を付けない(Permissions.cs#L257-L263)
UserSettings の AllowCreationAtTopSite・AllowGroupAdministration・AllowGroupCreation・AllowMovingFromTopSite・AllowApi常に true(UserSettings.cs#L72-L108)
システムログ(syslogs)閲覧・エクスポートは特権ユーザだけ
パラメータの画面管理・再起動EnableScreenManagement・EnableRestart が有効なときに特権ユーザだけ(Permissions.cs#L717-L720、#L764-L768)
ユーザの切り替え(SwitchUser / ReturnSwitchUser)特権ユーザだけ。無効なユーザーには切り替えられない(UserValidators.cs#L2174-L2197)
サーバースクリプトcontext.HasPrivilege で参照できる

コントローラごとの判定 ​

管理系のコントローラでは、CanRead・CanCreate などが context.Controller で分岐し、サイト権限ではなく次の条件で判定されます(1.5.8.1、Permissions.cs#L450-L705)。表の「テナント」は CanManageTenant()(テナント管理者または特権ユーザ)、「委任」は EnableManageTenant です。

コントローラReadCreateUpdateDeleteImportExport
tenantsテナント/委任不可テナント/委任不可不可不可
deptsテナント/委任テナント/委任テナント/委任テナント/委任テナント/委任テナント/委任
groupsCanReadGroup/委任CanCreateGroup/委任CanEditGroup/委任CanEditGroup/委任テナント/委任テナント/委任
usersテナント/本人/委任テナント(委任は不可)テナント/本人/委任テナント(本人は不可)テナントテナント/委任
extensionsテナントテナントテナントテナント——
  • users の Create は、EnableManageTenant が有効なユーザーだと、テナント管理者であっても false になります(委任が先に判定されるため)。
  • users の Delete は自分自身を消せません(context.UserId != context.Id)。削除対象が特権ユーザかどうかは見ていないため、テナント管理者は特権ユーザも削除できます(対策は 拡張 SQL の活用 と 特権ユーザの削除・編集の制限(改修案))。
  • ユーザー管理はさらに Service.json の ShowProfiles の影響を受けます。API での判定の流れは api/users の実行権限 を参照してください。

グループ管理の権限 ​

グループの閲覧・作成・編集は、ユーザーの設定とパラメータの組み合わせで決まります(Permissions.cs#L806-L828)。

メソッド条件
CanReadGroupAllowGroupAdministration が true で、かつ(一覧画面である、テナント管理者、そのグループのメンバー、特権ユーザ のいずれか)
CanCreateGroupAllowGroupCreation が true、または特権ユーザ
CanEditGroupAllowGroupAdministration が true で、かつ(一覧画面である、テナント管理者、そのグループの管理者(GroupMembers.Admin)、特権ユーザ のいずれか)

UserSettings の Allow* は、どれも次の形の式です(特権ユーザは常に true)。

text
(パラメータ User.json の DisableXxx が false  または  ユーザーの AllowXxx が true)
  かつ  UserSettings の DisableXxx が true でない
メソッドパラメータ(User.json)ユーザーの列UserSettings
AllowGroupAdministrationDisableGroupAdminAllowGroupAdministrationDisableGroupAdmin
AllowGroupCreationDisableGroupCreationAllowGroupCreationDisableGroupCreation
AllowCreationAtTopSiteDisableTopSiteCreationAllowCreationAtTopSiteDisableTopSiteCreation
AllowMovingFromTopSiteDisableMovingFromTopSiteAllowMovingFromTopSiteDisableMovingFromTopSite
AllowApiDisableApi(またはテナントの DisableApi)AllowApiDisableApi

つまり既定(パラメータの Disable* が false)では誰でも許可され、パラメータで禁止したときだけ、ユーザーの Allow* 列で個別に許可する形になります。

実践例:特定の列を管理者と担当者だけが更新できるようにする ​

ClassA(分類 A)を「管理者(Manager)または担当者(Owner)だけが更新できる」ようにする設定は、JSON で表すと次のとおりです。

json
{
  "UpdateColumnAccessControls": [
    {
      "ColumnName": "ClassA",
      "RecordUsers": ["Manager", "Owner"]
    }
  ]
}
ユーザーMine() の結果Allowed()ClassA の更新
作成者でしかないユーザー A["Creator"]false(Manager も Owner も含まれない)無視される
管理者列に入っているユーザー B["Manager"]true反映される

判定は IssueModel.SetByModel() / ResultModel.SetByModel() の中で ClassA.CanUpdate() → ColumnAccessControl.Allowed() の順に呼ばれて行われます。

関連ページ ​

変更履歴

第6版記事の確認版を繰り返す表現を整理する
第5版アクセス権限の内部 SQL を3種類のDBMSで説明
第4版管理機能の権限・グループの入れ子・トップ画面とテナント・api/users の実行権限の解説と、権限グループ・特権ユーザ保護の改修・設計メモを追加
第3版「内部実装を読む」を 1.5.8.1 のソースで検証して修正
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「内部実装を読む」にアクセス権限の実装を追加