Stvaranje Google korisnika iz PowerShell-a putem API-ja

Pozdrav!

Ovaj će članak opisati implementaciju interakcije PowerShell s Google API-jem za manipuliranje korisnicima G Suitea.

Koristimo nekoliko internih i cloud usluga u cijeloj organizaciji. Uglavnom se autorizacija u njima svodi na Google ili Active Directory između kojih ne možemo održavati repliku, shodno tome, kada novi zaposlenik ode potrebno je kreirati/omogućiti račun u ova dva sustava. Kako bismo automatizirali proces, odlučili smo napisati skriptu koja prikuplja informacije i šalje ih objema uslugama.

Autorizacija

Prilikom izrade zahtjeva odlučili smo koristiti prave ljudske administratore za autorizaciju; to pojednostavljuje analizu radnji u slučaju slučajnih ili namjernih velikih promjena.

Google API-ji koriste OAuth 2.0 protokol za autentifikaciju i autorizaciju. Slučajevi korištenja i detaljniji opisi mogu se pronaći ovdje: Korištenje OAutha 2.0 za pristup Google API-jima.

Odabrao sam skriptu koja služi za autorizaciju u desktop aplikacijama. Također postoji mogućnost korištenja računa usluge, koja ne zahtijeva nepotrebne pokrete od korisnika.

Slika ispod je shematski opis odabranog scenarija sa Google stranice.

Stvaranje Google korisnika iz PowerShell-a putem API-ja

  1. Prvo šaljemo korisnika na stranicu za provjeru autentičnosti Google računa, navodeći GET parametre:
    • id aplikacije
    • područja kojima aplikacija treba pristup
    • adresa na koju će korisnik biti preusmjeren nakon završetka postupka
    • način na koji ćemo ažurirati token
    • Sigurnosni kod
    • format prijenosa verifikacijskog koda

  2. Nakon dovršetka autorizacije, korisnik će biti preusmjeren na stranicu navedenu u prvom zahtjevu, s greškom ili autorizacijskim kodom koji prosljeđuju GET parametri
  3. Aplikacija (skripta) će morati primiti ove parametre i, ako primi kod, napraviti sljedeći zahtjev za dobivanje tokena
  4. Ako je zahtjev točan, Google API vraća:
    • Pristupni token s kojim možemo postavljati zahtjeve
    • Razdoblje valjanosti ovog tokena
    • Token za osvježavanje potreban je za osvježavanje tokena za pristup.

Prvo morate otići na Google API konzolu: Vjerodajnice - Google API konzola, odaberite željenu aplikaciju i u odjeljku Vjerodajnice izradite OAuth identifikator klijenta. Tamo (ili kasnije, u svojstvima stvorenog identifikatora) trebate navesti adrese na koje je dopušteno preusmjeravanje. U našem slučaju, to će biti nekoliko unosa localhost s različitim portovima (vidi dolje).

Kako biste lakše čitali algoritam skripte, možete prikazati prve korake u zasebnoj funkciji koja će vratiti Access i osvježiti tokene za aplikaciju:

$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

Postavljamo ID klijenta i Tajnu klijenta dobivene u svojstvima identifikatora klijenta OAuth, a verifikator koda je niz od 43 do 128 znakova koji se mora nasumično generirati iz nerezerviranih znakova: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Ovaj će se kod zatim ponovno poslati. Uklanja ranjivost u kojoj bi napadač mogao presresti odgovor vraćen kao preusmjeravanje nakon autorizacije korisnika.
Možete poslati verifikator koda u trenutnom zahtjevu u čistom tekstu (što ga čini besmislenim - ovo je prikladno samo za sustave koji ne podržavaju SHA256), ili stvaranjem hash-a pomoću algoritma SHA256, koji mora biti kodiran u BASE64Url (različit iz Base64 za dva znaka tablice) i uklanjanjem završetaka retka znakova: =.

Zatim, moramo početi slušati http na lokalnom računalu kako bismo primili odgovor nakon autorizacije, koji će biti vraćen kao preusmjeravanje.

Administrativni poslovi se obavljaju na posebnom poslužitelju, ne možemo isključiti mogućnost da više administratora pokreće skriptu u isto vrijeme, pa će nasumično odabrati port za trenutnog korisnika, ali ja sam naveo predefinirane portove jer također se moraju dodati kao pouzdani u API konzoli.

access_type=offline znači da aplikacija može sama ažurirati istekli token bez interakcije korisnika s preglednikom,
tip_odgovora=kod postavlja format kako će se kod vratiti (referenca na staru metodu autorizacije, kada je korisnik kopirao kod iz preglednika u skriptu),
djelokrug označava opseg i vrstu pristupa. Moraju biti odvojeni razmacima ili %20 (prema URL kodiranju). Popis pristupnih područja s vrstama možete vidjeti ovdje: Opseg OAuth 2.0 za Google API-je.

Nakon primitka autorizacijskog koda, aplikacija će pregledniku vratiti poruku o zatvaranju, prestati osluškivati ​​port i poslati POST zahtjev za dobivanje tokena. U njemu navodimo prethodno navedeni ID i tajnu iz API-ja konzole, adresu na koju će korisnik biti preusmjeren i grant_type u skladu sa specifikacijom protokola.

Kao odgovor ćemo dobiti Access token, njegov period valjanosti u sekundama i Refresh token, s kojim možemo ažurirati Access token.

Aplikacija mora pohraniti tokene na sigurno mjesto s dugim rokom trajanja, tako da dok ne opozovemo primljeni pristup, aplikacija neće vratiti token za osvježavanje. Na kraju sam dodao zahtjev za opozivom tokena; ako aplikacija nije uspješno dovršena i token za osvježavanje nije vraćen, ponovno će pokrenuti proceduru (smatrali smo nesigurnim pohranjivanje tokena lokalno na terminalu i ne ne želim komplicirati stvari s kriptografijom ili često otvarati preglednik).

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
}

Kao što ste već primijetili, prilikom opoziva tokena koristi se Invoke-WebRequest. Za razliku od Invoke-RestMethod, ne vraća primljene podatke u upotrebljivom formatu i prikazuje status zahtjeva.

Zatim, skripta od vas traži da unesete ime i prezime korisnika, generirajući prijavu + e-poštu.

Upiti

Sljedeći zahtjevi će biti - prije svega potrebno je provjeriti postoji li korisnik s istom prijavom kako bi se dobila odluka o kreiranju novog ili omogućavanju postojećeg.

Odlučio sam implementirati sve zahtjeve u formatu jedne funkcije s odabirom, koristeći prekidač:

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

U svakom zahtjevu morate poslati zaglavlje za autorizaciju koje sadrži vrstu tokena i sam token za pristup. Trenutno je tip tokena uvijek nositelj. Jer moramo provjeriti da token nije istekao i ažurirati ga nakon sat vremena od trenutka izdavanja, specificirao sam zahtjev za drugu funkciju koja vraća Access token. Isti dio koda nalazi se na početku skripte prilikom primanja prvog Access tokena:

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
}

Provjera postojanja prijave:

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
}

Zahtjev email:$query tražit će od API-ja da potraži korisnika s točno tom e-poštom, uključujući aliase. Također možete koristiti zamjenski znak: =, :, :{PREFIX}*.

Za dobivanje podataka koristite metodu GET request, za umetanje podataka (izrada računa ili dodavanje člana u grupu) - POST, za ažuriranje postojećih podataka - PUT, za brisanje zapisa (npr. člana iz grupe) - IZBRISATI.

Skripta će također tražiti telefonski broj (nepotvrđeni niz) i uključivanje u regionalnu distribucijsku grupu. Odlučuje koju organizacijsku jedinicu korisnik treba imati na temelju odabrane OU Active Directory i dolazi s lozinkom:

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"

A onda počinje manipulirati računom:

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

Funkcije za ažuriranje i kreiranje računa imaju sličnu sintaksu, nisu potrebna sva dodatna polja, u odjeljku s telefonskim brojevima potrebno je navesti niz koji može sadržavati do jedan zapis s brojem i njegovom vrstom.

Kako ne bismo dobili pogrešku prilikom dodavanja korisnika u grupu, prvo možemo provjeriti da li je on već član ove grupe tako što ćemo dobiti popis članova grupe ili sastav od samog korisnika.

Upit o članstvu u grupi određenog korisnika neće biti rekurzivan i pokazat će samo izravno članstvo. Uključivanje korisnika u nadređenu grupu koja već ima podređenu grupu čiji je korisnik član će uspjeti.

Zaključak

Sve što preostaje je poslati korisniku lozinku za novi račun. To radimo putem SMS-a, a opće informacije s uputama i prijavom šaljemo na osobni e-mail koji je uz broj telefona dobio od odjela za zapošljavanje. Kao alternativu, možete uštedjeti novac i poslati svoju lozinku na tajni telegram chat, što se također može smatrati drugim faktorom (MacBooks će biti iznimka).

Hvala što ste pročitali do kraja. Bit će mi drago vidjeti prijedloge za poboljšanje stila pisanja članaka i želim vam da uhvatite manje grešaka prilikom pisanja skripti =)

Popis poveznica koje mogu biti tematski korisne ili jednostavno odgovoriti na pitanja:

Izvor: www.habr.com

Dodajte komentar