Skip to content

マルチテナント運用 ​

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

プリザンターには、本来 SaaS 提供向けのマルチテナント機能があり、社内で事業部ごとにテナントを分けたい場合などにも使えます。ただし公式にアナウンスされている機能ではなく、テナントの追加は SQL で行い、テナント管理画面へのアクセス制御などは運用でカバーする必要があります。あわせて、テナント管理で設定できる HTML タイトルの仕様も説明します。

ライセンスの確認

商用ライセンスでは、無制限な SaaS 提供を禁止するために利用規約で提供範囲が制約されています。マルチテナントを使う場合は、利用規約に違反しないか確認してください。

テナントを追加する ​

1. テナントを作成する ​

GUI ではできないため、使用している DBMS の SQL で追加します。@テナント名 などの値は実行ツールでパラメータとして渡すか、対象の値に置き換えてください。PostgreSQL の SQL コンソールは、この @名前 の記法をそのまま解釈しません。

sql
INSERT INTO [dbo].[Tenants]
           ([TenantName],
            [Creator],
            [Updator])
     VALUES
           (@テナント名,
            0,
            0)
GO
sql
INSERT INTO "Tenants"
           ("TenantName",
            "Creator",
            "Updator")
     VALUES
           (@テナント名,
            0,
            0)
sql
INSERT INTO `Tenants`
           (`TenantName`,
            `Creator`,
            `Updator`)
     VALUES
           (@テナント名,
            0,
            0)

TenantId が自動採番されるので控えておきます。起動時に自動で作られる既定のテナントは TenantId = 1(DefaultTenant)です(TenantInitializer.cs)。追加したテナントには 2 以降が振られます。

2. テナントの 1 人目のユーザーを追加する ​

通常の手順(GUI)でユーザーを追加し、採番された UserId を控えてから、SQL で TenantId を付け替えます。

ユーザの新規作成画面(テナント管理者チェックボックスがある)

sql
UPDATE
    [Users]
SET
    [TenantId] = @先ほど採番されたTenantId
WHERE
    [UserId] = @先ほど採番されたUserId
sql
UPDATE
    "Users"
SET
    "TenantId" = @先ほど採番されたTenantId
WHERE
    "UserId" = @先ほど採番されたUserId
sql
UPDATE
    `Users`
SET
    `TenantId` = @先ほど採番されたTenantId
WHERE
    `UserId` = @先ほど採番されたUserId

このユーザーでログインすると、追加したテナントの環境に入れます。

3. テナント情報を編集する ​

追加したテナントのユーザー(テナント管理者の権限が必要)でログインし、「テナントの管理」(/tenants/edit)を開きます。確認したソースでは、テナントの管理画面の表示(Edit)も更新(Update)も、URL の値ではなくログイン中のユーザーのテナント(context.TenantId)を対象にします(TenantsController.cs)。/tenants/{TenantId}/edit のように URL を書き換えても、開くのは自分のテナントです。

テナントの管理画面。タイトル・ダッシュボード・テーマ、権限設定、ロゴ画像、HTML タイトル

運用上の注意 ​

テナントの管理はテナントごとに行う ​

インプリム社がホスティングする Pleasanter.net 以外の環境ではシングルテナントでの使用しか想定されておらず、複数のテナントをまとめて管理する画面はありません。1.5.8.1 では、前述のとおり管理画面の対象は常にログイン中のユーザーのテナントで、URL の /tenants/{id}/edit を書き換えても他のテナントは開けません。

そのため、各テナントの設定を変えるには、そのテナントにテナント管理者の権限を持つユーザーが必要です。テナント管理者はそのテナント内のユーザー・グループ・テナント設定を変更できるので、管理される側のテナントに誰をテナント管理者にするかは慎重に決めてください。

テナント間でデータは共有できない ​

内部で発行される SQL には常に TenantId が条件として組み込まれるため、標準機能ではテナント間でデータを共有できません。一方で、ユーザーも完全には分離されません。データを絶対に共有しないという条件でのみ、マルチテナント化を検討してください。

拡張 SQL・拡張機能はテナントを越える ​

  • 拡張 SQL はテナント間の制限を越えられる唯一の方法です。裏を返せば、クエリを正しく組まないと他テナントのデータを CRUD できてしまいます。クエリには必ず TenantId の条件を入れてください。TenantId を取得する変数は 公式マニュアル を参照してください。
  • 拡張スタイル・拡張サーバースクリプト・拡張ナビゲーションメニューなど もテナントの垣根を越えて実行されます。

いずれも、ファイルではなく Extensions テーブルに格納する方法を使うと、制限が緩和されるケースがあります。

不足している機能 ​

本格的にマルチテナント運用する場合、次の機能が不足しています。

分類不足している機能
GUI(テナントの管理)一覧表示、新規作成、履歴一覧、他テナントの編集(編集画面は自分のテナントしか開けない)。削除は不要かもしれない
Web API/api/tenant の new・update・get(コントローラごと存在しない)
スクリプト$p.tenantId
サーバースクリプトtenant、tenants
権限コード上に存在するサービスマネージャー権限を持つユーザーだけが他テナントの情報を扱える仕組み

トップ画面(SiteId = 0)とテナント ​

トップ画面(/items/index、/)は SiteId = 0 の「仮想的なルートサイト」として扱われます。Sites テーブルに SiteId = 0 の行はなく、Context.SiteTop() の判定(SiteId == 0 && Id == 0 && Controller == "items" && Action == "index")にも TenantId は入っていません(Context.cs#L1041-L1044)。つまり SiteId = 0 はどのテナントでも同じ意味で、テナントごとに別の SiteId がトップになるわけではありません。

一方、トップ画面に並ぶサイトはテナントで分かれます。

  • ItemModel.Index は ReferenceId == 0 のとき SiteUtilities.SiteTop() を返します(ItemModel.cs#L307-L312)。
  • SiteTop() はログインユーザのテナントのキャッシュ(SiteInfo.TenantCaches[context.TenantId])のサイトメニューを使います(SiteUtilities.cs#L4720-L4774)。テナントキャッシュはテナント ID をキーにした辞書で、サイト・組織・グループ・ユーザの情報をテナントごとに持ちます。
  • サイトの一覧は TenantId = ログインユーザのテナント かつ ParentId = 0 の条件で取得され、特権ユーザ以外にはさらに権限の条件が付きます(SiteUtilities.cs#L5305-L5319)。

トップ画面自体の権限は SiteTopPermission() で決まり、ユーザがトップでのサイト作成を許可されていれば Permissions.json の Manager(既定 511)、そうでなければ読み取り(1)だけです(Permissions.cs#L443-L448)。トップでの作成許可は User.json の DisableTopSiteCreation、ユーザの「トップサイトでの作成を許可」列、UserSettings.DisableTopSiteCreation の組み合わせで、特権ユーザは常に許可です(アクセス権限の実装)。

テナントごとのトップのダッシュボード ​

テナントの管理の「ダッシュボード」(Tenants.TopDashboards)で、テナントごとにトップとして開くダッシュボードを指定できます。値はダッシュボードの SiteId の JSON 配列(例: [123])で保存され、画面のドロップダウンは先頭の 1 件を表示します(TenantUtilities.cs#L698-L707)。

使われるのは次の 2 か所です(Locations.cs#L13-L45、UserModel.cs#L6095-L6111)。

場面動作
ログイン後の遷移先(戻り先の URL が空か / のとき)TopDashboards のうちユーザが読み取れる最初のダッシュボード。なければ Locations.json の LoginAfterUrl
パンくずリスト・戻るボタンなどの「トップ」(Locations.Top)同じくダッシュボード。なければ TopUrl、それもなければ /

図を読み込み中…

特権ユーザで Locations.json の LoginAfterUrlExcludePrivilegedUsers が true のときは、ダッシュボードや TopUrl を使いません(Locations.Top は /、ログイン後は戻り先の URL をそのまま使います)。/items/index を直接開いた場合はダッシュボードへは移らず、サイトメニューが表示されます。

HTML タイトル ​

テナント管理画面(管理 → テナントの管理)で、ブラウザのタブやブックマークに表示される <title> の文字列を 3 つの画面区分ごとに設定できます。

設定項目対象画面既定値
HTMLタイトル(トップ)トップページ、管理画面など[ProductName]
HTMLタイトル(サイト)フォルダ・テーブル・Wiki・ダッシュボードの一覧画面[ProductName]
HTMLタイトル(レコード)レコードの編集画面[ProductName]

使えるキーワード ​

キーワード置換される値例
[ProductName]製品名(App_Data/Displays/ProductName.json の値。日本語環境は「プリザンター」、英語環境は「Pleasanter」)プリザンター
[TenantTitle]テナントのタイトル株式会社〇〇
[SiteTitle]サイトのタイトル顧客管理
[RecordTitle]レコードのタイトル田中太郎
[Action]現在のアクション名一覧、編集、新規作成 など
設定例表示例
[TenantTitle] - [ProductName]株式会社〇〇 - プリザンター
[SiteTitle] | [TenantTitle]顧客管理 | 株式会社〇〇
[RecordTitle] - [SiteTitle]田中太郎 - 顧客管理
[SiteTitle]([Action])顧客管理(一覧)

[Action] は次のアクションに対応し、それ以外(ダッシュボードの index 等)は空文字列になります(HtmlTitle.cs GetActionName)。

アクション表示名アクション表示名
new新規作成timeseries時系列チャート
edit編集analy分析チャート
index一覧kambanカンバン
calendarカレンダーimagelib画像ライブラリ
crosstabクロス集計trashboxごみ箱
ganttガントチャートburndownバーンダウンチャート

どの設定が使われるか ​

context.Controller と context.Id で決まります(HtmlTitle.cs TitleText)。

図を読み込み中…

ControllerId使われる設定
items / publishes0トップ
items / publishesSiteId と同じサイト
items / publishesSiteId と異なるレコード
その他―トップ

キーワードの置換処理(FormattedHtmlTitle)には次の特徴があります。

ポイント内容
権限チェックサイトへのアクセス権限がない場合は製品名のみ表示
読み取り権限[RecordTitle] はレコードの読み取り権限がある場合のみ。ない場合は製品名に置換
フォールバック結果が空ならテナントのタイトル → 製品名の順に使う

テナント管理では変更できないケース ​

ケース理由
ログイン画面のタイトル常にトップの設定が使われるが、ログイン画面ではテナント情報がロードされないため、結果的に製品名固定になる
条件に応じた動的なタイトルキーワード置換のみで条件分岐はできない
項目の値をタイトルに含める[RecordTitle] 以外の項目値(分類や状況など)は使えない
テナント管理画面以外の管理画面ユーザー管理・グループ管理などは常にトップの設定

これらはスクリプトやサーバースクリプトで対応できます。

スクリプトでタイトルを変える ​

テーブルの管理 → スクリプトで document.title を書き換えます。

一覧画面でビューモードをタイトルに表示する例(出力先: 一覧)。

js
$p.events.on_grid_load = function () {
  const action = $('#Action').val();
  const actions = {
    index: '一覧',
    calendar: 'カレンダー',
    crosstab: 'クロス集計',
    gantt: 'ガントチャート',
    burndown: 'バーンダウンチャート',
    timeseries: '時系列チャート',
    analy: '分析チャート',
    kamban: 'カンバン',
    imagelib: '画像ライブラリ',
    trashbox: 'ごみ箱',
  };
  const viewMode = actions[action] || action;
  document.title = viewMode + ' | ' + document.title;
};

編集画面で状況をタイトルに表示する例(出力先: 新規作成・編集)。「[完了] 田中太郎 - 顧客管理」のように表示されます。

js
$p.events.on_editor_load = function () {
  const status = $p.getControl('Status').find('option:selected').text();
  if (status) {
    document.title = '[' + status + '] ' + document.title;
  }
};

一覧画面で現在のビュー名をタイトルに付ける例。

js
$p.events.on_grid_load = function () {
  const viewName = $('.current-view').text().trim();
  if (viewName) {
    document.title = viewName + ' - ' + document.title;
  }
};

サーバースクリプトと組み合わせる ​

サーバースクリプトから document.title は直接変更できませんが、context.AddResponse の Invoke でクライアント側の関数を呼び出せます。

期限切れレコードのタイトルに警告を出す例です。スクリプト(出力先: 新規作成・編集)に関数を用意します。Invoke は $p[target](value) の形で呼び出すため(_dispatch.js)、関数は $p のプロパティとして定義します。

js
$p.setOverdueTitle = function (message) {
  document.title = message + ' ' + document.title;
};

サーバースクリプト(条件: 画面表示の前)で呼び出します。期限を過ぎていて状況が完了(900)未満なら「【期限超過】 田中太郎 - 顧客管理」のように表示されます。

js
try {
  const completionTime = model.CompletionTime;
  const status = model.Status;
  if (completionTime && status < 900) {
    const deadline = new Date(completionTime);
    const now = new Date();
    if (deadline < now) {
      context.AddResponse('Invoke', 'setOverdueTitle', '【期限超過】');
    }
  }
} catch (e) {
  logs.LogException(e.stack);
}

同じ要領で、$p.setCustomTitle を用意して context.AddResponse('Invoke', 'setCustomTitle', model.ClassA) のようにサーバー側で求めた任意の値をタイトルに反映できます。

関連ページ ​

変更履歴

第8版記事の確認版を繰り返す表現を整理する
第7版テナント追加とユーザー割り当ての SQL を3種類のDBMSに対応
第6版「マルチテナント運用」にスクリーンショットを追加
第5版管理機能の権限・グループの入れ子・トップ画面とテナント・api/users の実行権限の解説と、権限グループ・特権ユーザ保護の改修・設計メモを追加
第4版Entra ID の SAML SSO にソースで確認した ACS URL・署名アルゴリズム・反映方法を反映
第3版「構築・運用」を 1.5.8.1 のソースで検証して修正
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「構築・運用」セクションの記事を追加