Ustvarjanje Google uporabnikov iz PowerShell prek API

Pozdrav!

V tem članku bo opisana implementacija interakcije PowerShell z Google API za upravljanje uporabniki G Suite.

V organizaciji uporabljamo več notranjih in oblačnih storitev. V večini primerov se avtorizacija v njih nanaša na Google ali Active Directory, med katerima ne moremo vzdrževati replike, zato ob nastopu novega zaposlenega moramo ustvariti/vklopiti račun v obeh sistemih. Za avtomatizacijo procesa smo se odločili, da napišemo skript, ki zbira informacije in jih pošlje obema storitvama.

Avtorizacija

Pri oblikovanju zahtev smo se odločili uporabiti za avtorizacijo prave administratorje, kar olajša obravnavo dejanj pri naključnih ali namernih masivnih spremembah.

Za avtentičnost in avtorizacijo Google API uporabljajo protokol OAuth 2.0. Scenarije uporabe in podrobnejši opis lahko najdete tukaj: Using OAuth 2.0 to Access Google APIs.

Izbral sem scenarij, ki se uporablja pri avtorizaciji v namiznih aplikacijah. Na voljo je tudi možnost uporabe servisnega računa, ki ne zahteva od uporabnika dodatnih korakov.

Slika spodaj prikazuje shematični opis izbranega scenarija s spletne strani Google.

Ustvarjanje Google uporabnikov iz PowerShell prek API

  1. Najprej uporabnika usmerimo na stran za avtorizacijo v Google račun, pri čemer navedemo GET parametre:
    • identifikator aplikacije
    • območja, ki jih aplikacija potrebuje za dostop
    • naslov, na katero bo uporabnik preusmerjen po zaključku postopka
    • metoda, s katero bomo osvežili žeton
    • koda za preverjanje
    • format prenosa kode za preverjanje

  2. Po zaključeni avtorizaciji bo uporabnik preusmerjen na naslov, naveden v prvem zahtevku, s skupno napako ali kodo avtorizacije, posredovano z GET parametri.
  3. Aplikacija (skript) bo morala pridobiti te parametre in v primeru, da prejme kodo, izvesti naslednji zahtevek za pridobitev žetonov.
  4. Pri pravilnem zahtevku Google API vrne:
    • Dostopni žeton, s katerim lahko pošiljamo zahtevke
    • Trajanje tega žetona
    • Osvežitveni žeton, potreben za osvežitev dostopnega žetona.

Najprej moramo iti v konzolo Google API: Credentials — Google API Console, izberite potrebno aplikacijo in v razdelku Credentials ustvarite identifikator OAuth klienta. Tam (ali kasneje, v lastnostih ustvarjenega identifikatorja) je treba navesti naslove, na katere je dovoljeno preusmerjanje. V našem primeru bo to več zapisov localhost z različnimi vrati (glej naprej).

Da bo bolj pregledno brati algoritem skripte, lahko prve korake izločimo v ločeno funkcijo, ki vrne Access in refresh žetone za aplikacijo:

$client_secret = 'Naša skrivnost klienta'
$client_id = 'Naš identifikator klienta'
function Get-GoogleAuthToken {
  if (-not [System.Net.HttpListener]::IsSupported) {
    "HttpListener ni podprt."
    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 "Začnite brskalnik..."
  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 {
    "Ni mogoče zagnati poslušalnika."
    exit 1
  }
  while (($code -eq $null)) {
    $context = $listener.GetContext()
    Write-Host "Povezava sprejeta" -f 'mag'
    $url = $context.Request.RawUrl
    $code = $url.split('?')[1].split('=')[1].split('&')[0]
    if ($url.split('?')[1].split('=')[0] -eq 'error') {
      Write-Host "Napaka!"$code -f 'red'
      $buffer = [System.Text.Encoding]::UTF8.GetBytes("Napaka!"+$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("Zdaj lahko zaprete to zavihek brskalnika.")
    $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

Nastavimo Client ID in Client Secret, pridobljene v lastnostih identifikatorja OAuth klienta, in code verifier – to je niz dolžine od 43 do 128 znakov, ki ga je treba naključno generirati iz nezakupljenih znakov: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".

Nato bo ta koda posredovana znova. Izključuje ranljivost, pri kateri lahko napadalec ujame odgovor, ki se vrne s preusmeritvijo po avtorizaciji uporabnika.
Verifikator kode lahko pošljemo v trenutnem poizvedovanju v odprti obliki (kar ga naredi nepomembnega – to ustreza samo sistemom, ki ne podpirajo SHA256), ali pa ustvarimo hash po algoritmu SHA256, ki ga je treba kodirati v BASE64Url (razlikuje se od Base64 v dveh znakih tabele) in odstraniti znak za konec vrstice: =.

Nato moramo začeti poslušati http na lokalnem računalniku, da bi dobili odgovor po avtorizaciji, ki se bo vrnil z redirekcijo.

Administrativne naloge se izvajajo na posebnem strežniku, ne moremo izključiti možnosti, da več skrbnikov hkrati zažene skript, zato bo naključno izbral pristanišče za trenutnega uporabnika, vendar sem vnaprej navedel določena pristanišča, saj jih je treba dodati kot zaupanja vredna v konzoli API.

access_type=offline pomeni, da lahko aplikacija samodejno posodobi potekli token brez interakcije uporabnika z brskalnikom,
response_type=code določa format, v katerem se bo vrnil koda (sklic na star način avtorizacije, ko je uporabnik kopiral in prilepil kodo iz brskalnika v skript),
scope določa področja in vrste dostopa. Ločena morajo biti s presledki ali (v skladu z URL kodiranjem). Seznam področij dostopa in vrst si lahko ogledate tukaj: OAuth 2.0 Scopes for Google APIs.

Po prejemu avtorizacijske kode bo aplikacija v brskalnik vrnila sporočilo o zaprtju, prenehala poslušati pristanišče in poslala POST poizvedbo za pridobitev tokena. V njem navedemo prej določena id in secret iz API konzole, naslov, na katerega bo uporabnik preusmerjen, in grant_type v skladu s specifikacijo protokola.

V odgovoru bomo prejeli dostopni token, njegovo obdobje veljavnosti v sekundah in osvežitveni token, s katerim lahko posodobimo dostopni token.

Aplikacija mora shranjevati tokene na varnem mestu z dolgim rokom hrambe, zato, dokler ne prekličemo pridobljenega dostopa, aplikaciji ne bo vrnjen osvežitveni token. Na koncu sem dodal poizvedbo za preklic tokena, če aplikacija ni bila uspešno zaključena in osvežitveni token ni bil vrnjen, začne postopek znova (menili smo, da je nespoštljivo shranjevati tokene lokalno na terminalu, oteževanje s kriptografijo ali pogosto odpiranje brskalnika pa ni zaželeno).

do {
  $token_result = Get-GoogleAuthToken
  $token = $token_result.access_token
  if ($token_result.refresh_token -eq $null) {
    Write-Host ("Seansa nije uništena. Oduzimanje tokena...")
    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ć primetili, prilikom opoziva tokena koristi se Invoke-WebRequest. Za razliku od Invoke-RestMethod, on ne vraća dobijene podatke u lako upotrebljivom formatu i prikazuje status zahteva.

Zatim, skripta će zatražiti da unesete ime i prezime korisnika, generišući korisničko ime + email.

Poizvedbe

Sledeći koraci biće zahtevi – prvo treba proveriti da li već postoji korisnik sa tim korisničkim imenom kako bi se odlučilo da li će se formirati novi ili uključiti trenutni.

Odlučio sam da realizujem sve zahteve u formatu jedne funkcije sa izborom, koristeći 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'])
      }
    }
  }
}

V vsakem zahtevku je treba poslati glavo Authorization, ki vsebuje vrsto žetona in sam dostopni žeton. V tem trenutku je vrsta žetona vedno Bearer. Ker moramo preveriti, ali žeton ni potekel in ga posodobiti čez eno uro od izdaje, sem navedel zahtevo na drugo funkcijo, ki vrne dostopni žeton. Ta del kode je prisoten tudi na začetku skripta pri prvem pridobivanju dostopnega žetona:

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 je potekel. Osvežujem..."
    $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
}

Preverjanje obstoja prijave:

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

Zahteva email: $query bo zahtevala, da API poišče uporabnika z natančno tem emailom, vključno z aliasi. Uporabite lahko tudi wildcard: =, :, :{PREFIX}*.

Za pridobivanje podatkov se uporablja metoda GET, za vstavljanje podatkov (ustvarjanje računa ali dodajanje člana v skupino) – POST, za posodabljanje obstoječih podatkov – PUT, za brisanje zapisa (na primer, člana iz skupine) – DELETE.

Skripta vas bo tudi povprašala po številki telefona (nepotrjena vrstica) in o vključitvi v regionalno skupino za obveščanje. Določa, katera organizacijska enota naj bo uporabnik glede na izbrano OU Active Directory in izmisli geslo:

do {
  $phone = Read-Host "Telefon v formatu +7хххххххх"
} while (-not $phone)
do {
    $moscow = Read-Host "V moskovski urad? (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 "Bodo ustvarjena v /Team delivery"
    $orgunit = "/Team delivery"
}
$Password = -join (48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba"

In nato začne s manipulacijami z računom:

$query = @{
  email = $email
  givenName = $firstname
  familyName = $lastname
  password = $password
  phone = $phone
  orgunit = $orgunit
}
if ($GMailExist) {
  Write-Host "Začenjamo spreminjanje računa" -f mag
  (GoogleQuery 'UpdateAccount' $query) | fl
  write-host "Ne pozabi preveriti skupin za vključenega $Username v Googlu."
} else {
  Write-Host "Začenjamo ustvarjanje računa" -f mag
  (GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
  write-host "Dodajamo v skupino moscowoffice"
  $query = @{
    groupkey = 'moscowoffice@rocketguys.com'
    email = $email
  }
  (GoogleQuery 'AddMember' $query) | fl
}

Funkcije za ažuriranje i kreiranje naloga imaju sličnu sintaksu, nisu sva dodatna polja obavezna, u odeljku za brojeve telefona treba navesti niz koji može sadržavati od jednog unosa sa brojem i njegovim tipom.

Kako bismo izbegli grešku prilikom dodavanja korisnika u grupu, možemo prethodno proveriti da li je već član te grupe dobijajući spisak članova grupe ili sastav samog korisnika.

Upit za sastav grupa određenog korisnika neće biti rekurzivan i prikazaće samo neposredno članstvo. Uključivanje korisnika u roditeljsku grupu, u kojoj već postoji podgrupa čiji je član korisnik, biće uspešno.

Zaključek

Preostaje poslati korisniku lozinku za novi nalog. To radimo putem SMS-a, a opšte informacije sa uputstvom i prijavom šaljemo na ličnu email adresu koju je, zajedno sa brojem telefona, dostavio odeljak za zapošljavanje. Kao alternativna opcija, možemo uštedeti novac i poslati lozinku u tajnom Telegram četu, što se takođe može smatrati drugim faktorom (izuzetak će biti MacBook-i).

Hvala što ste pročitali do kraja. Biću srećan da vidim predloge za poboljšanje stila pisanja članaka i želim vam da uhvatite što manje grešaka prilikom pisanja skripti =)

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

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster