Vytváranie používateľov Google z prostredia PowerShell prostredníctvom rozhrania API

Ahoj!

Tento článok popisuje implementáciu interakcie PowerShell s rozhraním Google API na manipuláciu používateľov G Suite.

V rámci organizácie využívame viacero interných a cloudových služieb. Autorizáciu v nich väčšinou zabezpečuje Google alebo Active Directory, medzi ktorými nemôžeme udržiavať repliku, preto si pri odchode nového zamestnanca musíte vytvoriť/povoliť účet v týchto dvoch systémoch. Aby sme tento proces zautomatizovali, rozhodli sme sa napísať skript, ktorý zbiera informácie a odosiela ich obom službám.

Povolenie

Pri zostavovaní požiadaviek sme sa rozhodli použiť na autorizáciu skutočných ľudských administrátorov, čo zjednodušuje analýzu akcií v prípade náhodných alebo úmyselných masívnych zmien.

Rozhrania Google API používajú na overenie a autorizáciu protokol OAuth 2.0. Prípady použitia a podrobnejšie popisy nájdete tu: Používanie protokolu OAuth 2.0 na prístup k rozhraniam Google API.

Vybral som si skript, ktorý sa používa na autorizáciu v desktopových aplikáciách. Nechýba ani možnosť využiť servisný účet, ktorý od užívateľa nevyžaduje zbytočné pohyby.

Na obrázku nižšie je schematický popis vybraného scenára zo stránky Google.

Vytváranie používateľov Google z prostredia PowerShell prostredníctvom rozhrania API

  1. Najprv používateľa pošleme na overovaciu stránku účtu Google s parametrami GET:
    • ID aplikácie
    • oblasti, ku ktorým aplikácia potrebuje prístup
    • adresu, na ktorú bude používateľ po dokončení postupu presmerovaný
    • spôsob, akým budeme aktualizovať token
    • Bezpečnostný kód
    • formát prenosu overovacieho kódu

  2. Po dokončení autorizácie bude používateľ presmerovaný na stránku špecifikovanú v prvej požiadavke s chybovým alebo autorizačným kódom odovzdaným parametrami GET
  3. Aplikácia (skript) bude musieť prijať tieto parametre a ak dostane kód, vykoná nasledujúcu požiadavku na získanie tokenov
  4. Ak je požiadavka správna, Google API vráti:
    • Prístupový token, pomocou ktorého môžeme zadávať požiadavky
    • Doba platnosti tohto tokenu
    • Na obnovenie prístupového tokenu je potrebný obnovovací token.

Najprv musíte prejsť do konzoly Google API: Poverenia – Google API Console, vyberte požadovanú aplikáciu a v časti Prihlasovacie údaje vytvorte identifikátor OAuth klienta. Tam (alebo neskôr, vo vlastnostiach vytvoreného identifikátora) musíte zadať adresy, na ktoré je povolené presmerovanie. V našom prípade to bude niekoľko položiek localhost s rôznymi portami (pozri nižšie).

Aby bolo čítanie skriptového algoritmu pohodlnejšie, môžete prvé kroky zobraziť v samostatnej funkcii, ktorá vráti prístupové a obnovovacie tokeny pre aplikáciu:

$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

Nastavíme ID klienta a tajný kľúč klienta získané vo vlastnostiach identifikátora klienta OAuth a overovač kódu je reťazec 43 až 128 znakov, ktorý musí byť náhodne vygenerovaný z nerezervovaných znakov: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Tento kód sa potom znova odošle. Odstraňuje zraniteľnosť, pri ktorej by útočník mohol zachytiť odpoveď vrátenú ako presmerovanie po autorizácii používateľa.
Verifikátor kódu môžete v aktuálnej požiadavke poslať ako čistý text (čo ho robí bezvýznamným – toto je vhodné len pre systémy, ktoré nepodporujú SHA256), alebo vytvorením hashu pomocou algoritmu SHA256, ktorý musí byť zakódovaný v BASE64Url (odlišné od z Base64 dvoma tabuľkovými znakmi) a odstránením koncov riadkov znakov: =.

Ďalej musíme začať počúvať http na lokálnom počítači, aby sme po autorizácii dostali odpoveď, ktorá sa vráti ako presmerovanie.

Administratívne úlohy sa vykonávajú na špeciálnom serveri, nemôžeme vylúčiť, že skript spustí niekoľko administrátorov súčasne, takže náhodne vyberie port pre aktuálneho používateľa, ale špecifikoval som preddefinované porty, pretože musia byť tiež pridané ako dôveryhodné v konzole API.

access_type=offline znamená, že aplikácia môže aktualizovať token, ktorého platnosť vypršala, sama bez interakcie používateľa s prehliadačom,
response_type=code nastavuje formát, akým sa kód vráti (odkaz na starú metódu autorizácie, keď používateľ skopíroval kód z prehliadača do skriptu),
rozsah označuje rozsah a typ prístupu. Musia byť oddelené medzerami alebo %20 (podľa kódovania URL). Zoznam prístupových oblastí s typmi si môžete pozrieť tu: Rozsahy OAuth 2.0 pre rozhrania Google API.

Po prijatí autorizačného kódu aplikácia vráti do prehliadača zatvorenú správu, prestane počúvať na porte a odošle POST požiadavku na získanie tokenu. Uvádzame v ňom predtým zadané id a tajomstvo z konzolového API, adresu, na ktorú bude používateľ presmerovaný a typ grantu v súlade so špecifikáciou protokolu.

Ako odpoveď dostaneme prístupový token, dobu jeho platnosti v sekundách a obnovovací token, pomocou ktorého môžeme aktualizovať prístupový token.

Aplikácia musí uchovávať tokeny na bezpečnom mieste s dlhou životnosťou, takže kým nezrušíme prijatý prístup, aplikácia nevráti obnovovací token. Na konci som pridal požiadavku na zrušenie tokenu, ak aplikácia nebola úspešne dokončená a obnovovací token sa nevrátil, spustí procedúru znova (považovali sme za nebezpečné ukladať tokeny lokálne na termináli a nechcem komplikovať veci s kryptografiou alebo často otvárať prehliadač).

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
}

Ako ste si už všimli, pri odvolaní tokenu sa používa Invoke-WebRequest. Na rozdiel od Invoke-RestMethod nevracia prijaté dáta v použiteľnom formáte a zobrazuje stav požiadavky.

Ďalej vás skript požiada o zadanie mena a priezviska používateľa, čím sa vygeneruje prihlasovacie meno + e-mail.

žiadosti

Ďalšie požiadavky budú - v prvom rade musíte skontrolovať, či už existuje používateľ s rovnakým prihlásením, aby ste mohli získať rozhodnutie o vytvorení nového alebo povolení súčasného.

Rozhodol som sa implementovať všetky požiadavky vo formáte jednej funkcie s výberom pomocou prepínača:

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 každej požiadavke je potrebné poslať autorizačnú hlavičku obsahujúcu typ tokenu a samotný prístupový token. V súčasnosti je typ tokenu vždy Nositeľ. Pretože potrebujeme skontrolovať, či nevypršala platnosť tokenu a aktualizovať ho po hodine od vydania, zadal som požiadavku na inú funkciu, ktorá vráti prístupový token. Rovnaký kus kódu je na začiatku skriptu pri prijímaní prvého prístupového tokenu:

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
}

Kontrola existencie prihlásenia:

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
}

Požiadavka email:$query požiada rozhranie API, aby vyhľadalo používateľa s presne týmto e-mailom vrátane aliasov. Môžete tiež použiť zástupný znak: =, :, :{PREFIX}*.

Na získanie údajov použite metódu požiadavky GET, na vloženie údajov (vytvorenie účtu alebo pridanie člena do skupiny) - POST, na aktualizáciu existujúcich údajov - PUT, na vymazanie záznamu (napríklad člena zo skupiny) - VYMAZAŤ.

Skript tiež požiada o telefónne číslo (neoverený reťazec) a o zaradenie do regionálnej distribučnej skupiny. Rozhodne, ktorú organizačnú jednotku by mal mať používateľ, na základe vybranej organizačnej jednotky Active Directory a príde s heslom:

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 potom začne manipulovať s účtom:

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

Funkcie na aktualizáciu a vytvorenie účtu majú podobnú syntax, nie sú potrebné všetky ďalšie polia, v časti s telefónnymi číslami je potrebné zadať pole, ktoré môže obsahovať až jeden záznam s číslom a jeho typom.

Aby sa pri pridávaní používateľa do skupiny neobjavila chyba, môžeme si najprv skontrolovať, či už nie je členom tejto skupiny, a to tak, že od samotného používateľa získame zoznam členov skupiny alebo zloženie.

Dotazovanie sa na členstvo v skupine konkrétneho používateľa nebude rekurzívne a zobrazí iba priame členstvo. Zahrnutie používateľa do nadradenej skupiny, ktorá už má podradenú skupinu, ktorej je používateľ členom, bude úspešné.

Záver

Zostáva už len poslať používateľovi heslo k novému účtu. Robíme to prostredníctvom SMS a všeobecné informácie s inštrukciami a prihlasovaním posielame na osobný email, ktorý nám spolu s telefónnym číslom poskytlo náborové oddelenie. Ako alternatívu môžete ušetriť peniaze a poslať svoje heslo do tajného telegramového chatu, čo možno tiež považovať za druhý faktor (výnimkou budú MacBooky).

Ďakujem za prečítanie až do konca. Budem rád, keď uvidím návrhy na zlepšenie štýlu písania článkov a prajem vám, aby ste pri písaní skriptov chytali menej chýb =)

Zoznam odkazov, ktoré môžu byť tematicky užitočné alebo jednoducho odpovedať na otázky:

Zdroj: hab.com

Pridať komentár