Get-O365Usage

Sie kennen doch sicher im Office 365 Portal die Webseite mit der "Adoption Rate". Auf der Grafik ist das schön anzuschauen und es gibt auch rechts oben den Link zum "Exportieren" als CSV-Datei aber ich hätte da schon gerne automatisiert in mein lokales Monitoring überführt.

Sehr schnell hatte ich die Links zum Thema gefunden, die mich aber auf die Graph-API verweisen.

Also war es nun doch mal an der Zeit sich per PowerShell der Schnittstelle zu nähern.

Hinweis: Einige Daten können Sie auch per PowerBI auswerten. Siehe dazu auch PowerBI und Microsoft 365 Usage

AppID und Key statt Benutzer/Kennwort

Wenn wir mal von anonymen Webseiten absehen, erfolgt heute jeder Zugriff auf Information erst nach einer erfolgreichen Authentifizierung des zugreifenden Prozesses unter Beachtung der zugewiesenen Berechtigungen (Autorisierung) und eventuell einer Protokollierung (Auditierung). Lange Zeit war es üblich, dass die Anmeldung mit einer Kombination aus Benutzername und Kennwort erfolgt. In Ausnahmefällen auch per Zertifikat, z.B. über eine Smartcard. In beiden Fällen konnten diese Informationen von unterschiedlichen Programmen genutzt werden. Es war also eine gewisse Mobilität des Zugriffs möglich. Interessanter ist hier natürlich die Funktion einer Applikation direkt Rechte zu geben. Ein Benutzername und Kennwort ist dann nicht mehr erforderlich.

Das bedeutet natürlich auch, dass Sie in Programmen und Skripten nicht mehr mit Benutzername und Kennworten hantieren müssen, sondern mit einer Zeichenkette für die Identifizierung der App (App ID) und einer kryptografischen Information (ClientSecret), mit der Sie ihre Anforderungen signieren und die dann nur von der Gegenseite verifiziert werden kann. Die Gegenseite ist dabei aber ein Token-Server, der ihnen dann ein Zugriffsticket ausstellt, in dem auch die Berechtigungen in Form von Rollen, hinterlegt sind. Damit kann eine Firma dann einer App bestimmte Rechte einräumen. Die App kann zwar auch andere Zugriffe versuchen aber das Zielsystem kann allein anhand des Zugriffstokens schon erkennen, ob der Zugriff gewährt werden soll.

Solange der Entwickler der App die ApplicationID und das ClientSecret geheim hält, kann er den Code auch anpassen und erweitern. Er kann aber nie mehr Rechte in Anspruch nehmen, als er bei der Anmeldung zugeteilt bekommt. Das funktioniert wunderbar per HTTPS und das Zielsystem muss auch keine direkte Verbindung zum Token-Issuer haben. Es muss aber dem Token-Issuer vertrauen. Aber schauen wir und das am Beispiel von AzureAD App. Graph und PowerShell einmal an. Mein Ziel ist es, die Nutzungsstatistiken von Office 365 Pro Plus auszulesen.

App Registration anlegen

Damit sich eine App später an Graph anmelden kann, muss ich die App erst einmal in meinem Azure Tenant registrieren. Das geht unter https://portal.azure.com mit folgenden Schritten:

Zuerst lege ich eine neue App Registration an:

Ich muss natürlich einen Namen vergeben und wähle "Web app / API" als Type. Die Sign-on URL ist für mein Beispiel nicht relevant aber muss ausgefüllt werden.

Danach ist die App angelegt und die Informationen werden angezeigt:

 

Hier ist der Inhalt des Feld "Application-ID" wichtig. Das ist eine GUID, die AzureAD mir nun zugewiesen hat. Diesen Wert muss ich später bei der Anmeldung im Skript verwenden.

Client_Secret anlegen

Als nächstes brauche ich einen Schlüssel, mit dem ich meine Anmeldung später signiere. Ich könnte mir selbst ein Schlüsselpaar errechnen und diesen dann hier hochladen. Einfacher ist es, von Azure einen Schlüssel generieren zu lassen. Dazu pflege ich einfach eine Beschreibung, wähle die Gültigkeit aus und drücke auf "Save".

Im nächsten Dialog ist dann der "Client Secret" zu sehen. Dies ist quasi das Kennwort für die App und sollte nie "öffentlich" sein. Kopieren Sie sich diesen String und übernehmen Sie ihn in ihr Programm. Sie können ihn nicht erneut anzeigen lassen. Sie können aber natürlich einen neuen Key generieren. Das ist hier nach einem Jahr auch erforderlich.

Berechtigungen für API zuweisen

Nachdem der Key erstellt ist, kann ich einen Punkt tiefer nun die Berechtigungen zuweisen. Dazu muss ich zuerst eine API auswählen. Bei mir ist es "Graph" als Zielsystem. Office 365 bietet ihnen natürlich noch viele andere APIs an.

Nach der Auswahl der API kann ich dann die zu dieser API unterschiedlichen Rollen, d.h. Berechtigungen, vergeben. Wenn sie mit der Maus etwas über dem Eintrag stehen bleiben, sehen Sie den internen Namen.

Das "Yes" dahinter bedeutet, dass dieses Recht nur durch einen Administrator freigegeben werden kann. Es gibt nämlich auch die Möglichkeit, dass ein Anwender selbst einer von jemand anderem bereitgestellten Applikation entsprechende Berechtigungen vergibt. Darauf gehe ich hier aber erst mal nicht weiter ein.

Für mein Beispiel brauche ich später das Application-Recht "Reports.Read.All".

Powershell: Graph Token erhalten

Ich habe das Skript in die Einzelteile aufgespalten, um die Abschnitte zu erklären.

get-o365usagedata.20190208.ps1
Hier ist das komplette Skript aber natürlich ohne die ClientSecrets meines Tenant

Zuerst definiere ich die wesentlichen Variablen als Parameter. Hier müssen Sie natürlich ihre eigenen AppID und AppKey eintragen

param (
    [string]$LoginUrl = "https://login.microsoft.com",  # Graph API URLs.
    [string]$ResourceUrl = "https://graph.microsoft.com",  # Ressource API URLs.
    [string]$AppID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", # App ID aus dem Azure Portal .
    [string]$Appkey = "xxxxxxxxxxxxxxx", # App Key from Portal
    [string]$TenantName = "msxfaq.onmicrosoft.com", #  tenant name.
    [string]$GraphUrl = "https://graph.microsoft.com/v1.0/reports/getOffice365ActivationsUserDetail"
    [string]$csvfilename = ".\report.csv" 
)

Diese Daten nutze ich dann um ein OAUTH Token zu erhalten. Dazu muss ich einen Body formatieren und einen REST-Aufruf starten

# Get OAUTH Token
$Body = @{ grant_type = "client_credentials"; 
            resource = $ResourceUrl;
            client_id = $AppID; 
            client_secret = $AppKey 
        }
[object]$OAuth = Invoke-RestMethod `
                     -Method Post `
                     -Uri "$($LoginUrl)/$($TenantName)/oauth2/token?api-version=1.0" `
                     -Body $Body

Die Variable OAUTH enthält dann in etwa folgen Daten. Das AccessToken habe ich natürlich gekürzt

token_type     : Bearer
expires_in     : 3600
ext_expires_in : 3600
expires_on     : 1549577032
not_before     : 1549573132
resource       : https://graph.microsoft.com
access_token   : eyJ0eXAiOiJKV1QiLCJub25jZSI6IkFRQUJBQUFBQUFDRWZleFh4amFtUWIzT2VHUTRHdWd2U3dkVFVvZnA2dll6Q2NOTlpsUThEdXBNcVNrQ1NJbmhVTHdEQmhHVnhBOVJjckVYSHR2
                 SmFiRVNfdW1XWWNmby1EbzFtX3QzU2E0Z2pUczFtQTVSc3lBQSIsImFsZyI6IlJTMjU2IiwieDV0IjoiLXN4TUpNTENJRFdNVFB2WnlKNnR4LUNEeHcwIiwia2lkIjoiLXN4TUpNTEN
                 JRFdNVFB2WnlKNnR4IsInN1YiI6ImRmNjJmZDRlLTVhMmMtNGQwNC1hNDg2LTI2OTAyNDYyMzQ5YSIsInRpZCI6ImRlMjFjMzAxLWE0YWUtNDI5Mi1hYTA5LTZkYjcxMGE1OTBhNiIs
                 9j39HXL9vsw

Sie können das Token auf verschiedenen Webseiten (z.B. jwt.ms auch decodieren lassen.

In dem Token müssen Sie ihre AppID und den Namen aber vor allem auch die Rollen sehen. Wenn die Rollen fehlen, dann ist die Zuweisung im AzureAD noch nicht korrekt umgesetzt worden.

Authentication Header bauen

Über den Reiter "claims" werden die Felder noch mit einer Beschreibung versehen. Diese Token nutze ich nun, um die eigentliche Anfrage an die Graph-API zu stellen. Dazu baue ich zuerst den Header mit der Authentifizierung zusammen:

$HeaderParams = @{ 'Authorization' = "$($OAuth.token_type) $($OAuth.access_token)" }

In der Variable landet dann etwas wie:

$HeaderParams

Name                           Value
----                           -----
Authorization                  Bearer eyJ0eXAiOiJKV1QiLCJub25jZSI6IkFR....

Damit spreche ich dann einfach die Graph-API an. Die Try/Catch-Anweisung ist erforderlich, damit ich im Falle eines Fehlers auf den Error-Stream zugreifen und die Details ausgeben kann:

if ($null -eq $OAuth.access_token) {
    Write-Error "No Access Token"
}
else  {  # Perform REST call.
    $HeaderParams = @{ 'Authorization' = "$($OAuth.token_type) $($OAuth.access_token)" }
    try {
        Invoke-WebRequest -UseBasicParsing -Headers $HeaderParams -Uri $GraphUrl -outfile $csvfilename
        write-host "  Result:"
        $Result
    }
    catch {
        $resultstream = $_.Exception.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($resultstream)
        $ErrorBody = $global:reader.ReadToEnd();
        write-host "  ResultError :"
        $ErrorBody 
    }
}

Wenn der Abruf fehlerfrei erfolgt ist, dann finden Sie eine CSV-Datei mit folgendem Aufbau:

Das ist natürlich die "Details-Seite, in der jeder Benutzer für jedes Produkt eine eigene Zeile hat. Allerdings sind hier die Computer selbst nicht enthalten.

Fiddler

Mit Fiddler kann auch sehr gut die Konversation analysiert werden. Es sind drei Request:

Im Detail bedeuten diese:

  • Paket 7
    Der Client greift auf login.microsoft.com zu, um mit den Anmeldedaten ein Bearer-Token zu erhalten
  • Paket 9
    Das Skript greift nun mit der Authorization "Bearer" auf die Ressource zu.

    Ich erhalte hier aber nicht die Information direkt, sondern einen "302 Found" mit einer Umleitung auf eine neue URL, die schon vorauthentifiziert ist
  • Paket 12: Download der CSV-Datei
    Invoke-WebRequest folgt alleine schon dem 302 und lädt direkt die CSV-Datei herunter

Alles kein Hexenwerk, wenn es einmal funktioniert.

Fehlermeldungen

Es hat aber schon einige Sessions und Versuche gebraucht, bis ich das Ergebnis letztlich erreicht habe. Selbst kleinste Tippfehler in der URL oder anderswo führen einfach zu einem Fehler. Hier eine kleine Sammlung und ihrer Ursachen:

Meldung Eine Ursache
{
  "error": {
    "code": "BadRequest",
    "message": "Resource not found for the segment 'getOffice365ActivationsUserDetails'.",
    "innerError": {
      "request-id": "bfbbc1ea-e315-452e-9527-8512e387dcc4",
      "date": "2019-02-08T09:22:43"
    }
  }
}

Einfach eine falsche GraphURL. Beliebt sind Verwechselungen von Plural und Singular, also "getOffice365ActivationsUserDetails" statt "getOffice365ActivationsUserDetail" oder "getOffice365ActivationsUserCount" statt "getOffice365ActivationsUserCounts"

Sie sehen schon hier, dass manchmal ein Plural und manchmal ein Singular genutzt wird.

{
  "error":"unauthorized_client",
  "error_description":"AADSTS700016: Application with identifier '<guid>' was not found 
   in the directory 'msxfaq.onmicrosoft.com'. This can happen if the application has not 
   been installed by the administrator of the tenant or consented to by any user in the tenant.
  You may have sent your authentication request to the wrong tenant\r\n
  Trace ID: 12345678-1234-1234-1234-1234567890ab\r\n
  Correlation ID: xxxx\r\nTimestamp: 2019-02-08 09:32:49Z",
  "error_codes":[700016],
  "timestamp":"2019-02-08 09:32:49Z",
  "trace_id":"12345678-1234-1234-1234-1234567890ab",
  "correlation_id":"12345678-1234-1234-1234-1234567890ab"}

Prüfen Sie die GUID der AppID

{
  "error":"invalid_client",
  "error_description":"AADSTS7000215: Invalid client secret is provided.\r\n
                       Trace ID: 12345678-1234-1234-1234-1234567890ab\r\n
                       Correlation ID:12345678-1234-1234-1234-1234567890ab\r\n
                       Timestamp: 2019-02-08 09:38:39Z",
  "error_codes":[7000215],
  "timestamp":"2019-02-08 09:38:39Z",
  "trace_id":"12345678-1234-1234-1234-1234567890ab",
  "correlation_id":"12345678-1234-1234-1234-1234567890ab"}

Das Client Secret passt nicht zur App

{
  "error": {
    "code": "Authorization_RequestDenied",
    "message": "Insufficient privileges to complete the operation.",
    "innerError": {
      "request-id": "12345678-1234-1234-1234-1234567890ab",
      "date": "2019-02-08T09:41:50"
    }
  }
}

SSie greifen auf eine URL zu, für die sie keine Berechtigungen haben

Weitere Auswertungen

Vielleicht reicht ihnen ja auch eine weniger umfangreiche Auswertung. Dann nutzen sie den gleichen Code einfach mit anderen Graph-URLs. Hier ein paar Beispiele für URls, die Sie für den Parameter "GraphURL" alternativ verwenden können

Berichtsurl

Datensatzbeispiel

Office 365 Aktivierungen nach Produkt als Übersicht
https://graph.microsoft.com/v1.0/reports/getOffice365ActivationsUserCounts

Report Refresh Date        : 2019-02-05
Product Type               : Office 365 ProPlus
Assigned                   : 124
Activated                  : 89
Shared Computer Activation : 26

Office 365 Aktivierungen nach Produkt und Client
"https://graph.microsoft.com/v1.0/reports/getOffice365ActivationCounts"

Report Refresh Date : 2019-02-05
Product Type        : Office 365 ProPlus
Windows             : 160
Mac                 : 3
Android             : 25
iOS                 : 38
Windows 10 Mobile   : 25

Office 365 Aktivierungen pro Benutzer und Client
"https://graph.microsoft.com/v1.0/reports/getOffice365ActivationsUserDetail"

Report Refresh Date          : 2019-02-05
User Principal Name          : user1@uclabor.de
Display Name                 : User1
Product Type                 : Office 365 ProPlus
Last Activated Date          : 2019-01-18
Windows                      : 3
Mac                          : 0
Windows 10 Mobile            : 0
iOS                          : 5
Android                      : 0
Activated On Shared Computer : True

SharePoint Benutzerinformation
"https://graph.microsoft.com/v1.0/reports/getSharePointActivityUserDetail(period='D7')"

Report Refresh Date          : 2019-02-05
User Principal Name          : user1@uclabor.de
Is Deleted                   : False
Deleted Date                 :
Last Activity Date           : 2018-12-19
Viewed Or Edited File Count  : 0
Synced File Count            : 0
Shared Internally File Count : 0
Visited Page Count           : 0
Assigned Products            : OFFICE 365 ENTERPRISE E3+Microsoft Flow Free
Report Period                : 7

SharePoint Dateiaktionen
https://graph.microsoft.com/v1.0/reports/getSharePointActivityFileCounts(period='D7')

Report Refresh Date : 2019-02-05
Viewed Or Edited    : 718
Synced              : 37
Shared Internally   : 4
Shared Externally   :
Report Date         : 2019-02-05
Report Period       : 7

Teams: Endgeräte-Details pro Anwender
https://graph.microsoft.com/v1.0/reports/getTeamsDeviceUsageUserDetail(period='D7')

Report Refresh Date : 2019-02-05
User Principal Name : user1@uclabor.de
Last Activity Date  : 2019-02-05
Is Deleted          : False
Deleted Date        :
Used Web            : No
Used Windows Phone  : No
Used iOS            : Yes
Used Mac            : No
Used Android Phone  : No
Used Windows        : Yes
Report Period       : 7

Teams: Nutzung pro Anwenders
https://graph.microsoft.com/v1.0/reports/getTeamsUserActivityUserDetail(period='D7')

Report Refresh Date        : 2019-02-05
User Principal Name        : user1@uclabor.de
Last Activity Date         : 2019-02-05
Is Deleted                 : False
Deleted Date               :
Assigned Products          : ENTERPRISE MOBILITY + SECURITY E3+OFFICE 365 ENTERPRISE E5 WITHOUT
                             AUDIO CONFERENCING+AUDIO CONFERENCING
Team Chat Message Count    : 0
Private Chat Message Count : 48
Call Count                 : 1
Meeting Count              : 0
Has Other Action           : Yes
Report Period              : 7

Mit dem Recht "Reports.Read.All" können Sie quasi alle URls aus dem Bereich der Office 365 Reports nutzen.

Weitere Links