Creazione di utenti Google da PowerShell tramite API

Hi!

Questo articolo descriverà l'implementazione dell'interazione di PowerShell con l'API di Google per manipolare gli utenti di G Suite.

Utilizziamo diversi servizi interni e cloud in tutta l'organizzazione. Per la maggior parte, l'autorizzazione in essi spetta a Google o Active Directory, tra i quali non possiamo mantenere una replica; di conseguenza, quando un nuovo dipendente lascia, è necessario creare/abilitare un account in questi due sistemi. Per automatizzare il processo, abbiamo deciso di scrivere uno script che raccolga informazioni e le invii a entrambi i servizi.

Autorizzazione

Nella stesura dei requisiti abbiamo deciso di avvalerci di veri amministratori umani per l'autorizzazione; questo semplifica l'analisi delle azioni in caso di modifiche massicce accidentali o intenzionali.

Le API di Google utilizzano il protocollo OAuth 2.0 per l'autenticazione e l'autorizzazione. Casi d'uso e descrizioni più dettagliate possono essere trovati qui: Utilizzo di OAuth 2.0 per accedere alle API di Google.

Ho scelto lo script utilizzato per l'autorizzazione nelle applicazioni desktop. C'è anche la possibilità di utilizzare un account di servizio, che non richiede movimenti non necessari da parte dell'utente.

L'immagine seguente è una descrizione schematica dello scenario selezionato dalla pagina Google.

Creazione di utenti Google da PowerShell tramite API

  1. Per prima cosa inviamo l'utente alla pagina di autenticazione dell'account Google, specificando i parametri GET:
    • ID applicazione
    • aree a cui l'applicazione deve accedere
    • l'indirizzo al quale l'utente verrà reindirizzato una volta completata la procedura
    • il modo in cui aggiorneremo il token
    • Codice di sicurezza
    • formato di trasmissione del codice di verifica

  2. Una volta completata l'autorizzazione, l'utente verrà reindirizzato alla pagina specificata nella prima richiesta, con un errore o un codice di autorizzazione passato dai parametri GET
  3. L'applicazione (script) dovrà ricevere questi parametri e, se ricevuto il codice, effettuare la seguente richiesta per ottenere i token
  4. Se la richiesta è corretta, l'API di Google restituisce:
    • Token di accesso con il quale possiamo effettuare richieste
    • Il periodo di validità di questo token
    • Token di aggiornamento necessario per aggiornare il token di accesso.

Per prima cosa devi andare alla console dell'API di Google: Credenziali - Console API di Google, seleziona l'applicazione desiderata e nella sezione Credenziali crea un identificatore OAuth del client. Lì (o successivamente, nelle proprietà dell'identificatore creato) è necessario specificare gli indirizzi a cui è consentito il reindirizzamento. Nel nostro caso, si tratterà di diverse voci localhost con porte diverse (vedi sotto).

Per rendere più comoda la lettura dell'algoritmo dello script, è possibile visualizzare i primi passaggi in una funzione separata che restituirà i token di accesso e di aggiornamento per l'applicazione:

$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

Impostiamo il Client ID e il Client Secret ottenuti nelle proprietà dell'identificatore client OAuth e il verificatore del codice è una stringa da 43 a 128 caratteri che deve essere generata casualmente da caratteri non riservati: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Questo codice verrà poi trasmesso nuovamente. Elimina la vulnerabilità in cui un utente malintenzionato potrebbe intercettare una risposta restituita come reindirizzamento dopo l'autorizzazione dell'utente.
Puoi inviare un verificatore di codice nella richiesta corrente in testo non crittografato (il che lo rende privo di significato - è adatto solo per sistemi che non supportano SHA256) o creando un hash utilizzando l'algoritmo SHA256, che deve essere codificato in BASE64Url (diverso da Base64 con due caratteri di tabella) e rimuovendo le terminazioni di riga dei caratteri: =.

Successivamente, dobbiamo iniziare ad ascoltare http sul computer locale per ricevere una risposta dopo l'autorizzazione, che verrà restituita come reindirizzamento.

Le attività amministrative vengono eseguite su un server speciale, non possiamo escludere la possibilità che più amministratori eseguano lo script contemporaneamente, quindi selezionerà casualmente una porta per l'utente corrente, ma ho specificato porte predefinite perché devono inoltre essere aggiunti come attendibili nella console API.

tipo_accesso=non in linea significa che l'applicazione può aggiornare autonomamente un token scaduto senza l'interazione dell'utente con il browser,
tipo_risposta=codice imposta il formato in cui verrà restituito il codice (un riferimento al vecchio metodo di autorizzazione, quando l'utente copiava il codice dal browser nello script),
portata indica l'ambito e la tipologia di accesso. Devono essere separati da spazi o %20 (secondo la codifica URL). Un elenco delle aree di accesso con i tipi può essere visualizzato qui: Ambiti OAuth 2.0 per le API di Google.

Dopo aver ricevuto il codice di autorizzazione, l'applicazione restituirà un messaggio di chiusura al browser, smetterà di ascoltare sulla porta e invierà una richiesta POST per ottenere il token. Indichiamo in esso l'ID e il segreto precedentemente specificati dall'API della console, l'indirizzo a cui verrà reindirizzato l'utente e Grant_Type in conformità con le specifiche del protocollo.

In risposta, riceveremo un token di accesso, il suo periodo di validità in secondi e un token di aggiornamento, con il quale potremo aggiornare il token di accesso.

L'applicazione deve archiviare i token in un luogo sicuro con una lunga durata, quindi finché non revocheremo l'accesso ricevuto, l'applicazione non restituirà il token di aggiornamento. Alla fine ho aggiunto una richiesta di revoca del token; se l'applicazione non è stata completata con successo e non è stato restituito il token di aggiornamento, ricomincerà la procedura (abbiamo ritenuto non sicuro archiviare i token localmente sul terminale e non non voglio complicare le cose con la crittografia o aprire frequentemente il browser).

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
}

Come hai già notato, quando si revoca un token, viene utilizzato Invoke-WebRequest. A differenza di Invoke-RestMethod, non restituisce i dati ricevuti in un formato utilizzabile e mostra lo stato della richiesta.

Successivamente, lo script ti chiede di inserire il nome e il cognome dell'utente, generando un login + email.

richieste

Le prossime richieste saranno: innanzitutto è necessario verificare se esiste già un utente con lo stesso login per poter decidere se crearne uno nuovo o abilitare quello attuale.

Ho deciso di implementare tutte le richieste nel formato di una funzione con una selezione, utilizzando 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']+" now is a member of "+$query['groupkey'])
      }
    }
  }
}

In ogni richiesta è necessario inviare un'intestazione di autorizzazione contenente il tipo di token e il token di accesso stesso. Attualmente il tipo di token è sempre Bearer. Perché dobbiamo verificare che il token non sia scaduto e aggiornarlo dopo un'ora dal momento dell'emissione, ho specificato una richiesta per un'altra funzione che restituisca un token di accesso. Lo stesso pezzo di codice si trova all'inizio dello script quando si riceve il primo token di accesso:

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
}

Verifica dell'esistenza del login:

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
}

La richiesta email:$query chiederà all'API di cercare un utente con esattamente quell'email, inclusi gli alias. Puoi anche usare il carattere jolly: =, :, :{PREFIX}*.

Per ottenere i dati, utilizzare il metodo di richiesta GET, per inserire dati (creando un account o aggiungendo un membro a un gruppo) - POST, per aggiornare i dati esistenti - PUT, per eliminare un record (ad esempio, un membro di un gruppo) - ELIMINARE.

Lo script richiederà inoltre un numero di telefono (una stringa non convalidata) e l'inclusione in un gruppo di distribuzione regionale. Decide quale unità organizzativa l'utente dovrebbe avere in base all'unità organizzativa di Active Directory selezionata e fornisce una password:

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"

E poi inizia a manipolare il conto:

$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
}

Le funzioni per l'aggiornamento e la creazione di un account hanno una sintassi simile; non tutti i campi aggiuntivi sono obbligatori; nella sezione con i numeri di telefono è necessario specificare un array che può contenere fino a un record con il numero e il suo tipo.

Per non ricevere un errore quando aggiungiamo un utente ad un gruppo, possiamo prima verificare se è già membro di questo gruppo ottenendo dall'utente stesso un elenco dei membri del gruppo o della composizione.

La query sull'appartenenza al gruppo di un utente specifico non sarà ricorsiva e mostrerà solo l'appartenenza diretta. L'inclusione di un utente in un gruppo principale che dispone già di un gruppo secondario di cui è membro avrà esito positivo.

conclusione

Non resta che inviare all'utente la password per il nuovo account. Lo facciamo tramite SMS e inviamo informazioni generali con istruzioni e accesso a un'e-mail personale che, insieme a un numero di telefono, è stata fornita dal dipartimento di reclutamento. In alternativa puoi risparmiare e inviare la tua password a una chat segreta di Telegram, che può essere considerato anche il secondo fattore (i MacBook faranno un'eccezione).

Grazie per aver letto fino alla fine. Sarò felice di vedere suggerimenti per migliorare lo stile di scrittura degli articoli e ti auguro di rilevare meno errori durante la scrittura degli script =)

Elenco di link che possono essere tematicamente utili o semplicemente rispondere a domande:

Fonte: habr.com

Aggiungi un commento