LayerX エンジニアブログ

LayerX の エンジニアブログです。

生産性とガバナンスを両立したグループ管理のため、SmartHR上の属性情報を元に擬似的なABACシステムを構築した話 #ベッテク月間

すべての経済活動を、デジタル化するために、すべての業務活動を、デジタル化したいコーポレートエンジニアリング室の @yuya-takeyama です。

7月はBet Technology Monthということでブログがたくさん出てくる月です。

そして7月といえば、第二四半期の始まりですね。

今月から転職や異動によって新しい環境で働き始める方も多いのではないでしょうか。

LayerXでは毎月のように入社・異動があるため、その度にやらないといけないことがあります。

それは、各種グループのメンバーの更新です。

LayerXにおけるグループメンバーの管理

LayerXではID基盤としてMicrosoft Entra IDを利用しています。

また、SCIMプロトコルを利用した自動プロビジョニングにより、そこから各種SaaS (Google Workspace, Slack, Notion, AWS, 1Password等) にユーザーやグループの情報が同期されるようになっています。

自動プロビジョニングがあることでかなり楽にはなっていますが、そもそものMicrosoft Entra ID上のグループのメンバー管理が大変です。

入社時のグループについては、配属部署や役職等の情報をもとに、ある程度自動化されていました。

ですが、異動時については、HRからもらう異動情報のシートを人間が読み取って、グループへの追加や削除を人力で行うという、とても煩雑なものでした。

例えば「全社マネージャー」のようなグループの場合、部署Aのマネージャーが部署Bに異動した場合、部署Bでまたマネージャーになるのであればグループに残留しますが、一般メンバーになるのであれば、グループから削除しなければなりません。

かと思いきや、実は部署Aのマネージャーとしても兼任として残るのであればやっぱり残留… といった形で、異動情報だけではなくあらゆる所属情報を照らし合わせなければ、正しく判断することはできません。

グループのメンバーを正しい状態に保てていないと、メーリングリストやカレンダーによる情報共有をスムーズにに行えなかったり、状態を正すための依頼のやり取りが発生したりと非生産的です。

さらに、グループはGoogle Drive上のファイル・ドライブのアクセス権、そしてプロダクトの管理システムの権限管理に使われているため、セキュリティやガバナンス上も重要な要素と言えます。

RBACを前提としたシステム上での擬似的なABACの実現

とあるシステムの権限を、以下のようなグループごとに割り当て、所属するメンバーはそのグループに応じた権限を持つものとします。

  • 管理者権限
  • 一般権限
  • 閲覧権限

こういったアクセス制御を、RBAC (Role-Based Access Control) と呼びます。

これは会社の規模が大きくなるまでは素朴でわかりやすいのですが、だんだんと適切に維持がするのが難しくなってくると思います。

LayerXは全社で300名を超え、Risk&Compliance室もできたりと、ガバナンスへの向き合い方のギアもさらに上げていく必要があります。

まず、それぞれのロールはどのような人々に割り当てるべきなのか、というルールを決めなくてはなりません。

例えば、「管理者権限は部署Aの正社員、一般権限は部署Bの全員、閲覧権限はそれ以外部署における職種Cの全員」といった具合です。

このような、所属部署・役職・雇用形態・職種といった属性情報を元にしたアクセス制御を ABAC (Attribute-Based Access Control) と呼びます。

ですが、LayerXが利用する多くのSaaSや社内システムは、RBACを前提としたものとなっています。

そこで、グループのメンバーを属性情報を元に自動的に更新し、正しい状態を維持し続けることで、擬似的なABACを実現しました。

擬似ABACを実現するコンポーネント: ABAC Generator

LayerXでは社員の以下のような属性情報をSmartHR上に持っています。

  • 所属部署
  • 役職
  • 契約形態
  • 主担当職種

これらを参照しつつ、Microsoft Entra ID上のグループメンバーをいい感じに更新できれば、あとは自動プロビジョニングによって各種サービスにも同期されます。

そこで、以下のような仕組みを構築しました。

  1. 「このグループにはこういう属性の人々が属する」という条件を記述した設定ファイルを用意
  2. SmartHRから全ての社員とその属性情報を読み取り、それぞれのグループに属すべきメンバーを割り出す
  3. 割り出されたメンバーと、現状の実際のメンバーを比較し、差分は追加・削除することで正しい状態にする

設定ファイルは、大体以下のようなものです。TypeScript の Object として記述します。

const config = {
  groups: [
    {
      resourceId: 'admin',
      name: '管理者権限',
      memberConditions: [
        {
          departmentCode: '1',
          employmentTypeId: EmploymentTypes.正社員,
        },
      ],
    ],
  },
  {
    resourceId: 'general',
    name: '一般権限',
    memberConditions: [
      {
        deparmentCode: '2',
      }
    ]
  },
  // その他のグループ
  // ...
};

departmentCode というのは部署を表す部署コードというものです。

部署コードはHR内で発番・管理されており、部署名の変更や統廃合が起きる場合も、その後の実態に即した形になるように部署や所属情報が更新されます。

employmentTypeId は SmartHR 上の雇用形態のマスターデータのIDを使っています。

部署のマスターデータは毎月のように変更があるのに対して、雇用形態のマスターデータに変化が起こることはかなり少なく、あっても新しいものが増えるぐらいなので、IDごとに以下のように定数化しています。

const EmploymentTypes = {
  役員: '00000000-0000-0000-0000-000000000000',
  正社員: '11111111-1111-1111-1111-111111111111',
  契約社員: '22222222-2222-2222-2222-222222222222',
  // その他の雇用形態
  // ...
} as const;

そしてこのような情報を元にABAC Generatorが何をするのかというと、実際には以下のようなTerraformのリソース定義ファイルを生成します。

これはTerraform Provider for Azure Active Directoryの、Microsoft Entra IDのグループとそのメンバーを定義するリソースです。

(Azure Active DirectoryはMicrosoft Entra IDの旧称)

resource "azuread_group" "admin" {
  name             = "管理者権限"
  security_enabled = true
    
  members = [
    '33333333-3333-3333-3333-333333333333', # kyoko.otonashi@example.com
  ]
}

resource "azuread_group" "general" {
  name             = "一般権限"
  security_enabled = true
    
  members = [
    '44444444-4444-4444-4444-444444444444', # yusaku.godai@example.com
    '55555555-5555-5555-5555-555555555555', # shun.mitaka@example.com
  ]
}

そして、これをTerraformのCI/CD基盤のあるGitHubリポジトリにpushしてPull Requestを作成しています。

現在は作ったばかりということで、何か不具合があった時のために私などが差分をチェックしてマージしていますが、最終的には自動マージまでやってしまおうと思っています。

レビューを行わないにしても、Pull Requestとして残すことで、何か意図しないメンバーの割り当てがあった時などに履歴を追いかけることで、いつその割り当てが行われたのかを確認することができます。

また、azuread_groupのmemberはユーザーのObject IDで指定する必要があるため、人間が見てわかりやすいようにメールアドレスをコメントとして付記していいます。

一連の流れをシーケンス図にまとめたものが以下です。

ABAC Generatorによるグループメンバー更新のシーケンス

実装上のポイント

ポイントは、なんといってもグループ・メンバーの更新をTerraformで行っていることです。

実は似たようなシステムをLayerX以前の職場でもメンテナンスしていたことがあったのですが、それはスクリプト内でクラウドサービス側のAPIを直接利用して、メンバーの追加・削除を行う、というものでした。

ですが、これには以下のような問題がありました。

  • スクリプトの実装が複雑になる
  • 副作用を伴うためユニットテストが困難になる

この問題を、ABAC Generatorでは以下のようなフローにすることで回避しています。

  1. SmartHRやMicrosoft Entra IDから全社員の情報をかき集める
  2. かき集めた全社員の情報と設定を入力として、グループとそのメンバーを表現するデータ構造を出力する
  3. グループとメンバーを表現するデータ構造を入力として、Terraformファイルを出力する

このようにすることで、1はHTTPリクエストを伴うためユニットテストが困難ですが、2と3は純粋関数として実装することができるので、ユニットテストを容易に行うことができます。

また、メンバーの追加・削除に関する処理を一切書かなくて良くなるので、ABAC Generatorの実装は、設定やユニットテストを除くと600行にも満たないTypeScriptで記述された非常にコンパクトなものになっています。

実際にはTerraformでなくても宣言的なInfrastructure as Codeツールでかつ、状態を保持できるものであれば大体なんでも良いとは思います。

宣言的な設定を定期的に適用することでリソースを望ましい状態に収束させるやり方は、KubernetesにおけるReconciliation Loopを意識しており、たくさんのリソースを安定的に管理するには便利な手法だと思います。

名前について

「ABAC Generator」というのは少し変な名前に聞こえるかもしれません。

より正確には「擬似ABACのためのグループGenerator」といったところですが、くどいので縮めてこのようになりました。

何かいい感じのキラキラネームをつけてもいいかもしれませんね。

ラッキーだったこと

このような仕組みを作っていく上で、私にとって最もラッキーだったのは、HRやコーポレートの方々の手によって、SmartHR上に組織情報が整備されていたことでした。

といってもこれは比較的最近行われたことで、別にこの擬似ABACの仕組みを作ることを直接の目的にしたものではありません。

ですが、根本的には似たような目的意識があったからこそ、結果的に実現したものだと考えます。

それぞれ専門性やバックグラウンドは違えど、より良い組織のあり方を考えた結果、デジタル化が少しずつ進んでいるなと感じます。

グループ管理の「のびしろ」

さて、ABAC Generatorによる擬似ABACシステムができたことで、確実にグループ管理のあり方が前進したとは思っていますが、これはまだ全体からすれば氷山の一角に過ぎません。

既存のグループのインポート

これまで人の手によって作られてきた多数のグループを、ABAC Generatorの設定上に取り込む必要があります。

今回は説明を省きましたが、Terraformのimport ブロックを活用するための設定も可能になっており、それをやっていくだけではあるんですが、単純に手数が必要です。

設定のベース部分を生成してくれるスクリプトは一応ありますが、所属条件についてはドキュメントを読み解いたり、場合によってはリバースエンジニアリングが必要です。

まぁこの辺はガッツと根性でやっていくだけです。

SmartHRへの属性情報の登録・更新が大変

社員の入社時には「入社オンボーディング」と呼ばれる業務フローを通して、Microsoftアカウントの発行やSmartHRへの登録が行われます。

それらの一部はシステムによって自動化されていますが、人間の手作業への依存はまだまだ大きいです。

根本的には誰かがどこかにデータを入力する必要がありますが、システム間の転記を無くしていくことで効率化とミスの削減が期待できます。

また、もっと大変なのは異動時で、異動情報を確定させるためには、定められた期限までに関係者に確実に情報をもらわなければなりません。

いい感じの申請システムによって効率化することはできなくはないですが、やはり所詮は人間の営みです。

申請システムというよりは、業務フローであったり、連携の効率を高めるための体制のあり方から考えていく必要があることでしょう。

ソフトウェアシステムはそれを支える舞台に過ぎません。

ここはコーポレートエンジニアリング室だけでなく、HRやコーポレートなどさまざまな関係者が関わる複雑な業務なので、協力してフローを作り上げる必要があります。

設定のセルフサービス化

ABAC Generatorの設定は前述の通りTypeScript で記述されており、これをPull Requestベースで変更しています。

今の段階としては、素早くシステムを稼働まで持って来れたので良い選択だったと思っていますが、この設定の変更はプログラミング経験のない人にはハードルの高い作業です。

そもそも、社内では全員がGitHubアカウントを持っているわけでもありません。

現状はコーポレートエンジニアリング室のメンバーを中心に依頼を元に設定を記述しているため、そこがネックになることはありませんが、やはり最終的には社内のあらゆる人が設定の変更を自分で申請し、コーポレートエンジニアリング室であったり、適切な承認者の承認を持って変更を取り込める形にしていきたいと思っています。

とはいえそういった管理用のWeb UIはメンテナンスコストもかかるので、いい塩梅のやり方を考えたいところです。

終わりに

グループの管理は多くの企業で共通の課題だと思います。

とても地味ですが、セキュリティやガバナンス上重要な要素です。

そんなグループ管理ごときのせいで社内の連携に取りこぼしがあったり、それをカバーするために依頼や作業の工数がかかったり、時にはインシデントにつながったりといった、これまでの当たり前を変えていきたいと考えています。

そんな新しい当たり前を一緒に作っていくメンバーを募集しています。

まずは話だけでも、という方は是非カジュアル面談や、LayerX Casual Nightといったイベントにお応募してみてください。