Directory roles for Azure AD Service Principal

In Azure Active Directory, every user, by default, has permission to read the directory - for example, to list all users in this directory. Using Azure CLI (2.0) we are speaking about command:

az ad user list

But in context of Azure AD Service Principals, the situation is different. SPs does not have permission to read directory. Of course we can change that. But it is not so simple as "click it" in the portal or type a command in CLI. We need to make some calls to the Azure AD Graph API. We will need two things:

  • Azure AD User account with directory administrator privileges
  • A new SP for test

I'm assuming that You have a directory privileges on Your tenant. I will show you how does it work on my company's tenant (free-media.eu). When I use the Azure CLI as a user (not SP), I will have to use interactive login method because of MFA enabled on my account.

As first, I will log-in to my tenant:

az login

and create Service Principal (SP) we will use later:

az ad sp create-for-rbac -n "lnx" --role contributor --scopes /subscriptions/ssssssss-ssss-ssss-ssss-ssssssssssss

I'm creating a SP with Contributor role in my subscription scope, where my subscription's id is ssssssss-ssss-ssss-ssss-ssssssssssss (of course it is just obfuscated version of id - I will also obfuscate all other ID's below).

After SP creation Azure CLI is giving me few precious information which I will use later so write them down:

{
  "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "displayName": "lnx",
  "name": "http://lnx",
  "password": "pppppppp-pppp-pppp-pppp-pppppppppppp",
  "tenant": "tttttttt-tttt-tttt-tttt-tttttttttttt"
}

Let's try to logout:

az logout

and log-in as newly created SP:

az login --service-principal --tenant tttttttt-tttt-tttt-tttt-tttttttttttt --username xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --password pppppppp-pppp-pppp-pppp-pppppppppppp

As you see, I have logged-in to my tenant and I see my subscription.

[
  {
    "cloudName": "AzureCloud",
    "id": "ssssssss-ssss-ssss-ssss-ssssssssssss",
    "isDefault": true,
    "name": "Subscription Name",
    "state": "Enabled",
    "tenantId": "tttttttt-tttt-tttt-tttt-tttttttttttt",
    "user": {
      "name": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "type": "servicePrincipal"
    }
  }
]

Let's try to list users in my directory:

az ad user list

As you see, it is not possible. Our SP is having insufficient privileges to complete this operation.

GraphErrorException: Insufficient privileges to complete the operation.

So, let's log-in as directory administrator:

az logout
az login

and check if we are able to list users in the directory:

az ad user list

The output depends on how many users you have in your directory. Below you can find a sample output of one of the users from my directory:

{
    "displayName": "tester",
    "mail": null,
    "mailNickname": "tester",
    "objectId": "oooooooo-oooo-oooo-oooo-oooooooooooo",
    "objectType": "User",
    "signInName": null,
    "usageLocation": null,
    "userPrincipalName": "tester@free-media.eu"
  }

Now, you need to go to Azure AD Graph API reference and save it as a bookmark :) Probably you will use it frequently...

https://msdn.microsoft.com/en-us/library/azure/ad/graph/api/api-catalog

Today the interesting part for us is:

https://msdn.microsoft.com/en-us/library/azure/ad/graph/api/directoryroles-operations

We are not going to make API calls from Azure CLI because there are no methods for that yet. We will also not use cURL because of complicated authentication. You can use tools like Postman if you want (it is able to use oAuth2 authentication), but there is a tool, created especially for that purposes by Microsoft:

https://graphexplorer.azurewebsites.net

We will start from listing all Directory Roles in our directory:

GET

https://graph.windows.net/free-media.eu/directoryRoles?api-version=1.6

(you need to change free-media.eu to your tenant name)

The output depends on how many directory roles you have enabled on your AAD tenant. Role that we are interested in, is "Directory Readers". Find it in the call output, and write down the objectId of this role (rrrrrrrr-rrrr-rrrr-rrrr-rrrrrrrrrrrr)

{
    "odata.metadata": "https://graph.windows.net/free-media.eu/$metadata#directoryObjects/Microsoft.DirectoryServices.DirectoryRole",
    "value": [
        {
            "odata.type": "Microsoft.DirectoryServices.DirectoryRole",
            "objectType": "Role",
            "objectId": "rrrrrrrr-rrrr-rrrr-rrrr-rrrrrrrrrrrr",
            "deletionTimestamp": null,
            "description": "Allows access to various read only tasks in the directory. ",
            "displayName": "Directory Readers",
            "isSystem": true,
            "roleDisabled": false,
            "roleTemplateId": "########-####-####-####-############"
        }
    ]
}

Now, we will check who is having this role in our directory.

GET

https://graph.windows.net/free-media.eu/directoryRoles/rrrrrrrr-rrrr-rrrr-rrrr-rrrrrrrrrrrr/$links/members?api-version=1.6

The output length depends on many factors. In my directory there are few SPs with "Directory Reader" role - for example PowerBI SP, Yammer SP etc. What's important, that there is no newly created SP (lnx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) there.

{
    "odata.metadata": "https://graph.windows.net/free-media.eu/$metadata#directoryObjects/$links/members",
    "value": [
        {
            "url": "https://graph.windows.net/free-media.eu/directoryObjects/11111111-1111-1111-1111-111111111111/Microsoft.DirectoryServices.ServicePrincipal"
        },
        {
            "url": "https://graph.windows.net/free-media.eu/directoryObjects/22222222-2222-2222-2222-222222222222/Microsoft.DirectoryServices.ServicePrincipal"
        },
        {
            "url": "https://graph.windows.net/free-media.eu/directoryObjects/33333333-3333-3333-3333-333333333333/Microsoft.DirectoryServices.ServicePrincipal"
        }
    ]
}

As you see, every role member on my list is a SP. You can find your list empty - then my example list should be helpful.
We need to add our newly created SP to that list. To do that, we need an oData object URL of our SP in the directory. It is not so complicated to obtain it. Let's use Azure CLI (as directory administrator - it will be much simpler than using AAD Graph Explorer):

az ad sp list --query "[?contains(displayName, 'lnx')]"

where "lnx" is my SP's name. The output should be as follows:

[
  {
    "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "displayName": "lnx",
    "objectId": "OOOOOOOO-OOOO-OOOO-OOOO-OOOOOOOOOOOO",
    "objectType": "ServicePrincipal",
    "servicePrincipalNames": [
      "http://lnx",
      "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    ]
  }
]

What we want to remember from the output is objectId - for me it will be: OOOOOOOO-OOOO-OOOO-OOOO-OOOOOOOOOOOO. Having that, we can build a SP URL:

https://graph.windows.net/free-media.eu/directoryObjects/ + OOOOOOOO-OOOO-OOOO-OOOO-OOOOOOOOOOOO + /Microsoft.DirectoryServices.ServicePrincipal?api-version=1.6

Let's check if it is working - use this URL in AAD Graph Explorer:

GET

https://graph.windows.net/free-media.eu/directoryObjects/OOOOOOOO-OOOO-OOOO-OOOO-OOOOOOOOOOOO/Microsoft.DirectoryServices.ServicePrincipal?api-version=1.6

As an output you should get a manifest of our SP (the same that you can find in Azure Portal in SP settings):

{
    "odata.metadata": "https://graph.windows.net/free-media.eu/$metadata#directoryObjects/Microsoft.DirectoryServices.ServicePrincipal/@Element",
    "odata.type": "Microsoft.DirectoryServices.ServicePrincipal",
    "objectType": "ServicePrincipal",
    "objectId": "OOOOOOOO-OOOO-OOOO-OOOO-OOOOOOOOOOOO",
    "deletionTimestamp": null,
    "accountEnabled": true,
    "addIns": [],
    "alternativeNames": [],
    "appDisplayName": "lnx",
    "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "appOwnerTenantId": "tttttttt-tttt-tttt-tttt-tttttttttttt",
    "appRoleAssignmentRequired": false,
    "appRoles": [],
    "displayName": "lnx",
    "errorUrl": null,
    "homepage": "http://lnx",
    "keyCredentials": [],
    "logoutUrl": null,
    "oauth2Permissions": [
        {
            "adminConsentDescription": "Allow the application to access lnx on behalf of the signed-in user.",
            "adminConsentDisplayName": "Access lnx",
            "id": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
            "isEnabled": true,
            "type": "User",
            "userConsentDescription": "Allow the application to access lnx on your behalf.",
            "userConsentDisplayName": "Access lnx",
            "value": "user_impersonation"
        }
    ],
    "passwordCredentials": [],
    "preferredTokenSigningKeyThumbprint": null,
    "publisherName": "Fundacja Aegis",
    "replyUrls": [],
    "samlMetadataUrl": null,
    "servicePrincipalNames": [
        "http://lnx",
        "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    ],
    "servicePrincipalType": "Application",
    "tags": [],
    "tokenEncryptionKeyId": null
}

So... we are ready to add this SP as a member of "Directory Readers" role. To do that, we need to perform an API POST call to https://graph.windows.net/free-media.eu/directoryRoles/rrrrrrrr-rrrr-rrrr-rrrr-rrrrrrrrrrrr/$links/members?api-version=1.6 method (the same we are using for role members listing):

POST

https://graph.windows.net/free-media.eu/directoryRoles/rrrrrrrr-rrrr-rrrr-rrrr-rrrrrrrrrrrr/$links/members?api-version=1.6

in the body of that PPOST call, there should be a simple JSON object with an URL of the SP (there is Content-Type: application/json header there of course - it is important if you are using any different tool for that):

{
    "url": "https://graph.windows.net/free-media.eu/directoryObjects/OOOOOOOO-OOOO-OOOO-OOOO-OOOOOOOOOOOO/Microsoft.DirectoryServices.ServicePrincipal"
}

There will be no response body after that call - just a 204 response code (No Content. Indicates success. No response body is returned.)

Let's check if our SP is on the list now:

GET

https://graph.windows.net/free-media.eu/directoryRoles/rrrrrrrr-rrrr-rrrr-rrrr-rrrrrrrrrrrr/$links/members?api-version=1.6

As you see, it works:

{
    "odata.metadata": "https://graph.windows.net/free-media.eu/$metadata#directoryObjects/$links/members",
    "value": [
        {
            "url": "https://graph.windows.net/free-media.eu/directoryObjects/11111111-1111-1111-1111-111111111111/Microsoft.DirectoryServices.ServicePrincipal"
        },
        {
            "url": "https://graph.windows.net/free-media.eu/directoryObjects/22222222-2222-2222-2222-222222222222/Microsoft.DirectoryServices.ServicePrincipal"
        },
        {
            "url": "https://graph.windows.net/free-media.eu/directoryObjects/33333333-3333-3333-3333-333333333333/Microsoft.DirectoryServices.ServicePrincipal"
        },
        {
            "url": "https://graph.windows.net/free-media.eu/directoryObjects/OOOOOOOO-OOOO-OOOO-OOOO-OOOOOOOOOOOO/Microsoft.DirectoryServices.ServicePrincipal"
        }
    ]
}

Right now we only need to login as our SP and check if the role is working and our SP is able to list the users from the directory. Log-out first (Azure CLI):

az logout

and login as SP:

az login --service-principal --tenant tttttttt-tttt-tttt-tttt-tttttttttttt --username xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --password pppppppp-pppp-pppp-pppp-pppppppppppp

List the users in the directory:

az ad user list

And... we have the same output as an user.

{
    "displayName": "tester",
    "mail": null,
    "mailNickname": "tester",
    "objectId": "oooooooo-oooo-oooo-oooo-oooooooooooo",
    "objectType": "User",
    "signInName": null,
    "usageLocation": null,
    "userPrincipalName": "tester@free-media.eu"
  }

Probably you want to (as I do) have those methods in Azure CLI. There is a place on GitHub, where API support in Azure CLI can be requested: https://github.com/Azure/azure-rest-api-specs. I have created an issue in this repository for case described in this post: https://github.com/Azure/azure-rest-api-specs/issues/2048. You can comment on it and help to push on this request.

The case described here is important for many scenarios in Azure. I will describe some of them in next few days.

comments powered by Disqus