GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。 NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは 別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
420 lines
57 KiB
Markdown
420 lines
57 KiB
Markdown
# 組織・社員管理データベース構築 設計文書
|
||
|
||
作成日: 2026-08-07
|
||
ステータス: 設計確定(LINEWORKS/プリザンター/Keycloak 3システムとも疎通確認済み。SAML SSO連携はテスト環境でログイン完走まで確認済み、Keycloak中間ユーザーDB方式でのプリザンターLoginId非変更運用・氏名連携ともに実証済み。マスタテーブル・退職者検知・グループ命名規則など主要論点は全て確定。実装計画フェーズへ移行可能)
|
||
|
||
## 1. 目的
|
||
|
||
LINEWORKS・プリザンター・Keycloakにまたがる社員・組織情報を、プリザンター上の一元マスタテーブルで管理。指示駆動(Claude Skill)で取得・マージ・反映・同期を実行可能にする。
|
||
|
||
## 2. 管理方針
|
||
|
||
- 主マスタ: LINEWORKS。社員情報の一次情報源はここのみ
|
||
- LINEWORKS削除者: マスタ物理削除せず、在籍フラグOFFで保持
|
||
- プリザンターのグループ機能: LINEWORKSの組織階層を再現、ユーザー配属先として使用
|
||
- プリザンターの組織機能: マスタテーブルからコントロール
|
||
- 突合キー: メールアドレス自動突合(初回のみ。以降はID保持で継続)。プリザンターのログインIDは現状特殊ID体系だが、将来メールアドレスへ移行予定(運用側で計画中)
|
||
- 社員ID: LINEWORKSのuserId(UUID)を採用。実データ確認の結果、`employeeNumber`(人事番号相当フィールド)は約25%のユーザーで未設定だったため主キーには使わない。プリザンター側のUserIdとは別管理とし、両者はメールアドレスで紐付ける
|
||
- 所属変更の編集UI: プリザンター標準のテーブル一覧・編集画面をそのまま使用。Node.js側に専用管理画面は作らない
|
||
|
||
## 3. 全体アーキテクチャ
|
||
|
||
新規独立フォルダ(既存の1アプリ=1フォルダ規約に準拠、他システムから独立して取り回せる構成)。
|
||
|
||
**検討して除外した代替案**: プリザンターの「ブラウザスクリプト」での実装。CORS制約(LINEWORKS/Keycloak APIへブラウザから直接到達できない)、認証情報の露出リスク(秘密鍵・Client Secretがブラウザ上で閲覧可能になる)、大量データ処理時の安定性の問題から却下。プリザンターの「サーバースクリプト」なら技術的制約は回避できるが、コードがプリザンター内に閉じてバージョン管理・レビューがしづらくなるため、Claude Skill(Node.js、本リポジトリで管理)方式を採用する。
|
||
|
||
```
|
||
apps/org-master-sync/ ※フォルダ名は仮、確定時に見直し可
|
||
src/
|
||
lib/
|
||
lineworksClient.js LINEWORKS Directory API。JWT Service Account署名はlineworks-board-syncの既存実装パターンを流用
|
||
pleasanterClient.js プリザンター REST API(マスタテーブル/ユーザー/組織/グループ)
|
||
keycloakClient.js Keycloak Admin REST API
|
||
commands/
|
||
fetch-lineworks.js ① LINEWORKS組織・社員情報取得
|
||
merge-master.js ② マスタテーブルへマージ・更新
|
||
sync-pleasanter.js ③ マスタ→プリザンターのユーザー/組織/グループへ反映
|
||
sync-keycloak.js ④ マスタ→Keycloakへ同期
|
||
.claude/skills/
|
||
org-fetch-lineworks/SKILL.md
|
||
org-merge-master/SKILL.md
|
||
org-sync-pleasanter/SKILL.md
|
||
org-sync-keycloak/SKILL.md
|
||
docs/ 仕様書・検討ログ
|
||
configs/ サイト情報(getsite)のスナップショット保存先(git管理対象外、2026-08-08追加)
|
||
site-{siteId}_{サイトタイトル}/sitesettings/site-{siteId}_latest.json 最新のサイト設定(getsite呼び出しのたびに上書き)
|
||
site-{siteId}_{サイトタイトル}/sitesettings/site-{siteId}_{timestamp}.json 更新のたびの履歴バックアップ(削除せず蓄積)
|
||
※ディレクトリ構造・命名規則は`C:\Users\k.nogi\#GitHub\ken_nogi\ClaudePleasanter`の各プロジェクトの
|
||
`configs/`(`site-paths.js`のhierarchicalレイアウト)と同一(2026-08-08、ユーザー指示)。
|
||
ただしorg-master-syncは他システムから独立した構成のため、ClaudePleasanter側のコードは
|
||
requireせず`src/lib/siteConfigStore.js`に同じ構造を独自実装している
|
||
.env.example
|
||
README.md
|
||
```
|
||
|
||
**データフロー**
|
||
|
||
```
|
||
LINEWORKS Directory API
|
||
│ ① fetch
|
||
▼
|
||
プリザンター"社員・組織マスタ管理テーブル" ← SSOT
|
||
│ ② merge(userId=社員IDで突合、初回はメール自動突合)
|
||
│ │ ③ sync
|
||
│ ▼
|
||
│ プリザンター ユーザー/組織/グループ
|
||
│ ④ sync
|
||
▼
|
||
Keycloak(nexthdレルム)ユーザー/グループ
|
||
```
|
||
|
||
## 4. 実行単位
|
||
|
||
4コマンドは独立実行。各々Claude Skillでラップし、「LINEWORKSから取得して」等の自然文指示で個別起動可能にする。一括パイプライン実行は今回のスコープ外(将来必要になれば別Skillとして追加)。
|
||
|
||
## 5. マスタテーブル カラム(確定・LINEWORKS準拠版、タブ構成)
|
||
|
||
**方針転換**: マスタテーブルの項目定義はLINEWORKSのユーザー情報に準拠する。プリザンター側に対応項目が無くても、LINEWORKSから取得できる情報は原則すべて保管する(他システムへの汎用性を考慮。将来Keycloak以外のシステム連携でも同じマスタを参照元にできるようにするため)。加えて、プリザンターのユーザー登録API(`/api/users/create`・`/api/users/update`)が受け付ける全項目もマスタで管理し、③sync-pleasanterが過不足なくプリザンターユーザーを作成・更新できるようにする。
|
||
|
||
LINEWORKS Directory API `/users`の実レスポンス(`data/lineworks-user-sample-raw.json`、`data/lineworks-users-full.json`で351人分を実際に取得・確認済み)を踏まえて確定。編集画面は3タブ構成とし、情報の出自ごとに配置する。
|
||
|
||
### 5.0 タブ構成
|
||
|
||
| タブ | 内容 |
|
||
|---|---|
|
||
| 1. 基本情報(既定表示) | 主キー・氏名(分割/カナ/英字)・生年月日・監査用日時・備考 |
|
||
| 2. プリザンター | プリザンターのユーザー登録APIが必要とする全項目 |
|
||
| 3. LINEWORKS | 上記2タブで拾いきれないLINEWORKS固有情報 |
|
||
|
||
### 5.1 タブ1: 基本情報
|
||
|
||
| No | 列名 | 内容 | 割当カラム | 補足 |
|
||
|---|---|---|---|---|
|
||
| 1 | 社員ID | `userId`(UUID) | `ClassA` | 主キー。プリザンター`GlobalId`へ同じ値を書き込む(③) |
|
||
| 2 | メールアドレス | `email` | `ClassB` | 突合キー(初回のみ。ローカル部一致で94%的中確認済み) |
|
||
| 3 | 姓 | `userName.lastName` | `Class001` | |
|
||
| 4 | 名 | `userName.firstName` | `Class002` | |
|
||
| 5 | 姓(カナ) | `userName.phoneticLastName` | `Class003` | |
|
||
| 6 | 名(カナ) | `userName.phoneticFirstName` | `Class004` | |
|
||
| 7 | 姓(英字) | `i18nNames[]`中`language: "en_US"`の`lastName` | `Class005` | 英語表記が未設定の社員は空。複数言語登録があってもen_US優先で1件のみ保持 |
|
||
| 8 | 名(英字) | `i18nNames[]`中`language: "en_US"`の`firstName` | `Class006` | 同上 |
|
||
| 9 | 生年月日 | `birthday` | `Date007` | プリザンターユーザー登録の「生年月日」項目とも共用(LINEWORKS由来の値をそのまま使い、二重管理しない)。番号は姓名等(Class001〜006)の続き番号(種類を問わず連番、2026-08-08ルール) |
|
||
| 10 | 備考 | 自由記述 | `Body`(標準項目) | |
|
||
| 11 | 最終LINEWORKS同期日時 | ②実行時に更新 | `DateA` | 監査・差分検出用(7章)。実機ラベルは「LW同期日時」に短縮 |
|
||
| 12 | プリザンター登録日時 | ③初回作成時に更新 | `DateB` | 実機ラベルは「PL登録日時」に短縮 |
|
||
| 13 | プリザンターのユーザー情報更新日時 | ③更新時に更新 | `DateC` | 実機ラベルは「PL更新日時」に短縮 |
|
||
| 14 | LINEWORKS有無 | ②実行時に判定(二択: 有/無) | `ClassZ` | LINEWORKS取得結果に存在すれば「有」、プリザンター専用ユーザー(LINEWORKSに存在しない)なら「無」。2026-08-08追加(ユーザー指示) |
|
||
| 15 | プリザンター有無 | ②実行時に判定(二択: 有/無) | `ClassY` | プリザンター実ユーザーとして突合できれば「有」、できなければ「無」。2026-08-08追加(ユーザー指示) |
|
||
|
||
氏名の結合表示(「姓 名」形式)は独立列を持たず、タブ2「名前」(`Class013`)がその役割を兼ねる。Keycloak同期時のカスタム属性`fullName`(9章の氏名連携の恒久対応)も、④sync-keycloakが`Class013`の値をそのまま使う。
|
||
|
||
**`ClassZ`/`ClassY`のChoicesText設定について**: この2項目のみ`masterFields.js`の`buildHashUpdate`(トップレベル`ClassHash`経由でのラベル一括設定)の対象外(`hash: "columns-only"`)とし、`scripts/add-existence-flag-fields.js`で個別に`SiteSettings.Columns`へ`ChoicesText: "有\n無"`を設定した。理由は5.7節「ハマった点」参照。
|
||
|
||
### 5.2 タブ2: プリザンター(ユーザー管理画面の全項目、2026-08-08確定版)
|
||
|
||
公式マニュアル([JSONデータレイアウト:User](https://pleasanter.org/ja/manual/api-user))の生データを確認したところ、ユーザー管理画面(スクリーンショットで確認)に表示される項目の一部(「管理者」ドロップダウン・テーマ・各種「許可」チェックボックス・秘密鍵有効・ログイン有効期限・無効化までの日数)に対応するプロパティが同ページには見当たらなかった。一度これらを除外する案を検討したが、**方針を変更し、ユーザー管理画面に表示される項目はすべてマスタで管理する**ことに確定。その後`/api/users/get`の実機レスポンス(公式マニュアルに記載の無いプロパティも含む全体像)を確認し、「対応プロパティ未確認」だった項目の実名がほぼすべて判明した(下表参照、2026-08-08)。
|
||
|
||
**番号ルール(2026-08-08確定)**: プリザンター項目(タブ2)は項目の種類(Class/Date/Description/Check)を問わず、`011`から通し番号を振る。真偽値項目は`CheckA`〜のような文字割当ではなく`Check020`のように番号付きで統一する。
|
||
|
||
| No | 列名 | 割当カラム | 備考 |
|
||
|---|---|---|---|
|
||
| 1 | ユーザID(プリザンター内部UserId) | `Class011` | 公式プロパティ`UserId` |
|
||
| 2 | ログインID | `Class012` | `LoginId` |
|
||
| 3 | 名前(プリザンター表示名、結合) | `Class013` | `Name` |
|
||
| 4 | ユーザコード | `Class014` | `UserCode` |
|
||
| 5 | 性別 | `Class015` | `Gender`。LINEWORKSに情報源なし、手動設定。画面上は現在非表示項目だが、将来表示される可能性があるためマスタでは維持 |
|
||
| 6 | 言語(既定: Japanese) | `Class016` | `Language` |
|
||
| 7 | タイムゾーン(既定: Japanese Standard Time) | `Class017` | `TimeZone` |
|
||
| 8 | 組織 | `Class018` | `[[Depts]]`選択式(5.5節)。**実機(ブラウザ)確認の結果、格納値は「組織ID」(プリザンター組織テーブルのDeptId、数値)と判明**。公式User APIの`DeptCode`(文字列)とは別物の可能性が高く、③sync-pleasanter実装時にDeptId→DeptCode変換の要否を検証する |
|
||
| 9 | 管理者(ユーザー選択) | `Class019` | `[[Users*]]`選択式(5.5節)。**実機確認済み、他ユーザーのUserId(数値)を指定する形式**。実プロパティ名`Manager`と判明(`/api/users/get`実機レスポンスで確認、2026-08-08) |
|
||
| 10 | テナント管理者 | `Check020` | `TenantManager`(真偽値)。上記「9. 管理者」ドロップダウンとは別物 |
|
||
| 11 | テーマ | `Class021` | `Theme` |
|
||
| 12 | 説明 | `Description022` | `Body` |
|
||
| 13 | パスワード有効期限 | `Class023` | `PasswordExpirationTime`(テナント管理者のみ取得可能) |
|
||
| 14 | サイトトップへの作成を許可 | `Check024` | `AllowCreationAtTopSite` |
|
||
| 15 | グループの管理を許可 | `Check025` | `AllowGroupAdministration` |
|
||
| 16 | グループの作成を許可 | `Check026` | `AllowGroupCreation` |
|
||
| 17 | APIを許可 | `Check027` | `AllowApi` |
|
||
| 18 | サイトトップからの移動を許可 | `Check028` | `AllowMovingFromTopSite` |
|
||
| 19 | 無効 | `Check029` | `Disabled`(テナント管理者のみ取得可能) |
|
||
| 20 | ロック | `Check030` | `Lockout`(**プロパティ名は"Lockout"**。テナント管理者のみ取得可能) |
|
||
| 21 | 秘密鍵有効 | `Check031` | `SecretKey`(文字列。空文字なら無効、値があれば有効とみなす) |
|
||
| 22 | ログイン有効期限 | `Class032` | `LoginExpirationLimit` |
|
||
| 23 | 無効化までの日数 | `Class033` | `LoginExpirationPeriod`(数値) |
|
||
| 24 | パスワード | `Class034` | データレイアウト表には無いが、`import-prod-to-test.js`で実際に`Password`パラメータでの作成に成功済み(書き込み専用のため一覧・取得APIレスポンスから省かれる) |
|
||
| 25 | 所属グループ(複数選択) | `Class035` | 標準プロパティではなく`[[Groups*]]`選択式(5.5節) |
|
||
|
||
「8. 組織」は**LINEWORKSとは連動しない独立管理項目**(一覧画面で人手管理、③はマスタ→プリザンターへの一方向反映のみ)。「25. 所属グループ」はLINEWORKSの全所属(`organizations[].orgUnits[]`、primary含む全部)に対応するプリザンターGroupIdの集合をそのまま再現し、①②で自動更新する(値の形式・マージ方式は5.5節参照)。
|
||
|
||
**②merge-masterによるタブ2ミラーリング(2026-08-08方針転換・実装済み)**: 当初「タブ2はLINEWORKS由来ではないため②は`groups`以外書き込まない」としていたが、ユーザー指摘により方針転換。②merge-master実行時、LINEWORKS取得データに加えて`/api/users/get`(`View:{ApiGetMailAddresses:true}`)でプリザンターの実ユーザーアカウント一覧も取得し、既にプリザンターにアカウントが存在する社員については上表No.1〜23(`Class011`〜`Check031`、`Class032`〜`Class033`。No.24パスワードは取得不可のため対象外)をタブ2へミラーリングする(読み取りのみ、プリザンター側への書き込みは行わない。プリザンター側への書き込みは従来どおり③sync-pleasanterの担当)。アカウント未作成の社員はタブ2が空のまま。突合キー・重複時の扱いは7章「プリザンター実ユーザーとの突合」参照。
|
||
|
||
編集画面での表示順(`EditorColumnHash`)はユーザー側で手動調整済み。項目の割当カラム(番号)自体は上表の通り変わらないが、表示順は「24. パスワード」が「12. 説明」の直後(「13. パスワード有効期限」より前)に配置されている(2026-08-08時点の実機、レビュー時の混乱防止のため記録)。
|
||
|
||
**`LastName`/`FirstName`(公式レイアウトに存在、姓名を個別に持つ)・`Birthday`・`GlobalId`について**: これらはタブ1の`Class001`〜`Class002`(姓・名)・`Date007`(生年月日)・`ClassA`(社員ID)と実質的に同じ情報のため、タブ2に別枠を設けず、③sync-pleasanter実装時にタブ1の値をそのままAPIの`LastName`/`FirstName`/`Birthday`/`GlobalId`へマッピングする(`LastName`←`Class002`名、`FirstName`←`Class001`姓、という対応になる可能性が高い点に注意。公式マニュアルの日本語ラベルとプロパティ名が入れ替わっているように見えるため、③実装時に実機で要検証)。
|
||
|
||
### 5.3 タブ3: LINEWORKS固有情報
|
||
|
||
タブ1・タブ2で既に置き場所のある項目(氏名各種・メール・生年月日等)は重複させず、ここには残りのLINEWORKS固有情報のみを置く。役職・職級・利用権限タイプはLINEWORKS上で別々の分類(`/directory/positions`・`/directory/levels`・`/directory/user-types`)のため独立した列として保持し、③のロジックで所属グループと合わせて対応するプリザンターグループへユーザーを所属させる(それぞれ同名のグループを事前に用意する運用)。
|
||
|
||
**番号ルール(2026-08-08確定)**: LINEWORKS固有項目(タブ3)も種類を問わず`051`から通し番号を振る(タブ2と同じ考え方)。
|
||
|
||
| No | 列名 | 内容 | 割当カラム |
|
||
|---|---|---|---|
|
||
| 1 | 私用メールアドレス | `privateEmail` | `Class051` |
|
||
| 2 | ニックネーム | `nickName` | `Class052` |
|
||
| 3 | 社員番号 | `employeeNumber` | `Class053`(約25%のユーザーで未設定のため主キーには不採用、2章) |
|
||
| 4 | 電話番号 | `telephone` | `Class054` |
|
||
| 5 | 携帯電話番号 | `cellPhone` | `Class055` |
|
||
| 6 | 勤務地 | `location` | `Class056` |
|
||
| 7 | 役職 | `positionName`(primary所属、8種) | `Class057`(選択式、5.5節参照) |
|
||
| 8 | 職級 | `levelName`(6種: 役員/正社員/嘱託・契約/パート社員/実習生/その他) | `Class058`(選択式、5.5節参照) |
|
||
| 9 | 利用権限タイプ | `userTypeName`(4種: 管理部門/非住宅事業/有期雇用/ダミーアカウント) | `Class059`(選択式、5.5節参照) |
|
||
| 10 | 入社日 | `hiredDate` | `Date060` |
|
||
| 11 | エイリアスメール(複数、`;`区切り) | `aliasEmails[]` | `Description061` |
|
||
| 12 | LINEWORKS在籍フラグ | 在籍中/削除済み | `Check062`(ON=在籍中。物理削除しない。退職者は取得結果から消えるため前回リストとの差分検知が必要、7章) |
|
||
| 13 | 休職中フラグ | `leaveOfAbsence.isLeaveOfAbsence` | `Check063` |
|
||
| 14 | 停止中フラグ | `isSuspended` | `Check064`(在籍はしているが一時的にアカウント停止中。在籍フラグとは別軸) |
|
||
| 15 | LINEWORKS管理者フラグ | `isAdministrator` | `Check065`(タブ2「10. テナント管理者」とは別概念、重複ではない) |
|
||
|
||
### 5.4 取得可能だが今回は列を割り当てなかった項目(実データ確認済み)
|
||
|
||
「LINEWORKSから取れる情報はすべて取って保管」の方針の例外として、以下は実データで内容を確認した上で意図的に除外した。理由とともに明記する。
|
||
|
||
| フィールド | 除外理由 |
|
||
|---|---|
|
||
| `employmentTypeId`/`employmentTypeName` | 351人全員で`userTypeId`/`userTypeName`と完全に同一の値を確認(別名の同一フィールドと判断)。列22(利用権限タイプ)と重複するため不採用 |
|
||
| `isDeleted` | `/users`の一覧結果に含まれる時点で常に`false`(削除されたユーザーはそもそも結果に含まれない、7章の退職者検知方式と整合)。値として意味を持たないため不採用 |
|
||
| `isAwaiting`・`isPending` | 招待中・承認待ちのステータス。実データ上ほぼ全員`false`で、現行の運用(招待フロー管理)では使う場面が無いため不採用。将来必要になれば追加可 |
|
||
| `customFields`・`relations`・`customProperties` | 351人全員で空配列/null。この組織のLINEWORKSテナントでは未使用の機能のため不採用 |
|
||
| `locale`・`timeZone` | 確認した範囲では全員`ja_JP`/`Asia/Tokyo`固定。実質定数のため不採用 |
|
||
| `domainId`・`userExternalKey`・`organizations[].organizationName` | LINEWORKSの内部ID/定数(`organizationName`は全員`"next"`固定)。社員ごとの差分情報を持たないため不採用 |
|
||
| `task`・`messenger` | 実データ上ほぼ全員未設定。プロフィールの補助項目で業務上の必要性が薄いため不採用 |
|
||
|
||
### 5.5 選択式項目の設計(組織・所属グループ・役職・職級・利用権限タイプ)
|
||
|
||
自由テキストではなく、プリザンター標準の選択肢機構を使い、選択式(ドロップダウン/複数選択)にする。`SiteSettings.Columns`配列の該当`ColumnName`エントリに`ChoicesText`を設定することで実現する(`getsite`実データで構造を確認済み、下記「ハマった点」参照)。
|
||
|
||
**組織(`Class018`)・管理者(`Class019`)・所属グループ(`Class035`)**: プリザンター標準のシステム参照キーワードを使う。実機(ユーザー側の手動設定)で最終確認した`ChoicesText`とプロパティは以下の通り(2026-08-08時点)。
|
||
|
||
| 項目 | ChoicesText | 補足プロパティ | 格納値(ブラウザ実機確認済み) |
|
||
|---|---|---|---|
|
||
| 組織 | `[[Depts]]`(**アスタリスク無し**、単一選択) | `UseSearch: true` | プリザンター組織テーブルの組織ID(DeptId、数値) |
|
||
| 管理者(ユーザー選択) | `[[Users*]]` | `UseSearch: true`, `SearchType: "PartialMatch"`。「9. 管理者」ドロップダウンの正体はUsersシステム参照と判明 | 他ユーザーのUserId(数値) |
|
||
| 所属グループ | `[[Groups*]]` | `UseSearch: true`, `MultipleSelections: true`, `SearchType: "ExactMatch"`。**複数選択の実体は`ChoicesText`の`*`ではなく`MultipleSelections: true`プロパティ**(実機確認済み) | GroupIdのカンマ区切り文字列(例: `"1,4"`) |
|
||
|
||
組織ID(`Class018`)をプリザンターのユーザー登録・更新API(`/api/users/update`等)へそのまま渡してよいか(`DeptId`として渡せるのか、`DeptCode`への変換が必要か)は、③sync-pleasanter実装時に実機で検証する(ユーザー確認済み、2026-08-08)。
|
||
|
||
所属グループは自由テキスト(旧`Description002`案)から**Class型・複数選択**に変更(`Class035`)。選択される値はプリザンターの実際の`GroupId`をカンマ区切りにした文字列(実機確認済み、例: `"1,4"`。`;`ではなく`,`)。LINEWORKSのグループ名テキストではない。
|
||
|
||
**分類グループの命名・自動作成(get-or-create、2026-08-08確定)**: LINEWORKS初期連携時点では、7章で定めた分類プレフィックス付きグループ(`組織:{orgUnitName}`/`役職:{positionName}`/`職級:{levelName}`/`利用権限タイプ:{userTypeName}`)はプリザンター側にまだ1つも存在しない(実データ確認済み。テスト環境の既存60グループはすべて業務用グループで、分類プレフィックス付きのものは無い)。そのため②merge-masterは以下の手順で解決する。
|
||
|
||
1. 事前に`/api/groups/get`で全グループを1回取得し、`GroupName → GroupId`のキャッシュを作る
|
||
2. 社員ごとに必要な分類グループ名(4分類、複数所属があれば「組織:」は複数になりうる)を組み立てる
|
||
3. キャッシュに無ければ`/api/groups/create`で新規作成し、キャッシュへ追加(同一実行内での重複作成を防ぐ)
|
||
4. 対象社員の既存`Class035`値(あれば)とマージ後のGroupIdの和集合をカンマ区切りで書き込む
|
||
|
||
`/api/groups/get`のレスポンス`GroupMembers`は`"User,{UserId},{Flag}"`/`"Dept,{DeptId},{Flag}"`形式の配列([公式マニュアル](https://pleasanter.org/ja/manual/api-group-get)で確認済み)。グループの新規メンバー登録は`GroupMembers`の更新で行う想定だが、Phase 1(②merge-master)のスコープでは社員側の`Class035`(マスタ内の選択)のみを更新し、グループ側の`GroupMembers`(実際のプリザンターユーザーのグループ所属)は③sync-pleasanterが担当する(マスタはあくまで「あるべき状態」を持つ台帳であり、実際のプリザンターユーザーへの反映は③の責務という原則、design全体を通じて一貫)。**追記は必ず既存の選択IDとの和集合(マージ)で行い、LINEWORKS由来でない手動選択済みのGroupIdを消さない**(他システム・手動運用で追加されたグループ選択を保持するため)。LINEWORKS側で外れたグループの選択解除(除去)は今回のスコープ外とし、将来の課題とする。
|
||
|
||
**役職(`Class057`)・職級(`Class058`)・利用権限タイプ(`Class059`)**: 外部の選択肢マスタテーブル(それぞれ独立したプリザンターサイト、ユーザー側で作成済み)への参照リンクとして選択式にする。
|
||
|
||
| 項目 | 参照先SiteId(`.env`) | 現在の値 |
|
||
|---|---|---|
|
||
| 役職 | `PLEASANTER_POSITION_MASTER_SITE_ID` | `3` |
|
||
| 職級 | `PLEASANTER_LEVEL_MASTER_SITE_ID` | `5` |
|
||
| 利用権限タイプ | `PLEASANTER_USERTYPE_MASTER_SITE_ID` | `6` |
|
||
|
||
ChoicesTextの書式(実機で動作確認済み):
|
||
|
||
```json
|
||
[{"TableName":"<参照先SiteId>","View":{"ColumnSorterHash":{"ClassA":"asc"}}}]
|
||
```
|
||
|
||
`ClassA`昇順でソート(`ClassA`は下記マスタの表示順、3桁ゼロ埋め)。
|
||
|
||
**3つの選択肢マスタテーブルの構造**(3サイトとも同一構造。役職マスタ=SiteId 3で先行カスタマイズし、職級=5・利用権限タイプ=6へ複製済み):
|
||
|
||
- 標準の「記録テーブル」(`ReferenceType: Results`)。カスタム列は`ClassA`のみ
|
||
- `ClassA`(ラベル「No」): 表示順を3桁ゼロ埋めで指定(例: `001`, `002`...)。LINEWORKSの`displayOrder`値の昇順に対応
|
||
- `Title`: 選択肢として表示される値そのもの(例:「取締役」「役員」「管理部門」)
|
||
- `Status`列を追加し、選択肢の有効/無効を管理する: `ChoicesText`は`"100,有効,有効,status-blue\n910,無効,無効,status-rejected"`。**LINEWORKSから役職・職級・利用権限タイプを再取得し、既存の値が結果から消えていた場合、レコードを削除せずStatus=910(無効)に更新する**(社員マスタ本体の在籍フラグ方式、7章と同じ思想。選択肢としては非表示/選択不可になるが、既存の参照整合性は壊さない)
|
||
- 初期データ投入済み: 役職8件(`ClassA=001`〜`008`)・職級6件(`001`〜`006`)・利用権限タイプ4件(`001`〜`004`)。全件`Status=100`(有効)
|
||
|
||
実データ調査で判明した3分類の値一覧(生値・displayOrder付き): `apps/org-master-sync/data/lineworks_positions.csv`・`lineworks_levels.csv`・`lineworks_usertypes.csv`。
|
||
|
||
### 5.6 プリザンター側の実装先サイト
|
||
|
||
マスタテーブルは新規サイト作成ではなく、既存の空サイト(SiteId=1、現在の`Title`は「記録テーブル1」、`ReferenceType`は`Results`、ユーザー側で用意済み)をカスタマイズして使う。`PLEASANTER_MASTER_SITE_ID`として`.env`に設定済み。
|
||
|
||
`getsite`(`/api/items/1/getsite`)で現状構造を実際に取得し確認済み。フィールドの型・ラベルは`Data`直下の`ClassHash`/`NumHash`/`DateHash`/`DescriptionHash`/`CheckHash`(現状すべて`{}`、未設定)で管理される方式(`{"ClassA": "社員ID"}`のようにフィールドキー→ラベルのハッシュ)。選択式項目(5.5節)の`ChoicesText`は別途`Data.SiteSettings.Columns`配列で設定する(`{"ColumnName":"Class057","ChoicesText":"..."}`)。上記表の「割当カラム」はこの方式に基づく。
|
||
|
||
反映は`updatesite`(`/api/items/1/updatesite`、Mode:full相当)で、現状の`Data`(`Title`/`ReferenceType`/`ParentId`/`InheritPermission`/`Permissions`等)を保持したまま、`Title`・5つのHash・`SiteSettings.Columns`(選択式5項目分)を書き換える。`updatesitesettings`(部分更新)は環境によって反映されないことが別プロジェクトで確認済みのため使わない。既存レコードは0件(新規サイトのため)、データ移行は不要。
|
||
|
||
### 5.7 ハマった点(サイト構築・API関連)
|
||
|
||
1. **`SiteSettings.Columns`のChoicesText等はUI手動設定→`getsite`で実際のJSON構造を確認→`updatesite`で他サイトへ複製、という手順が確実**: `[[Depts*]]`・`[[Groups*]]`・外部サイト参照リンク(`TableName`/`ColumnSorterHash`)・`Status`列の`ChoicesText`書式は、いずれもユーザーがUIで先行構築した実サイトを`getsite`で読み取って確定した(推測のJSON構造を先に書かず、実機確認を徹底)
|
||
2. **`/api/items/{SiteId}/get`(複数レコード取得)・`/api/items/{ResultId}/get`(単体取得)・`/api/items/{ResultId}/delete`がこの環境では404**: `getsite`・`create`・`updatesite`・`{ResultId}/update`は正常動作するが、取得系・削除系のみアプリ側の404ページが返る(`nginx`ではなくPleasanter自体のエラーページ)。原因未特定(APIキーの権限設定で「取得」「削除」が許可されていない可能性)。②merge-master等が既存レコード一覧を取得する処理(Task 4)・退職者検知で要再検証・代替手段の検討が必要
|
||
3. **レコード単体の更新URLは`/api/items/{ResultId}/update`(`ResultId`のみ、`SiteId`は不要)**: `/api/items/{SiteId}/{ResultId}/update`という3階層パスは`nginx`レベルで404になり、アプリにすら到達しない。`SiteId`を含めず`ResultId`単体を指定する2階層パスが正しい
|
||
4. **【解決済み】この環境のDB(PostgreSQL)には当初、3桁連番の拡張項目カラム(`Class001`〜、`Date001`〜、`Description001`〜、`Attachments001`〜等)が物理的に存在しなかった**: `ClassA`〜`ClassZ`等の1文字(26個)カラムは実在するが、`Class001`のような拡張項目を`Columns`/`EditorColumnHash`/`GridColumns`のどこか一箇所にでも参照すると、一覧のソート・レコード編集画面表示・一括削除など該当箇所の操作すべてが`PostgresException: 42703: column "Class001" of relation "Results" does not exist`で例外落ちしていた(システムログで実際のスタックトレース・SQLエラーを確認済み)。原因はEnterprise Edition機能「拡張DB書き込み」が未有効だったこと。ユーザー側でサーバー上の`Implem.CodeDefiner.dll _rds`(DBスキーマ拡張コマンド)を実行して解消(実行時に一度Postgresの権限エラー`42501: permission denied for schema`が発生したが、スキーマの所有権限を修正後に再実行し成功)。**2026-08-08、`Class001`〜`Class059`・`Date001`・`Date051`・`Description001`・`Description051`まで実際にレコード作成・取得できることを確認済み**
|
||
5. **上記4の解消により、`items/get`(一覧・単体取得)・`items/{id}/delete`のAPIも合わせて復旧した**: これらの404は権限起因ではなく、拡張カラム欠如によるSQLエラーがPleasanter側で404相当のエラーページに変換されて返っていたことが判明(4の対応と同時に解消)。誤って作成し削除不能だった旧レコード(`ResultId=2`)もこのタイミングでUI・APIとも削除に成功
|
||
6. **役職・職級・利用権限タイプ(`[[SiteId]]`参照リンク型ChoicesText)の実際の格納値を確認**: 実データ相当のテストレコード作成で検証した結果、`Class057`等には選択肢マスタの`Title`文字列がそのまま格納される(IDではなく文字列)。②merge-masterの`userToFields`が生成する`positionName`等の文字列をそのまま書き込む設計で問題ないことを確認済み(`Class018`の`[[Depts*]]`・`Class035`の`[[Groups*]]`は未検証、実際のDept/GroupIdでの検証が必要)
|
||
7. **画面のチェックボックス項目 ≠ 公式API JSONデータレイアウト**: ユーザー管理画面のスクリーンショットには「管理者」(ユーザー選択)・「テーマ」・各種「許可」チェックボックス・「秘密鍵有効」・「ログイン有効期限」・「無効化までの日数」等が表示されるが、公式マニュアル「[JSONデータレイアウト:User](https://pleasanter.org/ja/manual/api-user)」の生データ(Markdown表)にはこれらに対応するプロパティが存在しない。UI専用の設定、または別の権限体系(グループ単位の権限等)で管理されている可能性がある。一度これらをマスタから除外する案としたが、**画面の全項目を管理する方針に変更**(2026-08-08)。API経由での読み書き可否は③sync-pleasanter構築時に個別検証し、不可と判明した項目もマスタ自体には残す(5.2節)
|
||
8. **番号体系を「種類問わず連番」に統一**(2026-08-08): 当初はハッシュ種別(Class/Date/Description/Check)ごとに独立採番していたが、`CheckA`〜のような文字割当と`Class011`〜のような番号割当が混在し分かりにくくなったため、タブごとに種類を問わない通し番号(タブ2は`011`〜、タブ3は`051`〜、タブ1の姓名・個人情報系は`001`〜)に統一。真偽値も`Check020`のように番号付きで統一した。実機(`Check020`・`Date007`・`Description022`等)で作成・取得とも動作確認済み
|
||
9. **未使用の単独レターClass(`ClassC`〜`ClassZ`)は、プリザンター管理画面のUIで「項目追加」してからでないとAPIから使えない**: 新規追加した`ClassZ`/`ClassY`を`updatesite`のトップレベル`ClassHash`に含めて送ると404になった(既存の`ClassA`単体を再送信するだけでも同様に404になり、一見「サイト全体が壊れた」ように見えたが、実際は「未使用フィールドをそのAPIパスから初めて有効化しようとした」ことが原因だった)。ユーザーがプリザンター管理画面のUIで項目を追加(ラベル等の詳細は未設定のまま)した後は404が解消した
|
||
10. **`SiteSettings.Columns`(列の詳細設定=ChoicesText等)はAPIリクエストのトップレベルではなく`SiteSettings`オブジェクト配下にネストして送る必要がある**: `{ApiVersion, ApiKey, ..., Columns: [...]}`のようにトップレベルへ`Columns`を置いて送信すると`200 OK`が返るが実際には一切保存されない(サイレント失敗)。正しくは公式マニュアル`api-site-update`の例のとおり`{..., SiteSettings: {Version, ReferenceType, GridColumns, EditorColumnHash, Columns, ...}}`という形。トップレベルの`ClassHash`/`DateHash`等(フィールドラベル一括設定用)とは全く別の更新経路であることに注意。`ClassZ`/`ClassY`のChoicesText設定はこの正しい形式で送って初めて反映された(`scripts/add-existence-flag-fields.js`参照)
|
||
|
||
## 6. 認証・接続方針
|
||
|
||
3システムとも、本プロジェクト専用の新規認証情報を発行して使用する。既存の認証情報(board-sync用LINEWORKS Service Account、app-gateway用Keycloakクライアント等)とは混在させない。
|
||
|
||
| システム | 現状 | 必要な準備 |
|
||
|---|---|---|
|
||
| LINEWORKS | **疎通確認済み**。専用Service Account(board用`v4s3m`とは別の`wh4k8`アカウント)とスコープ`user.read orgunit.read group.read`で`/users`・`/orgunits`からの実データ取得を確認済み | なし(`.env`設定済み、秘密鍵は`keys/lineworks_private.key`に配置済み) |
|
||
| プリザンター | **疎通確認済み**。本番(`nextoffice.next-hd.co.jp`、587ユーザー)と別に、書き込みテスト専用環境(`nextoffice2.next-hd.net`、2ユーザーのみのクリーン環境)をユーザー側で用意済み。`.env`は現在テスト環境を指す | なし |
|
||
| Keycloak | **疎通確認済み**。`nexthd`レルムに専用クライアント`org-master-sync`を新規作成(Client Credentials方式、`realm-management`ロールの`manage-users`/`query-groups`付与)。既存`app-gateway`(Standard flow専用)とは別クライアントで、既存ログインフローに影響なし | なし(`.env`設定済み) |
|
||
|
||
`.env`等の機密情報は`AUTH_SECRETS.md`と同様の扱い(gitignore・チャット非表示)とする。
|
||
|
||
## 7. マージ・同期の詳細仕様(確定)
|
||
|
||
### 退職者検知・扱い
|
||
|
||
- **検知方法**: マスタテーブル自体を「前回リスト」として使う。②`merge-master.js`実行時、マスタに在籍中(在籍フラグON)で存在する社員IDのうち、今回のLINEWORKS取得結果(①)に含まれないものを退職と判定し、在籍フラグ(`Check062`)をOFFにする。別途の履歴ファイル管理は不要
|
||
- **【修正済み・2026-08-09】退職検知時に`ClassZ`(LINEWORKS有無)が更新されない不整合があった**: 当初`retirements`処理は`Check062`のみを`false`に更新しており、`ClassZ`は"有"のまま取り残されていた(④sync-keycloak設計時に発覚)。`Check062`と同時に`ClassZ`も`"無"`へ更新するよう修正済み(`planMerge`の`retirements`マッピング)
|
||
- **プリザンター・Keycloak双方の扱い**: 両方とも無効化のみ行い、削除はしない。プリザンター(③)は`Disabled: true`(本番で226件の実績あり、既存運用と整合)、Keycloak(④)は`enabled: false`
|
||
|
||
### プリザンター実ユーザーとの突合(②merge-master、2026-08-08追加)
|
||
|
||
- **突合キー**: メールアドレスの**ローカル部(`@`より前)一致**。完全一致ではない。実データ確認の結果、プリザンター実ユーザー592件中332件が`nexthd.jp`(ハイフン無し)ドメイン、`next-hd.co.jp`(LINEWORKSと同じドメイン)はわずか15件のみだったため、完全一致では大半が突合できなかった。管理方針(2章)の「将来メールアドレスへ移行予定」を踏まえ、ローカル部一致に緩めることで325/351件が突合できることを確認済み
|
||
- **重複時の扱い(get-or-confirm方式)**: 同一ローカル部に複数のプリザンターUserIdが紐づく場合(本番移行時のレガシーアカウント等が原因、実データで3件確認)、自動判定せず`plan.needsReview`に積んで実行時にコンソール警告する。採用するUserIdを`data/pleasanter-user-email-overrides.json`(`{"ローカル部": UserId}`形式、gitignore対象)に人手で1回追記すれば、以降の実行では自動的にその選択が使われる
|
||
- **同一ユーザーが複数メールアドレスを持つケース**: 同じUserIdが複数のメールアドレス(同ローカル部・別ドメイン等)を持つ場合は重複ではないため、`needsReview`の対象にしない(実データで発覚したバグ、修正済み)
|
||
- **ミラーリングの向き**: 一方向(プリザンター→マスタのタブ2)のみ。マスタからプリザンターへの書き込みは③sync-pleasanterの担当
|
||
- **プリザンターに登録されている全ユーザーは漏れなくマスタへ登録必須(2026-08-08、ユーザー指摘・2度の方針拡張)**:
|
||
1. まずLINEWORKSに存在しないプリザンター専用ユーザー(退職済み・無効化済み・LINEWORKS管理外等)もマスタへ登録するよう拡張。タブ1(基本情報)・タブ3(LINEWORKS固有情報)はLINEWORKS由来情報が無いため空のまま、タブ2(プリザンター管理項目)のみ埋める。`Check062`(LINEWORKS在籍フラグ)は`false`固定
|
||
2. さらに、メールアドレスを一切持たないプリザンターユーザー(テスト・退職者・システムアカウント等、実データで240件確認)もメールローカル部突合では拾えず漏れることが判明。**突合の主軸をプリザンターUserId(`Class011`)に変更**し、`pleasanterUsers`全件を直接ループする方式へ修正
|
||
- 実データ確認済み最終件数: 351件(LINEWORKS在籍)+268件(プリザンター専用、重複解決分含む)=619件(プリザンター実ユーザー592件全員がマスタのどこかに反映されていることを確認済み)
|
||
- **【ハマった点・修正済み】既存のLINEWORKS社員レコードを誤って上書きするバグ**: プリザンター専用ユーザーの突合に使う`byPleasanterUserId`・メールローカル部マップを、LINEWORKSループの**前**に(今回処理対象のLINEWORKS社員を除外せず)構築していたため、同一ローカル部を持つ「採用されなかった方」の重複プリザンターアカウントを処理する際、既にLINEWORKS社員として正しく更新済みのマスタレコードを誤ってヒットさせ、`Check062`をfalseで上書きしてしまう事故が発生(実データで発覚: 野木健一郎・高橋光晴の在籍フラグが誤ってfalseになった)。修正: 両マップの構築をLINEWORKSループの**後**に移し、`!seenEmployeeIds.has(i.ClassA)`(今回LINEWORKS社員として処理済みでないこと)でフィルタしてから使うよう変更
|
||
- **【ハマった点・修正済み】プリザンター専用ユーザー同士がメールローカル部突合で互いの既存レコードを奪い合うバグ**: 同一メールローカル部に**3つ以上**のプリザンターUserIdが紐づくケースが実データで見つかった(例: `kenichiro.nogi`に593/36/266/493の4件)。プリザンター専用ループの突合に「`Class011`優先、無ければメールローカル部」というフォールバックを使っていたため、あるUserIdが先にローカル部で既存レコードを掴むと、後から処理される別のUserIdも同じローカル部一致で同じレコードを掴んでしまい、実行順序によって1件だけしかマスタに定着しない不安定な事故が発生した(`592`件のはずが`617`件しかなく1件不足していた形で発覚)。修正: プリザンター専用ループの突合を**`Class011`一致のみ**に限定し、メールローカル部フォールバックを廃止
|
||
|
||
### 役職・職級・利用権限タイプの格納値(②merge-master、2026-08-08追加・訂正)
|
||
|
||
5.3節の`Class057`/`Class058`/`Class059`は、当初LINEWORKSの`positionName`/`levelName`/`userTypeName`文字列をそのまま格納する設計だったが、**格納すべき値は対応する外部選択肢マスタ(SiteId=3/5/6、5.5節)の該当レコードの`ResultId`である**と訂正(2026-08-08、ユーザー指摘)。②merge-master(`applyMerge`)が実行時に、3つの外部マスタから`Title→ResultId`のキャッシュを構築し、`Class035`(所属グループ)と同じget-or-createパターンで解決する。対応するTitleが外部マスタに存在しない場合は新規レコードを自動作成する(表示順`ClassA`は空のまま、手動調整が必要)。実データ確認済み: 役職8件・職級6件・利用権限タイプ4件がLINEWORKSの全件と完全一致しており、現状は既存レコードの参照のみで新規作成は発生しない
|
||
|
||
### グループ命名規則・自動作成
|
||
|
||
- **命名規則**: 分類プレフィックス付きで4分類を統一。`組織:{orgUnitName}`/`役職:{positionName}`/`職級:{levelName}`/`利用権限タイプ:{userTypeName}`
|
||
- **事前作成方法**: 手動での事前準備は不要。③④の同期処理が、存在しないグループを自動作成する(プリザンターCreate Group API・Keycloak Admin APIとも検証済み)。LINEWORKS側で新しい役職・組織が追加されても自動追従する
|
||
|
||
**実行トリガーは指示駆動に確定**: 定期実行(cron等)は導入せず、人が「同期して」等と依頼した時のみ①〜④を実行する運用とする。スケジューリング機構・エラー通知の追加検討は不要。
|
||
|
||
## 8. Keycloak同期(④sync-keycloak.js)詳細設計
|
||
|
||
### ユーザー同期
|
||
|
||
- **Upsert方式**。突合キーはKeycloakユーザーのカスタム属性`lineworksUserId`(社員ID=LINEWORKSのuserIdをそのまま格納。プリザンターの`GlobalId`と同じ役割)
|
||
- **初回のみemailで突合**し、見つかったユーザーに`lineworksUserId`属性を書き込む(プリザンターの初回メール自動突合と同じパターン)。以降は`lineworksUserId`で検索
|
||
- **Keycloak側での新規ユーザー作成はしない**(前提の確認)。LINEWORKS→Keycloakの既存SSO連携では初回ログイン時にユーザーが自動作成されるが、④はそのタイミングに依存しない。運用としては「LINEWORKSログイン時にはKeycloakユーザーが存在する」状態を、管理本部が任意のタイミングで④を実行することで事前に作っておく
|
||
- 書き込む属性: `fullName`(氏名、9章の氏名連携恒久対応)、`pleasanterLoginId`(プリザンター用LoginId、9章のNameID Mapper方式で実証済み)、`lineworksUserId`(突合キー自身)
|
||
- 退職者は`enabled: false`(7章)
|
||
|
||
### グループ同期
|
||
|
||
- LINEWORKSの「所属グループ(複数、`organizations[].orgUnits[]`全所属)」「役職」「職級」「利用権限タイプ」の4分類(5章)に対応するKeycloakグループを、命名規則(7章)に従って作成し、ユーザーを所属させる
|
||
- **プリザンター(③)だけでなくKeycloak側にも同じグループ構成を持たせる**。将来の他システム連携(XWiki等)でSAML/OIDCのgroupクレーム経由の部署単位アクセス制御を見据えた判断
|
||
- グループは同期処理が自動作成(7章)
|
||
|
||
## 9. LINEWORKS → Keycloak → プリザンター SAML SSO連携
|
||
|
||
「全社システム統合・AI活用基盤構想」の本題(README「未着手タスク」記載)。プリザンターのテスト環境(`nextoffice2.next-hd.net`)で設定スキームを確立し、疎通確認済み。設定ファイル一式は`apps/org-master-sync/pleasanter-saml/`。
|
||
|
||
LINEWORKS(IdP)は既存のKeycloak連携(`nexthd`レルム、Identity Providers > lineworks)をそのまま使用、変更不要。今回追加したのはKeycloak側の新規SAML Client(プリザンターをSPとして登録)と、プリザンター側`Authentication.json`のSAML設定。
|
||
|
||
### 確立した設定
|
||
|
||
**Keycloak側(`nexthd`レルム、Clients)**
|
||
|
||
| 項目 | 値 |
|
||
|---|---|
|
||
| Client type | SAML |
|
||
| Client ID | `https://nextoffice2.next-hd.net/pleasanter/Saml2` |
|
||
| Valid redirect URIs | `https://nextoffice2.next-hd.net/pleasanter/Saml2/Acs`(正確なACS URLに確定。下記「ハマった点4」参照) |
|
||
| Name ID format | `email` |
|
||
| Keys > Client signature required | OFF |
|
||
| Advanced > Authentication flow overrides > Browser Flow | `browser-idp-only`(下記参照) |
|
||
|
||
**Keycloak側 汎用Browser Flow「browser-idp-only」**(他システム、例: XWikiでも使い回す想定で汎用名にした)
|
||
|
||
`browser`フローを複製して作成。既定の`browser`フローとの差分のみ:
|
||
|
||
- サブフロー`browser-idp-only forms`(Username Password Formを含む): `Alternative` → **`Disabled`**
|
||
- `Identity Provider Redirector`の設定(歯車アイコン): `Alias`に任意の名前、**`Default Identity Provider`に`lineworks`**を設定
|
||
|
||
これにより、このFlowを割り当てたクライアントはログイン時に自動的にLINE WORKSへリダイレクトされ、username/passwordフォームやIdP選択画面を経由しない。他のクライアント(`app-gateway`等)には影響しない(Flowはクライアント単位のoverrideのため)。
|
||
|
||
**プリザンター側(`Authentication.json`、要点)**
|
||
|
||
| 項目 | 値 | 補足 |
|
||
|---|---|---|
|
||
| `Provider` | `"SAML"` | |
|
||
| `SPOptions.EntityId` | `https://nextoffice2.next-hd.net/pleasanter/Saml2` | |
|
||
| `SPOptions.ReturnUrl` | `https://nextoffice2.next-hd.net/pleasanter/Users/SamlLogin` | |
|
||
| `SPOptions.AuthenticateRequestSigningBehavior` | `"Never"` | |
|
||
| `IdentityProviders[0].EntityId` | `https://auth91.next-hd.net/realms/nexthd` | レルムのentityID。メタデータ取得URLとは別物 |
|
||
| `IdentityProviders[0].MetadataLocation` | `https://auth91.next-hd.net/realms/nexthd/protocol/saml/descriptor` | |
|
||
| `IdentityProviders[0].LoadMetadata` | `true` | |
|
||
| `IdentityProviders[0].SigningCertificate` | セクション自体を削除 | 残すと起動不能(下記参照) |
|
||
|
||
### ハマった点(トラブルシューティング知見)
|
||
|
||
1. **`EntityId`にメタデータURLをそのまま入れると起動時500エラー**: `Unexpected entity id ... found when loading metadata`。Sustainsys.Saml2は`EntityId`(=IdPのentityID)と`MetadataLocation`(=メタデータ取得先URL)を別々に指定する必要がある
|
||
2. **`SigningCertificate`セクションを未設定値(`FindValue: null`)のまま残すと起動不能**: `X509Certificate2Collection.Find`が`ArgumentNullException`を投げる。`LoadMetadata: true`でも証明書ストア参照処理は無条件で実行される実装のため、使わないならセクションごと削除する
|
||
3. **Keycloak Clientをレルムを間違えて作成(`master`)すると`Invalid Request`**: SAML ClientはIdPとして機能させたいレルム(`nexthd`)に作成する必要がある
|
||
4. **Valid redirect URIsの完全一致指定で`Invalid redirect uri`**: 実際にプリザンターが送信するAssertionConsumerServiceURLは`{SPOptions.EntityId}/Acs`という固定パターン(Sustainsys.Saml2の仕様)。今回の場合`https://nextoffice2.next-hd.net/pleasanter/Saml2/Acs`で、`SPOptions.ReturnUrl`(`/Users/SamlLogin`)とは別物。実際に送信されたSAMLRequest(HTTP-Redirect binding、Base64+deflate圧縮)をデコードして特定した。**対応済み**: 正確な値に絞り込み、動作確認済み
|
||
5. **プリザンターのLoginIdはAPI経由で変更不可**: `/api/users/{id}/update`にLoginIdを渡すと500エラー(レスポンスボディ空)。管理画面からの手動変更のみ可能
|
||
6. **氏名欄にメールアドレスが入る**: `Attributes.Name`がマッピングする`"Name"`というSAML属性をKeycloak側が送信していない(Attributeマッパー未設定)ため、プリザンター側が空のName属性の代わりにNameId(メール)を表示に流用している。**対応済み**(下記「氏名連携の恒久対応」参照)
|
||
7. **Identity Provider RedirectorでDefault Identity Providerを設定しても`Alias`が空だと保存できず、フォールバックで意図しないパス(Organization機能等)に入り`Invalid username or password`になる**: `Alias`は任意の名前でよいが必須項目
|
||
8. **Keycloak Admin APIの`PUT /users/{id}`は完全上書き**: 更新したいフィールド(例: `attributes`)だけを含めてPUTすると、リクエストに含まれない既存フィールド(`email`等)が消える。一度この事故で対象ユーザーの`email`が消え、Keycloak側で「Update Account Information」の強制入力画面が表示される事態になった。復旧は`username`・`email`・`enabled`等の既存値を含めて再度PUTすることで対応。**Upsert処理では常に現在の全フィールドを読み取ってからマージして送る**ことが必須(④`sync-keycloak.js`の実装注意点)
|
||
9. **Keycloakの「User Profile」機能が有効だと、未定義のカスタム属性はAPI経由で設定してもサイレントに無視される**: Realm settings > User profile で対象の属性(`fullName`・`pleasanterLoginId`・`lineworksUserId`)を明示的に属性定義として追加する必要がある。この設定変更には`manage-realm`相当の強い権限が必要で、`org-master-sync`クライアント(`manage-users`/`query-groups`のみ)では403になる。管理コンソールでの手動作業が必要
|
||
10. **SAML Mapperの`User Attribute`欄の指定ミス**: `fullName`を指定するつもりが`firstName`になっており、氏名連携が「名だけ」表示になる不具合が発生。設定値は必ず実際の属性名と一致するか確認すること
|
||
|
||
### 動作確認結果
|
||
|
||
- テストユーザー(本番UserId=460、プリザンターUserId=36)でSSOログイン完走を確認。LoginIdを事前に`kenichiro.nogi@next-hd.co.jp`(KeycloakがSAML NameIdとして送信するメールアドレス)に手動変更した上で、新規ユーザーが作られることなく既存ユーザーに正しく紐付いた。`NameId`=`LoginId`照合ロジックが期待通り機能することを確認済み
|
||
- **ディープリンク復帰**: 未ログイン状態で任意のテーブルURLを開き、SSOログインを経由した場合、ログイン後に元のテーブルURLへ正しく戻ることを確認済み。ACS URLは「SAMLレスポンスを受け取る技術的な窓口」にすぎず、SPが`RelayState`パラメータで元のURLを追跡してリダイレクトし直す仕組みのため、Valid redirect URIsを正確なACS URL単体に絞ってもこの復帰動作には影響しない
|
||
- Valid redirect URIsを正確なACS URL(`https://nextoffice2.next-hd.net/pleasanter/Saml2/Acs`)に確定済み
|
||
|
||
### Keycloakを中間ユーザーDBとして使う方式(プリザンターLoginId問題の解決)
|
||
|
||
プリザンターのLoginIdはAPI経由で変更不可(ハマった点5)という制約に対し、**Keycloak側にプリザンター固有のLoginIdをカスタム属性として持たせ、SAMLの`NameId`をそのカスタム属性から生成する**方式で解決できることを実証した。
|
||
|
||
**仕組み**: Keycloakの標準機能「User Attribute Mapper for NameID」を使う。ユーザーのカスタム属性(例: `pleasanterLoginId`)に既存のプリザンターLoginId(`ntkco12216`等、任意の文字列でよい)を設定し、SAML Client側でこの属性をNameID Format: `Unspecified`としてマッピングする。これにより、プリザンター側のLoginIdを一切変更せずにSSOを機能させられる。
|
||
|
||
**検証手順と結果**:
|
||
1. プリザンターに新規テストユーザー作成(UserId=592, LoginId=`nameid-mapper-test`、任意の非メール文字列)
|
||
2. Keycloak Admin APIで対応するテストユーザーを作成し、`attributes.pleasanterLoginId = ["nameid-mapper-test"]`を設定
|
||
3. SAML Clientに`User Attribute Mapper for NameID`(Attribute: `pleasanterLoginId`、Name ID Format: `Unspecified`)を追加
|
||
4. 検証のため一時的にBrowser Flow overrideを解除(`browser-idp-only`は外部IdP自動リダイレクトのためusername/passwordログインができないため)し、作成したKeycloak内部ユーザーでログイン
|
||
5. プリザンター側でLoginId=`nameid-mapper-test`のユーザーとして正しく認識されることを確認
|
||
|
||
**本番運用への含意**: この方式により「プリザンターLoginIdをメールアドレスへ統一する」対応は**不要**になった。マスタテーブルの「プリザンターログインID」列の値を、④`sync-keycloak.js`でKeycloakユーザーのカスタム属性`pleasanterLoginId`へ書き込む処理を追加すればよい。氏名(`fullName`属性、氏名連携の恒久対応)と同じパターンで統一的に扱える。将来のXWiki等、他システム連携でも同じ枠組み(システムごとのカスタム属性)を使い回せる。
|
||
|
||
### 氏名連携の恒久対応(実証済み)
|
||
|
||
「氏名欄にメールアドレスが入る」(ハマった点6)を、`pleasanterLoginId`と同じ「Keycloakカスタム属性→SAML Attribute」方式で解決した。
|
||
|
||
**手順**:
|
||
1. Keycloak Admin APIで対象ユーザーに`attributes.fullName`を設定(実運用では④`sync-keycloak.js`が行う)
|
||
2. Realm settings > User profile に`fullName`属性を追加定義(ハマった点9。これをやらないと1の設定がサイレントに無視される)
|
||
3. SAML Clientに`User Attribute`タイプのMapperを追加: `User Attribute: fullName`、`SAML Attribute Name: Name`、`SAML Attribute NameFormat: Basic`
|
||
4. 動作確認: SSOログイン後、プリザンターの氏名欄に正しく反映されることを確認済み
|
||
|
||
**運用上の注意**: Keycloakの`firstName`/`lastName`はUser Profileで`required`のままのため、初回ログイン時に「Update Account Information」画面でこれらの入力を求められる(README記載の既知の制約と同一事象)。`fullName`属性は`firstName`/`lastName`とは独立して管理され、これらが未入力でもSAML連携には影響しない。ただし`firstName`/`lastName`自体の必須設定を緩和するかどうかは運用側で別途判断が必要(今回は未対応のまま)。
|
||
|
||
### 未解決・要対応事項
|
||
|
||
- Keycloakの`firstName`/`lastName`必須設定の扱い(前項参照。初回ログイン時の入力画面をなくしたい場合はUser Profile側で必須を外す)
|
||
- ④`sync-keycloak.js`が同期すべきKeycloakカスタム属性は8章で確定(`fullName`・`pleasanterLoginId`・`lineworksUserId`)。あわせて、Upsert時は必ず既存の全フィールドを読み取ってからマージする実装が必須(ハマった点8)
|
||
|
||
## 10. 次のステップ
|
||
|
||
1. writing-plans skillで実装計画を作成
|