Microsoft Graph API duplicate users - microsoft-graph-api

I’m calling the Microsoft graph restful api in powershell to receive users from my organisation and all works splendidly when i want to use a full import. But when I try /users/delta I receive a large number of duplicates.
In my test environment it’s not an issue (the script runs for a couple of seconds long for a thousand users) but in the production environment of 70k users it returns 160k records. I need to use delta so that I can store the delta token somewhere and for future delta not require reading all 70k users again needlessly.
I have tried adding /users/delta?$Filter=&$distinct(UserPrincipalName) also with id and it still returns the duplicate users. Does anyone have a clue about how I can filter out the duplicates in the api call?
$Version = ‘Beta’
$BatchSize = 500
$Uri = "https://graph.microsoft.com/$Version/Users/delta?top=$BatchSize"
$users = $null
do{#read all pages into a single array
try
{ #Get api data
$response2 = Invoke-RestMethod -Method Get -Uri $Uri -Headers $Global:AuthHeader
}
catch
{
Log -ExtraLogging $Global:Debug -LoggingLevel 1 -ErrorCode 2 -Message "$($_.Exception). terminating run profile"
exit
}
if($response2 -ne $null) #if multiple pages read all pages at once
{
$Uri = $response2.'#odata.nextlink'
$users = $users + $response2.value
}
}until ($Uri -eq $null -or $Uri -contains 'Error')

How are you returning all the users? since there is a 999 return limit per page for the users graph endpoint. I have a feeling that your own code is causing duplicates and not the graph endpoint. I tried running the command on a huge tenant and I don't get duplicates at all, but then again it only shows max 999 at a time. then you have to use the nextlink to get the next batch of users. most likely somewhere in your powershell logic you ended up returning the same thing twice somewhere.
But you would have to give more details about your script if you want more help.

Related

How can I efficiently list all work item type states via ADO REST API?

I am trying to list all work item type states for an organisation (visible to the authenticated user) via REST API. It seemed more efficient to list all processes (https://learn.microsoft.com/en-us/rest/api/azure/devops/core/processes/list?view=azure-devops-rest-4.1) and then use the endpoint to list all work item types of those processes together with the states (https://learn.microsoft.com/en-us/rest/api/azure/devops/processes/work-item-types/list?view=azure-devops-rest-4.1&tabs=HTTP). However, I am missing some custom states in the response.
When I list all projects (https://learn.microsoft.com/en-us/rest/api/azure/devops/core/projects/list?view=azure-devops-rest-4.1&tabs=HTTP), then all work item types of those projects (https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-item-types/list?view=azure-devops-rest-4.1&tabs=HTTP) and then all states of those types (https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-item-type-states/list?view=azure-devops-rest-4.1&tabs=HTTP), there is everything. But that is sooo many requests.
Can someone explain, why is the first approach not working? Every projet should be associated with a process. Or not? Do you know a better way to get all those states as efficiently as possible?
Thanks in advance. :)
As we can see from the official documentation : Work Item Type States - List, it's in project level not the organization level. So, we need to get the states by project scope.
GET https://dev.azure.com/{organization}/{project}/_apis/wit/workitemtypes/{type}/states?api-version=4.1-preview.1
We can write a script to retrieve the projects and work item types in a loop, then get the states of each work item type.
UPDATE:
We can use States - List REST API to return a list of all state definitions in a work item type of the process.
Below PowerShell script for your reference to return the states from a specific process:
Param(
[string]$orgurl = "https://dev.azure.com/{org}",
[string]$processname = "Your-Process-Name",
[string]$user = "",
[string]$token = "PAT"
)
# Base64-encodes the Personal Access Token (PAT) appropriately
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user,$token)))
#Get Process ID
$processesurl = "$orgurl/_apis/process/processes?api-version=6.0"
$processes = (Invoke-RestMethod -Uri $processesurl -Method Get -Headers #{Authorization=("Basic {0}" -f $base64AuthInfo)}).value | where {$_.name -eq $processname }
$processesid = $processes.id
#List Work item types and witRefName
$witsurl = "$orgurl/_apis/work/processes/$processesid/workitemtypes?api-version=6.0"
$witRefNames = (Invoke-RestMethod -Uri $witsurl -Method Get -Headers #{Authorization=("Basic {0}" -f $base64AuthInfo)}).value.referenceName #| where {$_.name -eq $processname }
#List WIT states
foreach ($witRefName in $witRefNames){
$statesurl = "$orgurl/_apis/work/processes/$processesid/workItemTypes/$witRefName/states?api-version=6.0"
$states = (Invoke-RestMethod -Uri $statesurl -Method Get -Headers #{Authorization=("Basic {0}" -f $base64AuthInfo)}).value.name
Write-Host "==================================================================================="
Write-Host "$witRefName - States:" $states
Write-Host "==================================================================================="
echo `n
}
If you want to retrieve all states from all processes, then you can get the processes and loop them in the script.

How do you (quickly) count AAD B2C users?

We're trying to count the number of user objects in a B2C tenant, which is somewhat large. When it was small, the simple/obvious hack of just reading all the users worked easily and quickly.
Get-AzADUser | Measure-Object
Now this takes an absurd amount of time (30+ mins, and wastes AAD processing, network bandwidth, etc). Handily, the Graph API includes an endpoint to request the number of objects! Hooray! ;) https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-beta&tabs=http#example-6-get-only-a-count-of-users
Connect-AzAccount
Set-AzContext -Tenant <your 'normal' AAD tenant>
$AzToken = Get-AzAccessToken -ResourceUrl https://graph.microsoft.com
Invoke-RestMethod -Method Get -Authentication Bearer -Token (ConvertTo-SecureString -AsPlainText -Force -String $AZAccess.Token) -Headers #{ConsistencyLevel = 'eventual'} -Uri https://graph.microsoft.com/beta/users/`$count
1234
But! When using this method to attempt to find how many B2C accounts we have:
Connect-AzAccount
Set-AzContext -Tenant <your 'B2C' AAD tenant>
$AzToken = Get-AzAccessToken -ResourceUrl https://graph.microsoft.com
Invoke-RestMethod -Method Get -Authentication Bearer -Token (ConvertTo-SecureString -AsPlainText -Force -String $AZAccess.Token) -Headers #{ConsistencyLevel = 'eventual'} -Uri https://graph.microsoft.com/beta/users/`$count
Invoke-RestMethod: {"error":{"code":"Request_BadRequest","message":"$count is not currently supported.","innerError":{"date":"2021-04-29T07:06:09","request-id":"xxx","client-request-id":"xxx"}}}
So, how do you count users in a large B2C tenant?
Unfortunately the endpoint is not supported yet for B2C tenant. Neither does https://graph.microsoft.com/beta/users?$count=true.
It's only available in normal AAD tenant.
Currently you need to list all the users and get their count using method/function from other library.
A similar post and answer here for your reference.
This is a bit manual, but whenever I need a quick count, I do the following:
Go to Azure Portal (for B2C Tenant)
All Users
Bulk operations
Download users (This will get me an excel pretty quick)
Start
Takes about 2 minutes to finish (~40k users)
Go to Bulk operation results in page menu on the left
Download Excel
sum of UPNs in the excel
This should work for a B2C Tenant , using Connect-AzureAD in the AzureAD Module
Connect-AzureAD -tenant xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
(Get-AzureADUser -all $true).Count

How to modify BoardColumn field of a Work Item using REST API

I have a customized Board with:
First default column "New" mapped to "New" state.
Second column "To do" also mapped to "New" state (After weekly reviewing new WI, team moves them in this column to avoid reviewing them next time).
In the TFS Board I can move a WI from the First Column to the second columun.
With the REST API when I read an existing WI, I got the right information for the BoardColumn field.
But when I used the REST API to modify BoardColumn it raises an exception.
I have the bypass rules permission.
$tfsTargetUri = "https://path to my collection/"
$tfsTargetProject = "MyProject"
$MyWI = 56 #use an existing Id
$mycredentials = Get-Credential
$workitem2 =
#(
#{op="test";path="/rev";value="1"},
#{op="add";path="/fields/System.BoardColumn";value="New"}
##{op="add";path="/fields/System.State";value="Active"}
)
$json2 = $workitem2 | ConvertTo-Json -Depth 100
$url2= $tfsTargetUri + $tfsTargetProject + '/_apis/wit/workitems/' + $MyWI +'?bypassRules=true&api-version=2.0'
$targetbug = Invoke-RestMethod -Uri $url2 -Method Patch -Credential $mycredentials -Body ([System.Text.Encoding]::UTF8.GetBytes($json2)) -ContentType 'application/json-patch+json'
When I try with BoardColumn in workitem2 it raises an exception.
When I modify the comment in workitem2 to change the State field, this is working.
Any idea?
The field System.BoardColumn is read only by design, is not a regular work item rule, so bypass = true can helps in this case. This is the reason why you can not update this field with the regular way.
According to new Microsoft Docs you provided (the relevant section added 3 days ago) there is a solution. You can update the field if you update another field value - if you get the work item and investigate the fields you will see this kind of field:
WEF_432678B52358ACDA34ASDA243489FD343_Kanban.Column
When you update this field to the Board Column State the work item will move to this Board Column.
Example how to extract this field from work item details:
$url = $collection/_apis/wit/workitems/$id?api-version=4.0
$workItem = Invoke-RestMethod -Uri $url -Method Get ...
$boardColumnField = $workItem.fields.PSObject.Properties.Name.Where({$_.Contains("Kanban")})[0]
# Now in the work item json use it: /fields/$boardColumnFied
Here are the settings that worked for me when using Postman:

Error 400 Bad Request when attempting to pull Skype for Business activity via Graph API

Alright, so after chatting with Microsoft "Professional Support" they couldn't help me. Told me to access the reports via the SfB Report GUI and export to Excel.
Yeah that's completely worthless to me.
After doing a lot more reading, my 1st example to authenticate was out of date.
Now I have a fully functional oAuth2 PowerShell script that gets me a valid token.
But I'm having the same issue when I use the Graph Explorer (403 Forbidden).
I know the token is working because I can query other information \ if I take away the VAR for the token from the GET header it errors stating the the Bearer Token is empty so everything is right.
Microsoft, if you're out there can someone please confirm that the SfB Report API is up and running for the statistics I'm attempting to pull?
UPDATED SCRIPT
#Obtaining oAuth2 Token from Microsoft Azure \ communicate with Microsoft Graph API
$ClientID = "MyID"
$client_Secret = "MySecretKey"
#Define URI for Azure Tenant
$Uri = "https://login.microsoftonline.com/MyTenantID/oauth2/token"
#Define Microsoft Graph - Skype for Business reports interface URI
$exportSfB = "https://graph.microsoft.com/beta/reports/SfbActivity(view='Detail',date='2017-04-11')/content"
#Define the body of the request
$Body = #{grant_type='client_credentials';client_id=$ClientID;client_secret=$client_secret;resource='https://graph.microsoft.com'}
#Define the content type for the request
$ContentType = "application/x-www-form-urlencoded"
#Invoke REST method. This handles the deserialization of the JSON result. Less effort than invoke-webrequest
$admAuth=Invoke-RestMethod -Uri $Uri -Body $Body -Method Post
#Construct the header value with the access_token just recieved
$HeaderValue = "Bearer " + $admauth.access_token
#Query Microsoft Graph with new Token
$result = Invoke-RestMethod -Headers #{Authorization=$HeaderValue} -Uri $exportSfB –Method Get -Verbose
#Results should be a .CSV
$result.string.'#text'
ORIGINAL THREAD
Can someone please have a look at this REST code and tell me what they think?
I'm receiving:
Invoke-RestMethod : The remote server returned an error: (400) Bad Request.
CategoryInfo : InvalidOperation
I haven't been able to pull a single Skype for Business activity report using Graph.
$tenant = "MyTenant.onmicrosoft.com"
function GetAuthToken
{
param
(
[Parameter(Mandatory=$true)]
$TenantName
)
$adal = "${env:ProgramFiles(x86)}\Microsoft SDKs\Azure\PowerShell\ServiceManagement\Azure\Services\Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
$adalforms = "${env:ProgramFiles(x86)}\Microsoft SDKs\Azure\PowerShell\ServiceManagement\Azure\Services\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll"
[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null
[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null
$clientId = "MyID"
$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
$resourceAppIdURI = "https://graph.microsoft.com/"
$authority = "https://login.windows.net/$TenantName"
$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
$authResult = $authContext.AcquireToken($resourceAppIdURI, $clientId,$redirectUri, "Auto")
return $authResult
}
# Set Token var
$token = GetAuthToken -TenantName $tenant
# Building Rest Api header with authorization token
$authHeader = #{
'Content-Type'='application\json'
'Authorization'=$token.CreateAuthorizationHeader()
}
$uri = "https://graph.microsoft.com/beta/reports/SfbActivity(view='Detail',date='2017-04-11')"
$output = (Invoke-RestMethod -Uri $uri –Headers $authHeader –Method Get –Verbose).value
You're missing /content in your URI. See doc here.
$uri = "https://graph.microsoft.com/beta/reports/SfbActivity(view='Detail',date='2017-04-11')/content"
Ok so after messing with the API and reading I decided to start over from scratch.
Turns out the problem the entire time was the App type in Azure. I was using an app that someone had created prior to me taking on the project. It's application type was set to "Web \ API" in Azure which was incorrect apparently. I created a new App with application type "Native", altered my code with the new secret access key and application client ID and I was able to start pulling down .csv files. The problem now is that the PSTN data isn't there. I created a ticket with the Microsoft Graph group on GitHub requesting information about whether or not they're aware of this problem. At this time, I'm unable to find a programmatic method of exporting PSTN call details via the API. You can keep an eye on their progress here for that request but beyond that I'm stuck until Microsoft Dev makes the SfBActivity detail report include those data fields: https://github.com/microsoftgraph/microsoft-graph-docs/issues/1133

Retrieving Data in a While Loop from the Youtube API

I do have a hughe database where some data sets link to certain youtube videos. As we all know some youtube videos disappear after a while from youtube and this leads to my solution and my problem as well --> I'd like to check if the youtube video still exists by simply checking via JSON if there is data to retrieve from a video. If not than I'd simply delete that certain data set.
So the first part of my solution would be to go through each row of my data table and checking for each id if there is data to retrieve from youtube as seen in the following code:
$result = $db->query("SELECT id, link FROM songs");
while($row = $result->fetch_assoc())
{
$number = 1+$rown++;
$id = $row['id'];
$link = $row['link'];
$video_ID = $link;
$JSON = file_get_contents("https://gdata.youtube.com/feeds/api/videos/{$video_ID}?v=2&alt=json");
$JSON_Data = json_decode($JSON);
$views = $JSON_Data->{'entry'}->{'yt$statistics'}->{'viewCount'};
echo $number .' row<br />';
echo $link .' link<br />';
echo $views .' views<br /><br />';
}
This attempt works fine and outputs me the data I need. The only problem is, that it just gets me data from the first 150-190 rows and that's it. Now I am checking for a solution that checks each row for empty youtube data and this lead to two concrete questions I have:
1st) Might youtube be responsible for that due to a restriction in retrieving data from one single query?
2nd) Might this be a server issue of mine that stops queries after x-seconds (but I already expand the time limit by putting a line set_time_limit (10000000); into my php code but without success)?
Hope you can help, thanks in advance.
YouTube, naturally, enforces limits on how many requests you can make per period of time. Unfortunately, there are no clear guidelines on what those limits are ... for v2, the guidelines merely state:
The YouTube API enforces quotas to prevent problems associated with
irregular API usage. Specifically, a too_many_recent_calls error
indicates that the API servers have received too many calls from the
same caller in a short amount of time. If you receive this type of
error, then we recommend that you wait a few minutes and then try your
request again.
If time isn't an issue for you, you could slow down each query so that you only make 1 request per every 10-15 seconds or so. Alternatively, you'd probably have better luck batch processing. With this, you can make up to 50 requests at once (this counts as 50 requests against your overall request per day quota, but only as one against your per time quota). Batch processing with v2 of the API is a little involved, as you make a POST request to a batch endpoint first, and then based on those results you can send in the multiple requests. Here's the documentation:
https://developers.google.com/youtube/2.0/developers_guide_protocol?hl=en#Batch_processing
Batch processing is much easier with v3, as you just have the videoId parameter be a comma delimited list of the videos you want info on -- so in your case, you'd execute file_get_contents on a URL like this:
https://www.googleapis.com/youtube/v3/videos?part=id&id={comma-separated-list-of-IDs}&maxResults=50&key={YOUR_API_KEY}
Any video ID in your list that doesn't come back in the JSON response doesn't exist anymore. IF you do 50 at a time, wait for 15 seconds, do another 50, etc., that should give you better performance.

Resources