Vytváření uživatelů Google z prostředí PowerShell prostřednictvím rozhraní API

Ahoj!

Tento článek popisuje implementaci interakce PowerShell s rozhraním Google API za účelem manipulace s uživateli G Suite.

V celé organizaci využíváme několik interních a cloudových služeb. Autorizace v nich většinou spadá na Google nebo Active Directory, mezi nimiž nemůžeme udržovat repliku, takže když odejde nový zaměstnanec, musíte si vytvořit/povolit účet v těchto dvou systémech. Abychom tento proces zautomatizovali, rozhodli jsme se napsat skript, který shromažďuje informace a odesílá je oběma službám.

Povolení

Při sestavování požadavků jsme se rozhodli použít pro autorizaci skutečné lidské administrátory, což zjednodušuje analýzu akcí v případě náhodných nebo záměrných masivních změn.

Rozhraní Google API používají k ověřování a autorizaci protokol OAuth 2.0. Případy použití a podrobnější popisy naleznete zde: Používání protokolu OAuth 2.0 pro přístup k rozhraním Google API.

Zvolil jsem skript, který se používá pro autorizaci v desktopových aplikacích. Nechybí ani možnost využití servisního účtu, který od uživatele nevyžaduje zbytečné pohyby.

Na obrázku níže je schematický popis vybraného scénáře ze stránky Google.

Vytváření uživatelů Google z prostředí PowerShell prostřednictvím rozhraní API

  1. Nejprve uživatele přesměrujeme na stránku ověření účtu Google s parametry GET:
    • ID aplikace
    • oblasti, ke kterým aplikace potřebuje přístup
    • adresu, na kterou bude uživatel po dokončení postupu přesměrován
    • způsob, jakým budeme aktualizovat token
    • Bezpečnostní kód
    • formát přenosu ověřovacího kódu

  2. Po dokončení autorizace bude uživatel přesměrován na stránku uvedenou v prvním požadavku s chybovým nebo autorizačním kódem předaným parametry GET
  3. Aplikace (skript) bude muset obdržet tyto parametry a pokud obdrží kód, provede následující požadavek na získání tokenů
  4. Pokud je požadavek správný, Google API vrátí:
    • Přístupový token, pomocí kterého můžeme zadávat požadavky
    • Doba platnosti tohoto tokenu
    • K obnovení přístupového tokenu je vyžadován obnovovací token.

Nejprve musíte přejít do konzole Google API: Pověření – Google API Console, vyberte požadovanou aplikaci a v části Přihlašovací údaje vytvořte klientský identifikátor OAuth. Tam (nebo později ve vlastnostech vytvořeného identifikátoru) musíte zadat adresy, na které je povoleno přesměrování. V našem případě to bude několik položek localhost s různými porty (viz níže).

Aby bylo čtení algoritmu skriptu pohodlnější, můžete zobrazit první kroky v samostatné funkci, která vrátí Access a obnoví tokeny pro aplikaci:

$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

Nastavili jsme Client ID a Client Secret získané ve vlastnostech identifikátoru klienta OAuth a ověřovatel kódu je řetězec 43 až 128 znaků, který musí být náhodně vygenerován z nerezervovaných znaků: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Tento kód bude poté znovu odeslán. Eliminuje zranitelnost, při které by útočník mohl zachytit odpověď vrácenou jako přesměrování po autorizaci uživatele.
Ověřovač kódu můžete v aktuálním požadavku odeslat jako prostý text (což ztrácí smysl – to je vhodné pouze pro systémy, které nepodporují SHA256), nebo vytvořením hashe pomocí algoritmu SHA256, který musí být zakódován v BASE64Url (odlišné z Base64 o dva znaky tabulky) a odstranění zakončení řádků znaků: =.

Dále musíme začít poslouchat http na místním počítači, abychom po autorizaci obdrželi odpověď, která se vrátí jako přesměrování.

Administrativní úkony se provádějí na speciálním serveru, nelze vyloučit, že skript spustí několik administrátorů současně, takže náhodně vybere port pro aktuálního uživatele, ale specifikoval jsem předdefinované porty, protože musí být také přidány jako důvěryhodné v konzole API.

access_type=offline znamená, že aplikace může aktualizovat token, jehož platnost vypršela, sama bez interakce uživatele s prohlížečem,
response_type=code nastavuje formát, jakým bude kód vrácen (odkaz na starou autorizační metodu, kdy uživatel zkopíroval kód z prohlížeče do skriptu),
rozsah označuje rozsah a typ přístupu. Musí být odděleny mezerami nebo %20 (podle kódování URL). Seznam přístupových oblastí s typy lze vidět zde: Rozsahy OAuth 2.0 pro Google API.

Po obdržení autorizačního kódu aplikace vrátí do prohlížeče zprávu o uzavření, přestane naslouchat na portu a odešle POST požadavek na získání tokenu. Uvádíme v něm dříve zadané id a tajemství z konzolového API, adresu, na kterou bude uživatel přesměrován, a typ grantu v souladu se specifikací protokolu.

Jako odpověď obdržíme Access token, dobu jeho platnosti v sekundách a Refresh token, pomocí kterého můžeme aktualizovat Access token.

Aplikace musí ukládat tokeny na bezpečné místo s dlouhou životností, takže dokud neodvoláme přijatý přístup, aplikace obnovovací token nevrátí. Na závěr jsem přidal požadavek na revokaci tokenu, pokud aplikace nebyla úspěšně dokončena a obnovovací token se nevrátil, spustí proceduru znovu (ukládat tokeny lokálně na terminálu jsme považovali za nebezpečné a nechci komplikovat věci s kryptografií nebo často otevírat prohlížeč).

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
}

Jak jste si již všimli, při odvolávání tokenu se používá Invoke-WebRequest. Na rozdíl od Invoke-RestMethod nevrací přijatá data v použitelném formátu a zobrazuje stav požadavku.

Dále vás skript požádá o zadání jména a příjmení uživatele a vygeneruje přihlašovací jméno + e-mail.

žádosti

Další požadavky budou - nejprve je potřeba zkontrolovat, zda již existuje uživatel se stejným přihlášením, abyste získali rozhodnutí o vytvoření nového nebo povolení stávajícího.

Rozhodl jsem se implementovat všechny požadavky ve formátu jedné funkce s výběrem pomocí přepínače:

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ždém požadavku musíte odeslat autorizační hlavičku obsahující typ tokenu a samotný přístupový token. V současné době je typ tokenu vždy nositel. Protože potřebujeme zkontrolovat, zda nevypršela platnost tokenu a aktualizovat jej po hodině od okamžiku vydání, zadal jsem požadavek na jinou funkci, která vrací přístupový token. Stejný kus kódu je na začátku skriptu při příjmu prvního pří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 existence přihlášení:

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žadavek email:$query požádá API, aby vyhledalo uživatele s přesně tímto e-mailem, včetně aliasů. Můžete také použít zástupný znak: =, :, :{PREFIX}*.

Pro získání dat použijte metodu požadavku GET, pro vložení dat (vytvoření účtu nebo přidání člena do skupiny) - POST, pro aktualizaci existujících dat - PUT, pro smazání záznamu (například člena ze skupiny) - VYMAZAT.

Skript také požádá o telefonní číslo (neověřený řetězec) a o zařazení do regionální distribuční skupiny. Rozhodne, kterou organizační jednotku by měl uživatel mít, na základě vybrané organizační jednotky Active Directory a přijde s heslem:

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 pak začne s účtem manipulovat:

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

Funkce pro aktualizaci a vytvoření účtu mají podobnou syntaxi, nejsou vyžadována všechna další pole, v sekci s telefonními čísly je třeba zadat pole, které může obsahovat až jeden záznam s číslem a jeho typem.

Aby nedošlo k chybě při přidávání uživatele do skupiny, můžeme si nejprve ověřit, zda již není členem této skupiny, a to získáním seznamu členů skupiny nebo složení od samotného uživatele.

Dotazování na členství ve skupině konkrétního uživatele nebude rekurzivní a zobrazí pouze přímé členství. Zahrnutí uživatele do nadřazené skupiny, která již má podřízenou skupinu, jejímž členem je uživatel, bude úspěšné.

Závěr

Zbývá pouze zaslat uživateli heslo k novému účtu. Děláme to pomocí SMS a obecné informace s instrukcemi a přihlášením posíláme na osobní email, který spolu s telefonním číslem poskytlo náborové oddělení. Alternativně můžete ušetřit peníze a poslat své heslo do tajného telegramového chatu, což lze také považovat za druhý faktor (MacBooky budou výjimkou).

Děkuji, že jste dočetli až do konce. Budu rád, když uvidím návrhy na zlepšení stylu psaní článků a přeji vám, abyste při psaní skriptů chytali méně chyb =)

Seznam odkazů, které mohou být tematicky užitečné nebo jednoduše odpovídat na otázky:

Zdroj: www.habr.com

Přidat komentář