Ahoj!
V tomto článku opíšeme implementáciu interakcie PowerShellu s Google API na manipuláciu s používateľmi G Suite.
V organizácii používame niekoľko vnútorných a cloudových služieb. Vo väčšine prípadov je autentifikácia v nich založená na Google alebo Active Directory, medzi ktorými nemôžeme udržiavať replikáciu, a preto, pri nástupe nového zamestnanca je potrebné vytvoriť/aktivovať účet v týchto dvoch systémoch. Na automatizáciu procesu sme sa rozhodli napísať skript, ktorý zbiera informácie a odosiela ich do oboch služieb.
Autorizácia
Pri zostavovaní požiadaviek sme sa rozhodli použiť na autentifikáciu reálnych ľudí-adminov, čo uľahčuje analýzu akcií pri náhodných alebo úmyselných hromadných zmenách.
Na autentifikáciu a autorizáciu Google API používa protokol OAuth 2.0. Scenáre použitia a podrobnejší popis sú k dispozícii tu: .
Vybral som scenár, ktorý sa používa pri autentifikácii v desktopových aplikáciách. Existuje aj možnosť použiť servisný účet, ktorý nevyžaduje zbytočné úkony od používateľa.
Obrázok nižšie je schematický popis vybraného scenára z stránky Google.

- Najprv posielame používateľa na stránku autentifikácie do účtu Google, pričom uvádzame GET parametre:
- identifikátor aplikácie
- oblasti, k ktorým je potrebný prístup aplikácii
- adresa, na ktorú bude používateľ presmerovaný po ukončení postupu
- spôsob, ktorým budeme aktualizovať token
- overovací kód
- formát prenosu overovacieho kódu
- Po ukončení autorizácie bude používateľ presmerovaný na stránku určenú v prvej žiadosti, s chybou alebo s overovacím kódom, ktorý bude odovzdaný cez GET parametre.
- Aplikácia (skript) bude musieť získať tieto parametre a v prípade získania kódu vykonať nasledujúcu žiadosť na získanie tokenov.
- Pri správnej žiadosti Google API vracia:
- Access token, s ktorým môžeme vykonávať požiadavky
- Doba platnosti tohto tokenu
- Refresh token, nevyhnutný na obnovenie Access tokenu.
Najprv je potrebné ísť do konzoly Google API: , vyberte požadovanú aplikáciu a v sekcii Credentials vytvorte identifikátor OAuth klienta. Tam tiež (alebo neskôr, v vlastnostiach vytvoreného identifikátora) musíte uviesť adresy, na ktoré je povolené presmerovanie. V našom prípade to budú niekoľko záznamov localhost s rôznymi portami (viď ďalej).
Aby bol algoritmus skriptu prehľadnejší, môžeme prvé kroky vytrhnúť do samostatnej funkcie, ktorá vráti Access a refresh tokeny pre aplikáciu:
$client_secret = 'Náš klientsky tajomstvo'
$client_id = 'Náš klientsky ID'
function Get-GoogleAuthToken {
if (-not [System.Net.HttpListener]::IsSupported) {
"HttpListener nie je podporovaný."
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 "Spustite prehliadač..."
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 {
"Nie je možné spustiť poslucháča."
exit 1
}
while (($code -eq $null)) {
$context = $listener.GetContext()
Write-Host "Pripojenie akceptované" -f 'mag'
$url = $context.Request.RawUrl
$code = $url.split('?')[1].split('=')[1].split('&')[0]
if ($url.split('?')[1].split('=')[0] -eq 'error') {
Write-Host "Chyba!"$code -f 'red'
$buffer = [System.Text.Encoding]::UTF8.GetBytes("Chyba!"+$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("Teraz môžete uzavrieť túto kartu prehliadača.")
$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
Určujeme Client ID a Client Secret, získané v vlastnostiach identifikátora klienta OAuth, a code verifier – je to reťazec dlhý od 43 do 128 znakov, ktorý musí byť náhodne vygenerovaný z nerezervovaných znakov: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".
Tento kód bude následne odoslaný znova. Vylučuje zraniteľnosť, pri ktorej by útočník mohol zachytiť odpoveď, ktorá sa vrátila presmerovaním po autorizácii používateľa.
Odoslať code verifier v aktuálnom požiadavku je možné v otvorenej podobe (čo ho robí bezvýznamným – to je vhodné iba pre systémy, ktoré nepodporujú SHA256), alebo vytvorením hash pomocou algoritmu SHA256, ktorý treba zakódovať v BASE64Url (líši sa od Base64 dvoma symbolmi tabuľky) a odstrániť znak konca riadku: =.
Ďalej musíme začať počúvať http na lokálnom počítači, aby sme prijali odpoveď po autorizácii, ktorá sa vráti presmerovaním.
Administratívne úlohy sa vykonávajú na špeciálnom serveri, nemôžeme vylúčiť pravdepodobnosť, že niekoľko administrátorov súčasne spustí skript, preto náhodne vyberie port pre aktuálneho používateľa, ale predtým som určil vopred definované porty, keďže ich treba tiež pridať ako dôveryhodné v API konzole.
access_type=offline znamená, že aplikácia môže obnovovať vypršaný token sama bez interakcie používateľa s prehliadačom,
response_type=code určuje formát toho, ako sa vráti kód (odkaz na starý spôsob autorizácie, keď používateľ kopíroval kód z prehliadača do skriptu),
scope uvádza oblasti a typ prístupu. Musia byť oddelené medzerami alebo (v súlade s URL Encoding). Zoznam oblastí prístupu s typmi si môžete pozrieť tu: .
Po získaní autorizačného kódu aplikácia vráti do prehliadača správu o zatvorení, prestane počúvať port a odošle POST požiadavku na získanie tokenu. V nej uvádzame vopred zadané id a secret z API konzoly, adresu, na ktorú bude používateľ presmerovaný a grant_type podľa špecifikácie protokolu.
V odpovedi dostaneme Access token, jeho čas platnosti v sekundách a Refresh token, ktorým môžeme obnoviť Access token.
Aplikácia by mala uchovávať tokeny na bezpečnom mieste s dlhodobým úložiskom, takže kým neodvoláme získaný prístup, aplikácii sa nevráti refresh token. Na konci som pridal požiadavku na odvolanie tokenu, ak aplikácia nebola úspešne dokončená a refresh token sa nevrátil, začne proces znova (považovali sme za nebezpečné uchovávať tokeny lokálne na terminály, a komplikovať kryptografiou alebo často otvárať prehliadač sa nám nechcelo).
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 získané údaje v pohodlnom formáte a zobrazuje stav požiadavky.
Ďalej skript požaduje zadať meno a priezvisko používateľa, pričom generuje prístupové meno + email.
Dotazy
Následne budú požiadavky – predovšetkým je potrebné skontrolovať, či už existuje používateľ s takýmto prístupovým menom, aby sa rozhodlo o vytvorení nového alebo o zapojení aktuálneho.
Rozhodol som sa implementovať všetky požiadavky v formáte jednej funkcie s výberom, pomocou switch:
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'])
}
}
}
}Každý požiadavok musí obsahovať hlavičku Authorization, ktorá obsahuje typ tokenu a samotný Access token. V súčasnosti je typ tokenu vždy Bearer. Keďže potrebujeme overiť, či token nie je vypršaný a aktualizovať ho po uplynutí hodiny od vydania, pridal som požiadavok na inú funkciu, ktorá vracia Access token. Tento istý úryvok kódu je na začiatku skriptu pri získavaní prvého Access tokenu:
funkcia 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 vypršal. Obnovujeme..."
$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 prihlasovania:
funkcia Check_Google {
$query = (GoogleQuery 'SearchAccount' $username)
if ($query.users -ne $null) {
$user = $query.users[0]
Write-Host $user.name.fullName' - '$user.PrimaryEmail' - pozastavený: '$user.Suspended
$GAresult = $user
}
if ($GAresult) {
$return = $GAresult
} else {$return = 'gg'}
return $return
}Žiadosť o email: $query požiada API, aby vyhľadalo používateľa s presne týmto emailom, vrátane aliasov. Takisto je možné použiť zástupný znak: =, :, :{PREFIX}*.
Na získanie údajov sa používa metóda GET, na vloženie údajov (vytvorenie účtu alebo pridanie účastníka do skupiny) – POST, na aktualizáciu existujúcich údajov – PUT, na odstránenie záznamu (napríklad účastníka zo skupiny) – DELETE.
Skript sa tiež opýta na telefónne číslo (nevalidovaný reťazec) a na zapojenie do regionálnej skupiny na rozposielanie. Rozhoduje, aká organizačná jednotka by mala byť používateľovi na základe vybranej OU Active Directory a vymyslí heslo:
do {
$phone = Read-Host "Telefón vo formáte +7хххххххх"
} while (-not $phone)
do {
$moscow = Read-Host "Do moskovskej kancelárie? (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 "Bude vytvorené v /Team delivery"
$orgunit = " /Team delivery"
}
$Password = -join (48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba"
A potom začne manipulácie s účtom:
$query = @{
email = $email
givenName = $firstname
familyName = $lastname
password = $password
phone = $phone
orgunit = $orgunit
}
if ($GMailExist) {
Write-Host "Spúšťame zmenu účtu" -f mag
(GoogleQuery 'UpdateAccount' $query) | fl
write-host "Nezabudni skontrolovať skupiny zapojeného $Username v Google."
} else {
Write-Host "Spúšťame vytvorenie účtu" -f mag
(GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
write-host "Pridávame do skupiny moscowoffice"
$query = @{
groupkey = 'moscowoffice@rocketguys.com'
email = $email
}
(GoogleQuery 'AddMember' $query) | fl
}
Funkcie aktualizácie a vytvorenia účtu majú podobnú syntax, nie všetky doplnkové polia sú povinné, v časti s telefónnymi číslami je potrebné uviesť pole, ktoré môže obsahovať od jednej položky s číslom a jeho typom.
Aby sme sa vyhli chybe pri pridávaní používateľa do skupiny, môžeme najprv overiť, či už je v tejto skupine, získaním zoznamu členov skupiny alebo zložením užívateľa.
Dotaz na zloženie skupín určitého používateľa nebude rekurzívny a zobrazí iba priame členstvo. Zaradenie používateľa do rodičovskej skupiny, v ktorej už existuje podskupina, ktorej je používateľ členom, bude úspešné.
Záver
Zostáva odoslať používateľovi heslo k novému účtu. Robíme to prostredníctvom SMS, a celkové informácie s pokynmi a prihlasovacími údajmi posielame na osobný e-mail, ktorý spolu s telefónnym číslom poskytol personálny oddelenie. Ako alternatívnu možnosť je možné ušetriť peniaze a poslať heslo v tajnom chate na Telegrame, čo sa tiež dá považovať za druhý faktor (s výnimkou MacBookov).
Ďakujem, že ste si prečítali až do konca. Budem rád, ak mi pošlete návrhy na zlepšenie štýlu písania článkov a prajem vám, nech pri písaní skriptov chytíte čo najmenej chýb =)
Zoznam odkazov, ktoré môžu byť tematicky užitočné alebo odpovedať na vzniknuté otázky:
Zdroj: habr.com
