Crearea utilizatorilor Google din PowerShell prin API

Hi!

Acest articol va descrie implementarea interacțiunii PowerShell cu API-ul Google pentru a manipula utilizatorii G Suite.

Folosim mai multe servicii interne și cloud în cadrul organizației. În cea mai mare parte, autorizarea în ele se reduce la Google sau Active Directory, între care nu putem menține o replică; prin urmare, atunci când un nou angajat pleacă, trebuie să creați/activați un cont în aceste două sisteme. Pentru a automatiza procesul, am decis să scriem un script care colectează informații și le trimite ambelor servicii.

autorizare

La elaborarea cerințelor, am decis să folosim administratori umani reali pentru autorizare; acest lucru simplifică analiza acțiunilor în cazul unor modificări masive accidentale sau intenționate.

API-urile Google folosesc protocolul OAuth 2.0 pentru autentificare și autorizare. Cazuri de utilizare și descrieri mai detaliate pot fi găsite aici: Utilizarea OAuth 2.0 pentru a accesa API-urile Google.

Am ales scriptul care este folosit pentru autorizare în aplicațiile desktop. Există, de asemenea, o opțiune de utilizare a unui cont de serviciu, care nu necesită mișcări inutile din partea utilizatorului.

Imaginea de mai jos este o descriere schematică a scenariului selectat de pe pagina Google.

Crearea utilizatorilor Google din PowerShell prin API

  1. În primul rând, trimitem utilizatorul la pagina de autentificare a Contului Google, specificând parametrii GET:
    • ID aplicație
    • zonele la care aplicația are nevoie de acces
    • adresa către care utilizatorul va fi redirecționat după finalizarea procedurii
    • modul în care vom actualiza jetonul
    • Cod de securitate
    • formatul de transmitere a codului de verificare

  2. După finalizarea autorizației, utilizatorul va fi redirecționat către pagina specificată în prima solicitare, cu un cod de eroare sau de autorizare transmis de parametrii GET
  3. Aplicația (scriptul) va trebui să primească acești parametri și, dacă a primit codul, să facă următoarea solicitare pentru a obține jetoane
  4. Dacă solicitarea este corectă, API-ul Google returnează:
    • Token de acces cu care putem face cereri
    • Perioada de valabilitate a acestui simbol
    • Jeton de reîmprospătare necesar pentru a reîmprospăta jetonul de acces.

Mai întâi trebuie să accesați consola Google API: Acreditări - Consola API Google, selectați aplicația dorită și în secțiunea Acreditări creați un identificator OAuth client. Acolo (sau mai târziu, în proprietățile identificatorului creat) trebuie să specificați adresele către care este permisă redirecționarea. În cazul nostru, acestea vor fi mai multe intrări localhost cu porturi diferite (vezi mai jos).

Pentru a face mai convenabil citirea algoritmului de script, puteți afișa primii pași într-o funcție separată care va returna jetoanele de acces și de reîmprospătare pentru aplicație:

$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

Setăm ID-ul clientului și Secretul clientului obținute în proprietățile identificatorului clientului OAuth, iar verificatorul de cod este un șir de 43 până la 128 de caractere care trebuie generat aleatoriu din caractere nerezervate: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Acest cod va fi apoi transmis din nou. Elimină vulnerabilitatea în care un atacator ar putea intercepta un răspuns returnat ca redirecționare după autorizarea utilizatorului.
Puteți trimite un verificator de cod în cererea curentă în text clar (ceea ce îl face lipsit de sens - acest lucru este potrivit doar pentru sistemele care nu acceptă SHA256) sau prin crearea unui hash folosind algoritmul SHA256, care trebuie să fie codificat în BASE64Url (diferând din Base64 cu două caractere de tabel) și eliminând terminațiile liniei de caractere: =.

Apoi, trebuie să începem să ascultăm http pe mașina locală pentru a primi un răspuns după autorizare, care va fi returnat ca redirecționare.

Sarcinile administrative sunt efectuate pe un server special, nu putem exclude posibilitatea ca mai mulți administratori să ruleze scriptul în același timp, așa că va selecta aleatoriu un port pentru utilizatorul curent, dar am specificat porturi predefinite deoarece acestea trebuie, de asemenea, adăugate ca de încredere în consola API.

access_type=offline înseamnă că aplicația poate actualiza un token expirat pe cont propriu, fără a interacționa utilizatorul cu browserul,
tip de răspuns=cod setează formatul modului în care va fi returnat codul (o referire la vechea metodă de autorizare, când utilizatorul a copiat codul din browser în script),
domeniu indică domeniul și tipul de acces. Acestea trebuie separate prin spații sau %20 (în conformitate cu codificarea URL). O listă de zone de acces cu tipuri poate fi văzută aici: Domenii OAuth 2.0 pentru API-urile Google.

După primirea codului de autorizare, aplicația va returna browserului un mesaj de închidere, va opri ascultarea pe port și va trimite o solicitare POST pentru a obține tokenul. Indicăm în acesta id-ul și secretul specificat anterior din API-ul consolei, adresa către care va fi redirecționat utilizatorul și grant_type în conformitate cu specificația protocolului.

Ca răspuns, vom primi un token de acces, perioada de valabilitate a acestuia în secunde și un jeton de reîmprospătare, cu care putem actualiza jetonul de acces.

Aplicația trebuie să stocheze token-urile într-un loc sigur cu o durată de valabilitate lungă, astfel încât până când nu revocăm accesul primit, aplicația nu va returna jetonul de reîmprospătare. La sfârșit, am adăugat o solicitare de revocare a jetonului; dacă aplicația nu a fost finalizată cu succes și jetonul de reîmprospătare nu a fost returnat, va începe procedura din nou (am considerat că este nesigur să stocăm token-urile local pe terminal și nu nu vreau să complic lucrurile cu criptografie sau să deschid browserul frecvent).

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
}

După cum ați observat deja, atunci când revocați un token, este utilizat Invoke-WebRequest. Spre deosebire de Invoke-RestMethod, nu returnează datele primite într-un format utilizabil și arată starea cererii.

Apoi, scriptul vă solicită să introduceți numele și prenumele utilizatorului, generând un login + e-mail.

cereri

Următoarele solicitări vor fi - în primul rând, trebuie să verificați dacă un utilizator cu aceeași autentificare există deja pentru a obține o decizie privind crearea unuia nou sau activarea celui actual.

Am decis să implementez toate cererile în formatul unei singure funcții cu o selecție, folosind comutatorul:

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'])
      }
    }
  }
}

În fiecare solicitare, trebuie să trimiteți un antet de autorizare care să conțină tipul de jeton și jetonul de acces însuși. În prezent, tipul de jeton este întotdeauna Bearer. Deoarece trebuie să verificăm că token-ul nu a expirat și să îl actualizăm după o oră din momentul în care a fost emis, am specificat o solicitare pentru o altă funcție care returnează un token de acces. Aceeași bucată de cod se află la începutul scriptului când se primește primul token de acces:

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
}

Verificarea autentificarii pentru existenta:

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
}

Solicitarea email:$query va cere API-ului să caute un utilizator cu exact acel e-mail, inclusiv aliasuri. De asemenea, puteți utiliza wildcard: =, :, :{PREFIX}*.

Pentru a obține date, utilizați metoda de solicitare GET, pentru a insera date (crearea unui cont sau adăugarea unui membru într-un grup) - POST, pentru a actualiza datele existente - PUT, pentru a șterge o înregistrare (de exemplu, un membru dintr-un grup) - ȘTERGE.

Scriptul va cere, de asemenea, un număr de telefon (un șir nevalidat) și includerea într-un grup de distribuție regional. Acesta decide ce unitate organizațională ar trebui să aibă utilizatorul pe baza OU-ului Active Directory selectat și vine cu o parolă:

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"

Și apoi începe să manipuleze contul:

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

Funcțiile pentru actualizarea și crearea unui cont au o sintaxă similară; nu sunt necesare toate câmpurile suplimentare; în secțiunea cu numere de telefon, trebuie să specificați o matrice care poate conține până la o înregistrare cu numărul și tipul acesteia.

Pentru a nu primi o eroare la adăugarea unui utilizator într-un grup, putem verifica mai întâi dacă acesta este deja membru al acestui grup obținând o listă cu membrii grupului sau o compoziție de la utilizatorul însuși.

Interogarea apartenenței la grup a unui anumit utilizator nu va fi recursivă și va afișa doar apartenența directă. Includerea unui utilizator într-un grup părinte care are deja un grup copil din care utilizatorul este membru va reuși.

Concluzie

Tot ce rămâne este să trimiteți utilizatorului parola pentru noul cont. Facem acest lucru prin SMS și trimitem informații generale cu instrucțiuni și logare la un e-mail personal, care, împreună cu un număr de telefon, a fost furnizat de departamentul de recrutare. Ca alternativă, puteți economisi bani și trimite parola către un chat secret telegram, care poate fi considerat și al doilea factor (MacBooks va fi o excepție).

Vă mulțumesc că ați citit până la capăt. Voi fi bucuros să văd sugestii pentru îmbunătățirea stilului de a scrie articole și vă doresc să surprindeți mai puține erori atunci când scrieți scripturi =)

Lista de link-uri care pot fi utile tematic sau pur și simplu răspund la întrebări:

Sursa: www.habr.com

Adauga un comentariu