Ustvarjanje Googlovih uporabnikov iz lupine PowerShell prek API-ja

Lep pozdrav!

V tem članku bo opisana implementacija interakcije PowerShell z Googlovim API-jem za manipulacijo uporabnikov G Suite.

V celotni organizaciji uporabljamo več internih storitev in storitev v oblaku. Večinoma se avtorizacija v njih spušča na Google ali Active Directory, med katerima ne moremo vzdrževati replike, zato je treba ob odhodu novega zaposlenega ustvariti/omogočiti račun v teh dveh sistemih. Za avtomatizacijo procesa smo se odločili napisati skript, ki zbira informacije in jih pošilja obema storitvama.

Dovoljenje

Pri pripravi zahtev smo se odločili, da za avtorizacijo uporabimo prave človeške skrbnike, kar poenostavlja analizo dejanj v primeru naključnih ali namernih množičnih sprememb.

Googlovi API-ji za preverjanje pristnosti in avtorizacijo uporabljajo protokol OAuth 2.0. Primere uporabe in podrobnejše opise najdete tukaj: Uporaba OAuth 2.0 za dostop do Googlovih API-jev.

Izbral sem skript, ki se uporablja za avtorizacijo v namiznih aplikacijah. Obstaja tudi možnost uporabe storitvenega računa, ki od uporabnika ne zahteva nepotrebnih premikov.

Spodnja slika je shematski opis izbranega scenarija z Googlove strani.

Ustvarjanje Googlovih uporabnikov iz lupine PowerShell prek API-ja

  1. Najprej uporabnika pošljemo na stran za preverjanje pristnosti Google Računa, pri čemer podamo parametre GET:
    • id aplikacije
    • področja, do katerih aplikacija potrebuje dostop
    • naslov, na katerega bo uporabnik preusmerjen po zaključku postopka
    • način, kako bomo posodobili žeton
    • Varnostna koda
    • oblika prenosa potrditvene kode

  2. Ko je avtorizacija končana, bo uporabnik preusmerjen na stran, navedeno v prvi zahtevi, z napako ali kodo avtorizacije, ki jo posredujejo parametri GET
  3. Aplikacija (skript) bo morala prejeti te parametre in, če prejme kodo, narediti naslednjo zahtevo za pridobitev žetonov
  4. Če je zahteva pravilna, Googlov API vrne:
    • Dostopni žeton, s katerim lahko podajamo zahteve
    • Obdobje veljavnosti tega žetona
    • Žeton za osvežitev je potreben za osvežitev žetona za dostop.

Najprej morate odpreti konzolo Google API: Poverilnice – Google API konzola, izberite želeno aplikacijo in v razdelku Poverilnice ustvarite identifikator OAuth odjemalca. Tam (ali kasneje v lastnostih ustvarjenega identifikatorja) morate določiti naslove, na katere je dovoljena preusmeritev. V našem primeru bo to več vnosov localhost z različnimi vrati (glejte spodaj).

Za bolj priročno branje algoritma skripta lahko prikažete prve korake v ločeni funkciji, ki bo vrnila dostop in osvežila žetone za aplikacijo:

$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

ID odjemalca in skrivnost odjemalca nastavimo v lastnostih identifikatorja odjemalca OAuth, preverjanje kode pa je niz s 43 do 128 znaki, ki mora biti naključno ustvarjen iz nerezerviranih znakov: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Ta koda bo nato znova poslana. Odpravlja ranljivost, pri kateri bi lahko napadalec prestregel odgovor, vrnjen kot preusmeritev po avtorizaciji uporabnika.
Preverjevalnik kode lahko pošljete v trenutni zahtevi v čistem besedilu (zaradi česar je nesmiselna - to je primerno samo za sisteme, ki ne podpirajo SHA256) ali z ustvarjanjem zgoščene vrednosti z uporabo algoritma SHA256, ki mora biti kodiran v BASE64Url (različen iz Base64 z dvema znakoma tabele) in odstranitev končnic vrstic znakov: =.

Nato moramo začeti poslušati http na lokalnem računalniku, da prejmemo odgovor po avtorizaciji, ki bo vrnjen kot preusmeritev.

Administrativna opravila se izvajajo na posebnem strežniku, ne moremo izključiti možnosti, da bo skripto izvajalo več administratorjev hkrati, tako da bo naključno izbrala vrata za trenutnega uporabnika, vendar sem navedel vnaprej določena vrata, ker prav tako jih je treba dodati kot zaupanja vredne v konzoli API.

access_type=brez povezave pomeni, da lahko aplikacija sama posodobi žeton, ki je potekel, brez interakcije uporabnika z brskalnikom,
tip_odgovora=koda nastavi obliko, kako bo koda vrnjena (sklic na staro avtorizacijsko metodo, ko je uporabnik kopiral kodo iz brskalnika v skript),
Obseg označuje obseg in vrsto dostopa. Ločeni morajo biti s presledki ali %20 (glede na kodiranje URL-jev). Seznam območij dostopa s tipi si lahko ogledate tukaj: Obseg OAuth 2.0 za Googlove API-je.

Po prejemu avtorizacijske kode bo aplikacija brskalniku vrnila sporočilo o zaprtju, prenehala poslušati vrata in poslala zahtevo POST za pridobitev žetona. V njem navedemo predhodno določen id in skrivnost iz API-ja konzole, naslov, na katerega bo uporabnik preusmerjen, in grant_type v skladu s specifikacijo protokola.

V odgovor bomo prejeli Access token, njegovo veljavnost v sekundah in Refresh token, s katerim lahko posodobimo Access token.

Aplikacija mora hraniti žetone na varnem mestu z dolgim ​​rokom trajanja, zato dokler ne prekličemo prejetega dostopa, aplikacija ne bo vrnila žetona za osvežitev. Na koncu sem dodal zahtevo za preklic žetona; če aplikacija ni bila uspešno zaključena in žeton za osvežitev ni bil vrnjen, bo ponovno začela postopek (shranjevanje žetonov lokalno na terminalu se nam zdi nevarno in ne ne želim komplicirati s kriptografijo ali pogosto odpirati brskalnika).

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
}

Kot ste že opazili, se pri preklicu žetona uporablja Invoke-WebRequest. Za razliko od Invoke-RestMethod prejetih podatkov ne vrne v uporabni obliki in prikaže status zahteve.

Nato skript od vas zahteva, da vnesete ime in priimek uporabnika, kar ustvari prijavo + e-pošto.

zahteve

Naslednje zahteve bodo - najprej morate preveriti, ali uporabnik z isto prijavo že obstaja, da pridobite odločitev o ustvarjanju novega ali omogočanju trenutnega.

Odločil sem se, da bom vse zahteve implementiral v obliki ene funkcije z izbiro z uporabo stikala:

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 vsaki zahtevi morate poslati avtorizacijsko glavo, ki vsebuje vrsto žetona in žeton za dostop. Trenutno je tip žetona vedno nosilec. Ker preveriti moramo, da žeton ni potekel, in ga posodobiti po eni uri od trenutka, ko je bil izdan, določil sem zahtevo za drugo funkcijo, ki vrne žeton za dostop. Isti del kode je na začetku skripta, ko prejmete prvi žeton za dostop:

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
}

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' - suspended: '$user.Suspended
    $GAresult = $user
  }
  if ($GAresult) {
      $return = $GAresult
  } else {$return = 'gg'}
  return $return
}

Zahteva email:$query bo od API-ja zahtevala, da poišče uporabnika s točno to e-pošto, vključno z vzdevki. Uporabite lahko tudi nadomestni znak: =, :, :{PREFIX}*.

Za pridobivanje podatkov uporabite metodo GET request, za vnos podatkov (ustvarjanje računa ali dodajanje člana v skupino) - POST, za posodobitev obstoječih podatkov - PUT, za brisanje zapisa (npr. člana iz skupine) - IZBRIŠI.

Skript bo zahteval tudi telefonsko številko (nepreverjen niz) in vključitev v regionalno distribucijsko skupino. Odloči se, katero organizacijsko enoto naj ima uporabnik glede na izbrano OU Active Directory in pripravi geslo:

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"

In potem začne manipulirati z 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 posodobitev in ustvarjanje računa imajo podobno sintakso, vsa dodatna polja niso obvezna, v razdelku s telefonskimi številkami morate določiti matriko, ki lahko vsebuje do en zapis s številko in njeno vrsto.

Da ne bi prejeli napake pri dodajanju uporabnika v skupino, lahko najprej preverimo, ali je že član te skupine, tako da pridobimo seznam članov skupine ali sestavo od uporabnika samega.

Poizvedba o članstvu določenega uporabnika v skupini ne bo rekurzivna in bo prikazala samo neposredno članstvo. Vključitev uporabnika v nadrejeno skupino, ki že ima podrejeno skupino, katere član je uporabnik, bo uspela.

Zaključek

Vse kar ostane je, da uporabniku pošljemo geslo za nov račun. To naredimo preko SMS-a, splošne informacije z navodili in prijavo pa pošljemo na osebni e-mail, ki nam ga je poleg telefonske številke posredoval kadrovski oddelek. Kot alternativo lahko prihranite denar in pošljete svoje geslo v skrivni telegramski klepet, kar lahko prav tako štejemo za drugi dejavnik (MacBooks bodo izjema).

Hvala, ker ste prebrali do konca. Vesel bom predlogov za izboljšanje stila pisanja člankov in želim vam, da ujamete manj napak pri pisanju skriptov =)

Seznam povezav, ki so lahko tematsko uporabne ali zgolj odgovarjajo na vprašanja:

Vir: www.habr.com

Dodaj komentar