Crearea utilizatorilor Google din PowerShell prin API

Bună!

În acest articol, va fi descrisă implementarea interacțiunii între PowerShell și Google API pentru manipularea utilizatorilor G Suite.

În organizație, utilizăm mai multe servicii interne și cloud. În cea mai mare parte, autentificarea în acestea se reduce la Google sau Active Directory, între care nu putem menține o replică; prin urmare, atunci când un nou angajat intră, trebuie să creăm/activăm un cont în aceste două sisteme. Pentru a automatiza procesul, am decis să scriem un script care colectează informațiile și le trimite în ambele servicii.

Autorizare

În elaborarea cerințelor, am decis să folosim pentru autentificare persoane reale - administratori, ceea ce simplifică analiza acțiunilor în cazul modificărilor masive întâmplătoare sau intenționate.

Pentru autentificare și autorizare, Google API folosește protocolul OAuth 2.0. Scenariile de utilizare și o descriere mai detaliată pot fi găsite aici: Folosind OAuth 2.0 pentru a accesa Google APIs.

Am ales scenariul utilizat pentru autentificarea în aplicațiile desktop. Există, de asemenea, opțiunea de a utiliza un cont de serviciu, care nu necesită acțiuni suplimentare din partea utilizatorului.

Imaginea de mai jos reprezintă o descriere schematică a scenariului ales de pe pagina Google.

Crearea utilizatorilor Google din PowerShell prin API

  1. Mai întâi, trimitem utilizatorul pe pagina de autentificare a contului Google, specificând parametrii GET:
    • identificatorul aplicației
    • domeniile la care aplicația necesită acces
    • adresă la care utilizatorul va fi redirecționat după finalizarea procedurii
    • modul în care vom actualiza tokenul
    • codul de verificare
    • formatul de transmitere a codului de verificare

  2. După finalizarea autorizării, utilizatorul va fi redirecționat către pagina specificată în prima cerere, cu eroarea sau codul de autorizare transmise prin parametrii GET.
  3. Aplicația (scriptul) va trebui să preia acești parametri și, în cazul în care a obținut codul, să execute următoarea cerere pentru obținerea tokenurilor.
  4. La o cerere corectă, Google API returnează:
    • Token de acces, cu care putem face cereri
    • Durata de valabilitate a acestui token
    • Token de refresh, necesar pentru actualizarea tokenului de acces.

Mai întâi, trebuie să mergem în consola Google API: Credentiale — Google API Console, alege aplicația dorită și în secțiunea Credentials creează un ID OAuth client. Acolo (sau mai târziu, în proprietățile ID-ului creat) trebuie să specifici adresele la care este permis redirecționarea. În cazul nostru, vor fi mai multe înregistrări localhost cu porturi diferite (vezi mai departe).

Pentru a face algoritmul scriptului mai ușor de citit, putem extrage primele etape într-o funcție separată, care va returna token-urile Access și refresh pentru aplicație:

$client_secret = 'Clientul nostru Secret'
$client_id = 'Clientul nostru ID'
function Get-GoogleAuthToken {
  if (-not [System.Net.HttpListener]::IsSupported) {
    "HttpListener nu este suportat."
    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 {
    "Imposibil de pornit listener-ul."
    exit 1
  }
  while (($code -eq $null)) {
    $context = $listener.GetContext()
    Write-Host "Conexiune acceptată" -f 'mag'
    $url = $context.Request.RawUrl
    $code = $url.split('?')[1].split('=')[1].split('&')[0]
    if ($url.split('?')[1].split('=')[0] -eq 'error') {
      Write-Host "Eroare!"$code -f 'red'
      $buffer = [System.Text.Encoding]::UTF8.GetBytes("Eroare!"+$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("Acum poți închide acest tab de browser.")
    $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

Stabilim Client ID și Client Secret, obținute din proprietățile ID-ului client OAuth, iar code verifier - este un șir cu o lungime între 43 și 128 de caractere, care trebuie generat aleatoriu din caractere ne rezervate: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".

Ulterior, acest cod va fi retransmis. Acesta elimină vulnerabilitatea prin care un atacator ar putea intercepta răspunsul, returned by redirect after user authorization.
Trimiterea code verifier în cererea curentă se poate face în mod deschis (ceea ce îi anulează utilitatea – acest lucru se potrivește doar pentru sistemele care nu suportă SHA256) sau prin crearea unui hash folosind algoritmul SHA256, care trebuie codificat în BASE64Url (se deosebește de Base64 prin două simboluri din tabel) și eliminând simbolul de încheiere a liniei: =.

Apoi, trebuie să începem să ascultăm http pe mașina locală pentru a obține un răspuns după autorizare, care se va întoarce prin redirecționare.

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

access_type=offline înseamnă că aplicația poate actualiza tokenul expirat de sine stătător, fără interacțiunea utilizatorului cu browserul,
response_type=code specifică formatul în care se va întoarce codul (învechit, se referea la metoda veche de autorizare, când utilizatorul copia și lipsea codul din browser în script),
scope indică domeniile și tipul de acces. Acestea trebuie separate prin spații sau (conform URL Encoding). Lista domeniilor de acces cu tipurile poate fi văzută aici: OAuth 2.0 Scopes for Google APIs.

După obținerea codului de autorizare, aplicația va returna în browser un mesaj de închidere, va opri ascultarea portului și va trimite o cerere POST pentru obținerea tokenului. Specificăm în aceasta id-ul și secretul definite anterior din consola API, adresa la care utilizatorul va fi redirecționat și grant_type conform specificației protocolului.

În răspuns vom primi un token de acces, timpul său de valabilitate în secunde și un token de refresh, cu ajutorul căruia putem actualiza tokenul de acces.

Aplicația trebuie să stocheze tokenurile într-un loc sigur cu o durată lungă de stocare, așa că, până când nu revocăm accesul obținut, aplicației nu îi va reveni un token de refresh. La sfârșit, am adăugat o cerere de revocare a tokenului, dacă aplicația nu a fost finalizată cu succes și tokenul de refresh nu a fost obținut, aceasta va începe din nou procedura (am considerat nesigur să stocăm tokenurile local pe terminal și nu dorim să complicăm criptografia sau să deschidem frecvent browserul).

do {
  $token_result = Get-GoogleAuthToken
  $token = $token_result.access_token
  if ($token_result.refresh_token -eq $null) {
    Write-Host ("Sesiune neîntreruptă. Revocarea token-ului...")
    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
}

Așa cum ați observat, revocarea token-ului folosește Invoke-WebRequest. Spre deosebire de Invoke-RestMethod, acesta nu returnează datele obținute într-un format ușor de utilizat și afișează starea cererii.

Apoi, scriptul va cere să introduceți numele și prenumele utilizatorului, generând un nume de utilizator + email.

Cererile

Următoarele vor fi cererile - în primul rând, trebuie să verificăm dacă există deja un utilizator cu acest nume de utilizator pentru a decide dacă să formăm unul nou sau să activăm pe cel existent.

Am decis să implementez toate cererile într-un singur format de funcție cu selecție, folosind 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'])
      }
    }
  }
}

În fiecare cerere trebuie să trimitem antetul Authorization, care conține tipul de token și tokenul de acces propriu-zis. În prezent, tipul de token este întotdeauna Bearer. Deoarece trebuie să verificăm dacă tokenul nu a expirat și să-l actualizăm după o oră de la emitere, am inclus o cerere către o altă funcție care returnează tokenul de acces. Acest cod este inclus la începutul scriptului când obținem 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 existenței login-ului:

function Check_Google {
  $query = (GoogleQuery 'SearchAccount' $username)
  if ($query.users -ne $null) {
    $user = $query.users[0]
    Write-Host $user.name.fullName' - '$user.PrimaryEmail' - suspendat: '$user.Suspended
    $GAresult = $user
  }
  if ($GAresult) {
      $return = $GAresult
  } else {$return = 'gg'}
  return $return
}

Interogarea email: $query va cere API-ului să caute un utilizator exact cu acest email, inclusiv aliasurile vor fi găsite. De asemenea, se poate folosi wildcard: =, :, :{PREFIX}*.

Pentru obținerea datelor se utilizează metoda de interogare GET, pentru inserarea datelor (crearea unui cont sau adăugarea unui participant într-un grup) – POST, pentru actualizarea datelor existente – PUT, pentru ștergerea unui înregistrări (de exemplu, a unui participant dintr-un grup) – DELETE.

Scriptul va solicita de asemenea un număr de telefon (string nevalidat) și dacă utilizatorul dorește să fie inclus în grupul regional de distribuție. El decide care unitate organizațională ar trebui să aibă utilizatorul pe baza OU-ului Active Directory ales și generează o parolă:

do {
  $phone = Read-Host "Telefon în format +7xxxxxxx"
} while (-not $phone)
do {
    $moscow = Read-Host "În biroul din Moscova? (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 "Va fi creat în /Team delivery"
    $orgunit = "/Team delivery"
}
$Password = -join (48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba"

Și apoi începe manipulările cu contul:

$query = @{
  email = $email
  givenName = $firstname
  familyName = $lastname
  password = $password
  phone = $phone
  orgunit = $orgunit
}
if ($GMailExist) {
  Write-Host "Inițiem modificarea contului" -f mag
  (GoogleQuery 'UpdateAccount' $query) | fl
  write-host "Nu uita să verifici grupurile la care este inclus $Username în Google."
} else {
  Write-Host "Inițiem crearea contului" -f mag
  (GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
  write-host "Adăugăm în grupul moscowoffice"
  $query = @{
    groupkey = 'moscowoffice@rocketguys.com'
    email = $email
  }
  (GoogleQuery 'AddMember' $query) | fl
}

Funcțiile de actualizare și creare a conturilor au o sintaxă similară; nu toate câmpurile suplimentare sunt obligatorii. În secțiunea cu numere de telefon, trebuie să specificați un array care poate conține de la o singură înregistrare cu numărul și tipul acestuia.

Pentru a evita o eroare la adăugarea unui utilizator în grup, putem verifica în prealabil dacă acesta este deja în acel grup, obținând lista membrilor grupului sau compunerea utilizatorului însuși.

Cererea compunerii grupurilor pentru un anumit utilizator nu va fi recursivă și va arăta doar apartenența directă. Includerea utilizatorului într-un grup părinte în care există deja un grup copil, al cărui membru este utilizatorul, va avea succes.

Concluzie

Rămâne să trimitem utilizatorului parola contului nou. Facem acest lucru prin SMS, iar informațiile generale cu instrucțiuni și numele de utilizator sunt trimise pe e-mailul personal, pe care, împreună cu numărul de telefon, l-a furnizat departamentul de recrutare. Ca alternativă, putem economisi bani și trimite parola într-un chat secret pe Telegram, ceea ce poate fi considerat un al doilea factor (excepția vor fi MacBook-urile).

Vă mulțumesc că ați citit până la capăt. Aș fi bucuros să văd sugestii de îmbunătățire a stilului de redacție a articolelor și vă doresc să aveți mai puține erori la scrierea scripturilor =)

Lista link-urilor care pot fi utile tematic sau pur și simplu pentru a răspunde întrebărilor apărute:

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster