Hallo!
In diesem Artikel wird die Implementierung der Interaktion von PowerShell mit der Google API zur Durchführung von Benutzeroperationen in G Suite beschrieben.
In der Organisation nutzen wir mehrere interne und Cloud-Services. Im Wesentlichen basiert die Authentifizierung in diesen auf Google oder Active Directory, zwischen denen wir kein Replica unterstützen können. Daher muss beim Eintritt eines neuen Mitarbeiters ein Konto in diesen beiden Systemen erstellt/aktiviert werden. Zur Automatisierung dieses Prozesses haben wir beschlossen, ein Skript zu schreiben, das Informationen sammelt und an beide Dienste sendet.
Autorisierung
Bei der Festlegung der Anforderungen haben wir uns entschieden, echte Administratoren für die Authentifizierung zu verwenden, um die Analyse von Handlungen bei versehentlichen oder absichtlichen massiven Änderungen zu erleichtern.
Für die Authentifizierung und Autorisierung verwenden die Google APIs das OAuth 2.0-Protokoll. Anwendungsfälle und eine detailliertere Beschreibung finden Sie hier: .
Ich habe das Szenario gewählt, das bei der Authentifizierung in Desktop-Anwendungen verwendet wird. Es gibt auch die Möglichkeit, ein Dienstkonto zu verwenden, das keine zusätzlichen Schritte vom Benutzer erfordert.
Das Bild unten ist eine schematische Beschreibung des gewählten Szenarios von der Google-Seite.

- Zuerst senden wir den Benutzer zur Authentifizierungsseite für das Google-Konto, wobei wir die GET-Parameter angeben:
- Anwendungs-ID
- Bereiche, auf die die Anwendung zugreifen muss
- URL, auf die der Benutzer nach Abschluss des Verfahrens weitergeleitet wird
- Methode, mit der wir das Token aktualisieren werden
- Bestätigungscode
- Format der Übertragung des Bestätigungscodes
- Nach Abschluss der Authentifizierung wird der Benutzer auf die im ersten Antrag angegebene Seite mit einem Fehler oder dem Autorisierungscode, über GET-Parameter übergeben, weitergeleitet.
- Die Anwendung (das Skript) muss diese Parameter abrufen und, falls ein Code erhalten wird, die nächste Anfrage zum Erhalt der Tokens ausführen.
- Bei einer korrekten Anfrage gibt die Google API zurück:
- Access-Token, mit dem wir Anfragen stellen können
- Gültigkeitsdauer dieses Tokens
- Refresh-Token, das benötigt wird, um das Access-Token zu aktualisieren.
Zuerst müssen wir zur Google API-Konsole gehen: , wählen Sie die benötigte Anwendung aus und erstellen Sie im Bereich Anmeldeinformationen eine OAuth-Client-ID. Dort (oder später in den Eigenschaften der erstellten ID) müssen die Adressen angegeben werden, auf die die Weiterleitung erlaubt ist. In unserem Fall wird es mehrere localhost-Einträge mit verschiedenen Ports sein (siehe weiter unten).
Um den Algorithmus des Skripts leichter lesbar zu machen, kann man die ersten Schritte in eine separate Funktion auslagern, die die Access- und Refresh-Tokens für die Anwendung zurückgibt:
$client_secret = 'Unser Client-Geheimnis'
$client_id = 'Unsere Client-ID'
function Get-GoogleAuthToken {
if (-not [System.Net.HttpListener]::IsSupported) {
"HttpListener wird nicht unterstützt."
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 "Browser starten..."
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 {
"Kann den Listener nicht starten."
exit 1
}
while (($code -eq $null)) {
$context = $listener.GetContext()
Write-Host "Verbindung akzeptiert" -f 'mag'
$url = $context.Request.RawUrl
$code = $url.split('?')[1].split('=')[1].split('&')[0]
if ($url.split('?')[1].split('=')[0] -eq 'error') {
Write-Host "Fehler!"$code -f 'red'
$buffer = [System.Text.Encoding]::UTF8.GetBytes("Fehler!"+$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("Jetzt können Sie diesen Browsertab schließen.")
$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 geben die Client-ID und das Client-Geheimnis an, die in den Eigenschaften der OAuth-Client-ID erhalten wurden, und der Code-Verifier ist eine Zeichenkette mit einer Länge von 43 bis 128 Zeichen, die zufällig aus nicht reservierten Zeichen generiert werden muss: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".
Dieser Code wird später erneut übergeben. Er schließt eine Sicherheitsanfälligkeit aus, bei der ein Angreifer die Antwort abfangen könnte, die nach der Autorisierung des Benutzers über die Weiterleitung zurückkommt.
Den Codeverifier kann im aktuellen Antrag offen gesendet werden (was ihn bedeutungslos macht – dies ist nur für Systeme geeignet, die SHA256 nicht unterstützen), oder indem ein Hash mit dem SHA256-Algorithmus erstellt wird, der in BASE64Url kodiert werden muss (unterscheidet sich von Base64 durch zwei Zeichen) und das Zeilenende-Zeichen: = entfernt werden muss.
Als nächstes müssen wir auf dem lokalen Rechner den HTTP-Verkehr abhören, um die Antwort nach der Autorisierung zu erhalten, die durch eine Weiterleitung zurückkommt.
Verwaltungstasks werden auf einem speziellen Server durchgeführt, wir können nicht ausschließen, dass mehrere Administratoren gleichzeitig ein Skript starten, daher wird zufällig ein Port für den aktuellen Benutzer ausgewählt, aber ich habe vordefinierte Ports angegeben, da sie ebenfalls als vertrauenswürdig in der API-Konsole hinzugefügt werden müssen.
access_type=offline bedeutet, dass die Anwendung abgelaufene Tokens selbständig ohne Interaktion des Nutzers mit dem Browser aktualisieren kann,
response_type=code legt das Format fest, in dem der Code zurückgegeben wird (Verweis auf die alte Authentifizierungsmethode, bei der der Nutzer den Code vom Browser in das Skript kopierte),
scope gibt die Bereiche und den Zugriffstyp an. Sie müssen durch Leerzeichen oder (entsprechend der URL-Codierung) getrennt werden. Eine Liste der Zugriffsbereiche mit Typen finden Sie hier: .
Nach Erhalt des Autorisierungscodes gibt die Anwendung eine Nachricht zum Schließen im Browser zurück, stoppt das Abhören des Ports und sendet eine POST-Anfrage zum Abrufen des Tokens. In dieser Anfrage geben wir die zuvor festgelegten ID und das Secret aus der API-Konsole, die URL, zu der der Benutzer weitergeleitet werden soll, und den grant_type gemäß der Protokollspezifikation an.
Als Antwort erhalten wir ein Access-Token, dessen Gültigkeitsdauer in Sekunden und ein Refresh-Token, mit dem wir das Access-Token aktualisieren können.
Die Anwendung sollte die Tokens sicher an einem Ort mit langer Haltbarkeit speichern, daher wird der Anwendung kein Refresh-Token zurückgegeben, solange der erhaltene Zugriff nicht widerrufen wird. Am Ende habe ich einen Widerrufsantrag für das Token hinzugefügt, falls die Anwendung nicht erfolgreich abgeschlossen wurde und das Refresh-Token nicht zurückgegeben wurde, wird der Prozess erneut gestartet (wir hielten es für unsicher, Tokens lokal auf dem Terminal zu speichern, und es ist nicht gewünscht, dafür Kryptografie zu verwenden oder häufig den Browser zu öffnen).
do {
$token_result = Get-GoogleAuthToken
$token = $token_result.access_token
if ($token_result.refresh_token -eq $null) {
Write-Host ("Session wurde nicht beendet. Token wird entzogen...")
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 bereits bemerkt haben, wird bei der Widerrufung des Tokens Invoke-WebRequest verwendet. Im Gegensatz zu Invoke-RestMethod gibt es keine erhaltenen Daten in einem verwendbaren Format zurück und zeigt den Status der Anfrage an.
Dann wird das Skript darum bitten, den Vor- und Nachnamen des Benutzers einzugeben und dabei Anmeldeinformationen + E-Mail zu generieren.
Anfragen
Als Nächstes werden Anfragen gestellt - zuerst muss überprüft werden, ob bereits ein Benutzer mit diesem Benutzernamen existiert, um eine Entscheidung über die Erstellung eines neuen oder die Aktivierung des aktuellen zu treffen.
Ich habe beschlossen, alle Anfragen in einer Funktion mit einer Auswahl zu implementieren, unter Verwendung von switch:
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'] + " ist jetzt ein Mitglied von " + $query['groupkey'])
}
}
}
}In jeder Anfrage muss der Authorization-Header gesendet werden, der den Typ des Tokens und das Access Token selbst enthält. Derzeit ist der Typ des Tokens immer Bearer. Da wir überprüfen müssen, ob das Token nicht abgelaufen ist und es nach einer Stunde seit der Ausgabe aktualisieren müssen, habe ich eine Anfrage an eine andere Funktion angegeben, die das Access Token zurückgibt. Dieser Codeausschnitt befindet sich auch am Anfang des Skripts, wenn das erste Access Token abgerufen 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 abgelaufen. Erneuerung..."
$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üfung der Anmeldung 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' - ausgesetzt: '$user.Suspended
$GAresult = $user
}
if ($GAresult) {
$return = $GAresult
} else {$return = 'gg'}
return $return
}Die E-Mail-Abfrage: $query fordert die API auf, nach einem Nutzer mit dieser E-Mail zu suchen, einschließlich etwaiger Aliase. Wildcards können ebenfalls verwendet werden: =, :, :{PREFIX}*.
Für den Datenabruf wird die GET-Methode verwendet, um Daten einzufügen (Konto erstellen oder Mitglied zu einer Gruppe hinzufügen) – POST, um bestehende Daten zu aktualisieren – PUT und zum Löschen eines Datensatzes (z. B. ein Mitglied aus einer Gruppe) – DELETE.
Das Skript wird auch nach einer Telefonnummer (nicht validierte Zeichenfolge) und der Teilnahme an einer regionalen Versandgruppe fragen. Es bestimmt, welche Organisationseinheit der Benutzer auf Grundlage der gewählten OU im Active Directory haben sollte, und generiert ein Passwort:
do {
$phone = Read-Host "Telefon im Format +7хххххххх"
} while (-not $phone)
do {
$moscow = Read-Host "Im Moskauer Büro? (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 "Wird in /Team delivery erstellt"
$orgunit = "/Team delivery"
}
$Password = -join (48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba"
Und beginnt dann mit den Manipulationen am Konto:
$query = @{
email = $email
givenName = $firstname
familyName = $lastname
password = $password
phone = $phone
orgunit = $orgunit
}
if ($GMailExist) {
Write-Host "Ändern des Kontos wird gestartet" -f mag
(GoogleQuery 'UpdateAccount' $query) | fl
write-host "Vergiss nicht, die Gruppen des aktivierten $Username in Google zu überprüfen."
} else {
Write-Host "Kontoerstellung wird gestartet" -f mag
(GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
write-host "Hinzufügen zur Gruppe moscowoffice"
$query = @{
groupkey = 'moscowoffice@rocketguys.com'
email = $email
}
(GoogleQuery 'AddMember' $query) | fl
}
Die Funktionen zum Aktualisieren und Erstellen von Konten haben eine ähnliche Syntax. Nicht alle zusätzlichen Felder sind obligatorisch, im Abschnitt mit den Telefonnummern muss ein Array angegeben werden, das mindestens einen Datensatz mit der Nummer und ihrem Typ enthalten kann.
Um einen Fehler beim Hinzufügen eines Benutzers zu einer Gruppe zu vermeiden, können wir zunächst überprüfen, ob er bereits in dieser Gruppe ist, indem wir die Liste der Gruppenmitglieder abrufen oder die Zusammensetzung des Benutzers überprüfen.
Die Anfrage an die Gruppenmitgliedschaft eines bestimmten Benutzers wird nicht rekursiv sein und zeigt nur die unmittelbare Mitgliedschaft an. Das Hinzufügen eines Benutzers zu einer übergeordneten Gruppe, in der bereits eine untergeordnete Gruppe existiert, deren Mitglied der Benutzer ist, wird erfolgreich sein.
Fazit
Es bleibt nur, dem Benutzer das Passwort für das neue Konto zuzusenden. Wir tun dies per SMS und senden die allgemeinen Informationen mit Anweisungen und Benutzernamen an die persönliche E-Mail-Adresse, die die Personalabteilung zusammen mit der Telefonnummer bereitgestellt hat. Als alternative Möglichkeit kann man Geld sparen und das Passwort in einem geheimen Telegramm-Chat senden, was auch als zweite Authentifizierung betrachtet werden kann (Ausnahme sind MacBooks).
Danke, dass Sie bis zum Ende gelesen haben. Ich freue mich über Vorschläge zur Verbesserung des Schreibstils von Artikeln und wünsche Ihnen, dass Sie beim Schreiben von Skripten weniger Fehler machen =)
Eine Liste von Links, die thematisch nützlich sein könnten oder einfach auf auftretende Fragen antworten:
Quelle: habr.com
