Skip to content

権限グループ(管理画面の操作範囲・サイト設定 API の制限)の設計 ​

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

プリザンターには、サイト管理権限(ManageSite)を持つユーザに対して「一覧と履歴の設定だけ触らせる」「サイト設定 API は使わせない」といった細かい制限をかける機能はありません。このページは、ユーザグループとは別の「権限グループ」を追加し、管理画面の操作範囲と API の使用可否をグループ単位で制御する本体改修の設計メモです。

要件 ​

要件内容
管理画面の範囲サイト管理者が使えるサイトの管理のタブを限定する(一覧・履歴だけ、など)
API の制限サイト設定 API(updatesitesettings など)の使用可否をグループ単位で決める
サイトの範囲特定の SiteId(とその配下)だけに制限を適用する
管理者テナント管理者または特権ユーザが権限グループを追加・管理する
所属ユーザは複数の権限グループに所属でき、ユーザグループに近い UI で運用する

前提にした現行実装(1.5.8.1) ​

権限モデル ​

サイトの権限は Permissions テーブルに組織・グループ・ユーザ(と全員)単位で付与され、複数の経路の権限はビット OR で合算されます。拒否のルールはありません。詳細は アクセス権限の実装 を参照してください。

値権限
1 / 2 / 4 / 8 / 16Read / Create / Update / Delete / SendMail
32 / 64Export / Import
128ManageSite(サイトの管理)
256ManagePermission(アクセス権の管理)

サイトの管理のタブ ​

サイトの管理画面のタブは SiteUtilities.EditorTabs で組み立てられます(SiteUtilities.cs#L4337)。各タブの表示条件はサイトの種類(テーブル・Wiki・ダッシュボードなど)や契約設定によるもので、権限で出し分けているのは「サイトのアクセス制御」(CanManagePermission)と「レコードのアクセス制御」などに限られます(SiteUtilities.cs#L4648-L4665)。つまり ManageSite を持つユーザは、ほぼすべてのタブを使えます。

タブの ID は #GridSettingsEditor(一覧)、#FiltersSettingsEditor(フィルタ)、#EditorSettingsEditor(エディタ)、#HistoriesSettingsEditor(履歴)、#ViewsSettingsEditor(ビュー)、#NotificationsSettingsEditor(通知)、#ScriptsSettingsEditor(スクリプト)、#ServerScriptsSettingsEditor(サーバースクリプト)、#FieldSetSiteAccessControl(サイトのアクセス制御)などです。

サイト設定 API ​

エンドポイント処理権限チェック
POST /api/items/{id}/getsiteItemModel.GetByApi(referenceType: "Sites")認証済みであること+読み取りの検証
POST /api/items/{id}/updatesitesettingsItemModel.UpdateSiteSettingsByApi → SiteUtilities.UpdateSiteSettingsByApiSiteValidators.OnUpdating

(ItemsController.cs#L198-L215、#L327-L344)

SiteValidators.OnUpdating は、CanManageSite、継承(InheritPermission)やレコード権限を変える場合の CanManagePermission、列ごとの CanUpdate を確かめます(SiteValidators.cs#L131-L147)。API の制限は SiteModel.WithinApiLimits() による契約の呼び出し回数の上限だけで、ユーザやグループ単位で API の使用可否を決める仕組みはありません。

グループ ​

既存のユーザグループは Groups(TenantId・GroupId・GroupName・Body・Disabled)と GroupMembers(GroupId・DeptId・UserId・ChildGroup・Admin)、子グループの GroupChildren で構成されます。グループの編集画面のタブは「全般」「メンバー」「子グループ」「変更履歴」です。権限グループのテーブル・画面はこの構成にならいます。

設計 ​

概要 ​

権限グループは既存の Permissions とは独立した第 2 のレイヤーとして、サイトへのアクセスが許可された後に「管理操作」を制限します。

図を読み込み中…

テーブル ​

既存の Groups / GroupMembers と同じ形にし、CodeDefiner の自動生成に乗せます。Ver・Creator・Updator・CreatedTime・UpdatedTime は共通列(_Bases)として自動で付きます。

図を読み込み中…

テーブル列説明
PermissionGroupsTenantId(PK 1)、PermissionGroupId(PK 2、IDENTITY)、PermissionGroupName(nvarchar(256))、Body(nvarchar(max)、Markdown)、Disabled(bit、既定 0、Ix1)権限グループの定義
PermissionGroupMembersPermissionGroupId(PK 1)、UserId(PK 2、既定 0)、Admin(bit、既定 0)ユーザとの紐付け。Ix1 = (PermissionGroupId)、Ix2 = (UserId, PermissionGroupId) で逆引き
PermissionGroupPoliciesPermissionGroupId(PK 1)、PolicyId(PK 2、IDENTITY)、PolicyType(nvarchar(64)、Ix2)、PolicyValue(nvarchar(max)、JSON)、SiteId(bigint、既定 0 = テナント全体)、IncludeChildren(bit、既定 0)権限グループごとの制限。Ix1 = (PermissionGroupId, SiteId)
DDL(SQL Server)
sql
CREATE TABLE "PermissionGroups" (
    "TenantId"            int           NOT NULL,
    "PermissionGroupId"   int           IDENTITY(1, 1) NOT NULL,
    "PermissionGroupName" nvarchar(256) NOT NULL,
    "Body"                nvarchar(max) NULL,
    "Disabled"            bit           NOT NULL DEFAULT(0),
    "Ver"                 int           NOT NULL DEFAULT(1),
    "Creator"             int           NOT NULL,
    "Updator"             int           NOT NULL,
    "CreatedTime"         datetime      NOT NULL DEFAULT(GETDATE()),
    "UpdatedTime"         datetime      NOT NULL DEFAULT(GETDATE()),
    CONSTRAINT "Pk_PermissionGroups" PRIMARY KEY ("TenantId", "PermissionGroupId")
);
CREATE INDEX "Ix1_PermissionGroups" ON "PermissionGroups" ("Disabled" ASC);

CREATE TABLE "PermissionGroupMembers" (
    "PermissionGroupId" int      NOT NULL,
    "UserId"            int      NOT NULL DEFAULT(0),
    "Admin"             bit      NOT NULL DEFAULT(0),
    "Ver"               int      NOT NULL DEFAULT(1),
    "Creator"           int      NOT NULL,
    "Updator"           int      NOT NULL,
    "CreatedTime"       datetime NOT NULL DEFAULT(GETDATE()),
    "UpdatedTime"       datetime NOT NULL DEFAULT(GETDATE()),
    CONSTRAINT "Pk_PermissionGroupMembers" PRIMARY KEY ("PermissionGroupId", "UserId")
);
CREATE INDEX "Ix1_PermissionGroupMembers" ON "PermissionGroupMembers" ("PermissionGroupId" ASC);
CREATE INDEX "Ix2_PermissionGroupMembers" ON "PermissionGroupMembers" ("UserId" ASC, "PermissionGroupId" ASC);

CREATE TABLE "PermissionGroupPolicies" (
    "PermissionGroupId" int           NOT NULL,
    "PolicyId"          int           IDENTITY(1, 1) NOT NULL,
    "PolicyType"        nvarchar(64)  NOT NULL,
    "PolicyValue"       nvarchar(max) NOT NULL,
    "SiteId"            bigint        NOT NULL DEFAULT(0),
    "IncludeChildren"   bit           NOT NULL DEFAULT(0),
    "Ver"               int           NOT NULL DEFAULT(1),
    "Creator"           int           NOT NULL,
    "Updator"           int           NOT NULL,
    "CreatedTime"       datetime      NOT NULL DEFAULT(GETDATE()),
    "UpdatedTime"       datetime      NOT NULL DEFAULT(GETDATE()),
    CONSTRAINT "Pk_PermissionGroupPolicies" PRIMARY KEY ("PermissionGroupId", "PolicyId")
);
CREATE INDEX "Ix1_PermissionGroupPolicies" ON "PermissionGroupPolicies" ("PermissionGroupId" ASC, "SiteId" ASC);
CREATE INDEX "Ix2_PermissionGroupPolicies" ON "PermissionGroupPolicies" ("PolicyType" ASC);
DDL(PostgreSQL)
sql
CREATE TABLE "PermissionGroups" (
    "TenantId"            integer      NOT NULL,
    "PermissionGroupId"   serial       NOT NULL,
    "PermissionGroupName" varchar(256) NOT NULL,
    "Body"                text         NULL,
    "Disabled"            boolean      NOT NULL DEFAULT false,
    "Ver"                 integer      NOT NULL DEFAULT 1,
    "Creator"             integer      NOT NULL,
    "Updator"             integer      NOT NULL,
    "CreatedTime"         timestamp    NOT NULL DEFAULT NOW(),
    "UpdatedTime"         timestamp    NOT NULL DEFAULT NOW(),
    CONSTRAINT "Pk_PermissionGroups" PRIMARY KEY ("TenantId", "PermissionGroupId")
);
CREATE INDEX "Ix1_PermissionGroups" ON "PermissionGroups" ("Disabled" ASC);

CREATE TABLE "PermissionGroupMembers" (
    "PermissionGroupId" integer   NOT NULL,
    "UserId"            integer   NOT NULL DEFAULT 0,
    "Admin"             boolean   NOT NULL DEFAULT false,
    "Ver"               integer   NOT NULL DEFAULT 1,
    "Creator"           integer   NOT NULL,
    "Updator"           integer   NOT NULL,
    "CreatedTime"       timestamp NOT NULL DEFAULT NOW(),
    "UpdatedTime"       timestamp NOT NULL DEFAULT NOW(),
    CONSTRAINT "Pk_PermissionGroupMembers" PRIMARY KEY ("PermissionGroupId", "UserId")
);
CREATE INDEX "Ix1_PermissionGroupMembers" ON "PermissionGroupMembers" ("PermissionGroupId" ASC);
CREATE INDEX "Ix2_PermissionGroupMembers" ON "PermissionGroupMembers" ("UserId" ASC, "PermissionGroupId" ASC);

CREATE TABLE "PermissionGroupPolicies" (
    "PermissionGroupId" integer     NOT NULL,
    "PolicyId"          serial      NOT NULL,
    "PolicyType"        varchar(64) NOT NULL,
    "PolicyValue"       text        NOT NULL,
    "SiteId"            bigint      NOT NULL DEFAULT 0,
    "IncludeChildren"   boolean     NOT NULL DEFAULT false,
    "Ver"               integer     NOT NULL DEFAULT 1,
    "Creator"           integer     NOT NULL,
    "Updator"           integer     NOT NULL,
    "CreatedTime"       timestamp   NOT NULL DEFAULT NOW(),
    "UpdatedTime"       timestamp   NOT NULL DEFAULT NOW(),
    CONSTRAINT "Pk_PermissionGroupPolicies" PRIMARY KEY ("PermissionGroupId", "PolicyId")
);
CREATE INDEX "Ix1_PermissionGroupPolicies" ON "PermissionGroupPolicies" ("PermissionGroupId" ASC, "SiteId" ASC);
CREATE INDEX "Ix2_PermissionGroupPolicies" ON "PermissionGroupPolicies" ("PolicyType" ASC);

CodeDefiner では、App_Data/Definitions/Definition_Column/ に PermissionGroups_TenantId.json のような列定義を列ごとに追加します。既存の Groups_*.json と同じ書き方です。

列定義の例(PermissionGroups_Disabled.json)
json
{
    "Id": "PermissionGroups_Disabled",
    "ModelName": "PermissionGroup",
    "TableName": "PermissionGroups",
    "Label": "権限グループ",
    "ColumnName": "Disabled",
    "LabelText": "無効",
    "No": "103",
    "History": "103",
    "GridColumn": "103",
    "GridEnabled": "1",
    "FilterColumn": "103",
    "FilterEnabled": "1",
    "EditorColumn": "103",
    "EditorEnabled": "1",
    "TypeName": "bit",
    "Ix1": "1",
    "Default": "0",
    "ReadAccessControl": "ManageTenant",
    "CreateAccessControl": "ManageTenant",
    "UpdateAccessControl": "ManageTenant",
    "ControlType": "ChoicesText",
    "ChoicesText": "無効\n-"
}
CodeDefiner で追加するもの内容
Definition_Column3 テーブルの列定義
Definition_Code のテンプレートModel・Utilities・Validators などの生成
Definition_SqlGetPermissionGroupPolicies などの SQL
テーブル作成DDL の生成
Controller・View管理画面の生成

ポリシーの種類 ​

PolicyTypePolicyValue の例意味
SiteSettingsTabs["GridSettingsEditor","HistoriesSettingsEditor"]使ってよいサイトの管理のタブ ID。含まれないタブは表示しない
SiteSettingsApiAccess{"allow": false, "reason": "API でのサイト設定変更を禁止"}サイト設定 API の使用可否
SiteSettingsApiMethods["GetSite"]許可するサイト設定 API。含まれないものは 403

SiteSettingsApiMethods の値は GetSite(/api/items/{id}/getsite)と UpdateSiteSettings(/api/items/{id}/updatesitesettings)です。

ポリシーの取得 ​

リクエストの開始時に、PermissionHash と同じようにユーザのポリシーを一括で取ってキャッシュします。

sql
SELECT
    "PermissionGroupPolicies"."PermissionGroupId",
    "PermissionGroupPolicies"."PolicyType",
    "PermissionGroupPolicies"."PolicyValue",
    "PermissionGroupPolicies"."SiteId",
    "PermissionGroupPolicies"."IncludeChildren"
FROM "PermissionGroupPolicies"
INNER JOIN "PermissionGroupMembers"
    ON "PermissionGroupPolicies"."PermissionGroupId" = "PermissionGroupMembers"."PermissionGroupId"
INNER JOIN "PermissionGroups"
    ON "PermissionGroupPolicies"."PermissionGroupId" = "PermissionGroups"."PermissionGroupId"
WHERE "PermissionGroupMembers"."UserId" = @UserId
    AND "PermissionGroups"."TenantId" = @TenantId
    AND "PermissionGroups"."Disabled" = 0
cs
// Context に追加するプロパティ(イメージ)
public Dictionary<string, List<PermissionGroupPolicy>> PermissionGroupPolicyHash { get; set; }

// Permissions に追加するメソッド(イメージ)
public static Dictionary<string, List<PermissionGroupPolicy>> GetPermissionGroupPolicies(Context context)
{
    if (!context.Authenticated)
        return new Dictionary<string, List<PermissionGroupPolicy>>();
    return Repository.ExecuteTable(
        context: context,
        statements: new SqlStatement(
            context.Sqls.GetPermissionGroupPolicies,
            new SqlParamCollection()))
        .AsEnumerable()
        .GroupBy(r => r.String("PolicyType"))
        .ToDictionary(
            g => g.Key,
            g => g.Select(r => new PermissionGroupPolicy
            {
                PermissionGroupId = r.Int("PermissionGroupId"),
                PolicyType = r.String("PolicyType"),
                PolicyValue = r.String("PolicyValue"),
                SiteId = r.Long("SiteId"),
                IncludeChildren = r.Bool("IncludeChildren")
            }).ToList());
}

改修箇所 ​

1. サイトの管理のタブ ​

SiteUtilities.EditorTabs の各タブの _using に、権限グループの判定を足します。

cs
hb.Li(
    action: () => hb.A(
        href: "#GridSettingsEditor",
        text: Displays.Grid(context: context)),
    _using: PermissionGroupService.IsTabAllowed(
        context: context,
        siteId: siteModel.SiteId,
        tabId: "GridSettingsEditor"));
cs
public static bool IsTabAllowed(Context context, long siteId, string tabId)
{
    // 特権ユーザ・テナント管理者は常に許可
    if (context.HasPrivilege) return true;
    if (context.User?.TenantManager == true) return true;
    var policies = GetPolicies(context, siteId, "SiteSettingsTabs");
    // ポリシーがなければ制限しない(既存の動作を保つ)
    if (!policies.Any()) return true;
    // どれかの権限グループで許可されていれば許可(OR)
    return policies.Any(p => p.AllowedTabs.Contains(tabId));
}

2. サイト設定 API ​

SiteValidators.OnUpdating(updatesitesettings から呼ばれる)に、API のときだけの判定を足します。1.5.8.1 の Context には API からのリクエストかを表す Api プロパティがあります(Context.cs#L129)。

cs
public static ErrorData OnUpdating(Context context, SiteSettings ss, SiteModel siteModel)
{
    if (!context.CanManageSite(ss: ss))
    {
        return new ErrorData(type: Error.Types.HasNotPermission);
    }
    // 権限グループによる API の制限
    if (context.Api
        && !PermissionGroupService.IsApiAllowed(
            context: context,
            siteId: siteModel.SiteId,
            apiMethod: "UpdateSiteSettings"))
    {
        return new ErrorData(type: Error.Types.HasNotPermission);
    }
    // ...既存の検証
}

getsite の制限は、ItemModel.GetByApi の referenceType: "Sites" の経路に同じ判定を入れます。

3. サイトの範囲 ​

ポリシーの SiteId・IncludeChildren で、適用するサイトを決めます。SiteId = 0 はテナント全体です。

図を読み込み中…

cs
public static bool IsSiteInScope(
    Context context, long targetSiteId, long scopeSiteId, bool includeChildren)
{
    if (scopeSiteId == 0) return true;            // テナント全体
    if (targetSiteId == scopeSiteId) return true;
    if (!includeChildren) return false;
    // ParentId をたどって scopeSiteId の子孫かを判定
    return IsDescendant(context, targetSiteId, scopeSiteId);
}

4. 複数の権限グループに属する場合 ​

ポリシーは OR で合わせます(どれかの権限グループで許可されていれば許可)。既存のサイト権限がビット OR で合算されるのと同じ考え方です。

図を読み込み中…

管理画面 ​

テナントの管理のメニューに「権限グループ」を追加し、既存のグループ管理(GroupUtilities)の画面構成にならいます。

画面内容
一覧権限グループ名(リンク)、メンバー数、ポリシー数、有効・無効
編集: 全般名前・説明・無効
編集: メンバーユーザの追加・削除(グループのメンバー画面と同様)
編集: ポリシータブ・API の制限とサイトの範囲
編集: 変更履歴変更履歴

権限グループの作成・編集・削除とメンバーの追加は、テナント管理者または特権ユーザだけに許可します。

既存の機能への影響 ​

項目影響内容
既存の権限チェック小追加のレイヤーで、既存の判定は変えない
Permissions・Groups テーブルなし変更しない
サイト設定 API中SiteValidators.OnUpdating などに条件を追加
サイトの管理画面中EditorTabs に条件を追加
Context小ポリシーのキャッシュを追加
性能小リクエストごとにポリシー取得の SQL が 1 本増える(PermissionHash と同じくキャッシュで軽減)

ポリシーを 1 つも持たないユーザは現行どおり動くため、導入しても既存の運用は変わりません。

関連ページ ​

変更履歴

第1版管理機能の権限・グループの入れ子・トップ画面とテナント・api/users の実行権限の解説と、権限グループ・特権ユーザ保護の改修・設計メモを追加