Google felhasználók létrehozása PowerShell segítségével az API-n keresztül

Helló!

Ebben a cikkben bemutatásra kerül a PowerShell és a Google API közötti interakció megvalósítása, amely lehetővé teszi G Suite felhasználókkal kapcsolatos műveletek végrehajtását.

Szervezetünkben több belső és felhőalapú szolgáltatást használunk. A legtöbb esetben a belépés Google-ra vagy Active Directory-ra korlátozódik, amelyeket nem tudunk replikálni, így új munkatársak felvételekor szükséges az account létrehozása vagy aktiválása ezeken a rendszereken. A folyamat automatizálása érdekében úgy döntöttünk, hogy készítünk egy szkriptet, amely összegyűjti az adatokat, és elküldi mindkét szolgáltatásba.

Engedélyezés

A követelmények megfogalmazásakor úgy döntöttünk, hogy valódi adminisztrátorok hitelesítését használjuk, mivel ez megkönnyíti a cselekvések elemzését váratlan vagy szándékos tömeges változások esetén.

A Google API az OAuth 2.0 protokollt használja az azonosításra és hitelesítésre. A használati forgatókönyvek és további részletes leírás itt tekinthetők meg: OAuth 2.0 használata a Google API-khoz való hozzáféréshez.

A desktop alkalmazások hitelesítésekor használt forgatókönyvet választottam. Van lehetőség egy szolgáltatói fiók használatára is, amely nem igényel további lépéseket a felhasználótól.

Az alábbi kép a választott forgatókönyv vázlatos leírása a Google oldaláról.

Google felhasználók létrehozása PowerShell segítségével az API-n keresztül

  1. Először a felhasználót a Google fiók hitelesítési oldalára irányítjuk, GET paraméterek megadásával:
    • alkalmazásazonosító
    • a hozzáférési területek, amelyekhez az alkalmazásnak szüksége van
    • az az URL, ahová a felhasználót átirányítjuk a folyamat befejezése után
    • a mód, ahogyan a tokent frissítjük
    • ellenőrzési kód
    • ellenőrzési kód átadásának formátuma

  2. A hitelesítési folyamat befejezése után a felhasználót a kérésben megadott URL-re irányítják, a hibával vagy a hitelesítési kóddal átadott GET paraméterekkel.
  3. Az alkalmazásnak (szkriptnek) le kell kérnie ezeket a paramétereket, és ha a kódot megkapja, végre kell hajtania a következő kérést a tokének megszerzéséhez.
  4. Helyes kérés esetén a Google API a következőket adja válaszként:
    • Hozzáférési tokent, amellyel kérdéseket intézhetünk
    • Ennek a tokénak a lejárati ideje
    • Frissítő tokent, amely szükséges a Hozzáférési token frissítéséhez.

Először is, látogassunk el a Google API konzolra: Hitelesítési adatok — Google API konzol, válassza ki a szükséges alkalmazást, és a Credentials részben hozzon létre egy OAuth kliensazonosítót. Ott (vagy később, az elkészített azonosító tulajdonságai között) meg kell adnia azokat a címeket, ahova a visszairányítás engedélyezett. Ebben az esetben több localhost bejegyzést fogunk használni különböző portokkal (lásd később).

A szkript algoritmusának könnyebb olvasásához kihozhatjuk az első lépéseket egy külön funkcióba, amely visszaadja az Access és refresh tokeneket az alkalmazás számára:

$client_secret = 'Our Client Secret'
$client_id = 'Our Client ID'
function Get-GoogleAuthToken {
  if (-not [System.Net.HttpListener]::IsSupported) {
    "A HttpListener nem támogatott."
    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 "Böngésző indítása..."
  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 {
    "Nem sikerült elindítani a hallgatót."
    exit 1
  }
  while (($code -eq $null)) {
    $context = $listener.GetContext()
    Write-Host "Kapcsolat elfogadva" -f 'mag'
    $url = $context.Request.RawUrl
    $code = $url.split('?')[1].split('=')[1].split('&')[0]
    if ($url.split('?')[1].split('=')[0] -eq 'error') {
      Write-Host "Hiba!"$code -f 'red'
      $buffer = [System.Text.Encoding]::UTF8.GetBytes("Hiba!"+$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("Most már bezárhatja ezt a böngészőfület.")
    $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

Beállítjuk a Client ID-t és a Client Secret-et, amelyeket az OAuth kliensazonosító tulajdonságaiból kaptunk, és a code verifier egy 43-tól 128-ig terjedő karakterhosszúságú string, amelyet véletlenszerűen kell generálni az érvénytelen karakterekből: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".

Ezután ezt a kódot újra át fogjuk adni. Ez kizárja azokat a sebezhetőségeket, ahol a támadó elérheti a választ, amely a felhasználó hitelesítése után visszatér a visszairányítással.
A kódellenőrzőt az aktuális kérésben szabadon elküldheti (ami értelmetlenné teszi – ez csak az SHA256-ot nem támogató rendszerek számára megfelelő), vagy létrehozhat egy SHA256 algoritmus által készített hash-t, amelyet BASE64Url-ben kell kódolni (ami két karaktert különbözik a Base64-től) és eltávolítani a sortörést: =.

Ezután el kell kezdenünk hallgatni a http-t a helyi gépen, hogy megkapjuk a választ az engedélyezés után, amely átirányítással érkezik vissza.

Az adminisztrációs feladatokat egy külön szerveren hajtják végre, nem zárhatjuk ki annak lehetőségét, hogy több adminisztrátor egyszerre futtassa a szkriptet, ezért véletlenszerűen választ egy portot az aktuális felhasználó számára, de előre meghatározott portokat adtam meg, mivel ezeket is hozzá kell adni a megbízható listához az API konzolban.

access_type=offline jelenti, hogy az alkalmazás önállóan frissítheti a lejárt token-t, anélkül, hogy a felhasználó közvetlenül a böngészővel interakcióba lépne,
response_type=code meghatározza annak formátumát, ahogyan a kód visszatér (utalás a régi engedélyezési módra, amikor a felhasználó a böngészőből másolta és beillesztette a kódot a szkriptbe),
scope megmutatja a területeket és a hozzáférési típusokat. Ezeket szóközökkel vagy (URL-kódolás szerint) kell elválasztani. A hozzáférési területek és típusok listáját itt tekintheti meg: OAuth 2.0 területek a Google API-khoz.

A kód engedélyezése után az alkalmazás egy bezárási üzenetet küld vissza a böngészőbe, leállítja a port hallgatását, és POST kérést küld a token megszerzésére. Megadjuk benne a korábban a vezetéknevével és titkával rendelkező API konzolból származó azonosítót, a felhasználó által átirányított címet és grant_type-ot a protokoll specifikációja szerint.

Válaszként megkapjuk az Access tokent, a másodpercekben megadott érvényességi idejét és a Refresh tokent, amellyel frissíthetjük az Access tokent.

Az alkalmazásnak biztonságos helyen kell tárolnia a tokent hosszú távú megőrzés céljából, ezért amíg nem visszavonjuk a megszerzett hozzáférést, az alkalmazás nem kap vissza refresh tokent. Végül hozzáadtam a token visszavonására vonatkozó kérdést, ha az alkalmazás nem sikeresen zárult és a refresh token nem tért vissza, akkor újra megkezdi a folyamatot (nem tartottuk biztonságosnak a tokenek helyi tárolását a terminálon, és nem akarjuk bonyolítani a kriptográfiát vagy gyakran megnyitni a böngészőt).

do {
  $token_result = Get-GoogleAuthToken
  $token = $token_result.access_token
  if ($token_result.refresh_token -eq $null) {
    Write-Host ("A munkamenet nincs megszüntetve. Token visszavonása...")
    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
}

Ahogy már észrevette, a token visszavonásakor az Invoke-WebRequest-t használjuk. Ezzel szemben az Invoke-RestMethod nem adja vissza az adatokat könnyen használható formátumban, és nem mutatja a kérelem állapotát.

Ezután a szkript megkéri a felhasználó nevét és vezetéknevét, generálva a felhasználónevet + email címet.

Kérések

A következő lépések a kérések lesznek – először is ellenőrizni kell, hogy létezik-e már felhasználó az adott felhasználónévvel, hogy döntést tudjunk hozni az új kialakításáról vagy a meglévő aktiválásáról.

Úgy döntöttem, hogy az összes kérést egy funkció formájában valósítom meg, amely a switch szerkezetet használja:

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'])
      }
    }
  }
}

Minden kérésnél el kell küldeni egy Authorization fejlécet, amely tartalmazza a token típusát és magát az Access tokent. Jelenleg a token típusa mindig Bearer. Mivel ellenőriznünk kell, hogy a token nem járt-e le, és frissítenünk kell egy órával a kiadás után, megadtam egy másik funkcióra történő hivatkozást, amely visszaadja az Access tokent. Ez a kódrészlet megtalálható a szkript elején is, amikor megszerezzük az első Access tokent:

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 "A token lejárt. Frissítés..."
    $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
}

Bejelentkezés ellenőrzése:

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

Az email lekérdezés:$query megkéri az API-t, hogy keressen felhasználót ezzel az email címmel, beleértve az aliasokat is. Használhatók wildcard karakterek is: =, :, :{PREFIX}*.

Adatok lekéréséhez GET metódust használunk, adatok beszúrásához (fiók létrehozás vagy tag hozzáadása csoporthoz) POST-ot, meglévő adatok frissítéséhez PUT-ot, és adatok törléséhez (például tag törlése csoportból) DELETE-t.

A szkript szintén megkérdezi a telefonszámot (nem érvényesített szöveg) és a regionális hírlevélbe való felvételről. Meghatározza, hogy melyik szervezeti egység tartozzon a felhasználóhoz a kiválasztott OU Active Directory alapján, és kitalál egy jelszót:

do {
  $phone = Read-Host "Telefonszám formátumban: +7хххххххх"
} while (-not $phone)
do {
    $moscow = Read-Host "Moskvába? (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 "A következőben hoz létre: /Team delivery"
    $orgunit = "/Team delivery"
}
$Password = -join (48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba"

Majd műveleteket hajt végre a fiókban:

$query = @{
  email = $email
  givenName = $firstname
  familyName = $lastname
  password = $password
  phone = $phone
  orgunit = $orgunit
}
if ($GMailExist) {
  Write-Host "Fiók módosításának megkezdése" -f magenta
  (GoogleQuery 'UpdateAccount' $query) | fl
  write-host "Ne felejtsd el ellenőrizni a csoportokat a $Username Google-ban.
} else {
  Write-Host "Fiók létrehozásának megkezdése" -f magenta
  (GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
  write-host "Tag hozzáadása a moscowoffice csoporthoz"
  $query = @{
    groupkey = 'moscowoffice@rocketguys.com'
    email = $email
  }
  (GoogleQuery 'AddMember' $query) | fl
}

A fiók frissítési és létrehozási funkcióinak hasonló szintaxisa van, nem minden kiegészítő mező kötelező, a telefonszámok szakaszában pedig meg kell adni egy tömböt, amely tartalmazhat egy vagy több bejegyzést a számmal és annak típusával.

Annak elkerülése érdekében, hogy hiba lépjen fel a felhasználó csoportba való hozzáadásakor, előzetesen ellenőrizhetjük, hogy a felhasználó már része van-e ennek a csoportnak, ha lekérjük a csoporttagok listáját vagy a felhasználó összetételét.

A meghatározott felhasználó csoporttagjainak lekérdezése nem rekurzív, és csak a közvetlen tagságot mutatja. A felhasználó belépése a szülőcsoportba, amelyben már létezik a felhasználó által részt vett leánycsoport, sikeres lesz.

Zárás

Most már csak a felhasználónak kell elküldeni az új fiók jelszavát. Ezt SMS-ben küldjük el, a bejelentkezési információt pedig a személyzet-toborzó által megadott személyes e-mail címre küldjük el, együtt a telefonszámmal. Alternatív megoldásként pénzt takaríthatunk meg, és a jelszót titkos Telegram-csatornán is elküldhetjük, ez is második tényezőként említhető (kivéve a MacBookokat).

Köszönöm, hogy elolvasta a cikket. Örömmel várom a javaslatait a cikkírás stílusának javítására, és kívánom, hogy minél kevesebb hibát találjon a szkriptek írása során =)

Linkek listája, amelyek tematikusan hasznosak lehetnek, vagy egyszerűen válaszolhatnak felmerülő kérdésekre:

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster