Erstellen Sie Google-Nutzer aus PowerShell über die API

Hallo!

In diesem Artikel wird beschrieben, wie PowerShell mit der Google-API interagiert, um G Suite-Benutzer zu bearbeiten.

In der Organisation verwenden wir mehrere interne und Cloud-Dienste. Die Autorisierung in diesen beiden Fällen hängt größtenteils von Google oder Active Directory ab, zwischen denen wir kein Replikat verwalten können. Wenn ein neuer Mitarbeiter freigegeben wird, müssen Sie in diesen beiden Systemen ein Konto erstellen / aktivieren. Um den Prozess zu automatisieren, haben wir beschlossen, ein Skript zu schreiben, das Informationen sammelt und an beide Dienste sendet.

Login


Als wir die Anforderungen zusammenstellten, entschieden wir uns, echte Personen als Administratoren für die Autorisierung zu verwenden. Dies vereinfacht die Analyse von Aktionen im Falle versehentlicher oder absichtlicher massiver Änderungen.

Die Google-APIs verwenden das OAuth 2.0-Protokoll zur Authentifizierung und Autorisierung. Anwendungsfälle und eine detailliertere Beschreibung finden Sie hier: Verwenden von OAuth 2.0 für den Zugriff auf Google APIs .

Ich habe das Skript ausgewählt, das für die Autorisierung in Desktop-Anwendungen verwendet wird. Es besteht auch die Möglichkeit, ein Dienstkonto zu verwenden, das keine unnötigen Bewegungen des Benutzers erfordert.

Das Bild unten ist eine schematische Beschreibung des ausgewählten Szenarios auf der Google-Seite.



  1. Zuerst senden wir den Benutzer auf die Authentifizierungsseite im Google-Konto und geben die GET-Parameter an:
    • Anwendungs-ID
    • Bereiche, auf die die Anwendung Zugriff benötigt
    • Adresse, an die der Benutzer nach Abschluss des Vorgangs umgeleitet wird
    • die Art und Weise, wie wir das Token aktualisieren
    • Bestätigungscode
    • Übertragungsformat des Bestätigungscodes

  2. Nach Abschluss der Autorisierung wird der Benutzer mit einem Fehler oder Autorisierungscode, der von den GET-Parametern übertragen wird, auf die in der ersten Anforderung angegebene Seite umgeleitet
  3. Die Anwendung (das Skript) muss diese Parameter abrufen und, wenn der Code empfangen wird, die folgende Anforderung für Token ausführen
  4. Wenn die Anforderung korrekt ist, gibt die Google-API Folgendes zurück:

    • Zugriffstoken, mit dem wir Anfragen stellen können
    • Gültigkeit dieses Tokens
    • Aktualisierungstoken zum Aktualisieren des Zugriffstokens erforderlich.

Zuerst müssen Sie zur Google API-Konsole gehen: Anmeldeinformationen - Google API Console , die gewünschte Anwendung auswählen und die Client-OAuth-ID im Abschnitt Anmeldeinformationen erstellen. An derselben Stelle (oder später in den Eigenschaften des erstellten Bezeichners) müssen Sie die Adressen angeben, an die die Umleitung zulässig ist. In unserem Fall handelt es sich um mehrere localhost-Einträge mit unterschiedlichen Ports (siehe unten).

Um das Lesen des Skriptalgorithmus zu vereinfachen, können Sie die ersten Schritte in einer separaten Funktion ausgeben, die Zugriffs- und Aktualisierungstoken für die Anwendung zurückgibt:

$client_secret = 'Our Client Secret' $client_id = 'Our Client ID' function Get-GoogleAuthToken { if (-not [System.Net.HttpListener]::IsSupported) { "HttpListener is not supported." exit 1 } $codeverifier = -join ((65..90) + (97..122) + (48..57) + 45 + 46 + 95 + 126 |Get-Random -Count 60| % {[char]$_}) $hasher = new-object System.Security.Cryptography.SHA256Managed $hashByteArray = $hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($codeverifier)) $base64 = ((([System.Convert]::ToBase64String($hashByteArray)).replace('=','')).replace('+','-')).replace('/','_') $ports = @(10600,15084,39700,42847,65387,32079) $port = $ports[(get-random -Minimum 0 -maximum 5)] Write-Host "Start browser..." Start-Process "https://accounts.google.com/o/oauth2/v2/auth?code_challenge_method=S256&code_challenge=$base64&access_type=offline&client_id=$client_id&redirect_uri=http://localhost:$port&response_type=code&scope=https://www.googleapis.com/auth/admin.directory.user https://www.googleapis.com/auth/admin.directory.group" $listener = New-Object System.Net.HttpListener $listener.Prefixes.Add("http://localhost:"+$port+'/') try {$listener.Start()} catch { "Unable to start listener." exit 1 } while (($code -eq $null)) { $context = $listener.GetContext() Write-Host "Connection accepted" -f 'mag' $url = $context.Request.RawUrl $code = $url.split('?')[1].split('=')[1].split('&')[0] if ($url.split('?')[1].split('=')[0] -eq 'error') { Write-Host "Error!"$code -f 'red' $buffer = [System.Text.Encoding]::UTF8.GetBytes("Error!"+$code) $context.Response.ContentLength64 = $buffer.Length $context.Response.OutputStream.Write($buffer, 0, $buffer.Length) $context.Response.OutputStream.Close() $listener.Stop() exit 1 } $buffer = [System.Text.Encoding]::UTF8.GetBytes("Now you can close this browser tab.") $context.Response.ContentLength64 = $buffer.Length $context.Response.OutputStream.Write($buffer, 0, $buffer.Length) $context.Response.OutputStream.Close() $listener.Stop() } Return Invoke-RestMethod -Method Post -Uri "https://www.googleapis.com/oauth2/v4/token" -Body @{ code = $code client_id = $client_id client_secret = $client_secret redirect_uri = 'http://localhost:'+$port grant_type = 'authorization_code' code_verifier = $codeverifier } $code = $null 

Wir legen die Client-ID und das Client-Geheimnis fest, die in den Eigenschaften der OAuth-Client-ID erhalten wurden, und der Code-Verifizierer ist eine Zeichenfolge mit einer Länge von 43 bis 128 Zeichen, die zufällig aus nicht reservierten Zeichen generiert werden sollte: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Weiterhin wird dieser Code erneut übertragen. Dadurch wird die Sicherheitsanfälligkeit beseitigt, bei der ein Angreifer die Antwort abfangen kann, die nach der Benutzerautorisierung als Weiterleitung zurückgegeben wurde.
Sie können den Code-Verifizierer in der aktuellen Anforderung in klarer Form senden (was es sinnlos macht - dies ist nur für Systeme geeignet, die SHA256 nicht unterstützen) oder indem Sie einen SHA256-Hash erstellen, der in BASE64Url codiert werden muss (unterscheidet sich von Base64 durch zwei Zeichen der Tabelle) und das Zeichen löschen Zeilenende: =.

Als Nächstes müssen wir http auf dem lokalen Computer abhören, um nach der Autorisierung eine Antwort zu erhalten, die als Umleitung zurückgegeben wird.

Verwaltungsaufgaben werden auf einem speziellen Server ausgeführt. Wir können nicht ausschließen, dass mehrere Administratoren das Skript gleichzeitig ausführen. Daher wählt er zufällig einen Port für den aktuellen Benutzer aus. Ich habe jedoch vordefinierte Ports angegeben, da Sie müssen auch in der API-Konsole als vertrauenswürdig hinzugefügt werden.

access_type = offline bedeutet, dass die Anwendung das abgelaufene Token unabhängig aktualisieren kann, ohne dass der Benutzer mit dem Browser interagieren muss.
response_type = code legt das Format für die Rückgabe des Codes fest (unter Bezugnahme auf die alte Autorisierungsmethode, als der Benutzer den Code vom Browser in das Skript kopierte).
Umfang gibt den Umfang und die Art des Zugriffs an. Sie müssen durch Leerzeichen oder% 20 getrennt werden (gemäß URL-Codierung). Eine Liste der Zugriffsbereiche mit Typen finden Sie hier: OAuth 2.0- Bereiche für Google-APIs .

Nach Erhalt des Autorisierungscodes gibt die Anwendung eine Abschlussnachricht an den Browser zurück, hört auf, den Port abzuhören, und sendet eine POST-Anforderung zum Empfangen des Tokens. Wir geben darin die zuvor festgelegte ID und das Geheimnis der Konsolen-API an, die Adresse, an die der Benutzer umgeleitet wird, und grant_type gemäß der Protokollspezifikation.

Als Antwort erhalten wir ein Zugriffstoken, dessen Dauer in Sekunden und ein Aktualisierungstoken, mit dem wir das Zugriffstoken aktualisieren können.

Die Anwendung sollte die Token an einem sicheren Ort mit langer Haltbarkeit aufbewahren. Bis wir den empfangenen Zugriff widerrufen, wird das Aktualisierungstoken nicht an die Anwendung zurückgegeben. Am Ende habe ich eine Aufforderung zum Widerruf des Tokens hinzugefügt. Wenn die Anwendung nicht erfolgreich abgeschlossen wurde und das Aktualisierungstoken nicht zurückgegeben wurde, wird der Vorgang erneut gestartet (wir hielten es für unsicher, Token lokal auf dem Terminal zu speichern, möchten jedoch die Kryptografie nicht komplizieren oder den Browser häufig öffnen).

 do { $token_result = Get-GoogleAuthToken $token = $token_result.access_token if ($token_result.refresh_token -eq $null) { Write-Host ("Session is not destroyed. Revoking token...") Invoke-WebRequest -Uri ("https://accounts.google.com/o/oauth2/revoke?token="+$token) } } while ($token_result.refresh_token -eq $null) $refresh_token = $token_result.refresh_token $minute = ([int]("{0:mm}" -f ([timespan]::fromseconds($token_result.expires_in))))+((Get-date).Minute)-2 if ($minute -lt 0) {$minute += 60} elseif ($minute -gt 59) {$minute -=60} $token_expire = @{ hour = ([int]("{0:hh}" -f ([timespan]::fromseconds($token_result.expires_in))))+((Get-date).Hour) minute = $minute } 

Wie Sie vielleicht bemerkt haben, wird beim Aufrufen eines Tokens Invoke-WebRequest verwendet. Im Gegensatz zu Invoke-RestMethod werden die empfangenen Daten nicht in einem geeigneten Format zur Verwendung zurückgegeben und der Status der Anforderung angezeigt.

Als Nächstes werden Sie vom Skript aufgefordert, den Vor- und Nachnamen des Benutzers einzugeben und einen Benutzernamen + eine E-Mail zu generieren.

Anfragen


Im Folgenden sind die Anforderungen aufgeführt. Zunächst müssen Sie überprüfen, ob der Benutzer bereits mit einem solchen Login vorhanden ist, um eine Entscheidung über das Erstellen eines neuen oder das Aktivieren des aktuellen Logins zu treffen.

Ich habe beschlossen, alle Anforderungen im Format einer einzelnen Funktion mit einer Auswahl über switch zu implementieren:

 function GoogleQuery { param ( $type, $query ) switch ($type) { "SearchAccount" { Return Invoke-RestMethod -Method Get -Uri "https://www.googleapis.com/admin/directory/v1/users" -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body @{ domain = 'rocketguys.com' query = "email:$query" } } "UpdateAccount" { $body = @{ name = @{ givenName = $query['givenName'] familyName = $query['familyName'] } suspended = 'false' password = $query['password'] changePasswordAtNextLogin = 'true' phones = @(@{ primary = 'true' value = $query['phone'] type = "mobile" }) orgUnitPath = $query['orgunit'] } Return Invoke-RestMethod -Method Put -Uri ("https://www.googleapis.com/admin/directory/v1/users/"+$query['email']) -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body (ConvertTo-Json $body) -ContentType 'application/json; charset=utf-8' } "CreateAccount" { $body = @{ primaryEmail = $query['email'] name = @{ givenName = $query['givenName'] familyName = $query['familyName'] } suspended = 'false' password = $query['password'] changePasswordAtNextLogin = 'true' phones = @(@{ primary = 'true' value = $query['phone'] type = "mobile" }) orgUnitPath = $query['orgunit'] } Return Invoke-RestMethod -Method Post -Uri "https://www.googleapis.com/admin/directory/v1/users" -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body (ConvertTo-Json $body) -ContentType 'application/json; charset=utf-8' } "AddMember" { $body = @{ userKey = $query['email'] } $ifrequest = Invoke-RestMethod -Method Get -Uri "https://www.googleapis.com/admin/directory/v1/groups" -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body $body $array = @() foreach ($group in $ifrequest.groups) {$array += $group.email} if ($array -notcontains $query['groupkey']) { $body = @{ email = $query['email'] role = "MEMBER" } Return Invoke-RestMethod -Method Post -Uri ("https://www.googleapis.com/admin/directory/v1/groups/"+$query['groupkey']+"/members") -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body (ConvertTo-Json $body) -ContentType 'application/json; charset=utf-8' } else { Return ($query['email']+" now is a member of "+$query['groupkey']) } } } } 

In jeder Anforderung müssen Sie einen Autorisierungsheader senden, der den Tokentyp und das Zugriffstoken selbst enthält. Im Moment ist die Art des Tokens immer Träger. Weil Wir müssen überprüfen, ob das Token nicht abgelaufen ist, und es nach einer Stunde ab dem Zeitpunkt seiner Ausgabe aktualisieren. Ich habe eine Anforderung für eine andere Funktion angegeben, die ein Zugriffstoken zurückgibt. Der gleiche Code befindet sich am Anfang des Skripts, wenn das erste Zugriffstoken empfangen wird:

 function Get-GoogleToken { if (((Get-date).Hour -gt $token_expire.hour) -or (((Get-date).Hour -ge $token_expire.hour) -and ((Get-date).Minute -gt $token_expire.minute))) { Write-Host "Token Expired. Refreshing..." $request = (Invoke-RestMethod -Method Post -Uri "https://www.googleapis.com/oauth2/v4/token" -ContentType 'application/x-www-form-urlencoded' -Body @{ client_id = $client_id client_secret = $client_secret refresh_token = $refresh_token grant_type = 'refresh_token' }) $token = $request.access_token $minute = ([int]("{0:mm}" -f ([timespan]::fromseconds($request.expires_in))))+((Get-date).Minute)-2 if ($minute -lt 0) {$minute += 60} elseif ($minute -gt 59) {$minute -=60} $script:token_expire = @{ hour = ([int]("{0:hh}" -f ([timespan]::fromseconds($request.expires_in))))+((Get-date).Hour) minute = $minute } } return $token } 

Überprüfen des Logins auf Existenz:

 function Check_Google { $query = (GoogleQuery 'SearchAccount' $username) if ($query.users -ne $null) { $user = $query.users[0] Write-Host $user.name.fullName' - '$user.PrimaryEmail' - suspended: '$user.Suspended $GAresult = $user } if ($GAresult) { $return = $GAresult } else {$return = 'gg'} return $return } 

E-Mail-Anfrage: $ query fordert die API auf, nach einem Benutzer mit genau dieser E-Mail zu suchen, einschließlich Aliasnamen. Sie können auch Platzhalter verwenden: =,:,: {PREFIX} * .

Um Daten zu erhalten, wird die GET-Anforderungsmethode verwendet, um Daten einzufügen (ein Konto zu erstellen oder ein Mitglied zu einer Gruppe hinzuzufügen) - POST, um vorhandene Daten zu aktualisieren - PUT, um einen Eintrag zu löschen (z. B. einen Teilnehmer aus einer Gruppe) - DELETE.

Das Skript fordert außerdem eine Telefonnummer (eine ungültige Zeichenfolge) an und nimmt sie in eine regionale Verteilergruppe auf. Sie entscheidet anhand der ausgewählten Active Directory-Organisationseinheit, welche Organisationseinheit der Benutzer haben soll, und gibt ein Kennwort aus:

 do { $phone = Read-Host "   +7" } while (-not $phone) do { $moscow = Read-Host "  ? (y/n) " } while (-not (($moscow -eq 'y') -or ($moscow -eq 'n'))) $orgunit = '/' if ($OU -like "*OU=Delivery,OU=Users,OU=ROOT,DC=rocket,DC=local") { Write-host "   /Team delivery" $orgunit = "/Team delivery" } $Password = -join ( 48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba" 

Und dann beginnt das Konto zu manipulieren:

 $query = @{ email = $email givenName = $firstname familyName = $lastname password = $password phone = $phone orgunit = $orgunit } if ($GMailExist) { Write-Host "  " -f mag (GoogleQuery 'UpdateAccount' $query) | fl write-host "      $Username  Google." } else { Write-Host "  " -f mag (GoogleQuery 'CreateAccount' $query) | fl } if ($moscow -eq "y"){ write-host "   moscowoffice" $query = @{ groupkey = 'moscowoffice@rocketguys.com' email = $email } (GoogleQuery 'AddMember' $query) | fl } 

Die Funktionen zum Aktualisieren und Erstellen eines Kontos haben dieselbe Syntax. Es sind nicht alle zusätzlichen Felder erforderlich. In dem Abschnitt mit Telefonnummern müssen Sie ein Array angeben, das aus einem Datensatz eine Nummer und einen Typ enthalten kann.

Um beim Hinzufügen eines Benutzers zu einer Gruppe keinen Fehler zu erhalten, können wir zunächst überprüfen, ob er bereits in dieser Gruppe ist, indem wir vom Benutzer selbst eine Liste der Gruppenmitglieder oder die Zusammensetzung erhalten.

Eine Anfrage für die Zusammensetzung von Gruppen eines bestimmten Benutzers ist nicht rekursiv und zeigt nur eine direkte Mitgliedschaft. Die Aufnahme des Benutzers in die übergeordnete Gruppe, in der die untergeordnete Gruppe, zu der der Benutzer gehört, bereits erfolgreich ist.

Fazit


Es bleibt dem Benutzer das Passwort für das neue Konto zu senden. Wir tun dies per SMS und senden allgemeine Informationen mit Anweisungen und Login an die persönliche Post, die zusammen mit der Telefonnummer von der Personalauswahlabteilung bereitgestellt wurde. Alternativ können Sie Geld sparen und ein Passwort an einen geheimen Telegramm-Chat senden, was auch als zweiter Faktor angesehen werden kann (Macbooks sind eine Ausnahme).

Vielen Dank für das Lesen bis zum Ende. Ich freue mich über Vorschläge zur Verbesserung des Schreibstils von Artikeln und möchte, dass Sie beim Schreiben von Skripten weniger Fehler feststellen =)

Liste der Links, die thematisch nützlich sein können oder nur Ihre Fragen beantworten:

Source: https://habr.com/ru/post/de468969/


All Articles