Erstellen von Google-Benutzern aus PowerShell über die API

Hallo!

In diesem Artikel wird die Implementierung der PowerShell-Interaktion mit der Google-API zur Manipulation von G Suite-Benutzern beschrieben.

Wir nutzen im gesamten Unternehmen mehrere interne und Cloud-Dienste. In den meisten Fällen erfolgt die Autorisierung bei Google oder Active Directory, zwischen denen wir keine Replikate pflegen können. Wenn ein neuer Mitarbeiter ausscheidet, müssen Sie dementsprechend ein Konto in diesen beiden Systemen erstellen/aktivieren. Um den Prozess zu automatisieren, haben wir uns entschieden, ein Skript zu schreiben, das Informationen sammelt und an beide Dienste sendet.

Genehmigung

Bei der Erstellung der Anforderungen haben wir uns für die Autorisierung durch echte menschliche Administratoren entschieden, was die Analyse von Aktionen im Falle versehentlicher oder absichtlicher massiver Änderungen vereinfacht.

Google APIs verwenden das OAuth 2.0-Protokoll zur Authentifizierung und Autorisierung. Anwendungsfälle und detailliertere Beschreibungen 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 von der Google-Seite.

Erstellen von Google-Benutzern aus PowerShell über die API

  1. Zuerst leiten wir den Benutzer zur Authentifizierungsseite des Google-Kontos weiter und geben dabei GET-Parameter an:
    • Anwendungs-ID
    • Bereiche, auf die die Anwendung Zugriff benötigt
    • die Adresse, an die der Nutzer nach Abschluss des Vorgangs weitergeleitet wird
    • die Art und Weise, wie wir das Token aktualisieren
    • Sicherheitscode
    • Format für die Übertragung des Bestätigungscodes

  2. Nach Abschluss der Autorisierung wird der Benutzer auf die in der ersten Anfrage angegebene Seite weitergeleitet, wobei ein Fehler oder ein Autorisierungscode über GET-Parameter übergeben wird
  3. Die Anwendung (das Skript) muss diese Parameter empfangen und, wenn sie den Code erhalten hat, die folgende Anfrage stellen, um Tokens zu erhalten
  4. Wenn die Anfrage korrekt ist, gibt die Google API Folgendes zurück:
    • Zugriffstoken, mit dem wir Anfragen stellen können
    • Die Gültigkeitsdauer dieses Tokens
    • Zum Aktualisieren des Zugriffstokens ist ein Aktualisierungstoken erforderlich.

Zuerst müssen Sie zur Google API-Konsole gehen: Anmeldeinformationen – Google API Console, wählen Sie die gewünschte Anwendung aus und erstellen Sie im Abschnitt „Anmeldeinformationen“ eine Client-OAuth-ID. Dort (oder später in den Eigenschaften des erstellten Identifikators) 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 anzeigen, 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 wir in den Eigenschaften der OAuth-Client-ID erhalten, und der Code-Verifizierer ist eine Zeichenfolge von 43 bis 128 Zeichen, die zufällig aus nicht reservierten Zeichen generiert werden muss: [AZ] / [az] / [0-9] / "-" / "." / "_" / "~".

Dieser Code wird dann erneut übermittelt. Es beseitigt die Schwachstelle, durch die ein Angreifer eine Antwort abfangen könnte, die nach der Benutzerautorisierung als Weiterleitung zurückgegeben wird.
Sie können einen Code-Verifizierer in der aktuellen Anfrage im Klartext senden (was ihn bedeutungslos macht – dies ist nur für Systeme geeignet, die SHA256 nicht unterstützen) oder indem Sie einen Hash mit dem SHA256-Algorithmus erstellen, der in BASE64Url codiert sein muss (anders). von Base64 um zwei Tabellenzeichen) und Entfernen der Zeichenzeilenenden: =.

Als nächstes müssen wir damit beginnen, http auf dem lokalen Computer abzuhören, um nach der Autorisierung eine Antwort zu erhalten, die als Weiterleitung 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, sodass zufällig ein Port für den aktuellen Benutzer ausgewählt wird. Ich habe jedoch vordefinierte Ports angegeben, weil Sie müssen außerdem in der API-Konsole als vertrauenswürdig hinzugefügt werden.

access_type=offline bedeutet, dass die Anwendung ein abgelaufenes Token selbstständig aktualisieren kann, ohne dass der Benutzer mit dem Browser interagiert.
Antworttyp=Code legt das Format fest, in dem der Code zurückgegeben wird (ein Verweis auf die alte Autorisierungsmethode, als der Benutzer den Code vom Browser in das Skript kopiert hat),
Umfang gibt den Umfang und die Art des Zugriffs an. Sie müssen durch Leerzeichen oder %20 (je nach URL-Kodierung) getrennt werden. Eine Liste der Zugangsbereiche mit Typen finden Sie hier: OAuth 2.0-Bereiche für Google APIs.

Nach Erhalt des Autorisierungscodes sendet die Anwendung eine Schließmeldung an den Browser zurück, beendet die Überwachung des Ports und sendet eine POST-Anfrage, um das Token abzurufen. Wir geben darin die zuvor angegebene ID und das Geheimnis der Konsolen-API, die Adresse, an die der Benutzer umgeleitet wird, und grant_type gemäß der Protokollspezifikation an.

Als Antwort erhalten wir einen Access-Token, dessen Gültigkeitsdauer in Sekunden und einen Refresh-Token, mit dem wir den Access-Token aktualisieren können.

Die Anwendung muss Tokens an einem sicheren Ort mit langer Haltbarkeit speichern. Bis wir den erhaltenen Zugriff widerrufen, wird die Anwendung das Aktualisierungstoken also nicht zurückgeben. Am Ende habe ich eine Aufforderung zum Widerruf des Tokens hinzugefügt; wenn der Antrag nicht erfolgreich abgeschlossen wurde und das Aktualisierungstoken nicht zurückgegeben wurde, wird der Vorgang erneut gestartet (wir hielten es für unsicher, Tokens lokal auf dem Terminal zu speichern, und wir verzichten darauf). Sie möchten die Dinge nicht mit Kryptografie 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 bereits bemerkt haben, wird beim Widerrufen eines Tokens Invoke-WebRequest verwendet. Im Gegensatz zu Invoke-RestMethod gibt es die empfangenen Daten nicht in einem verwendbaren Format zurück und zeigt den Status der Anfrage an.

Als nächstes fordert Sie das Skript auf, den Vor- und Nachnamen des Benutzers einzugeben, wodurch ein Login und eine E-Mail generiert werden.

Anfragen

Die nächsten Anfragen werden sein: Zunächst müssen Sie prüfen, ob bereits ein Benutzer mit demselben Login vorhanden ist, um eine Entscheidung über die Erstellung eines neuen oder die Aktivierung des aktuellen zu erhalten.

Ich habe beschlossen, alle Anfragen im Format einer Funktion mit einer Auswahl umzusetzen, indem ich den Schalter verwende:

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 Anfrage müssen Sie einen Autorisierungsheader senden, der den Tokentyp und das Zugriffstoken selbst enthält. Derzeit ist der Tokentyp immer Bearer. Weil Wir müssen überprüfen, ob das Token nicht abgelaufen ist, und es eine Stunde nach seiner Ausstellung aktualisieren. Ich habe eine Anfrage für eine andere Funktion angegeben, die ein Zugriffstoken zurückgibt. Der gleiche Codeabschnitt steht 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üfung 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
}

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

Um Daten zu erhalten, verwenden Sie die GET-Anfragemethode, um Daten einzufügen (Erstellen eines Kontos oder Hinzufügen eines Mitglieds zu einer Gruppe) – POST, um vorhandene Daten zu aktualisieren – PUT, um einen Datensatz zu löschen (z. B. ein Mitglied aus einer Gruppe) – LÖSCHEN.

Das Skript fragt außerdem nach einer Telefonnummer (einer nicht validierten Zeichenfolge) und nach der Aufnahme in eine regionale Verteilergruppe. Es entscheidet anhand der ausgewählten Active Directory-Organisationseinheit, welche Organisationseinheit der Benutzer haben soll, und erstellt ein Passwort:

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 er, 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 = '[email protected]'
    email = $email
  }
  (GoogleQuery 'AddMember' $query) | fl
}

Die Funktionen zum Aktualisieren und Erstellen eines Kontos haben eine ähnliche Syntax; nicht alle zusätzlichen Felder sind erforderlich; im Abschnitt mit Telefonnummern müssen Sie ein Array angeben, das bis zu einen Datensatz mit der Nummer und ihrem Typ enthalten kann.

Um beim Hinzufügen eines Benutzers zu einer Gruppe keine Fehlermeldung zu erhalten, können wir zunächst prüfen, ob er bereits Mitglied dieser Gruppe ist, indem wir vom Benutzer selbst eine Liste der Gruppenmitglieder oder der Gruppenzusammensetzung einholen.

Die Abfrage der Gruppenmitgliedschaft eines bestimmten Benutzers erfolgt nicht rekursiv und zeigt nur die direkte Mitgliedschaft an. Das Einschließen eines Benutzers in eine übergeordnete Gruppe, die bereits über eine untergeordnete Gruppe verfügt, in der der Benutzer Mitglied ist, ist erfolgreich.

Abschluss

Es bleibt nur noch, 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 eine persönliche E-Mail-Adresse, die zusammen mit einer Telefonnummer von der Personalabteilung bereitgestellt wurde. Alternativ können Sie Geld sparen und Ihr Passwort an einen geheimen Telegram-Chat senden, was auch als zweiter Faktor angesehen werden kann (MacBooks sind eine Ausnahme).

Vielen Dank, 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 =)

Liste von Links, die thematisch nützlich sein können oder einfach Fragen beantworten:

Source: habr.com

Kommentar hinzufügen