Hej!
I den hÀr artikeln kommer vi att beskriva implementationen av interaktionen mellan PowerShell och Google API för att utföra manipulationer med G Suite-anvÀndare.
I vÄr organisation anvÀnder vi flera interna och molntjÀnster. För det mesta begrÀnsas autentiseringen i dem till Google eller Active Directory, mellan vilka vi inte kan upprÀtthÄlla en replikering, och dÀrför behöver vi vid anstÀllning av en ny medarbetare skapa/aktivera ett konto i dessa tvÄ system. För att automatisera processen beslutade vi att skriva ett skript som samlar information och skickar den till bÄda tjÀnsterna.
Behörighet
NÀr vi sammanstÀllde kraven beslutade vi att anvÀnda verkliga administratörs-anvÀndare för autentisering, vilket gör det lÀttare att förstÄ ÄtgÀrder vid oavsiktliga eller avsiktliga massÀndringar.
För autentisering och auktorisering anvÀnder Google API OAuth 2.0-protokollet. AnvÀndningsscenarier och mer detaljerad information kan ses hÀr: .
Jag valde scenariot som anvÀnds vid autentisering i stationÀra applikationer. Det finns Àven ett alternativ att anvÀnda ett tjÀnstekonto som inte krÀver nÄgra onödiga ÄtgÀrder frÄn anvÀndaren.
Bilden nedan Àr en schematisk beskrivning av det valda scenariot frÄn Googles sida.

- Först skickar vi anvÀndaren till autentiseringssidan för Google-kontot och anger GET-parametrarna:
- applikationsidentifierare
- omrÄden som applikationen behöver tillgÄng till
- adressen dit anvÀndaren kommer att omdirigeras efter att proceduren Àr klar
- metoden som vi kommer att anvÀnda för att uppdatera token
- verifieringskod
- format för överföring av verifieringskod
- Efter att autentiseringen har avslutats kommer anvÀndaren att omdirigeras till den angivna sidan frÄn den första begÀran med felmeddelande eller auktoriseringskod som anges som GET-parametrar.
- Applikationen (skriptet) behöver hÀmta dessa parametrar och, om koden erhÄlls, utföra nÀsta begÀran för att fÄ tokens.
- Vid korrekt begÀran returnerar Google API:
- Access-token som vi kan anvÀnda för att göra förfrÄgningar
- Denna tokens giltighetstid
- Refresh-token som behövs för att uppdatera Access-token.
Först behöver du gÄ till Google API-konsolen: , vÀlj den önskade applikationen och skapa en OAuth-klientidentifierare i avsnittet Credentials. DÀr (eller senare, i instÀllningarna för den skapade identifieraren) mÄste adresser anges dit omdirigering Àr tillÄten. I vÄrt fall kommer detta att vara flera poster för localhost med olika portar (se lÀngre fram).
För att göra det enklare att lÀsa skriptets algoritm kan du extrahera de första stegen till en separat funktion som returnerar Access- och refresh-token för applikationen:
$client_secret = 'VÄrt Klienthemlighet'
$client_id = 'VÄrt Klient-ID'
function Get-GoogleAuthToken {
if (-not [System.Net.HttpListener]::IsSupported) {
"HttpListener stöds inte."
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 "Starta webblÀsaren..."
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 {
"Kunde inte starta lyssnaren."
exit 1
}
while (($code -eq $null)) {
$context = $listener.GetContext()
Write-Host "Anslutning accepterad" -f 'mag'
$url = $context.Request.RawUrl
$code = $url.split('?')[1].split('=')[1].split('&')[0]
if ($url.split('?')[1].split('=')[0] -eq 'error') {
Write-Host "Fel!"$code -f 'red'
$buffer = [System.Text.Encoding]::UTF8.GetBytes("Fel!"+$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("Nu kan du stÀnga denna flik i webblÀsaren.")
$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
Vi anger Client ID och Client Secret, som vi fĂ„tt i instĂ€llningarna för OAuth-klientidentifieraren, och code verifier â en strĂ€ng som Ă€r mellan 43 och 128 tecken lĂ„ng, vilket mĂ„ste genereras slumpmĂ€ssigt frĂ„n icke-reserverade tecken: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".
Denna kod kommer sedan att sÀndas igen. Den utesluter en sÄrbarhet dÀr en illasinnad person kan avlyssna svaret som returneras genom en omdirigering efter att anvÀndaren har autentiserat sig.
Att skicka code verifier i den aktuella begĂ€ran kan göras i klartext (vilket gör det meningslöst â detta Ă€r endast lĂ€mpligt för system som inte stöder SHA256), eller genom att skapa en hash med SHA256-algoritmen som mĂ„ste kodas i BASE64Url (vilket skiljer sig frĂ„n Base64 med tvĂ„ tecken i tabellen) och ta bort radslutet: =.
DÀrefter behöver vi börja lyssna pÄ http pÄ den lokala maskinen för att fÄ svaret efter autentisering, som kommer tillbaka som en omdirigering.
Administrativa uppgifter utförs pÄ en speciell server, vi kan inte utesluta sannolikheten att flera administratörer samtidigt startar skriptet, sÄ det vÀljer slumpmÀssigt en port för den aktuella anvÀndaren, men jag har specificerat fördefinierade portar, eftersom de ocksÄ mÄste lÀggas till som betrodda i API-konsolen.
access_type=offline betyder att applikationen kan uppdatera den utgÄngna token sjÀlvstÀndigt utan anvÀndarinteraktion med webblÀsaren,
response_type=code anger formatet för hur koden kommer tillbaka (en hÀnvisning till det gamla sÀttet att autentisera, nÀr anvÀndaren kopierade koden frÄn webblÀsaren till skriptet),
scope anger en typ av Ätkomst. De ska separeras med mellanrum eller (enligt URL Encoding). Listan över ÄtkomstomrÄden med typer kan ses hÀr: .
Efter att ha fÄtt auktorisationskoden kommer applikationen att Äterföra ett meddelande till webblÀsaren om stÀngning, sluta lyssna pÄ porten och skicka en POST-begÀran för att fÄ token. Vi anger i den angivna id och hemlighet frÄn API-konsolen, adressen dit anvÀndaren kommer att omdirigeras och grant_type enligt protokollets specifikation.
Som svar fÄr vi Access-token, dess giltighetstid i sekunder och Refresh-token, med hjÀlp av vilken vi kan uppdatera Access-token.
Applikationen bör lagra tokens pÄ en sÀker plats med lÄng lagringstid, sÄ lÀnge vi inte Äterkallar den erhÄllna Ätkomsten, kommer applikationen inte att fÄ tillbaka refresh-token. I slutet har jag lagt till en begÀran om Äterkallelse av token om applikationen stÀngdes utan framgÄng och refresh-token inte returnerades, den kommer att pÄbörja proceduren pÄ nytt (vi ansÄg att det var osÀkert att lagra tokens lokalt pÄ terminalen, och att komplicera med kryptografi eller ofta öppna webblÀsaren Àr inte önskvÀrt).
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
}
Som du redan har mÀrkt anvÀnds Invoke-WebRequest för att Äterkalla token. Till skillnad frÄn Invoke-RestMethod returnerar den inte de mottagna uppgifterna i ett anvÀndarvÀnligt format och visar status för begÀran.
DÀrefter kommer skriptet att be om anvÀndarens för- och efternamn och generera anvÀndarnamn + e-post.
FörfrÄgningar
De kommande förfrĂ„gningarna â först och frĂ€mst mĂ„ste du kontrollera om det redan finns en anvĂ€ndare med detta anvĂ€ndarnamn för att avgöra om en ny ska skapas eller den aktuella ska anvĂ€ndas.
Jag har beslutat att implementera alla förfrÄgningar i en funktion med val, med hjÀlp av 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']+" is now a member of "+$query['groupkey'])
}
}
}
}I varje begÀran mÄste en Authorization-header skickas med som innehÄller token-typen och access-token. För nÀrvarande Àr token-typen alltid Bearer. Eftersom vi behöver kontrollera att token inte har gÄtt ut och uppdatera den efter en timme frÄn utfÀrdandet, har jag angett en begÀran till en annan funktion som returnerar access-token. Denna koddel finns ocksÄ i början av skriptet nÀr den första access-token erhÄlls:
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 UtgÄnget. Uppdaterar..."
$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
}Kontrollera inloggningens existens:
function Check_Google {
$query = (GoogleQuery 'SearchAccount' $username)
if ($query.users -ne $null) {
$user = $query.users[0]
Write-Host $user.name.fullName' - '$user.PrimaryEmail' - avstÀngd: '$user.Suspended
$GAresult = $user
}
if ($GAresult) {
$return = $GAresult
} else {$return = 'gg'}
return $return
}FörfrÄgan email:$query kommer att be API att söka efter anvÀndaren med denna specifika email, inklusive alias. En wildcard kan ocksÄ anvÀndas: =, :, :{PREFIX}*.
För att hĂ€mta data anvĂ€nds GET-förfrĂ„gan, för att införa data (skapa konto eller lĂ€gga till medlem i grupp) â POST, för att uppdatera existerande data â PUT, för att ta bort en post (till exempel, en medlem ur grupp) â DELETE.
Skriptet kommer ocksÄ att frÄga efter telefonnummer (ogiltig strÀng) och om deltagande i den regionala e-postlistan. Det avgör vilken organisatorisk enhet anvÀndaren ska tillhöra baserat pÄ valda OU i Active Directory och genererar ett lösenord:
do {
$phone = Read-Host "Telefon i formatet +7Ń
Ń
Ń
Ń
Ń
Ń
Ń
Ń
"
} while (-not $phone)
do {
$moscow = Read-Host "I Moskva kontoret? (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 "Kommer att skapas i /Team delivery"
$orgunit = "/Team delivery"
}
$Password = -join (48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba"
Och börjar sedan manipulera med kontot:
$query = @{
email = $email
givenName = $firstname
familyName = $lastname
password = $password
phone = $phone
orgunit = $orgunit
}
if ($GMailExist) {
Write-Host "Startar Àndring av kontot" -f mag
(GoogleQuery 'UpdateAccount' $query) | fl
write-host "Glöm inte kontrollera grupper för den aktiverade $Username i Google."
} else {
Write-Host "Startar skapande av konto" -f mag
(GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
write-host "LĂ€gger till i gruppen moscowoffice"
$query = @{
groupkey = 'moscowoffice@rocketguys.com'
email = $email
}
(GoogleQuery 'AddMember' $query) | fl
}
Funktionerna för att uppdatera och skapa konton har en liknande syntax. Inte alla ytterligare fÀlt Àr obligatoriska; i avsnittet med telefonnummer behöver vi ange en array som kan innehÄlla minst en post med nummer och dess typ.
För att undvika ett fel nÀr man lÀgger till en anvÀndare i en grupp kan vi först kontrollera om de redan Àr med i den gruppen genom att hÀmta en lista över gruppmedlemmarna eller medlemskapet för den enskilda anvÀndaren.
BegÀran om medlemskapet i grupper för en viss anvÀndare kommer inte att vara rekursiv och visar endast direkt medlemskap. Att inkludera anvÀndaren i en övergripande grupp dÀr en undergrupp redan ingÄr, dÀr anvÀndaren Àr medlem, kommer att vara framgÄngsrik.
Slutsats
Det ÄterstÄr att skicka lösenordet till den nya kontot till anvÀndaren. Vi gör detta via SMS, och den allmÀnna informationen med instruktioner och inloggning skickas till den personliga e-posten som personalavdelningen tillhandahöll tillsammans med telefonnumret. Som ett alternativ kan vi spara pengar och skicka lösenordet i en hemlig Telegram-chatt, vilket ocksÄ kan betraktas som en andra faktor (undantag görs för MacBooks).
Tack för att du lÀste till slutet. Jag ser fram emot förslag pÄ hur skrivstilen kan förbÀttras och önskar att du fÄr fÀrre fel nÀr du skriver skript =)
En lista över lÀnkar som kan vara tematisk anvÀndbara eller helt enkelt svara pÄ uppkomna frÄgor:
KĂ€lla: habr.com
