diff --git a/cloud-accounts/connecting-a-cloud-account.mdx b/cloud-accounts/connecting-a-cloud-account.mdx index 9d68cf6..e806890 100644 --- a/cloud-accounts/connecting-a-cloud-account.mdx +++ b/cloud-accounts/connecting-a-cloud-account.mdx @@ -206,15 +206,8 @@ Before Porter can create a cluster, you need to grant it access to your cloud ac - `Microsoft.Blueprint/blueprintAssignments/delete` - `Microsoft.Compute/galleries/share/action` - Creates the `azure-porter-federated-sp` app registration and matching service principal, then assigns the custom role at the subscription scope - - Adds these Microsoft Graph application permissions: - - Application.ReadWrite.All - - Directory.ReadWrite.All - - Domain.Read.All - - Group.Create - - Group.ReadWrite.All - - RoleManagement.ReadWrite.Directory - - User.ReadWrite.All - - Grants admin consent for the Graph permissions + - Makes the service principal an owner of its own app registration + - Adds the `Application.ReadWrite.OwnedBy` Microsoft Graph application permission and grants admin consent for it. This is the only Graph permission Porter uses, and Porter uses it solely to manage the federated identity credentials on the app registration the service principal owns. Its write access is limited to applications the service principal creates or owns, so Porter cannot modify any other application in your tenant - Adds a federated identity credential on the app registration trusting Porter's OIDC issuer (audience `api://AzureADTokenExchange`) - Waits for Azure's [eventually consistent IAM service](https://devblogs.microsoft.com/identity/designing-for-eventual-consistency-for-microsoft-entra/) to propogate changes. - Outputs the app registration's metadata: **Subscription ID**, **Application (Client) ID**, and **Tenant ID** diff --git a/scripts/setup-azure-porter-wif.sh b/scripts/setup-azure-porter-wif.sh index b0fb830..ac70a43 100755 --- a/scripts/setup-azure-porter-wif.sh +++ b/scripts/setup-azure-porter-wif.sh @@ -334,6 +334,27 @@ assign_custom_role() { print_success "Role assignment ensured" } +# Make the service principal an owner of its own app registration. Application.ReadWrite.OwnedBy +# only reaches registrations the caller owns, so without this Porter cannot manage the federated +# identity credentials on the registration. Kept separate from create_app_registration so it is +# reconciled on every run, like assign_custom_role. +ensure_app_owner() { + print_status "Ensuring the service principal owns its app registration..." + + SP_OBJECT_ID=$(az ad sp show --id "$APP_ID" --query id -o tsv) + if [ -z "$SP_OBJECT_ID" ]; then + print_fatal "Failed to look up the service principal for app registration $APP_ID" + fi + + if az ad app owner list --id "$APP_ID" --query "[].id" -o tsv 2>/dev/null | grep -qx "$SP_OBJECT_ID"; then + print_success "Service principal is already an owner of the app registration" + return + fi + + az ad app owner add --id "$APP_ID" --owner-object-id "$SP_OBJECT_ID" + print_success "Service principal added as an owner of the app registration" +} + # Function to add API permissions add_api_permissions() { print_status "Adding Microsoft Graph API permissions..." @@ -341,15 +362,11 @@ add_api_permissions() { # Microsoft Graph App ID (constant) MSGRAPH_APP_ID="00000003-0000-0000-c000-000000000000" - # Required permission names + # Porter's only Microsoft Graph operation is managing federated identity credentials on this + # app registration. Application.ReadWrite.OwnedBy covers that and is scoped to registrations + # the service principal owns (see ensure_app_owner) rather than every application in the tenant. required_permissions=( - "Application.ReadWrite.All" - "Directory.ReadWrite.All" - "Domain.Read.All" - "Group.Create" - "Group.ReadWrite.All" - "RoleManagement.ReadWrite.Directory" - "User.ReadWrite.All" + "Application.ReadWrite.OwnedBy" ) # Get Microsoft Graph service principal to fetch permission IDs dynamically @@ -668,6 +685,7 @@ main() { create_custom_role create_app_registration assign_custom_role + ensure_app_owner add_api_permissions # Try to grant admin consent, but don't fail if it doesn't work