diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index d2d8c0cd..7abb89af 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -44,13 +44,17 @@ export default defineConfig({ collapsed: true, items: [ { - text: "Gadgetron examples", - link: "/reference/gadgetron/gadgetron", + text: "Bootstrapping access control", + link: "/reference/access-control-bootstrap", }, { text: "Database management", link: "/reference/database-management", }, + { + text: "Gadgetron examples", + link: "/reference/gadgetron/gadgetron", + }, { text: "tyger-proxy", link: "/reference/tyger-proxy" }, ], }, diff --git a/docs/introduction/installation/cloud-installation.md b/docs/introduction/installation/cloud-installation.md index 9b882fd0..e533896a 100644 --- a/docs/introduction/installation/cloud-installation.md +++ b/docs/introduction/installation/cloud-installation.md @@ -327,6 +327,16 @@ the config file and running: tyger access-control apply -f config.yml ``` +::: info Note +If you do not have permission to create app registrations in your Entra ID +tenant, or if your organization will not allow running `tyger +access-control apply` directly against the directory, see +[Bootstrapping access control with an Entra ID +admin](../../reference/access-control-bootstrap.md) for a one-time `az`-only +bootstrap that an Entra admin can run on your behalf. After that bootstrap, +the rest of this section applies as written. +::: + The part of the config file to edit is under the path `organizations[*].api.accessControl`. The first part of that section is parameters for authentication: diff --git a/docs/reference/access-control-bootstrap.md b/docs/reference/access-control-bootstrap.md new file mode 100644 index 00000000..85113e07 --- /dev/null +++ b/docs/reference/access-control-bootstrap.md @@ -0,0 +1,282 @@ +# Bootstrapping access control with an Entra ID admin + +The [`tyger access-control apply`](../introduction/installation/cloud-installation.md#set-up-access-control) +command is a convenience that creates and maintains the two Microsoft Entra ID +app registrations that Tyger uses for authentication (one for the API server and +one for the CLI), and that keeps the user/group role assignments on them in +sync. + +In some organizations, the people who will operate Tyger day-to-day do not have +the directory permissions required to create app registrations, and the Entra ID +admins who do have those permissions are not willing to run a third-party tool +such as `tyger` against the directory. This page describes how to do the initial +bootstrap using only `az` commands an Entra admin can review, after which the +Tyger operators ("owners") can take over and run `tyger access-control apply` +themselves for all subsequent changes. + +## What this bootstrap does + +The script below performs the minimum set of steps that require Entra admin +privileges: + +1. Creates the API app registration and its service principal. +2. Creates the CLI app registration and its service principal. +3. Adds the designated Tyger owners as **app owners** on both registrations. + +Once an account is listed as an app owner, it can update the application object +(define app roles, OAuth2 scopes, pre-authorized clients, redirect URIs, etc.) +and grant role assignments on the API app's service principal. Everything else +that `tyger access-control apply` normally does is then performed by the Tyger +owner under their own identity — no further Entra admin involvement is required. + +## Step 1 — Collect the Tyger owners' object IDs + +Designate at least one Tyger owner. These are the people who will subsequently +run `tyger access-control apply` to manage the app registrations and role +assignments. + +Each owner must be an individual user account — Entra ID app owners cannot be +groups or service principals. + +Each owner can find their own Entra ID object ID by running: + +```bash +az ad user show --id "$(az account show --query user.name -o tsv)" --query id -o tsv +``` + +Collect the object IDs of all the intended owners and pass them to the Entra +admin. + +## Step 2 — Have the Entra admin run one of the bootstrap scripts + +The admin should run **exactly one** of the two scripts below. They both produce +the same end state — two app registrations, two service principals, and the +Tyger owners listed as app owners on both — and both print the values the +Tyger owner needs in Step 3. They differ only in the form of the +[application ID URI](https://learn.microsoft.com/entra/identity-platform/security-best-practices-for-app-registration#application-id-uri) +they set on each app: + +- **Script A** uses `api://{tenantId}/tyger-server` and + `api://{tenantId}/tyger-cli`. These URIs are more readable than the app-ID + form and are accepted by Entra by default. If your tenant has a + [verified domain](https://learn.microsoft.com/entra/identity/users/domains-manage) + and you would prefer an even friendlier URI such as + `api://tyger.contoso.com/tyger-server`, edit the `api_app_uri` and + `cli_app_uri` variables before running the script. +- **Script B** uses `api://{appId}` — each app's own client ID — as the + identifier URI. The URI is less meaningful to a human reader, but this form + is always accepted regardless of tenant policy. Use this variant if Script A + is rejected (some tenants restrict identifier URIs to verified-domain hosts + only). + +In both scripts, fill in: + +- `owner_object_ids` — the object IDs collected in Step 1. +- `api_app_display_name` and `cli_app_display_name` — friendly names shown in + the Entra admin portal. + +The admin only needs to run their chosen script once. If a step fails, the +script halts immediately (so there is no risk of partial corruption); the +admin can investigate, clean up any partially-created resources, and either +re-run the script or complete the remaining steps manually. + +### Script A — tenant-ID (or verified-domain) URI + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# One or more object IDs of the Tyger owners. These accounts will be added as +# app owners on both app registrations so they can subsequently run +# `tyger access-control apply` themselves. +owner_object_ids=( + "FILL-IN-OWNER-OBJECT-ID-1" + # "FILL-IN-OWNER-OBJECT-ID-2" +) + +# Identifier URIs. The default `api://{tenantId}/...` form is human-readable +# and is accepted by Entra by default. If your tenant has a verified domain +# you may prefer `api://tyger.contoso.com/tyger-server` instead. +tenant_id="$(az account show --query tenantId -o tsv)" +api_app_uri="api://${tenant_id}/tyger-server" +cli_app_uri="api://${tenant_id}/tyger-cli" + +api_app_display_name="Tyger API" +cli_app_display_name="Tyger CLI" + +# --- API app --------------------------------------------------------------- +api_app_id="$(az ad app create \ + --display-name "$api_app_display_name" \ + --identifier-uris "$api_app_uri" \ + --requested-access-token-version 2 \ + --query appId -o tsv)" + +az ad sp create --id "$api_app_id" + +for owner_object_id in "${owner_object_ids[@]}"; do + az ad app owner add --id "$api_app_id" --owner-object-id "$owner_object_id" +done + +# --- CLI app --------------------------------------------------------------- +cli_app_id="$(az ad app create \ + --display-name "$cli_app_display_name" \ + --identifier-uris "$cli_app_uri" \ + --requested-access-token-version 2 \ + --query appId -o tsv)" + +az ad sp create --id "$cli_app_id" + +for owner_object_id in "${owner_object_ids[@]}"; do + az ad app owner add --id "$cli_app_id" --owner-object-id "$owner_object_id" +done + +echo +echo "Bootstrap complete." +echo " tenantId: ${tenant_id}" +echo " apiAppUri: ${api_app_uri}" +echo " cliAppUri: ${cli_app_uri}" +echo " ownerObjectIds:" +for owner_object_id in "${owner_object_ids[@]}"; do + echo " - ${owner_object_id}" +done +``` + +### Script B — `api://{appId}` fallback + +Use this variant only if Script A is rejected by tenant policy (typically with +an error such as *"Values of identifierUris property must use a verified +domain of the organization or its subdomain"*). + +```bash +#!/usr/bin/env bash +set -euo pipefail + +owner_object_ids=( + "FILL-IN-OWNER-OBJECT-ID-1" + # "FILL-IN-OWNER-OBJECT-ID-2" +) + +api_app_display_name="Tyger API" +cli_app_display_name="Tyger CLI" + +tenant_id="$(az account show --query tenantId -o tsv)" + +# --- API app --------------------------------------------------------------- +api_app_id="$(az ad app create \ + --display-name "$api_app_display_name" \ + --requested-access-token-version 2 \ + --query appId -o tsv)" + +api_app_uri="api://${api_app_id}" +az ad app update --id "$api_app_id" --identifier-uris "$api_app_uri" +az ad sp create --id "$api_app_id" + +for owner_object_id in "${owner_object_ids[@]}"; do + az ad app owner add --id "$api_app_id" --owner-object-id "$owner_object_id" +done + +# --- CLI app --------------------------------------------------------------- +cli_app_id="$(az ad app create \ + --display-name "$cli_app_display_name" \ + --requested-access-token-version 2 \ + --query appId -o tsv)" + +cli_app_uri="api://${cli_app_id}" +az ad app update --id "$cli_app_id" --identifier-uris "$cli_app_uri" +az ad sp create --id "$cli_app_id" + +for owner_object_id in "${owner_object_ids[@]}"; do + az ad app owner add --id "$cli_app_id" --owner-object-id "$owner_object_id" +done + +echo +echo "Bootstrap complete." +echo " tenantId: ${tenant_id}" +echo " apiAppUri: ${api_app_uri}" +echo " cliAppUri: ${cli_app_uri}" +echo " ownerObjectIds:" +for owner_object_id in "${owner_object_ids[@]}"; do + echo " - ${owner_object_id}" +done +``` + +### Reporting back + +Whichever script was used, ask the admin to send back the values printed at +the end (`tenantId`, `apiAppUri`, `cliAppUri`, and the owner object IDs) along +with confirmation that the script completed successfully. + +## Step 3 — Owner takes over with `tyger access-control apply` + +Once the bootstrap is complete, one of the Tyger owners edits the main Tyger +[cloud configuration file](../introduction/installation/cloud-installation.md#generate-an-installation-configuration-file) +(`config.yml`) and fills in the `accessControl` section under the relevant +organization, at the path `organizations[*].api.accessControl`: + +```yaml +api: + accessControl: + tenantId: c546d652-e328-4c56-9cc3-030af6b7b194 # reported by the admin + apiAppUri: api://c546d652-e328-4c56-9cc3-030af6b7b194/tyger-server # reported by the admin + cliAppUri: api://c546d652-e328-4c56-9cc3-030af6b7b194/tyger-cli # reported by the admin + + apiAppId: "" # `tyger access-control apply` will fill in this value + cliAppId: "" # `tyger access-control apply` will fill in this value + + roleAssignments: + owner: + # At minimum, list the Tyger owners here so they can use Tyger themselves. + # These are the owner object IDs reported by the admin. + - kind: User + objectId: 33333333-3333-3333-3333-333333333333 + + contributor: [] +``` + +The values to set are: + +- `tenantId`, `apiAppUri`, `cliAppUri` — reported by the admin at the end of + the bootstrap script. +- `apiAppId` / `cliAppId` — leave empty; `tyger access-control apply` will + fill them in from the app registrations. +- `roleAssignments.owner` — list each Tyger owner using the `objectId` value + reported by the admin. Using `objectId` avoids the ambiguity of the user + principal name, which does not always match a user's email address. +- `roleAssignments.contributor` — list any additional users, groups, or + service principals that should have the contributor role. See + [Set up access control](../introduction/installation/cloud-installation.md#set-up-access-control) + for the supported principal forms. + +Then apply the configuration: + +```bash +tyger access-control apply -f config.yml +``` + +(If the configuration file contains more than one organization, also pass +`--org ` to select the one to apply.) + +This is the command that fills in the rest of the app registration details: +defining the `owner` and `contributor` app roles, declaring the OAuth2 +permission scope, configuring the CLI app as a public client with a +`http://localhost` redirect URI, marking the CLI app as a pre-authorized +client of the API app, and creating the requested app role assignments on the +API app's service principal. + +From this point on, the Tyger owners can re-run `tyger access-control apply` +themselves whenever role assignments need to change — no further Entra admin +involvement is required. + +## What is *not* done by the bootstrap script + +For transparency when reviewing the script with the Entra admin, note that the +script intentionally does **not**: + +- Define any app roles, OAuth2 permission scopes, pre-authorized clients, or + redirect URIs. Those are configured later by the owner via + `tyger access-control apply`. +- Grant admin consent for any permissions. The CLI app only requests a + delegated scope on the API app (which is in the same tenant and pre-authorized + by `tyger access-control apply`), so admin consent is not required. +- Assign any users to Tyger roles. That is done later by the owner via + `roleAssignments` in the configuration file.