Hallo!
In diesem Artikel wird die Implementierung der Interaktion zwischen PowerShell und der Google API beschrieben, um Manipulationen an G Suite-Benutzern durchzuführen.
In unserem Unternehmen nutzen wir verschiedene interne und Cloud-Dienste. Die Authentifizierung erfolgt größtenteils über Google oder Active Directory, zwischen denen wir keine Replikation unterstützen können. Daher muss beim Eintritt eines neuen Mitarbeiters ein Konto in beiden Systemen erstellt bzw. 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 entschieden, echte Administratoren für die Authentifizierung zu verwenden, um die Analyse von Handlungen bei versehentlichen oder absichtlichen Massenänderungen zu vereinfachen.
Für die Authentifizierung und Autorisierung verwenden die Google APIs das OAuth 2.0-Protokoll. Nutzungsszenarien und eine detailliertere Beschreibung finden Sie hier: .
Ich habe das Szenario gewählt, das bei der Autorisierung in Desktop-Anwendungen verwendet wird. Es gibt auch die Möglichkeit, ein Dienstkonto zu verwenden, das keine zusätzlichen Maßnahmen vom Benutzer verlangt.
Das Bild unten ist eine schematische Darstellung des ausgewählten Szenarios von der Google-Seite.

- Zunächst leiten wir den Benutzer zur Authentifizierungsseite des Google-Kontos weiter und geben dabei die GET-Parameter an:
- Anwendungs-ID
- Berechtigungsbereich, auf den die Anwendung zugreifen muss
- URL, auf die der Benutzer nach Abschluss des Verfahrens umgeleitet wird
- Methode, mit der wir das Token aktualisieren werden
- Überprüfungscode
- Format der Übertragung des Überprüfungscodes
- Nach Abschluss der Autorisierung wird der Benutzer auf die im ersten Antrag angegebene Seite umgeleitet, mit der Fehlermeldung oder dem Autorisierungscode, die über GET-Parameter übermittelt werden.
- Die Anwendung (das Skript) muss diese Parameter abrufen und, im Falle eines erhaltenen Codes, die nächste Anfrage zur Tokenbeschaffung durchführen.
- Bei korrekter Anfrage gibt die Google API zurück:
- Access-Token, mit dem wir Anfragen stellen können
- Gültigkeitsdauer dieses Tokens
- Refresh-Token, das erforderlich ist, 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 Abschnitt Anmeldeinformationen eine OAuth-Client-ID. Dort (oder später in den Eigenschaften der erstellten ID) müssen die Adressen angegeben werden, auf die eine Weiterleitung erlaubt ist. In unserem Fall wird es mehrere localhost-Einträge mit verschiedenen Ports sein (siehe weiter unten).
Um die Lesbarkeit des Skriptalgorithmus zu verbessern, können Sie die ersten Schritte in eine separate Funktion auslagern, die die Access- und Refresh-Token für die Anwendung zurückgibt:
$client_secret = 'Unser Client Secret'
$client_id = 'Unser 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 {
"Listener konnte nicht gestartet werden."
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 dieses Browser-Tab 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 setzen die Client-ID und das Client Secret aus den Eigenschaften des OAuth-Client-Identifiers ein. Der Code-Verifier ist eine Zeichenfolge 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 anschließend erneut übertragen. Damit wird eine Sicherheitsanfälligkeit ausgeschlossen, bei der ein Angreifer die Antwort abfangen könnte, die nach der Autorisierung des Benutzers durch eine Weiterleitung zurückkommt.
Den Code-Verifier kann man im aktuellen Request offen übermitteln (was ihn bedeutungslos macht – das eignet sich nur für Systeme, die SHA256 nicht unterstützen), oder man erstellt einen Hash nach dem SHA256-Algorithmus, der in BASE64Url kodiert werden muss (unterscheidet sich von Base64 durch zwei Zeichentabellen) und das Zeilenende-Zeichen: = entfernt.
Jetzt müssen wir anfangen, HTTP auf der lokalen Maschine abzuhören, um die Antwort nach der Autorisierung zu erhalten, die durch eine Weiterleitung zurückkommt.
Administrative Aufgaben werden auf einem speziellen Server ausgeführt. Wir können die Wahrscheinlichkeit nicht ausschließen, dass mehrere Administratoren gleichzeitig ein Skript ausführen. Daher wählt das Skript zufällig einen Port für den aktuellen Benutzer. Ich habe jedoch im Voraus definierte Ports angegeben, da diese ebenfalls als vertrauenswürdig in der API-Konsole hinzugefügt werden müssen.
access_type=offline bedeutet, dass die Anwendung den abgelaufenen Token selbständig aktualisieren kann, ohne dass der Benutzer mit dem Browser interagiert.
response_type=code legt das Format fest, in dem der Code zurückgegeben wird (dies erinnert an die alte Methode der Autorisierung, bei der der Benutzer den Code aus dem Browser in das Skript kopierte).
scope zeigt die Bereiche und den Zugriffstyp an. Sie müssen durch Leerzeichen oder (gemäß URL-Encoding) getrennt werden. Eine Liste der Zugriffsbereiche mit Typen finden Sie hier: .
Nach Erhalt des Autorisierungscodes sendet die Anwendung eine Nachricht an den Browser, dass sie schließen wird, hört auf, den Port zu überwachen, und sendet eine POST-Anfrage zur Token-Erhaltung. Dabei geben wir die zuvor festgelegten IDs und Secrets aus der API-Konsole, die Adresse, zu der der Benutzer umgeleitet wird, 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 Token an einem sicheren Ort mit langer Speicherdauer aufbewahren. Solange der erhaltene Zugriff nicht zurückgezogen wird, erhält die Anwendung kein Refresh-Token. Am Ende habe ich eine Anfrage zum Widerruf des Tokens hinzugefügt, falls die Anwendung nicht erfolgreich abgeschlossen wurde und das Refresh-Token nicht zurückgegeben wurde, damit sie den Prozess von vorne beginnen kann (wir halten es für unsicher, Tokens lokal auf dem Terminal zu speichern, und möchten die kryptographischen Anforderungen oder häufige Browseröffnungen vermeiden).
do {
$token_result = Get-GoogleAuthToken
$token = $token_result.access_token
if ($token_result.refresh_token -eq $null) {
Write-Host ("Sitzung ist nicht zerstört. Token wird widerrufen...")
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 beim Zurückziehen des Tokens Invoke-WebRequest verwendet. Im Gegensatz zu Invoke-RestMethod gibt er die empfangenen Daten nicht in einem benutzerfreundlichen Format zurück und zeigt den Status der Anfrage an.
Als Nächstes wird das Skript aufgefordert, Vorname und Nachname des Benutzers einzugeben, wobei der Benutzername und die E-Mail generiert werden.
Abfragen
Die nächsten Schritte beinhalten Anfragen – zunächst muss überprüft werden, ob bereits ein Benutzer mit diesem Benutzernamen existiert, um zu entscheiden, ob ein neuer erstellt oder der bestehende aktiviert werden soll.
Ich habe mich entschieden, alle Anfragen in einem einzigen Funktionsformat mit einer Auswahl unter Verwendung von 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']+" ist jetzt ein Mitglied von "+$query['groupkey'])
}
}
}
}Bei jeder Anfrage muss ein Authorization-Header gesendet werden, der den Typ des Tokens und das Access-Token selbst enthält. Der Token-Typ ist derzeit immer Bearer. Da wir sicherstellen müssen, dass das Token nicht abgelaufen ist und es eine Stunde nach Ausstellung aktualisieren müssen, habe ich eine Anforderung an eine andere Funktion angegeben, die das Access-Token zurückgibt. Dieser Codeabschnitt 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. Aktualisiere..."
$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 des Benutzernamens 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' - gesperrt: '$user.Suspended
$GAresult = $user
}
if ($GAresult) {
$return = $GAresult
} else {$return = 'gg'}
return $return
}Die E-Mail-Abfrage: $query fordert die API auf, nach einem Benutzer mit genau dieser E-Mail zu suchen, einschließlich gefundener Aliase. Wildcards können ebenfalls verwendet werden: =, :, :{PREFIX}*.
Für den Abruf von Daten wird die GET-Anfragemethode verwendet, zum Einfügen von Daten (Erstellen eines Kontos oder Hinzufügen eines Teilnehmers zu einer Gruppe) – POST, zum Aktualisieren bestehender Daten – PUT, zum Löschen eines Eintrags (z. B. Teilnehmer aus einer Gruppe) – DELETE.
Das Skript fragt auch nach der Telefonnummer (nicht valide String) und ob der Benutzer in die regionale Versandgruppe aufgenommen werden möchte. Es bestimmt, welche organisatorische Einheit der Benutzer basierend auf der gewählten OU im Active Directory haben soll, 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 fährt dann mit den Manipulationen am Konto fort:
$query = @{
email = $email
givenName = $firstname
familyName = $lastname
password = $password
phone = $phone
orgunit = $orgunit
}
if ($GMailExist) {
Write-Host "Wir starten die Kontenänderung" -f mag
(GoogleQuery 'UpdateAccount' $query) | fl
write-host "Vergessen Sie nicht, die Gruppen von $Username in Google zu überprüfen."
} else {
Write-Host "Wir starten die Kontoerstellung" -f mag
(GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
write-host "Fügen wir die Gruppe moscowoffice hinzu"
$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 Telefonnummern muss ein Array angegeben werden, das mindestens einen Eintrag mit der Nummer und ihrem Typ enthalten kann.
Um einen Fehler beim Hinzufügen eines Benutzers zu einer Gruppe zu vermeiden, können wir zuerst überprüfen, ob er bereits in dieser Gruppe ist, indem wir die Mitgliederliste der Gruppe oder die Zugehörigkeit des Benutzers selbst abrufen.
Die Abfrage der Gruppenzugehörigkeit eines bestimmten Benutzers erfolgt nicht rekursiv und zeigt nur die unmittelbare Mitgliedschaft an. Das Hinzufügen eines Benutzers zur übergeordneten Gruppe, in der bereits eine untergeordnete Gruppe existiert, deren Mitglied der Benutzer ist, wird erfolgreich sein.
Fazit
Es bleibt nur noch, das Passwort für das neue Konto an den Benutzer zu senden. Dies geschieht per SMS, während wir allgemeine Informationen mit Anleitung und Login an die persönliche E-Mail schicken, die von der Personalabteilung zusammen mit der Telefonnummer bereitgestellt wurde. Als alternative Option können Sie Geld sparen und das Passwort im geheimen Telegram-Chat versenden, was ebenfalls als zweiter Faktor betrachtet werden kann (mit Ausnahme von MacBooks).
Vielen Dank, dass Sie bis zum Ende gelesen haben. Ich freue mich auf Vorschläge zur Verbesserung des Schreibstils und wünsche Ihnen, dass Sie beim Schreiben von Skripten möglichst wenige Fehler machen =)
Eine Liste von Links, die thematisch nützlich sein könnten oder einfach Fragen beantworten:
Quelle: habr.com
