Google-felhasználók létrehozása a PowerShellből API-n keresztül

Hi!

Ez a cikk a PowerShell és a Google API közötti interakció megvalósítását írja le a G Suite-felhasználók manipulálására.

Számos belső és felhőszolgáltatást használunk a szervezeten belül. A bennük lévő jogosultság többnyire a Google-hoz vagy az Active Directoryhoz tartozik, amelyek között nem tudunk replikát karbantartani, ennek megfelelően új alkalmazott távozásakor ebben a két rendszerben kell fiókot létrehozni/engedélyezni. A folyamat automatizálása érdekében úgy döntöttünk, hogy írunk egy szkriptet, amely információkat gyűjt és elküldi mindkét szolgáltatásnak.

Meghatalmazás

A követelmények kidolgozásakor úgy döntöttünk, hogy valódi emberi adminisztrátorokat használunk az engedélyezéshez, ami leegyszerűsíti a műveletek elemzését véletlen vagy szándékos jelentős változtatások esetén.

A Google API-k az OAuth 2.0 protokollt használják a hitelesítéshez és engedélyezéshez. A felhasználási esetek és a részletesebb leírások itt találhatók: Az OAuth 2.0 használata a Google API-k eléréséhez.

Azt a szkriptet választottam, amelyet az asztali alkalmazások hitelesítésére használnak. Lehetőség van szolgáltatásfiók használatára is, amely nem igényel felesleges mozgást a felhasználótól.

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

Google-felhasználók létrehozása a PowerShellből API-n keresztül

  1. Először a felhasználót a Google Fiók hitelesítési oldalára küldjük, megadva a GET paramétereket:
    • pályázati azonosító
    • területek, amelyekhez az alkalmazásnak hozzá kell férnie
    • az a cím, amelyre a felhasználó az eljárás befejezése után át lesz irányítva
    • ahogyan frissítjük a tokent
    • Biztonsági kód
    • ellenőrző kód átviteli formátuma

  2. Az engedélyezés befejezése után a felhasználó az első kérésben megadott oldalra kerül a GET paraméterek által átadott hiba vagy engedélyezési kóddal.
  3. Az alkalmazásnak (szkriptnek) meg kell kapnia ezeket a paramétereket, és ha megkapta a kódot, akkor a következő kérést kell végrehajtania a tokenek beszerzéséhez
  4. Ha a kérés helyes, a Google API a következőket adja vissza:
    • Hozzáférési token, amellyel kéréseket tudunk benyújtani
    • Ennek a tokennek az érvényességi ideje
    • Frissítési token szükséges a hozzáférési token frissítéséhez.

Először a Google API-konzolra kell lépnie: Hitelesítési adatok – Google API-konzol, válassza ki a kívánt alkalmazást, és a Hitelesítési adatok részben hozzon létre egy ügyfél OAuth-azonosítót. Ott (vagy később, a létrehozott azonosító tulajdonságaiban) meg kell adnia azokat a címeket, amelyekre az átirányítás megengedett. Esetünkben ez több localhost bejegyzés lesz különböző portokkal (lásd alább).

A szkriptalgoritmus kényelmesebb olvasásának érdekében megjelenítheti az első lépéseket egy külön függvényben, amely hozzáférési és frissítési tokeneket ad vissza az alkalmazáshoz:

$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

Beállítjuk az OAuth-kliens azonosító tulajdonságaiban kapott ügyfélazonosítót és ügyféltitkot, a kódellenőrző pedig egy 43-128 karakterből álló karakterlánc, amelyet véletlenszerűen kell előállítani nem fenntartott karakterekből: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Ez a kód ezután újra elküldésre kerül. Megszünteti azt a sebezhetőséget, amelyben a támadó elkaphat egy átirányításként visszaküldött választ a felhasználó engedélyezése után.
Az aktuális kérésben kódellenőrzőt küldhet szöveges formában (ami értelmetlenné teszi - ez csak az SHA256-ot nem támogató rendszerekre alkalmas), vagy az SHA256 algoritmussal hash létrehozásával, amelyet BASE64Url-ben kell kódolni (eltérő a Base64-ből két táblázatkarakterrel) és eltávolítjuk a karaktersorvégződéseket: =.

Ezután el kell kezdenünk hallgatni a http-t a helyi gépen, hogy az engedélyezés után választ kapjunk, amelyet átirányításként adunk vissza.

Az adminisztrációs feladatokat egy speciális szerveren hajtják végre, nem zárhatjuk ki annak lehetőségét, hogy a szkriptet egyszerre több rendszergazda is lefuttassa, így véletlenszerűen választ ki egy portot az aktuális felhasználó számára, de előre meghatározott portokat adtam meg, mert ezeket is megbízhatóként kell hozzáadni az API-konzolhoz.

access_type=offline azt jelenti, hogy az alkalmazás képes önállóan frissíteni egy lejárt tokent anélkül, hogy a felhasználó interakcióba lépne a böngészővel,
válasz_típus=kód beállítja a kód visszaküldésének formátumát (hivatkozás a régi engedélyezési módra, amikor a felhasználó a kódot a böngészőből a szkriptbe másolta),
hatálya a hozzáférés hatókörét és típusát jelzi. Ezeket szóközzel vagy %20-al kell elválasztani (az URL-kódolás szerint). A hozzáférési területek listája típusokkal itt tekinthető meg: OAuth 2.0 hatókörei a Google API-khoz.

Az engedélyezési kód kézhezvétele után az alkalmazás bezárási üzenetet küld a böngészőnek, leállítja a port figyelését, és egy POST kérést küld a token beszerzéséhez. Megjelöljük benne a korábban megadott azonosítót és titkot a konzol API-ból, a címet, ahová a felhasználó átirányítja, valamint a protokoll specifikációjának megfelelő grant_type-ot.

Válaszul kapunk egy Access tokent, annak érvényességi idejét másodpercben, valamint egy Refresh tokent, amellyel frissíthetjük az Access tokent.

Az alkalmazásnak biztonságos helyen, hosszú szavatossági idővel kell tárolnia a tokeneket, így amíg a kapott hozzáférést nem vonjuk vissza, az alkalmazás nem adja vissza a frissítési tokent. A végén hozzáadtam egy kérelmet a token visszavonására; ha az alkalmazás nem fejeződött be sikeresen, és a frissítési token nem érkezett vissza, akkor újraindítja az eljárást (nem tartottuk biztonságosnak a tokenek helyi tárolását a terminálon, és nem nem akarom bonyolítani a dolgokat a kriptográfiával, 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 ("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
}

Amint azt már észrevette, egy token visszavonásakor az Invoke-WebRequest használatos. Az Invoke-RestMethoddal ellentétben nem használható formátumban adja vissza a kapott adatokat, és a kérés állapotát mutatja.

Ezután a szkript arra kéri, hogy adja meg a felhasználó vezeték- és utónevét, ezzel bejelentkezési + e-mail címet generál.

kérelmek

A következő kérések a következők lesznek - mindenekelőtt ellenőriznie kell, hogy létezik-e már azonos bejelentkezési névvel rendelkező felhasználó, hogy döntést hozzon egy új létrehozásáról vagy a jelenlegi engedélyezéséről.

Úgy döntöttem, hogy az összes kérést egy funkció formátumában valósítom meg egy kiválasztással, a kapcsoló segítségével:

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érelemben el kell küldenie egy engedélyezési fejlécet, amely tartalmazza a token típusát és magát a hozzáférési tokent. Jelenleg a token típusa mindig Bearer. Mert ellenőriznünk kell, hogy a token nem járt-e le, és frissítenünk kell a kiadás pillanatától számított egy óra elteltével, megadtam egy kérést egy másik függvényre, amely hozzáférési tokent ad vissza. Ugyanez a kódrészlet található a szkript elején, amikor megkapja az első hozzáférési 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 "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
}

A bejelentkezési név létezésének 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' - suspended: '$user.Suspended
    $GAresult = $user
  }
  if ($GAresult) {
      $return = $GAresult
  } else {$return = 'gg'}
  return $return
}

Az email:$query kérés arra kéri az API-t, hogy keressen egy felhasználót pontosan ezzel az e-mail-címmel, beleértve az álneveket is. Használhat helyettesítő karaktert is: =, :, :{PREFIX}*.

Adatok beszerzéséhez használja a GET kérési módszert, adatok beszúrásához (fiók létrehozása vagy tag felvétele egy csoportba) - POST, meglévő adatok frissítése - PUT, rekord törlése (például egy tag egy csoportból) - TÖRÖL.

A szkript egy telefonszámot (egy érvényesítetlen karakterláncot) és egy regionális terjesztési csoportba való felvételt is kér. A kiválasztott Active Directory szervezeti egység alapján eldönti, hogy a felhasználónak melyik szervezeti egységgel kell rendelkeznie, és kiad egy jelszót:

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"

Aztán elkezdi manipulálni a fiókot:

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

A fiókfrissítés és a fiók létrehozásának függvényei hasonló szintaxisúak, nem minden további mezőt kell kitölteni, a telefonszámokat tartalmazó részben meg kell adni egy tömböt, amely legfeljebb egy rekordot tartalmazhat a számmal és annak típusával.

Annak érdekében, hogy ne kapjon hibaüzenetet, amikor egy felhasználót adunk hozzá egy csoporthoz, először ellenőrizhetjük, hogy tagja-e már ennek a csoportnak, ha lekérjük a csoporttagok listáját vagy összetételét magától a felhasználótól.

Egy adott felhasználó csoporttagságának lekérdezése nem lesz rekurzív, és csak a közvetlen tagságot mutatja. Sikeres lesz egy felhasználó felvétele egy olyan szülőcsoportba, amelynek már van gyermekcsoportja, amelynek a felhasználó tagja.

Következtetés

Nincs más hátra, mint elküldeni a felhasználónak az új fiók jelszavát. Ezt SMS-ben tesszük, és az általános információkat instrukciókkal és bejelentkezéssel egy személyes e-mailre küldjük, amelyet a telefonszámmal együtt a toborzási osztály biztosított. Alternatív megoldásként pénzt takaríthat meg, és elküldheti jelszavát egy titkos távirati chatre, ami szintén a második tényezőnek tekinthető (a MacBookok kivételek lesznek).

Köszönöm, hogy a végéig elolvastad. Örömmel fogadok javaslatokat a cikkírás stílusának javítására, és kívánom, hogy kevesebb hibát észleljen a szkriptek írásakor =)

Azon linkek listája, amelyek tematikusan hasznosak lehetnek, vagy egyszerűen válaszolhatnak a kérdésekre:

Forrás: will.com

Hozzászólás