Cross-Tenant Sync - API/Graph

Konfiguration und Überwachen per Browser unter https://portal.azure.com ist einfach und bei Fehlern eine Mail zu erhalten, ist auch kein Enterprise Monitoring Diese Seite beschäftigt sich dem Zugriff per Graph und anderen APIs zum Cross-Tenant Sync.

Verfügbare APIs

Da "Cross-Tenant Sync" im Juni 2023 noch ein sehr junges Produkt ist, und ich auf die Schnelle erst einmal keine API gefunden habe, bin ich auf https://portal.azure.com gegangen und habe mir angeschaut, was der Browser denn im Hintergrund so macht. Quasi alle Browseroberflächen nutzen heute ein clientbasiertes Rendering mit JavaScript und rufen Daten von REST-Services ab. Über den F12-Debugger im Browser kann ich gut verfolgen, welche Schnittstellen angesprochen werden.

Wenn ich im Browser z.B. den Dirsync stoppe, starte oder einen Restart anstoße, dann sehe ich direkt HTTP POST-Requests auf folgende URLs:

https://graph.microsoft.com/beta/servicePrincipals/<guid>/synchronization/jobs/Azure2Azure.eef62a097718406382dbd7582dc8916f.<guid>/start
https://graph.microsoft.com/beta/servicePrincipals/<guid>/synchronization/jobs/Azure2Azure.eef62a097718406382dbd7582dc8916f.<guid>/stop
https://graph.microsoft.com/beta/servicePrincipals/<guid>/synchronization/jobs/Azure2Azure.eef62a097718406382dbd7582dc8916f.<guid>/restart

Damit ist schon mal klar dass Microsoft hier mit Graph und servicePrincipals arbeitet. Aber das ist nicht alles, denn ich habe immer noch API-Aufrufe gegen das Portal selbst gesehen. Microsoft selbst dokumentiert insbesondere die "B2B-Connec"-Einstellungen sehr gut. Das mag auch daran liegen, dass in der Beta-Phase einfach noch keine GUI da war und die frühen Piloten zwangsweise per PowerShell die gewünschten Einstellungen für Teams Shared Channel gesetzt werden mussten. Aber auch eine Abfrage des Audit Log per Azure-Portal hat letztlich nur folgenden Aufruf gestartet:

https://graph.microsoft.com/v1.0/auditLogs/provisioning?
    $filter=(
              (        contains(tolower(sourceIdentity/id),%20%27aduser%27)
               %20or%20contains(tolower(sourceIdentity/displayName),%20%27aduser%27)
               %20or%20contains(tolower(targetIdentity/id),%20%27aduser%27)
               %20or%20contains(tolower(targetIdentity/displayName),%20%27aduser%27)
              )
            %20and%20(        contains(tolower(servicePrincipal/id),%20%27804f3a94-2255-42a4-98de-c012d5574488%27)
                      %20or%20contains(tolower(servicePrincipal/displayName),%20%27804f3a94-2255-42a4-98de-c012d5574488%27)
              )
            %20and%20activityDateTime%20gt%202023-06-08T14:28:11.405Z
            %20and%20activityDateTime%20lt%202023-06-09T14:28:11.405Z
            )
           &$top=500
           &$orderby=activityDateTime%20desc

Statusabfrage

Auch wenn ich bislang alle Einstellungen per Browser im Azure-Portal verwalten konnte, sollte ich schon wissen, welche APIs der Browser anspricht und welche APIs ich direkt nutzen kann, z.B.: um den Status und die Protokolle automatisiert abzufragen und damit zu überwachen. Im Portal gibt es dazu eine Übersichtsseite:

Die Werte werden per REST-Aufruf gegen folgende URL nachgeladen.

https://graph.microsoft.com/beta/servicePrincipals/<guid>/synchronization/jobs

Die JSON-Antwort enthält noch viel mehr Informationen, als das GUI anzeigt.

{
    "@odata.context": "https://graph.microsoft.com/beta/$metadata#servicePrincipals('<guid>')/synchronization/jobs",
    "value": [
        {
            "id": "Azure2Azure.eef62a097718406382dbd7582dc8916f.<guid>",
            "templateId": "Azure2Azure",
            "schedule": {
                "expiration": null,
                "interval": "PT40M",
                "state": "Active"
            },
            "status": {
                "countSuccessiveCompleteFailures": 0,
                "escrowsPruned": false,
                "code": "Active",
                "lastSuccessfulExecutionWithExports": null,
                "quarantine": null,
                "steadyStateFirstAchievedTime": "2023-06-12T07:34:21.4337447Z",
                "steadyStateLastAchievedTime": "2023-06-12T08:33:56.0416458Z",
                "troubleshootingUrl": "",
                "lastExecution": {
                    "activityIdentifier": "<guid>",
                    "countEntitled": 0,
                    "countEntitledForProvisioning": 0,
                    "countEscrowed": 1,
                    "countEscrowedRaw": 1,
                    "countExported": 0,
                    "countExports": 0,
                    "countImported": 2,
                    "countImportedDeltas": 0,
                    "countImportedReferenceDeltas": 0,
                    "state": "Succeeded",
                    "error": null,
                    "timeBegan": "2023-06-12T08:33:54.9829697Z",
                    "timeEnded": "2023-06-12T08:33:56.0426453Z"
                },
                "lastSuccessfulExecution": {
                    "activityIdentifier": null,
                    "countEntitled": 0,
                    "countEntitledForProvisioning": 0,
                    "countEscrowed": 0,
                    "countEscrowedRaw": 0,
                    "countExported": 3,
                    "countExports": 0,
                    "countImported": 0,
                    "countImportedDeltas": 0,
                    "countImportedReferenceDeltas": 0,
                    "state": "Succeeded",
                    "error": null,
                    "timeBegan": "0001-01-01T00:00:00Z",
                    "timeEnded": "2023-06-12T07:34:21.4337447Z"
                },
                "progress": [],
                "synchronizedEntryCountByType": [
                    {
                        "key": "User to User",
                        "value": 9
                    }
                ]
            },
            "synchronizationJobSettings": [
                {
                    "name": "AzureIngestionAttributeOptimization",
                    "value": "True"
                },
                {
                    "name": "LookaheadQueryEnabled",
                    "value": "False"
                }
            ]
        }
    ]
}

Wenn ich diese URL nun einfach mit einem "Invoke-Webrequest" aufrufen, dann bekomme ich natürlich einen Fehler:

{"error":
   {
      "code":"InvalidAuthenticationToken",
      "message":"Access token is empty.",
      "innerError":
      {
         "date":"2023-06-12T08:58:25",
         "request-id":"e0800998-6781-4968-9334-601246658921",
         "client-request-id":"e0800998-6781-4968-9334-601246658921"
      }
   }
}

So einfach bekomme ich es natürlich nicht gemacht. Ich muss mich schon entsprechend anmelden. Im F12-Debugger sehe ich im Request das Bearer-Token:

Den kann ich z.B. auf https://jwt.io oder https://jwt.ms einfach decodieren. In diesem Token stehen sehr viele Berechtigungen darin:

AccessReview.ReadWrite.All AuditLog.Read.All ConsentRequest.ReadWrite.All 
Directory.AccessAsUser.All Directory.Read.All Directory.ReadWrite.All Directory.Write.Restricted 
DirectoryRecommendations.Read.All DirectoryRecommendations.ReadWrite.All 
email 
EntitlementManagement.Read.All 
Group.ReadWrite.All 
IdentityProvider.ReadWrite.All IdentityRiskEvent.ReadWrite.All IdentityUserFlow.Read.All 
openid Policy.Read.All 
Policy.Read.IdentityProtection Policy.ReadWrite.AuthenticationFlows Policy.ReadWrite.AuthenticationMethod 
Policy.ReadWrite.ConditionalAccess Policy.ReadWrite.ExternalIdentities Policy.ReadWrite.IdentityProtection 
Policy.ReadWrite.MobilityManagement 
profile 
Reports.Read.All RoleManagement.ReadWrite.Directory 
SecurityEvents.ReadWrite.All 
TrustFrameworkKeySet.Read.All 
User.Export.All User.ReadWrite.All UserAuthenticationMethod.ReadWrite.All"

Das ist aber einfach dadurch zu erklären, dass die "App Azure-Portal" natürlich nicht nur den "Cross-Tenant Sync" konfiguriert, sondern ich als Global Admin auch alle anderen Aspekte des Tenants verändern könnte und daher mein Token sehr groß ist.

Service Principal finden

Um per Graph weitere Details zu einer Sync-Konfiguration zu ermitteln, muss ich aber erst einmal die GUID des Service Principal ermitteln. Zwar gibt es mit List servicePrincipals (https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list?view=graph-rest-1.0&tabs=http) einen Weg alle Einträge zu listen aber wonach muss ich denn suchen? Auch hier hilft der F12-Debugger, mit dem ich den relevanten Aufruf ermitteln konnte. Es war nicht auf den ersten Blick sichtbar, da es ein "Batch"-Abruf ist.

Da ich aber zwei Konfigurationen hatte, reichte eine Suche nach den Strings um die Antwort und den dazu passenden Request zu finden:

Die Abfrage des Webseite nutzt folgenden Query.

GET /servicePrincipals?
   $count=true
   &$filter=(applicationTemplateId eq '518e5f48-1fc8-4c48-9387-9fdf28b0dfe7')
   &$top=20
   &$orderby=createdDateTime asc

Interessanterweise bekam ich im GraphExplorer die Fehlermeldung, dass "Sorting" bei der Anfrage nicht erlaubt wäre. Ich habe sie dann etwas gekürzt:

https://graph.microsoft.com/v1.0/servicePrincipals?$count=true&$filter=(applicationTemplateId eq '518e5f48-1fc8-4c48-9387-9fdf28b0dfe7')

Damit habe ich beide Sync-Jobs bekommen. Mit der GUID des ServicePrincipal komme ich dann auch an die Einstellungen heran:

Ich habe es hier aber auf "Lesen" belassen.  

Reduzierte Rechte

Mit dem Graph Explorer kann ich aber den Request auch einfach ausführen und mir dabei anzeigen lassen, welche Berechtigungen ich effektiv für diesen Aufruf benötige:

Um den Status auszulesen liefert mit GraphExplorer die Berechtigungen "Synchronization.Read.All" und "Synchronization.ReadWrite.All". Ich denke, dass dies aber allein zum Lesen des Status auch schon zu viel ist und sich ein Skript auch mit "Synchronization.Read.All" begnügen könnte. Bei der Einrichtung eine "App" kann das Recht entsprechend spezifiziert werden:

Weitere Links

John Savill's Technical Training: Azure AD Cross-Tenant Sync
https://www.youtube.com/watch?v=z0J5kteqUVQ