Skip to main content
Glama
darrenjrobinson

entra-scim-mcp

entra-scim-mcp

Microsoft Entra SCIM 2.0 プロビジョニング API(2026年4月 GA)向けの Model Context Protocol サーバー。https://graph.microsoft.com/rp/scim に対するユーザーおよびグループのライフサイクル操作を、Claude などのエージェント向けの MCP ツールとして公開します。

できること

  • テナントの SCIM 機能を検出する(get_service_provider_config、list_resource_types、list_schemas)

  • ユーザーのプロビジョニング、読み取り、更新、プロビジョニング解除を行う(Custom Security Attributes とライフサイクル属性を含む)

  • グループの作成、更新、削除を行い、API の厳格な PATCH ルールを自動的に尊重したメンバーシップ管理を行う

Related MCP server: mcp-server-microsoft365

前提条件

このサーバーがテナントと通信できるようにするには、Microsoft のドキュメントに記載された一度きりのセットアップを完了してください。

  1. Entra ID P1(または P1 を含む任意の SKU)と、課金のためにリンクする Azure サブスクリプション。

  2. ID ガバナンス → ダッシュボードで SCIM Provisioning API を有効にし、課金リソースグループをリンクする。

  3. 必要な Microsoft Graph アプリケーション権限を持つアプリケーションを登録する:

    • User.ReadWrite.All、Group.ReadWrite.All(コアのライフサイクル)

    • CustomSecAttributeAssignment.ReadWrite.All、CustomSecAttributeDefinition.Read.All(CSA ツール)

    • User-LifeCycleInfo.ReadWrite.All(ライフサイクルツール)

    • User-Mail.ReadWrite.All、User-Phone.ReadWrite.All、User.EnableDisableAccount.All(最小権限の代替) 管理者の同意を付与する。

  4. クライアントシークレット を作成するか、PEM クライアント証明書をアップロードする。

すべての SCIM API 呼び出しは課金対象です。このサーバーは、API が要求する範囲を超えたバッチ処理は行いません。

Entra テナントなしで試す

パッケージには Entra SCIM API のローカルモック(entra-scim-mock-server)が同梱されており、Azure のセットアップも API の課金もなしですべてのツールを操作できます:

# shell 1 — start the mock (seeds a small demo tenant)
npx -y --package entra-scim-mcp entra-scim-mock-server

次に、MCP サーバーをモックに向けます:

{
  "mcpServers": {
    "entra-scim-mock": {
      "command": "npx",
      "args": ["-y", "entra-scim-mcp"],
      "env": {
        "ENTRA_SCIM_BASE_URL": "http://127.0.0.1:8990",
        "ENTRA_SCIM_STATIC_TOKEN": "dev-token"
      }
    }
  }
}

モックのフラグ:--port、--token、--seed <file.json>、--no-seed、--capture <file.jsonl>(すべてのリクエスト/レスポンスを記録)、--validator-compat(Microsoft SCIM Validator 向けの RFC 標準準拠の動作 — docs/scim-validator.md を参照)。

インストール / 実行方法

このサーバーは stdio MCP サーバーであり、MCP クライアント(Claude Desktop、Claude Code など)から起動されることを想定しています。

npx -y entra-scim-mcp

必要な環境変数:

変数

必須

説明

ENTRA_TENANT_ID

はい

ディレクトリ(テナント)GUID

ENTRA_CLIENT_ID

はい

アプリ登録(クライアント)GUID

ENTRA_CLIENT_SECRET

いずれか

クライアントシークレットの値(開発用)

ENTRA_CLIENT_CERT_PATH

いずれか

証明書 と 秘密鍵を含む PEM へのパス

ENTRA_CLIENT_CERT_PASSWORD

任意

PEM が暗号化されている場合のパスワード

ENTRA_CLIENT_SECRET と ENTRA_CLIENT_CERT_PATH のいずれか一方のみを設定してください。

開発 / テスト用の環境変数

変数

説明

ENTRA_SCIM_BASE_URL

SCIM のベース URL を上書きします(デフォルトは https://graph.microsoft.com/rp/scim)。ローカルモックを指定します。

ENTRA_SCIM_STATIC_TOKEN

Azure AD の代わりに固定のベアラートークンを使用します。ガードレール: ENTRA_SCIM_BASE_URL が必要で、*.microsoft.com / *.microsoft.us ホストを拒否し、非ループバックホストでは警告し、実際の資格情報と組み合わせることはできません。このモードではテナント ID とクライアント ID は不要です。

ENTRA_SCIM_DRY_RUN

1 に設定:ツールはすべてのクライアント側検証を実行し、実際に送信する代わりに送信されていたはずの正確なリクエストを返します。トークンは取得されません — 資格情報の設定がゼロでも動作します。

ドライラン結果は成功したペイロードとして返されます:

{
  "dryRun": true,
  "request": {
    "method": "DELETE",
    "url": "https://graph.microsoft.com/rp/scim/users/u-1",
    "headers": {}
  }
}

(DELETE は Accept ヘッダーを付けません。API はその場所で特定の JSON メディアタイプを拒否します。それ以外のすべてのメソッドは Accept: application/json; charset=utf-8 を送信します。)

複数リクエストを行うツール(20 を超える ID を扱う add_group_members など)は、ドライランでは最初のチャンク要求のみを表示します。

Claude Desktop の設定

~/Library/Application Support/Claude/claude_desktop_config.json(macOS)または %APPDATA%\Claude\claude_desktop_config.json(Windows):

{
  "mcpServers": {
    "entra-scim": {
      "command": "npx",
      "args": ["-y", "entra-scim-mcp"],
      "env": {
        "ENTRA_TENANT_ID": "00000000-0000-0000-0000-000000000000",
        "ENTRA_CLIENT_ID": "11111111-1111-1111-1111-111111111111",
        "ENTRA_CLIENT_SECRET": "..."
      }
    }
  }
}

本番環境では、シークレットを証明書に置き換えます:

{
  "env": {
    "ENTRA_TENANT_ID": "...",
    "ENTRA_CLIENT_ID": "...",
    "ENTRA_CLIENT_CERT_PATH": "/secure/path/entra-scim-mcp.pem"
  }
}

ツール

ツール

目的

get_service_provider_config

一度きりの機能検出。

list_resource_types

SCIM リソースタイプ(User、Group)を列挙する。

list_schemas

SCIM スキーマと Entra 拡張機能を列挙する。

list_users

ユーザーを一覧表示する。API の制限されたフィルター(eq/ew、and のみ)とカーソルページネーションをサポートする。

get_user

ID により単一のユーザーを読み取る。必要に応じて属性の射影を行う。

provision_user

必須属性セット(userName、password、displayName、name.givenName、name.familyName、mailNickname)を強制してユーザーを作成する。

update_user

ユーザーを PATCH する。mailNickname の remove をブロックし、アドレスパスに [type eq "work"] を強制する。

deprovision_user

ユーザーを削除する。

update_user_lifecycle

終了属性(例:employeeLeaveDateTime)を設定する。User-LifeCycleInfo.ReadWrite.All が必要です。

get_user_custom_security_attributes

属性セットによって射影されたユーザーの CSA を読み取る。attributeSets は 必須 です — API は裸の拡張 URN を拒否し、通常の get_user では CSA は一切返されません。

update_user_custom_security_attributes

ユーザーに CSA を PATCH する。

list_groups

API の制限されたフィルタセットでグループを一覧表示する。

get_group

単一のグループを読み取する(メンバーは返されません — members.value フィルタを指定して list_groups を使用してください)。

create_group

グループを作成します。Entra 拡張機能により mailEnabled、securityEnabled、mailNickname、description を設定します。

update_group

グループ属性のみを PATCH します(メンバーシップ操作はここでは拒否されます)。

add_group_members

グループに 1 人以上のユーザーを追加します — 1 回の PATCH(API 上限)につき 20 ID で自動的にチャンク分割し、PATCH ごとに 1 つの Operation を実行します。シーケンス途中で失敗した場合は、addedMemberIds / failedMemberIds / notAttemptedMemberIds を報告するため、部分的な書き込みが黙って行われることはありません。

remove_group_member

グループから 1 人のユーザーを削除します(API は 1 回の PATCH で他の操作と同時に削除できるのは 1 件のみです)。

delete_group

グループを削除します。

このサーバーが代わりに強制する内容

Entra SCIM API には見逃しがちな制約があります。ツールレイヤーは、リクエスト送信前に不正な入力を拒否します。

  • フィルタの許可リスト:リソースごとに文書化された属性と演算子のみ。or は拒否されます。externalId は別の節と組み合わせることはできません。

  • クエリ文字列:= の周囲に空白は含めない(API は空白があると 400 を返します)。

  • ユーザー PATCH:mailNickname の remove はブロックされます。アドレスパスのフィルタは正確に [type eq "work"] でなければなりません。

  • グループ PATCH:メンバーシップ操作は専用ツールを通るため、20 メンバー追加の上限と単一削除のルールが保証されます。

  • メンバー追加のべき等性:API は再追加を成功として扱います add_group_members は入力を重複排除します。

エラーは、status、scimType、detail を含む構造化ペイロードとしてエージェントに返されます。

知っておく価値のある API の動作

ドキュメントが曖昧にしか述べていない、または全く述べていないライブ API の動作です。それぞれ実テナントまたは Microsoft SCIM Validator に対する実行で発見されたもので、すべてが処理されます。レスポンスの読み方に影響を与えるため、ここに列挙しています。

DELETE には Accept ヘッダーを付けてはならない。 API は 400 Accept header application/json is invalid と応答する。4 つのバリアントをすべてライブで検証した: ヘッダーなし → 204、*/* → 204、application/json → 400、application/scim+json → 400。このルールを逆転させているのは DELETE だけである。他のすべてのメソッドは JSON の Accept を 必須 とし、それを省略すると文書化された 400 になる。この問題は、最初のライブ実行まで deprovision_user と delete_group を静かに壊し続けた。

Custom Security Attributes は、通常の読み取りでは一切返ってこない。 /Schemas では、属性セットレベルの属性は returned: "request" である。したがって get_user は何を要求しても CSA を含まない。意図が明確な名前で指定したときだけ出現し、射影は セット単位 である: urn:...:CustomSecurityAttributes:<Set>。裸の拡張 URN は即座に拒否される (400 ... not supported in the "attributes" or "excludedAttributes" query parameter)。そのため attributeSets はオプションではなく必須の入力になっている。

CSA の値は型付きであり、型が強制される。 Boolean、Integer、String、複数値 String のすべては、一致する JSON 型として送れば往復しても無傷のまま保持される。1 つの PATCH で複数の属性を同時に送ることもできる。削除に関する動作は 2 つあり、どちらも Microsoft のドキュメントには存在しない:

  • CSA パスに対する op: "remove" は、その 1 つの割り当てをクリアし、他の割り当てを残す。

  • 複数値属性に対する [] を使った replace は、割り当てを 削除 する。その後の読み取りでは、空配列ではなく属性自体が省略される。

password は作成時に必須だが、読み取ることは決してできない。 これは writeOnly / returned: never であり、どの応答にも反映されない。作成に必要な完全なセットは userName, password, displayName, name.givenName, name.familyName、 mailNickname である。userName しか要求しない RFC 7643 より,かなり厳格。

グループの displayName は一意ではない。 Entra は重複グループ名を受け入れ、201 を返す。RFC 志向のツールはここで 409 を想定することが多いので、既存グループの検出方法として作成失敗に依存せず、まずフィルターで特定すること。

グループメンバーの削除は、ユーザーではなくメンバーシップの問題である。 members[value eq "<id>"] に対する remove が何にも一致しない場合は、その id が誰のものであっても 404 になる。つまり、単にメンバーではなかったライブユーザーも、ユーザーではなかった GUID と同じ拒否される。これは、メンバーが常に 1 人以上残っている実テナントに対して検証しており、グループが空になった結果ではない。

ケース

結果

実際のメンバー

204

一度もメンバーでない実在ユーザー

404

ユーザーとして存在したことのない well-formed GUID

404

先にユーザーが削除されたメンバー

404

次の 2 つの結果を認識しておくとよい。ユーザーを削除すると、そのユーザーのメンバーシップは消える。つまり、「削除してからメンバーシップを削除する」順序は、他の非メンバーとまったく同じ結果になる。これは削除の前後に members.value フィルター付きで list_groups を検索して確認した。また、エラーメッセージが名前を挙げるのはメンバーではなく グループ である。(Resource '<groupId>' does not exist or one of its queried reference-property objects are not present)。グループは明らかに存在するので、変に読める。意図的に存在しないグループ id で探したプローブは同じ文章でその id を名指しした。つまりテキストは単に PATCH ターゲットを反映しているだけである。モックはこの挙動を修正しはせず、まさにこのとおりを再現する。npx tsx scripts/probe-member-removal.ts --confirm (約 17 課金呼び出し) で再実行できる。

エラーは構造化されており、そのまま表示する価値がある。 失敗は status、scimType、detail を持ち、detail のテキストは異常になるほど具体的である (失敗した操作特徴インデックスと制約を特定する)。ツールはエラーを単なるメッセージに平坦化せず、そのまま通過させる。

すべての呼び出しが課金される。 API 自体が必須とする以外にバッチ処理は存在しない。そのため、頻繁に話しかけるエージェントは実際のコストになる。add_group_members は API の 20 メンバー上限で分割され、バッチが発生する唯一の場所である。

実テナントでのテスト

テストスイートは実テナントには一切触れない。ライブの Entra に対してツールを検証するには、gitignore している .env に資格情報を入れ、スモーク スクリプトを実行する。

cd node
cp .env.example .env      # then fill in tenant id, client id, and the secret VALUE

.env を読むのは scripts/ 内のスクリプトだけである。公開されるサーバーは常に process.env を読むため、MCP クライアントが起動するどのディレクトリにも関係なく、.env が誤って拾われることはない。環境変数にすでに設定されている内容は、常にファイルより優先される。

変数

目的

ENTRA_SCIM_SMOKE_DOMAIN

使い捨ての scim-smoke-* アイデンティティが作成される検証済みドメイン

ENTRA_SCIM_SMOKE_CSA_SET

属性セット名。2 つの Custom Security Attribute ツールをカバーするように設定する

ENTRA_SCIM_SMOKE_CSA_ATTR

そのセット内の属性名

ENTRA_SCIM_SMOKE_CSA_VALUE

割り当てる値。CSA は型付きなので、API は型の不一致を拒否する: true/false は JSON ブール、裸の整数は数値、この他は文字列として送られる。デフォルトは文字列のため、Boolean または Integer 属性の場合はこれを設定する

スモーク スクリプト

ENTRA_SCIM_LIVE=1 npm run smoke:live              # bash
$env:ENTRA_SCIM_LIVE=1; npm run smoke:live        # PowerShell

フラグを渡すには、スクリプトを直接呼び出すこと。npm run x -- --flag は Windows では確実に転送されない:

npx tsx scripts/live-smoke.ts --confirm

18 個すべてのツールを順序付きで一回パスし、約 21 課金呼び出しを消費する。2 つのユーザーと 1 つのグループを作成し、すべての読み取り、PATCH、delete をそれらに対して実行し、最後に削除する。重要な点:

  • うっかり実行されない。 ENTRA_SCIM_LIVE=1 または --confirm がない場合、テナント、エンドポイント、コストを表示して終了する。さらに ENTRA_SCIM_DRY_RUN または ENTRA_SCIM_STATIC_TOKEN が設定されている場合は、--rehearse を付けない限り完全に拒否する。それは実行してもライブ API について何も証明しないからだ。

  • 最初の失敗では止まらない。 失敗したステップは、それに依存するものを skip とマークし、独立なものはすべて実行し続ける。したがって 1 回の実行で、ライブ API がどのツールを受け入れるのかがわかる。失敗がの場合は終了コウードが非ゼロになる。

  • テスト・アイデンティティは一目瞭然。 scim-smoke-<runId>-1@<domain> と SCIM Smoke <runId> グループ。

  • クリーンアップは保証されている。 作成されたものはすべて finally ブロックで削除され、残ったものには id が表示される。クラッシュによる実行からは npx tsx scripts/live-smoke.ts --sweep で、取り残された scim-smoke-* ユーザーを一覧表示し、次に --confirm を付けて削除する。sweep は scim-smoke- プレフィックスを持たないアカウントには絶対に触れない。

2 つの Custom Security Attribute ツールは、テナントに属性セットが存在し、Entra 管理ポータル -> Protection -> Custom security attributes で ENTRA_SCIM_SMOKE_CSA_SET / ENTRA_SCIM_SMOKE_CSA_ATTR がそれを指定するまで skip を報告する。それ以外はすべて無人で実行される。

セットが存在すると、約 9 課金呼び出しで、その 2 つのツールだけを検証でき、21 回の代わりに済む。これはすべての値を読み戻すもので、これは API が受け取るが何も保存しない PATCH が 合格に見えてしまうのを防ぎ、各データ型をカバーし、削除も実行する:

npx tsx scripts/live-smoke.ts --csa-only --confirm

セットの形を一度宣言すると、実行で型ごとにテスト値が導出される:

ENTRA_SCIM_SMOKE_CSA_ATTRS=isManaged:bool,accountType:string,trustLevel:int,locations:string[]

4 つのすべての型でライブ確認済み: 値は無損で往復し、op: "remove" は 1 つの割り当てをクリアして残りを残す。複数値属性を [] に置換するとその属性は削除され、読み返すと空配列ではなく属性自体が消える。

お金を使う本番前にゼロコストでリハーサルする — これは API ではなくスクリプトを検証する:

# no network at all
ENTRA_SCIM_DRY_RUN=1 npx tsx scripts/live-smoke.ts --rehearse

# or against the local mock: start it in one shell...
npm run mock
# ...and in another, aim the script at it
export ENTRA_SCIM_BASE_URL=http://127.0.0.1:8990
export ENTRA_SCIM_STATIC_TOKEN=dev-token
npx tsx scripts/live-smoke.ts --rehearse

価値があるのはモック・リハーサルである。これは実 HTTP、実 ids、完全な create/patch/delete の順序を実施するため、コストがかかる前に順序バグとクリーンアップバグを発見できる。ライブ実行の代替にはならない。最初のライブ実行は、どのモック実行でも見つけられなかった 2 つのバグを見つけた (テストレグが実際に捉えたもの を参照)。

テストレグが実際に発見したもの

3 つの独立したレグがあり、それぞれが他には発見できないことを見つけた。この 3 つを常に揃えている理由である:

レグ

コスト

発見した内容

モック + ユニット スイート

無料

連続、検証、クリーンアップのバグ。高速だが、同じ前提を共有しているため、誤った前提を検出できない。

ライブ テナント (smoke:live)

約 21 課金呼び出し

DELETE の Accept バグ (正常に動いたことがない 2 つのツール)、悪い裸の CSA URN、CSA 型/削除の意味論。

SCIM Validator

無料

モックの忠実さの 7 箇所のギャップ — 実 GSCIM クライアントが期待するより寛容 だった箇所が、それぞれ実際の挙動を隠していた。

持ち帰る価値のあるパターン: モックの寛容さは実際の推移を隠す。 ライブ実行が発見したすべての不具合は、まず完全なモックスイートを通り抜けていた。その理由は、モックがクライアントと同じドキュメントの読み方から書かれていたからだ。三方クライアント (SCIM Validator) と実際のテナントだけが、その循環を打ち破ることができた。

ライブテナントを対話的に操作する

リポジトリルートの .mcp.json は、scripts/dev-server.mjs を使ってサーバーを Claude Code に登録する。これは node/.env を読み、ビルド済みサーバーを起動するため、コミット済みの設定ファイルにシークエレットが含まれることはない。

これはビルド済みサーバーを実行するので、MCP クライアントが起動する前に node/dist が存在していなければならない。新規クローンの場合は次の手順で作成する:

cd node && npm install     # the "prepare" script builds as part of install

ソースコードを変更した後は、再ビルドし、クライアントを再起動することでこのコマンドが再起動する:

cd node && npm run build

開発

cd node
npm install             # installs, then builds via "prepare"
npm test
npm run lint            # ESLint, type-aware
npm run format:check    # Prettier
npm run typecheck       # strict tsc over src, test and scripts
npm run test:coverage   # vitest with the coverage gate
npm run build           # rebuild after a source change
npm run mock            # run the local mock server (tsx, no build needed)
npm run mock:capture    # mock in validator-compat mode, capturing traffic to captures/

npm audit --audit-level=high に加えて、CI はすべてのプッシュとプル リクエストで npm run lint, format:check, typecheck, test の 4 つのゲートを実行する。

サーバーは、実テナントへのテスト依存を持たない。ユニットテストはフィルター、パッチ、クエリ、クライアント層をカバーし、結合テストはプロセス内モックサーバーを起動し、実際の HTTP を経由して すべての MCP ツールをエンドツーエンドで実行する (node/test/integration/)。取得した SCIM Validator セッションは npm run fixtures:convert でリプレイフィクスチャに変換できる。それらでは証明できない唯一のこと、つまりライブ API が実際にペイロードを受け付けることは、実テナントでのテスト の章を参照すること。

リリース

バージョンは次の 4 箇所に定義されている: node/package.json, node/package-lock.json (2 箇所)、そして server.json (2 箇所、一回はレジストリ記録、もう一回は npm パッケージの指定)。1 つのコマンドでこれらすべてに書き込む:

cd node
npm version minor          # or patch / major — writes all four, stages three
cd ..
git commit -m "v0.2.0"     # the version npm just printed
git tag -a v0.2.0 -m v0.2.0
git push --follow-tags

-a が重要です。--follow-tags は注釈付きタグのみをプッシュするため、軽量タグの git tag v0.2.0 はマシン上に残り、プッシュはタグを一切送らずに成功を報告します — つまり、リリースが単に実行されないのです。

npm version は package.json とロックファイルを更新し、その後 version ライフサイクルスクリプトがその内容を server.json に反映し、結果をステージします。通常 npm version はコミットとタグの両方を行いますが、ここではどちらも 行いません。npm はバージョニング対象のパッケージの隣に .git を探しますが、このパッケージは node/ にあり、リポジトリの .git はその一つ上にあるため、npm は自分が git リポジトリの中にいないと判断し、何も言わずにそれらの手順をスキップします。だからこそ、上記の明示的なコミットとタグ付けが必要なのです。これを間違えると、git push --follow-tags は黙って何もプッシュしません。追従するはずだったタグが作成されていないからです。

npm run check:version は4つのすべてが一致することを検証します。CI はすべてのプッシュでそれを実行し、リリースワークフローはタグ自体に対してもう一度実行します。つまり、package.json と不一致のタグは、何かが公開される前に失敗します。サーバーが MCP ハンドシェイクで報告するバージョンは、実行時に package.json から読み取られるため、自動的に追従されます。

v* タグをプッシュすると、.github/workflows/release.yml が実行されます:

  1. verify — lint、format、typecheck、カバレッジ付きテスト、バージョン/タグのチェック、そしてライブレジストリに対する mcp-publisher validate を実行します。

  2. verify on Windows — windows-latest 上で同じテストを再度実行します。モックが実際のソケットにバインドし、キャプチャシンクが実際のパスを書き込むためです。これがないと、リリースゲートは通常のコミットに対するゲート(CI は Windows 上でも実行します)よりも弱くなります。

  3. publish — npm publish を実行し、新しいバージョンが npm 上で表示可能になるまで待機し、その後 server.json を MCP Registry に公開します。

これらすべてが成功するまで、何も公開されません。

どちらの公開も GitHub OIDC で認証されるため、リポジトリには秘密情報が一切存在しません — 漏洩したり、最悪の瞬間に失効していると判明する publish トークンは存在しないのです。

レジストリ

このワークフローを認証する方法

npm

パッケージの trusted publisher を、このリポジトリと、環境指定なしの workflow ファイル release.yml に設定しています。npm は OIDC クレームをそれと照合し、同じトークンから provenantce 証明を添付します。npm 13.5.1 が必要なため、ジョブは公開前に npm をアップグレードします。

MCP Registry

mcp-publisher login github-oidc を実行します。darrenjrobinson/entra-scim-mcp 内で実行されることが、io.github.darrenjrobinson/* ネームスペースを許可します。

この両者が、公開ジョブが id-token: write を要求する理由です。

MCP Registry は、package.json を取得して server.json の name と mcpName を比較することで、npm パッケージの所有を証明します。両方とも io.github.darrenjrobinson/entra-scim-mcp であり、check:version はそれらが一致し続けることを検証します。

ワークフローのファイル名を変更したり、公開ジョブに environment: を追加したりすると、npmjs.com 上の設定が一致するよう更新されるまで、npm の trusted publisher は機能しません — OIDC クレームは厳密に照合されます。

ライセンス

MIT — LICENSE を参照してください。

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Enables identity provisioning and management for Microsoft 365/Entra ID via Microsoft Graph, including user creation, license assignment, group membership management, and more, with a focus on least-privilege and idempotency.
    18
    Apache 2.0