It always impresses and scares me when I am trying to figure something out and while Googling I come across one of my own blog posts. Impressive, coz “wow I was so smart a few months ago and had figured this out!” but scary coz “shit, I’ve forgotten I ever did this”. 😵💫
Case in hand, yesterday I blogged out the HTTP with Azure AD connector. I had forgotten there was a blog post on it, and while I was looking to do some more stuff with it today I came across two more on my side.
- This one, which is super informative on what else one can use that connector for. Heck, I even figured out how to use it for Forms and other stuff – sure, it was about 9 months ago and I have been busy with other stuff so don’t really remember it, but still…
- And this one, which is quite cool in that I use this connector to make requests to my Logic Apps.
Between these two posts, I think they capture everything I know (or should know) about this connector.
Anyways, a colleague wanted to use the HTTP with Azure AD connector to get reports. The account in question has the Reports Reader role, and he was trying the following URL: https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityDetail(period='D90')
But it failed:
1 2 3 4 5 6 7 8 9 10 11 |
{ "error": { "code": "UnknownError", "message": "{\"error\":{\"code\":\"S2SUnauthorized\",\"message\":\"Invalid permission.\"}}", "innerError": { "date": "2023-08-04T09:25:01", "request-id": "364325b0-65aa-4034-ae12-53f1ac1c4ce3", "client-request-id": "364325b0-65aa-4034-ae12-53f1ac1c4ce3" } } } |
It’s not surprising it failed. The connector has a limited set of scopes:
That is to say, it probably doesn’t have the Reports.Read.All
delegated permission assigned to it, so even though the account can do it the connector can’t. Bummer.
I could go the route of an HTTP connector, and I did spend some time fooling around with that, but eventually gave up. Using an HTTP connector has the draw back that I need to send the username and password as part of the password flow. There are options to authenticate via OAuth 2.0 with the HTTP connector but I couldn’t quite figure out how to make it work with an App Registration I created that gives the delegated Reports.Read.All
permissions. Could be that I was just being thick when figuring it out.
Anyways, using a custom connector is more fun. And reusable. Plus I can setup the connector for others in their environment, without handing over the secret to others. Way more nifty in my opinion.
So here’s what I did.
First, I created an App Registration. Just a standard one, but I added the delegated Reports.Read.All
permission to it and did an admin consent.
Then I created a secret, and noted that.
Next, I went to Power Automate and created a custom connector.
Go to Data > Custom Connectors > New custom connector.
Create from blank. Give it a name.
And description. And change host to graph.microsoft.com.
Then go to the next page, Security.
I want OAuth 2.0 and Azure AD.
Fill in the rest of the details from the App Registration. I left the Tenant ID as common, but filled in the resource URL as https://graph.microsoft.com
(fill that exactly; I know this experience).
Click “Create Connector”.
That should generate a Redirect URI.
I added that to the App Registration.
Go to the next section, Definition.
Add an action.
Update: Later, I changed the above to be like this:
That’s because I realized there’s no way to take an input of the number of days, so I might as well create separate ones.
And a request, for that action.
The URL is https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityDetail
Update: Later, I changed the URL to be https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityDetail(period='D90')
to match the updated name above
Click on Response, add a default response, and I added the following:
1 2 |
Content-Type: text/plain Location: https://reports.office.com/data/download/JDFKdf2_eJXKS034dbc7e0t__XDe |
I got this from the API documentation.
This one’s a bit odd in that the response is the headers.
And that’s it, save/ update the connector.
Now in my Power Automate, I can call this custom connector.
After selecting I can see the action I created.
Save, and test/ run it.
On the face of it, it throws an error.
But that’s misleading, because the headers show the content I want:
So I create a Compose action after this connector, and use the Location as input.
And set it to run even after the connector has failed.
Run it now, and the flow succeeds. 🙂
I downloaded the JSON and added more actions to it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
{ "swagger": "2.0", "info": { "title": "Graph API - Reports", "description": "Gets reports from Graph API", "version": "1.0" }, "host": "graph.microsoft.com", "basePath": "/", "schemes": [ "https" ], "consumes": [], "produces": [], "paths": { "/v1.0/reports/getOffice365GroupsActivityDetail(period='D7')": { "get": { "responses": { "default": { "description": "default", "schema": { "type": "string" }, "headers": { "Content-Type": { "description": "Content-Type", "type": "string" }, "Location": { "description": "Location", "type": "string" } } } }, "summary": "Get O365 Groups Activity Details - 7 days", "description": "Get O365 Groups Activity Details - 7 days", "operationId": "GetOffice365GroupsActivityDetail7", "parameters": [] } }, "/v1.0/reports/getOffice365GroupsActivityDetail(period='D30')": { "get": { "responses": { "default": { "description": "default", "schema": { "type": "string" }, "headers": { "Content-Type": { "description": "Content-Type", "type": "string" }, "Location": { "description": "Location", "type": "string" } } } }, "summary": "Get O365 Groups Activity Details - 30 days", "description": "Get O365 Groups Activity Details - 30 days", "operationId": "GetOffice365GroupsActivityDetail30", "parameters": [] } }, "/v1.0/reports/getOffice365GroupsActivityDetail(period='D90')": { "get": { "responses": { "default": { "description": "default", "schema": {}, "headers": { "Content-Type": { "description": "Content-Type", "type": "string" }, "Location": { "description": "Location", "type": "string" } } } }, "summary": "Get O365 Groups Activity Details - 90 days", "description": "Get O365 Groups Activity Details - 90 days", "operationId": "GetOffice365GroupsActivityDetail90", "parameters": [] } }, "/v1.0/reports/getOffice365GroupsActivityDetail(period='D180')": { "get": { "responses": { "default": { "description": "default", "schema": { "type": "string" }, "headers": { "Content-Type": { "description": "Content-Type", "type": "string" }, "Location": { "description": "Location", "type": "string" } } } }, "summary": "Get O365 Groups Activity Details - 180 days", "description": "Get O365 Groups Activity Details - 180 days", "operationId": "GetOffice365GroupsActivityDetail180", "parameters": [] } } }, "definitions": {}, "parameters": {}, "responses": {}, "securityDefinitions": { "oauth2-auth": { "type": "oauth2", "flow": "accessCode", "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", "tokenUrl": "https://login.windows.net/common/oauth2/authorize", "scopes": { "Reports.Read.All": "Reports.Read.All" } } }, "security": [ { "oauth2-auth": [ "Reports.Read.All" ] } ], "tags": [] } |
These are all the valid “period” options for that call. I wish there was some way to take an input for GET operations. There is, if I switch to POST, but that’s not what I need.
Update: Later in the day, when Googling on something else, I came across this blog post from Microsoft. It’s a good one. And worth noting this point:
Custom connectors are supported by Microsoft Azure API Management infrastructure. When a connection to the underlying API is created, the API Management gateway stores the API credentials or tokens, depending on the type of authentication used, on a per-connection basis in a token store. This solution enables authentication at the connection level.