Skip to content

Instantly share code, notes, and snippets.

@richeney
Last active March 5, 2026 17:11
Show Gist options
  • Select an option

  • Save richeney/60f487bf788cfd52c89cfd7800d5adef to your computer and use it in GitHub Desktop.

Select an option

Save richeney/60f487bf788cfd52c89cfd7800d5adef to your computer and use it in GitHub Desktop.
Azure CLI commands to show tenant, user, and partner ID for Partner Admin Link.
#!/usr/bin/env bash
# [GitHub Gist - richeney/pal_attestation.sh](https://gist.github.com/richeney/60f487bf788cfd52c89cfd7800d5adef)
# Usage: curl -sSL https://aka.ms/pal/attestation | bash
error() { echo "ERROR: $@" >&2; exit 1; }
usage() {
cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]:-$0}") [-a|--assignments] [--object-id <id>] [-h|--help]
Outputs a JSON attestation object for the current Azure login and Partner Admin Link (PAL).
Options:
-a, --assignments Include direct RBAC role assignments in the output
--object-id <id> Query assignments for a specific object ID instead of the current login
(PAL fields will be N/A when using this option)
-h, --help Show this help message
Examples:
# Standard usage - pipe from URL
curl -sSL https://aka.ms/pal/attestation | bash
# Include role assignments
curl -sSL https://aka.ms/pal/attestation | bash -s -- --assignments
# Query a specific object ID
curl -sSL https://aka.ms/pal/attestation | bash -s -- --assignments --object-id <objectId>
# Local usage
./pal_attestation.sh
./pal_attestation.sh --assignments
./pal_attestation.sh --assignments --object-id <objectId>
EOF
exit 0
}
# Parse arguments
includeAssignments=false
targetObjectId=""
while [[ $# -gt 0 ]]; do
case $1 in
-a|--assignments) includeAssignments=true ;;
--object-id) targetObjectId="$2"; shift ;;
-h|--help|-\?) usage ;;
esac
shift
done
# Prerequisites
command -v jq &>/dev/null || error "jq is required but not installed"
command -v az &>/dev/null || error "az is required but not installed"
command -v base64 &>/dev/null || error "base64 is required but not installed"
account=$(az account show --output json)
token=$(az account get-access-token --query accessToken -o tsv)
tenantId=$(jq -r '.tenantId' <<< "$account")
callerObjectId=$(echo "$token" | cut -d. -f2 | base64 -d 2>/dev/null | jq -r '.oid')
type=$(jq -r '.user.type' <<< "$account")
# Determine target objectId and PAL fields
if [[ -n "$targetObjectId" ]]; then
objectId="$targetObjectId"
palId="Unavailable"
partnerId="Unavailable"
partnerName="Unavailable"
echo "INFO: PAL information is not available for a specified object ID - authenticate as that identity to retrieve it." >&2
domain=$(jq -r '.tenantDefaultDomain' <<< "$account")
tenantName=$(jq -r '.tenantDisplayName' <<< "$account")
type="unknown"
# Try resolving as a service principal (accepts appId or SP objectId)
spJson=$(az ad sp show --id "$targetObjectId" --output json 2>/dev/null)
if [[ -n "$spJson" ]]; then
spObjectId=$(jq -r '.id' <<< "$spJson")
spAppId=$(jq -r '.appId' <<< "$spJson")
if [[ "$targetObjectId" == "$spAppId" ]]; then
echo "INFO: Resolved appId '$targetObjectId' to service principal objectId '$spObjectId'" >&2
objectId="$spObjectId"
elif [[ "$targetObjectId" != "$spObjectId" ]]; then
echo "INFO: Resolved to service principal objectId '$spObjectId'" >&2
objectId="$spObjectId"
fi
name=$(jq -r '.displayName' <<< "$spJson")
appId="$spAppId"
upn=null
type="servicePrincipal"
else
# Try resolving as an app registration objectId
appJson=$(az ad app show --id "$targetObjectId" --output json 2>/dev/null)
if [[ -n "$appJson" ]]; then
resolvedAppId=$(jq -r '.appId' <<< "$appJson")
spJson=$(az ad sp show --id "$resolvedAppId" --output json 2>/dev/null)
if [[ -n "$spJson" ]]; then
spObjectId=$(jq -r '.id' <<< "$spJson")
echo "INFO: Resolved app registration objectId '$targetObjectId' to service principal objectId '$spObjectId'" >&2
objectId="$spObjectId"
name=$(jq -r '.displayName' <<< "$spJson")
appId="$resolvedAppId"
upn=null
type="servicePrincipal"
fi
else
# Fall back to user lookup
userJson=$(az ad user show --id "$targetObjectId" --output json 2>/dev/null)
if [[ -n "$userJson" ]]; then
name=$(jq -r '.displayName' <<< "$userJson")
upn=$(jq -r '.userPrincipalName' <<< "$userJson")
appId=null
type="user"
else
name="$targetObjectId"
upn=null
appId=null
fi
fi
fi
else
objectId="$callerObjectId"
pal=$(az rest --uri "https://management.azure.com/providers/microsoft.managementpartner/partners?api-version=2018-02-01" --output json) || error "No Partner Admin Link for this tenant?"
palId=$(jq -r .id <<< $pal)
partnerId=$(jq -r .properties.partnerId <<< $pal)
partnerName=$(jq -r .properties.partnerName <<< $pal)
if [[ "$type" == "user" ]]; then
upn=$(jq -r '.user.name' <<< "$account")
name=$(az ad signed-in-user show --query displayName --output tsv) || error "Stale token issue?"
appId=null
domain=$(jq -r '.tenantDefaultDomain' <<< "$account")
tenantName=$(jq -r '.tenantDisplayName' <<< "$account")
else
appId=$(jq -r '.user.name' <<< "$account")
name=$(az ad sp show --id "$appId" --query displayName --output tsv 2>/dev/null || echo "$appId")
upn=null
domain=null
tenantName=null
fi
[[ "$tenantId" == "$(jq -r .properties.tenantId <<< $pal)" ]] || error "Tenant IDs do not match."
[[ "$objectId" == "$(jq -r .properties.objectId <<< $pal)" ]] || error "Object IDs do not match."
fi
# Build role definition lookup and fetch assignments if requested
assignmentsArray=null
if [[ "$includeAssignments" == "true" ]]; then
tmpfile=$(mktemp)
az role definition list \
--scope "/providers/Microsoft.Management/managementGroups/$tenantId" \
--query '[].{id:name, roleName:roleName, roleType:roleType}' \
-o json > "$tmpfile"
assignments=$(az graph query \
--management-groups "$tenantId" \
--first 1000 \
-q "authorizationresources
| where type =~ 'microsoft.authorization/roleassignments'
| where properties.principalId == '$objectId'
| extend scope = tostring(properties.scope)
| extend principalType = tostring(properties.principalType)
| extend roleDefGuid = tostring(split(properties.roleDefinitionId, '/')[4])
| project scope, principalType, roleDefGuid" \
-o json)
assignmentsArray=$(echo "$assignments" | jq --slurpfile defs "$tmpfile" '
($defs[0] | INDEX(.id)) as $lookup |
[ .data[] | . as $a |
($lookup[$a.roleDefGuid] // {roleName: "unknown", roleType: "unknown"}) as $def |
{
scope: $a.scope,
id: $a.roleDefGuid,
roleName: $def.roleName,
roleType: (if $def.roleType == "BuiltInRole" then "BuiltIn" else "Custom" end)
}
]')
rm "$tmpfile"
fi
jq -n --arg tenantId "$tenantId" --arg tenantName "$tenantName" --arg domain "$domain" \
--arg type "$type" --arg upn "$upn" --arg appId "$appId" --arg objectId "$objectId" --arg name "$name" \
--arg palId "$palId" --arg partnerId "$partnerId" --arg partnerName "$partnerName" \
--argjson assignments "$assignmentsArray" \
'{
"tenantId": $tenantId,
"tenantDisplayName": (if $tenantName == "null" then null else $tenantName end),
"tenantDefaultDomain": (if $domain == "null" then null else $domain end),
"type": $type,
"displayName": $name,
"userPrincipalName": (if $upn == "null" then null else $upn end),
"appId": (if $appId == "null" then null else $appId end),
"objectId": $objectId,
"partnerAdminLink": $palId,
"partnerName": $partnerName,
"partnerId": $partnerId,
"assignments": $assignments
}'
exit
@richeney
Copy link
Author

Note that tenant display name and default domain are not easily available for service principals.

@richeney
Copy link
Author

richeney commented Mar 5, 2026

Added usage section and also direct RBAC role assignments. Thank goodness for GitHub Copilot CLI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment