Hej!
I denne artikel vil implementeringen af PowerShell-interaktion med Google API blive beskrevet for at udføre manipulationer med G Suite-brugere.
I organisationen bruger vi flere interne og cloud-tjenester. Størstedelen af godkendelsen i dem er relateret til Google eller Active Directory, mellem hvilke vi ikke kan opretholde en replika; derfor skal der oprettes/aktiveres en konto i begge systemer, når en ny medarbejder tiltræder. For at automatisere processen besluttede vi at skrive et script, der samler information og sender den til begge tjenester.
Godkendelse
Da vi udarbejdede kravene, besluttede vi at anvende reelle administrator-brugere til godkendelse, hvilket letter forståelsen af handlinger ved utilsigtede eller tilsigtede masseændringer.
Til autentificering og autorisation bruger Google API OAuth 2.0-protokollen. Brugsscenarier og mere detaljeret beskrivelse kan ses her: .
Jeg valgte et scenarie, der anvendes ved godkendelse i desktop-applikationer. Der er også en mulighed for at bruge en tjenestekonto, som ikke kræver ekstra input fra brugeren.
Billedet nedenfor er en schematisk beskrivelse af det valgte scenarie fra Googles side.

- Først sender vi brugeren til autentificeringssiden for Google-kontoen og angiver følgende GET-parametre:
- applikations-id
- områderne, som applikationen har brug for adgang til
- adressen, som brugeren vil blive omdirigeret til efter afslutningen af proceduren
- metoden, hvormed vi vil opdatere tokenet
- bekræftelseskode
- format til transmission af bekræftelseskoden
- Efter afslutning af godkendelsen vil brugeren blive omdirigeret til den angivne side i den første anmodning, med fejl eller autorisationskode sendt som GET-parametre.
- Applikationen (scriptet) skal modtage disse parametre og, hvis en kode er modtaget, udføre den næste anmodning for at få tokens.
- Ved en korrekt anmodning returnerer Google API:
- Access-token, som vi kan bruge til at foretage anmodninger
- Udløbsdato for dette token
- Refresh-token, som er nødvendigt for at opdatere access-token.
Først skal du gå til Google API-konsollen: , vælg den ønskede applikation, og opret en OAuth-klient-id i sektionen Credentials. Her (eller senere i egenskaberne for den oprettede id) skal du angive de adresser, som omdirigering er tilladt til. I vores tilfælde vil dette være flere localhost-poster med forskellige porte (se længere nede).
For at gøre det lettere at læse algoritmen i scriptet, kan vi udtrække de første trin til en separat funktion, der returnerer Access- og refresh-tokens for applikationen:
$client_secret = 'Vores Klient Hemmelighed'
$client_id = 'Vores Klient ID'
function Get-GoogleAuthToken {
if (-not [System.Net.HttpListener]::IsSupported) {
"HttpListener understøttes ikke."
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 "Starter 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 {
"Kunne ikke starte lytteren."
exit 1
}
while (($code -eq $null)) {
$context = $listener.GetContext()
Write-Host "Forbindelse accepteret" -f 'mag'
$url = $context.Request.RawUrl
$code = $url.split('?')[1].split('=')[1].split('&')[0]
if ($url.split('?')[1].split('=')[0] -eq 'error') {
Write-Host "Fejl!"$code -f 'red'
$buffer = [System.Text.Encoding]::UTF8.GetBytes("Fejl!"+$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("Du kan nu lukke denne browserfane.")
$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 angiver Client ID og Client Secret, som vi har fået fra egenskaberne for OAuth-klient-id'et, og code verifier – dette er en streng, der er mellem 43 og 128 tegn lang, og som skal genereres tilfældigt fra ikke-reserverede tegn: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".
Denne kode vil blive sendt igen. Den udelukker sårbarheden, hvor en angriber kan opsnappe svaret, der returneres som en omdirigering efter brugerens autorisation.
Det er muligt at sende code verifier i den aktuelle anmodning i klar tekst (hvilket gør det meningsløst – dette gælder kun for systemer, der ikke understøtter SHA256), eller ved at oprette en hash med SHA256-algoritmen, der skal kodes i BASE64Url (som adskiller sig fra Base64 med to tegn fra tabellen) og fjerne slut-tegnsymbolet: =.
Derefter skal vi begynde at lytte til http på den lokale maskine for at modtage svaret efter autentificering, som returneres via en omdirigering.
Administrative opgaver udføres på en særlig server, vi kan ikke udelukke muligheden for, at flere administratorer starter scriptet samtidigt, så det vil tilfældigt vælge en port for den aktuelle bruger, men jeg har angivet foruddefinerede porte, da de også skal tilføjes som betroede i API-konsollen.
access_type=offline betyder, at applikationen kan opdatere det udløbne token selv uden brugerinteraktion med browseren,
response_type=code definerer formatet for, hvordan koden returneres (henvisning til den gamle metode til autentificering, hvor brugeren kopierede koden fra browseren til scriptet),
scope angiver områder og adgangstype. De skal adskilles med mellemrum eller (i overensstemmelse med URL Encoding). Listen over adgangsområder med typer kan ses her: .
Efter at have modtaget autoriseringskoden, vil applikationen sende en meddelelse til browseren om lukningen, stoppe med at lytte på porten og sende en POST-anmodning for at få adgangstokenet. Vi angiver de tidligere angivne id og secret fra API-konsollen, adressen, som brugeren vil blive omdirigeret til, samt grant_type i henhold til protokolspecifikationen.
Som svar vil vi modtage Access token, dens gyldighed i sekunder samt Refresh token, som vi kan bruge til at opdatere Access token.
Applikationen skal opbevare tokens på et sikkert sted med lang holdbarhed, derfor vil applikationen ikke modtage refresh token, så længe vi ikke tilbagekalder den indhentede adgang. Til sidst tilføjede jeg en anmodning om tilbagekaldelse af tokenet, hvis applikationen ikke blev afsluttet korrekt, og refresh token ikke blev returneret, så vil den starte proceduren forfra (vi anser det for usikkert at opbevare tokens lokalt på terminalen, og at komplicere det med kryptografi eller ofte åbne browseren er ikke ønskeligt).
gør {
$token_result = Get-GoogleAuthToken
$token = $token_result.access_token
hvis ($token_result.refresh_token -eq $null) {
Write-Host ("Session er ikke ødelagt. Tilbageretrækning af token...")
Invoke-WebRequest -Uri ("https://accounts.google.com/o/oauth2/revoke?token="+$token)
}
} mens ($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
hvis ($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 måske har bemærket, anvendes Invoke-WebRequest til at tilbagekalde token. I modsætning til Invoke-RestMethod returnerer den ikke data i et brugervenligt format og viser status for anmodningen.
Derefter vil skriptet bede om at indtaste brugerens fornavn og efternavn, og generere login + email.
Anmodninger
De næste vil være anmodninger – først skal vi tjekke, om der allerede findes en bruger med dette login for at træffe beslutning om at oprette en ny eller inkludere den nuværende.
Jeg besluttede at implementere alle anmodninger i formatet af én funktion med valg, ved hjælp af 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'])
}
}
}
}Alle anmodninger skal sende et Autorisationsheader, der indeholder både typen af token og selve Access-tokenet. I øjeblikket er token-typen altid Bearer. Da vi skal kontrollere, at tokenet ikke er udløbet og opdatere det efter en time fra udstedelsen, har jeg angivet en anmodning til en anden funktion, der returnerer Access-tokenet. Denne del af koden findes også i starten af scriptet ved hentning af det første Access-token:
funktion Get-GoogleToken {
hvis (((Get-date).TimeStamp -gt $token_expire.hour) -eller (((Get-date).TimeStamp -ge $token_expire.hour) -og ((Get-date).Minute -gt $token_expire.minute))) {
Write-Host "Token er udløbet. Opfrisker..."
$request = (Invoke-RestMethod -Metode 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
hvis ($minute -lt 0) {$minute += 60}
ellers hvis ($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
}Kontrol af login for eksisterende:
funktion Check_Google {
$query = (GoogleQuery 'SearchAccount' $username)
hvis ($query.users -ne $nul) {
$user = $query.users[0]
Write-Host $user.name.fullName' - '$user.PrimaryEmail' - suspenderet: '$user.Suspended
$GAresult = $user
}
hvis ($GAresult) {
$return = $GAresult
} ellers {$return = 'gg'}
return $return
}Anmodning email:$query beder API'et om at søge efter brugeren med den specifikke email, herunder vil aliaser også blive fundet. Wildcard kan også anvendes: =, :, :{PREFIX}*.
Til at hente data bruges GET-anmodningsmetoden, til at indsætte data (oprette en konto eller tilføje et medlem til en gruppe) – POST, til at opdatere eksisterende data – PUT, til at slette en post (for eksempel et medlem fra en gruppe) – DELETE.
Scriptet vil også spørge om et telefonnummer (ugyldig strenge) og om at tilslutte sig den regionale mailingliste. Det bestemmer hvilken organisatorisk enhed brugeren skal være tilknyttet baseret på den valgte OU Active Directory og genererer et kodeord:
do {
$phone = Read-Host "Telefon i formatet +7xx xxxx xxx"
} mens (-not $phone)
do {
$moscow = Read-Host "I Moskva-kontoret? (y/n) "
} mens (-not (($moscow -eq 'y') -eller ($moscow -eq 'n')))
$orgunit = ' /
if ($OU -like "*OU=Delivery,OU=Users,OU=ROOT,DC=rocket,DC=local") {
Write-host "Oprettelse i /Team delivery"
$orgunit = "/Team delivery"
}
$Password = -join (48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_}) + "*Ba"
Og derefter begynder manipulationerne med kontoen:
$query = @{
email = $email
givenName = $firstname
familyName = $lastname
password = $password
phone = $phone
orgunit = $orgunit
}
if ($GMailExist) {
Write-Host "Starter ændringen af kontoen" -f mag
(GoogleQuery 'UpdateAccount' $query) | fl
Write-host "Glem ikke at kontrollere grupper for den tilsluttede $Username i Google."
} ellers {
Write-Host "Starter oprettelsen af kontoen" -f mag
(GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
Write-host "Tilføjer til gruppen moscowoffice"
$query = @{
groupkey = 'moscowoffice@rocketguys.com'
email = $email
}
(GoogleQuery 'AddMember' $query) | fl
}
Funktionerne til opdatering og oprettelse af en konto har en tilsvarende syntaks, og ikke alle ekstra felter er obligatoriske. I sektionen med telefonnumre skal du angive et array, der kan indeholde fra én post med nummeret og dets type.
For at undgå en fejl ved tilføjelse af en bruger til en gruppe kan vi først tjekke, om brugeren allerede er i denne gruppe ved at få listen over gruppens medlemmer eller brugerens sammensætning.
Anmodningen om sammensætningen af grupper for en bestemt bruger vil ikke være rekursiv og viser kun det umiddelbare medlemskab. At inkludere en bruger i en forældregruppe, hvor en datagruppe, som brugeren allerede er medlem af, allerede eksisterer, vil være vellykket.
Konklusion
Vi skal blot sende adgangskoden til den nye konto til brugeren. Dette gør vi via SMS, mens generel information med instruktioner og brugernavn sendes til den personlige e-mail, som blev givet af HR-afdelingen sammen med telefonnummeret. Som et alternativ kan vi spare nogle penge og sende adgangskoden i en hemmelig Telegram-chat, hvilket også kan betragtes som en anden faktor (MacBooks er undtaget).
Tak fordi du læste til ende. Jeg ser frem til forslag til forbedring af skrive-stilen og ønsker dig at finde færre fejl, når du skriver scripts =)
Liste over links, som kan være tematisk nyttige eller blot svare på opståede spørgsmål:
Kilde: habr.com
