Handing a team Contributor on the resource group is the default and it is wrong twice over: it gives them the ability to regenerate your keys, and — counter-intuitively — it does not give them the ability to call the model with their own identity.
The four roles
Microsoft documents four built-in roles relevant to an Azure OpenAI resource. The names still say Cognitive Services; the strings are what the API accepts.
- Cognitive Services OpenAI User — the data-plane consumer.
- Cognitive Services OpenAI Contributor — the above, plus deployments and fine-tuning.
- Cognitive Services Contributor — the control-plane role: create resources, read keys.
- Cognitive Services Usages Reader — quota visibility, and nothing else.
One structural warning first, because it defeats everything below: subscription-level Owner and Contributor roles are inherited and take priority over roles applied at the resource group level. Scoping carefully at the resource while leaving somebody subscription Contributor achieves nothing. Microsoft, Role-based access control for Azure OpenAI.
What the User role actually allows
This is the role most application identities should have, and it is narrower than it sounds. With only Cognitive Services OpenAI User, a principal can:
- View the resource in the Azure portal, and its endpoint under Keys and Endpoint.
- View the resource and its model deployments in the Foundry portal.
- Use the Chat, Completions and image playground experiences against already-deployed models.
- Make inference API calls with Microsoft Entra ID.
And explicitly cannot:
- View, copy or regenerate keys under Keys and Endpoint. It can see the endpoint; it cannot see the key. That distinction is the whole reason to prefer this role for a human.
- Create new model deployments or edit existing ones.
- Create or deploy custom fine-tuned models, or upload fine-tuning datasets.
- View, query or filter stored completions data — worth noting where prompts are sensitive.
- Access quota.
- Create customised guardrails.
Quota being absent from every one of these roles is deliberate. Microsoft documents quota visibility as belonging to Cognitive Services Usages Reader, which must be applied at the subscription level and does not exist at the resource level. So the answer to “why can this engineer not see how much TPM is left” is almost always that nobody has assigned that role, and it cannot be assigned where they were looking.
The inversion nobody expects
Read the documented matrix carefully and one row does not follow the pattern. Cognitive Services Contributor — the broadest of the three — can create resources, can view, copy and regenerate keys, can create deployments, can create guardrails, and cannot make inference API calls with Microsoft Entra ID.
Control-plane authority and data-plane authority are separate grants, and this role has only the first. The practical consequence is a confusing support ticket: an administrator with Cognitive Services Contributor who can deploy the model, can read the key, can use the playground — and whose DefaultAzureCredential-based script gets a 401. Nothing is broken. They need Cognitive Services OpenAI User in addition, and the two roles compose exactly as you would hope.
The same logic gives you the sensible default assignments: application identities get OpenAI User and no key at all; the platform team gets Cognitive Services Contributor for lifecycle work; whoever manages capacity gets Usages Reader at the subscription; and OpenAI Contributor goes only to people who genuinely create deployments or fine-tune.
Roles are meant to be combined
Microsoft documents Usages Reader as providing “little value by itself”, and the useful assignments are pairs. The documented combinations:
- Usages Reader + OpenAI User — everything the User role does, plus the ability to view quota allocations in the Foundry portal. This is the right pair for an engineer who owns a workload but does not own capacity.
- Usages Reader + OpenAI Contributor — the same visibility, plus deployments and fine-tuning.
- Usages Reader + Cognitive Services Contributor — the only combination that can view and edit quota allocations in the Foundry portal, and create or edit model deployments through it. Note that model deployment via the Foundry portal is documented as partially dependent on the presence of the Usages Reader role, so a Contributor who can deploy through the API and not through the portal is not seeing a bug.
When a combination does not behave as the summary table implies, read the role definition rather than the article. In the portal, go to the resource, then Access control (IAM), then Roles, then View under the details column for the role. Microsoft is explicit that you must examine both Actions and DataActions to understand a role’s full scope — the Actions list is selected by default, and the data-plane grant that decides whether inference works lives in the other one. That single detail explains most of the confusion on this page.
Two adjacent permission failures show up as Azure OpenAI problems and are not. Both are documented, and both need permission at subscription scope rather than on the resource:
- Search indexes never load. Adding an Azure AI Search data source in the playground spins forever because listing search services calls the ARM
Microsoft.Search/searchServicesprovider endpoint, which needs a subscription-level role — Reader is enough, or one of the search-specific roles. - File upload for “on your data” fails. The portal calls
listAccountSason the storage account, so the missing permission isMicrosoft.Storage/storageAccounts/listAccountSas/action. No Azure OpenAI role grants it.
Assigning them
-
Get the scope. Assign at the resource, not the resource group, unless you have a reason.
AOAI_ID=$(az cognitiveservices account show \ --resource-group rg-model --name my-aoai --query id -o tsv) -
Assign the data-plane role to the application’s managed identity.
az role assignment create \ --role "Cognitive Services OpenAI User" \ --assignee-object-id <principal-id> \ --assignee-principal-type ServicePrincipal \ --scope $AOAI_ID -
Assign quota visibility separately, at the subscription.
SUB_ID=$(az account show --query id -o tsv) az role assignment create \ --role "Cognitive Services Usages Reader" \ --assignee-object-id <principal-id> \ --assignee-principal-type User \ --scope "/subscriptions/$SUB_ID" Then remove the reason keys exist. With Entra authentication working, disable local key authentication on the account so a leaked key is not a path back in, and verify nothing in your estate still depends on one before you do.
Use --assignee-object-id with an explicit --assignee-principal-type rather than --assignee. The latter performs a Graph lookup that a service principal running your pipeline often lacks permission to do, and the error it produces looks like a role problem rather than a directory one.
Calling the API with a token
With the role assigned, the client swaps the key for a token provider. The token audience is https://cognitiveservices.azure.com/.default:
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from openai import AzureOpenAI
token_provider = get_bearer_token_provider(
DefaultAzureCredential(),
"https://cognitiveservices.azure.com/.default",
)
client = AzureOpenAI(
azure_endpoint="https://my-aoai.openai.azure.com/",
azure_ad_token_provider=token_provider,
api_version="2024-10-21",
)
response = client.chat.completions.create(
model="gpt-4o-prod", # deployment name
messages=[{"role": "user", "content": "Hello"}],
)
The provider is a callable, not a token — the SDK invokes it per request, so refresh is handled for you and a long-running worker does not start returning 401s an hour after it started. Passing a token string instead is the common mistake and it works for exactly one token lifetime.
Role names and their documented capability lists change; Microsoft has amended this matrix more than once, and the article records which capabilities were added in which release. Check the current table before designing a permission model around a single row.
Top comments (0)