アクセス権限の実装(サイト・レコード・項目)
プリザンターはサイト単位・レコード単位・項目単位の 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 テーブルに保持されています。
| カラム名 | 型 | 説明 |
|---|---|---|
ReferenceId | bigint | 権限の付与対象 ID(Sites.InheritPermission と紐づく) |
DeptId | int | 組織 ID(組織への付与時に使用、それ以外は 0) |
GroupId | int | グループ ID(グループへの付与時に使用、それ以外は 0) |
UserId | int | ユーザー ID(ユーザーへの付与時に使用、それ以外は 0)。-1 は「全員」を意味する特殊値 |
DeptName | nvarchar | 組織名(表示用の非正規化キャッシュ) |
GroupName | nvarchar | グループ名(表示用の非正規化キャッシュ) |
Name | nvarchar | ユーザー名(表示用の非正規化キャッシュ) |
PermissionType | bigint | 権限のビットマスク値 |
Creator | int | 登録者ユーザー ID |
Updator | int | 更新者ユーザー ID |
CreatedTime | datetime | 登録日時 |
UpdatedTime | datetime | 更新日時 |
1 レコードにつき DeptId・GroupId・UserId のいずれか 1 つだけが非ゼロになります。
モデルクラスの定義は次のとおりです。
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 型のビットフィールドで、各ビットが独立した権限に対応しています。
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
}「アクセス権の管理」で選べる標準パターンは、Parameters/Permissions.json の Pattern に定義された 4 つです(1.5.8.1 のソースでは Permissions.json#L5-L10)。
| 値 | パターン名(画面の表示) | 内訳 |
|---|---|---|
| 1 | ReadOnly(読取専用) | Read |
| 31 | ReadWrite(書き込み) | Read + Create + Update + Delete + SendMail |
| 255 | Leader(リーダー) | Read 〜 ManageSite |
| 511 | Manager(管理者) | Read 〜 ManagePermission |
63 や 127 のような値には、パターン名はありません。標準パターンに個別の権限(エクスポート・インポートなど)を足したときの値で、次のような内訳です。
| 値 | 内訳 |
|---|---|
| 63 | Read + Create + Update + Delete + SendMail + Export |
| 127 | Read 〜 Import |
権限の有無はビット演算で確認できます。
var permissionType = 63L;
var deleteFlag = (long)Permissions.Types.Delete; // 8
if ((permissionType & deleteFlag) == deleteFlag)
{
// Delete 権限あり
}Sites.InheritPermission と権限の継承
サイトがどの権限を使うかは、Sites テーブルの InheritPermission カラムで決まります。
-- 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) |
-- 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;-- 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;-- 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 Server | PostgreSQL・MySQL | 意味 |
|---|---|---|
@_T | @ipT | TenantId(テナント ID) |
@_D | @ipD | DeptId(ログインユーザーの組織 ID) |
@_U | @ipU | UserId(ログインユーザーの 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 ビット演算でマージされます。
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 にセットされます。
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;
}読み取れるポイントは次のとおりです。
| 状況 | 結果 |
|---|---|
公開(Publish) | サイト権限・レコード権限とも Read |
| 通常 | PermissionHash[ss.InheritPermission] がサイト権限、PermissionHash[referenceId] がレコード権限 |
テーブルロック中(ss.LockedTable()) | どちらも Read / Export / SendMail だけに絞られる |
ContextPermission は PermissionType(サイトレベルの権限)と ItemPermissionType(レコード個別の権限)だけを持つ小さなクラスです。
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() で解決してから返します。
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;
}権限確認メソッド
コントローラーやモデルからの権限確認には、主に次のメソッドが使われます(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() を呼んでいます。
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
}HasPrivilege は Security.json の PrivilegedUsers に含まれるログイン ID のユーザーが該当し、すべての権限チェックをバイパスします(ほかにバイパスされるものは管理機能の権限を参照)。
権限の更新(全削除 → 再登録)
「アクセス権の管理」で権限を変更すると、PermissionUtilities.UpdatePermissions() が呼ばれます。対象 ReferenceId の行をいったん全削除し、再登録する REPLACE パターンです。
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 行ずつ行われます。
INSERT INTO "Permissions" (
"ReferenceId", "PermissionType",
"DeptId", "GroupId", "UserId"
)
VALUES (
@ReferenceId, @PermissionType,
@DeptId, @GroupId, @UserId
);レコードのアクセス制御
「サイトのアクセス権の管理」で「レコードのアクセス制御を使用する」を有効にすると、各レコードにサイト権限とは別の独自の権限を設定できます。
格納先はサイト権限と同じ Permissions テーブル
レコード個別の権限も Permissions テーブルに保持されます。違いは ReferenceId に入る値だけです。
| ケース | ReferenceId の値 |
|---|---|
| サイト権限 | Sites.InheritPermission(サイト ID) |
| レコード権限 | Items.ReferenceId(レコード ID) |
-- サイト権限の例(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 合成されます。
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;
}CanRead などはこの値を使うため、サイト権限とレコード権限のどちらかを持っていれば許可されます。site: true で呼ばれた場合はサイト権限だけで判定します。
開いているレコードの権限の取得(GetPermissionsById)
レコードを開くときは、ログイン時と同じ GetPermissions に加えて、そのレコード固有の権限を取る GetPermissionsById が発行されます。
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 になっている点です。①の組織経由の部分だけを示します(②〜⑤も同じパターンです)。
-- 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'-- 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'-- 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 から生成)。
"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 サブクエリを追加します。
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));
}
// ...
}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> として保持されます。
// SiteSettings の JSON の一例
{
"PermissionForCreating": {
"User": 511, // 作成者自身に ManagePermission 以下の権限を付与
"Dept": 63, // 作成者の組織に付与
"Group": 31 // 作成者のグループに付与
}
}ResultModel / IssueModel の CreateStatements() で、Items と Results(Issues)への INSERT、リンク情報の挿入に続いて PermissionUtilities.InsertStatements() が呼ばれます。
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 のユーザー/組織 |
// 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 は次のようになります(トランザクション内)。
-- ① 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 の例-- ① 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 の例-- ① 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() が既存のレコード権限を全削除してから再設定します。
// 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;
}-- ① 既存のレコード権限を全削除
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() が呼ばれます。サイト権限の更新と同じ「全削除 → 再登録」です。更新時権限が設定されている場合はそちらが優先されます。
// 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 判定に使われます。
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 操作分の権限設定を持ちます。
[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 で保存されます。
// 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 にして最小化します。
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 つも設定されていなければ全員許可です。
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 は「このサイト権限レベルを持つユーザーに許可する」という設定です。
// 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 も同じ構造)は、該当列の設定がなければ列定義由来のデフォルト設定を使います。
// 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 値で表します。
public enum ColumnPermissionTypes
{
Deny, // 非表示(列ごと見えない)
Read, // 読み取り専用(入力不可)
Update // 編集可能
}Permissions.ColumnPermissionType() が次のように決めます。
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;
}
}| 値 | 新規作成時 | 編集時 |
|---|---|---|
Update | CanCreate かつ CanEdit | CanRead かつ CanEdit |
Read | 上記以外で CanRead | 上記以外で CanRead |
Deny | CanRead でない | 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(= 全員許可)です。
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 の値が変わる場合に呼ばれます)。
// 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)、必要に応じて CheckRecordPermission | SQL は全列取得。レスポンス生成時に読み取り不可の列は値を返さない |
| CREATE | CanCreate の事前チェック | 作成時権限があれば Permissions へ INSERT | CanCreate = false の列は値のセットから除外 |
| UPDATE | CanUpdate の事前チェック | 更新時権限・手動変更があれば全削除 → 再登録 | CanUpdate = false の列は SET 句に含まれない |
| DELETE | CanDelete の事前チェック | ゴミ箱移動では消えず、完全削除時に削除 | — |
SELECT
一覧の WHERE 句は、レコードのアクセス制御の節の「一覧の SELECT に付く権限条件」のとおりです。項目のアクセス制御は SQL には現れず、Column.CanRead() の判定でレスポンスに値を含めるかが決まります。
// 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 列に含めます。
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 が評価されます。
// 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 自体には権限条件は付きません。更新の流れの概略は次のとおりです。
-- ① 現行レコードを履歴テーブルにコピー
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;-- ① 現行レコードを履歴テーブルにコピー
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;-- ① 現行レコードを履歴テーブルにコピー
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 として評価されます。
// 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 を送っても、値は変更されません。
-- UpdateColumnAccessControls で ClassA が禁止されているユーザーの更新
UPDATE "Results"
SET
"Title" = @Title,
"Body" = @Body,
"Ver" = "Ver" + 1,
"Updator" = @_U,
"UpdatedTime" = getdate()
-- ClassA は含まれない
WHERE "ResultId" = @ResultId;-- UpdateColumnAccessControls で ClassA が禁止されているユーザーの更新
UPDATE "Results"
SET
"Title" = @Title,
"Body" = @Body,
"Ver" = "Ver" + 1,
"Updator" = @ipU,
"UpdatedTime" = CURRENT_TIMESTAMP
-- ClassA は含まれない
WHERE "ResultId" = @ResultId;-- UpdateColumnAccessControls で ClassA が禁止されているユーザーの更新
UPDATE "Results"
SET
"Title" = @Title,
"Body" = @Body,
"Ver" = "Ver" + 1,
"Updator" = @ipU,
"UpdatedTime" = CURRENT_TIMESTAMP(3)
-- ClassA は含まれない
WHERE "ResultId" = @ResultId;DELETE
CanDelete のチェック後に論理削除(ゴミ箱への移動)が行われます。
-- ゴミ箱への移動(論理削除)
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 のレコード権限は削除されません。ゴミ箱から完全に削除したときに初めて削除されます。
-- 完全削除時のクリーンアップ
DELETE FROM "Results_deleted" WHERE "ResultId" = @ResultId;
DELETE FROM "Permissions" WHERE "ReferenceId" = @ResultId;
DELETE FROM "Items" WHERE "ReferenceId" = @ResultId;サイト自体を削除する場合(ManageSite 以上の権限が必要)は、Permissions の関連行も合わせて削除されます。
DELETE FROM "Permissions" WHERE "ReferenceId" = @SiteId;
DELETE FROM "Sites" WHERE "SiteId" = @SiteId;コピー時の扱い
レコードのコピー時には SetCopyDefault() が呼ばれ、読み取りできない列(CanRead = false)はデフォルト値にリセットされます。コピーボタン・参照コピーボタンの動作全体は コピーボタンと参照コピーボタン を参照してください。
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 です。
| コントローラ | Read | Create | Update | Delete | Import | Export |
|---|---|---|---|---|---|---|
tenants | テナント/委任 | 不可 | テナント/委任 | 不可 | 不可 | 不可 |
depts | テナント/委任 | テナント/委任 | テナント/委任 | テナント/委任 | テナント/委任 | テナント/委任 |
groups | CanReadGroup/委任 | 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)。
| メソッド | 条件 |
|---|---|
CanReadGroup | AllowGroupAdministration が true で、かつ(一覧画面である、テナント管理者、そのグループのメンバー、特権ユーザ のいずれか) |
CanCreateGroup | AllowGroupCreation が true、または特権ユーザ |
CanEditGroup | AllowGroupAdministration が true で、かつ(一覧画面である、テナント管理者、そのグループの管理者(GroupMembers.Admin)、特権ユーザ のいずれか) |
UserSettings の Allow* は、どれも次の形の式です(特権ユーザは常に true)。
(パラメータ User.json の DisableXxx が false または ユーザーの AllowXxx が true)
かつ UserSettings の DisableXxx が true でない| メソッド | パラメータ(User.json) | ユーザーの列 | UserSettings |
|---|---|---|---|
AllowGroupAdministration | DisableGroupAdmin | AllowGroupAdministration | DisableGroupAdmin |
AllowGroupCreation | DisableGroupCreation | AllowGroupCreation | DisableGroupCreation |
AllowCreationAtTopSite | DisableTopSiteCreation | AllowCreationAtTopSite | DisableTopSiteCreation |
AllowMovingFromTopSite | DisableMovingFromTopSite | AllowMovingFromTopSite | DisableMovingFromTopSite |
AllowApi | DisableApi(またはテナントの DisableApi) | AllowApi | DisableApi |
つまり既定(パラメータの Disable* が false)では誰でも許可され、パラメータで禁止したときだけ、ユーザーの Allow* 列で個別に許可する形になります。
実践例:特定の列を管理者と担当者だけが更新できるようにする
ClassA(分類 A)を「管理者(Manager)または担当者(Owner)だけが更新できる」ようにする設定は、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() の順に呼ばれて行われます。