Kreiranje Google korisnika iz PowerShell-a putem API-ja

Zdravo!

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

Koristimo nekoliko internih i cloud servisa 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 nalog u ova dva sistema. Kako bismo automatizirali proces, odlučili smo napisati skriptu koja prikuplja informacije i šalje ih na oba servisa.

Autorizacija

Prilikom sastavljanja 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 detaljnije opise možete pronaći ovdje: Korištenje OAuth 2.0 za pristup Google API-jima.

Odabrao sam skriptu koja se koristi za autorizaciju u desktop aplikacijama. Postoji i mogućnost korištenja servisnog računa, koji ne zahtijeva nepotrebna kretanja od korisnika.

Slika ispod je šematski opis odabranog scenarija sa Google stranice.

Kreiranje Google korisnika iz PowerShell-a putem API-ja

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

  2. Nakon što je autorizacija završena, korisnik će biti preusmjeren na stranicu navedenu u prvom zahtjevu, s greškom ili kodom za autorizaciju koji je proslijeđen od strane GET parametara
  3. Aplikacija (skripta) će morati primiti ove parametre i, ako je primila kod, uputiti sljedeći zahtjev za dobivanje tokena
  4. Ako je zahtjev tačan, Google API vraća:
    • Pristupni token sa kojim možemo postavljati zahtjeve
    • Period važenja ovog tokena
    • Osvježavanje tokena je potrebno za osvježavanje tokena pristupa.

Prvo morate otići na Google API konzolu: Akreditivi - Google API konzola, odaberite željenu aplikaciju i u odjeljku Credentials kreirajte klijentski OAuth identifikator. Tamo (ili kasnije, u svojstvima kreiranog identifikatora) morate navesti adrese na koje je preusmjeravanje dozvoljeno. U našem slučaju, to će biti nekoliko unosa lokalnog hosta s različitim portovima (pogledajte dolje).

Da bi bilo praktičnije čitanje algoritma skripte, možete prikazati prve korake u zasebnoj funkciji koja će vratiti tokene za pristup i osvježavanje 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 Client ID i Client Secret dobijene u svojstvima identifikatora OAuth klijenta, a verifikator koda je niz od 43 do 128 znakova koji se mora nasumično generirati od nerezerviranih znakova: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Ovaj kod će se zatim ponovo prenijeti. On eliminiše 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 sisteme koji ne podržavaju SHA256), ili kreiranjem hash koristeći SHA256 algoritam, koji mora biti kodiran u BASE64Url (različit iz Base64 po dva znaka tabele) i uklanjanje završetaka redova znakova: =.

Zatim trebamo početi slušati http na lokalnoj mašini kako bismo dobili odgovor nakon autorizacije, koji će biti vraćen kao preusmjeravanje.

Administrativni zadaci se obavljaju na posebnom serveru, ne možemo isključiti mogućnost da će više administratora istovremeno pokrenuti skriptu, pa će nasumično odabrati port za trenutnog korisnika, ali sam naveo unaprijed definirane portove jer oni također moraju biti dodati kao pouzdani u API konzoli.

access_type=offline znači da aplikacija može samostalno ažurirati token koji je istekao bez interakcije korisnika s preglednikom,
response_type=code postavlja format kako će kod biti vraćen (referenca na staru metodu autorizacije, kada je korisnik kopirao kod iz pretraživača u skriptu),
obim označava obim i vrstu pristupa. Moraju biti odvojeni razmacima ili %20 (prema URL kodiranju). Spisak pristupnih područja sa tipovima možete vidjeti ovdje: Opseg OAuth 2.0 za Google API-je.

Nakon što primi autorizacijski kod, aplikacija će vratiti pregledniku poruku o zatvaranju, prestati slušati 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, dobit ćemo token za pristup, njegov period važenja u sekundama i token za osvježavanje, pomoću kojeg možemo ažurirati Access token.

Aplikacija mora čuvati tokene na sigurnom mjestu sa dugim rokom trajanja, tako da dok ne opozovemo primljeni pristup, aplikacija neće vratiti token za osvježavanje. Na kraju sam dodao zahtjev za opoziv tokena; ako aplikacija nije uspješno završena i token za osvježavanje nije vraćen, ponovo će pokrenuti proceduru (smatrali smo da nije bezbedno pohranjivati ​​tokene lokalno na terminalu, a mi ne ne želim komplicirati stvari kriptografijom ili često otvarati pretraživač).

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, on ne vraća primljene podatke u upotrebljivom formatu i prikazuje status zahtjeva.

Zatim, skripta traži od vas da unesete ime i prezime korisnika, generirajući login + email.

Zahtjevi

Sljedeći zahtjevi će biti - prije svega, potrebno je provjeriti da li korisnik sa istim login-om već postoji da biste dobili odluku o kreiranju novog ili omogućavanju postojećeg.

Odlučio sam implementirati sve zahtjeve u formatu jedne funkcije sa 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 autorizacijsko zaglavlje koje sadrži tip tokena i sam pristupni token. Trenutno je tip tokena uvijek nosilac. Jer moramo provjeriti da token nije istekao i ažurirati ga nakon sat vremena od trenutka kada je izdat, naveo sam zahtjev za drugu funkciju koja vraća Access token. Isti dio koda nalazi se na početku skripte kada se primi prvi Access token:

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 logina:

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 upravo tom e-poštom, uključujući pseudonime. Također možete koristiti zamjenski znak: =, :, :{PREFIX}*.

Za dobijanje podataka koristite metod GET zahtjeva, za umetanje podataka (kreiranje naloga ili dodavanje člana u grupu) - POST, za ažuriranje postojećih podataka - PUT, za brisanje zapisa (na primjer, člana iz grupe) - IZBRIŠI.

Skripta će takođe tražiti broj telefona (nepotvrđen niz) i uključivanje u regionalnu distribucionu grupu. Odlučuje koju organizacionu jedinicu korisnik treba da ima na osnovu odabranog Active Directory OU i dolazi sa 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 da manipuliše 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 naloga imaju sličnu sintaksu; nisu potrebna sva dodatna polja; u odjeljku s brojevima telefona potrebno je navesti niz koji može sadržavati do jedan zapis s brojem i njegovom vrstom.

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

Postavljanje upita o članstvu u grupi određenog korisnika neće biti rekurzivno i pokazaće samo direktno članstvo. Uključivanje korisnika u roditeljsku grupu koja već ima podređenu grupu čiji je korisnik član će uspjeti.

zaključak

Ostaje samo da pošaljete korisniku lozinku za novi nalog. To radimo putem SMS-a, a opšte informacije sa uputstvima i prijavom šaljemo na lični email, koji je, uz broj telefona, dao odeljenje za zapošljavanje. Kao alternativu, možete uštedjeti novac i poslati svoju lozinku u tajni telegram chat, što se također može smatrati drugim faktorom (MacBook će biti izuzetak).

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

Lista linkova koji mogu biti tematski korisni ili jednostavno odgovoriti na pitanja:

izvor: www.habr.com

Dodajte komentar