Google-brûkers meitsje fan PowerShell fia API

Hallo!

Dit artikel sil de ymplemintaasje beskriuwe fan PowerShell-ynteraksje mei de Google API om G Suite-brûkers te manipulearjen.

Wy brûke ferskate ynterne en wolktsjinsten oer de hiele organisaasje. Foar it grutste part komt de autorisaasje yn har del op Google of Active Directory, wêrtussen wy gjin replika kinne ûnderhâlde; dus moatte jo as in nije meiwurker fuortgiet in akkount oanmeitsje / ynskeakelje yn dizze twa systemen. Om it proses te automatisearjen, hawwe wy besletten om in skript te skriuwen dat ynformaasje sammelt en stjoert nei beide tsjinsten.

Oanmelde

By it opstellen fan 'e easken hawwe wy besletten om echte minsklike behearders te brûken foar autorisaasje; dit ferienfâldigt de analyze fan aksjes yn gefal fan tafallige of opsetlike massale feroarings.

Google API's brûke it OAuth 2.0-protokol foar autentikaasje en autorisaasje. Gebrûksgefallen en mear detaillearre beskriuwingen kinne hjir fûn wurde: OAuth 2.0 brûke om tagong te krijen ta Google API's.

Ik keas it skript dat wurdt brûkt foar autorisaasje yn buroblêdapplikaasjes. D'r is ek in opsje om in tsjinstkonto te brûken, dat gjin ûnnedige bewegingen fan 'e brûker fereasket.

De ôfbylding hjirûnder is in skematyske beskriuwing fan it selekteare senario fan 'e Google-side.

Google-brûkers meitsje fan PowerShell fia API

  1. Earst stjoere wy de brûker nei de Google Account-autentikaasje-side, mei de GET-parameters oantsjutte:
    • applikaasje id
    • gebieten dêr't de applikaasje tagong moat ta
    • it adres wêrnei't de brûker nei it foltôgjen fan de proseduere sil wurde omlaat
    • de manier wêrop wy it token sille bywurkje
    • Feilichheidskoade
    • ferifikaasje koade transmission format

  2. Nei't de autorisaasje foltôge is, sil de brûker trochferwiisd wurde nei de side oantsjutte yn it earste fersyk, mei in flater of autorisaasjekoade trochjûn troch GET-parameters
  3. De applikaasje (skript) sil dizze parameters moatte ûntfange en, as de koade ûntfongen is, it folgjende fersyk meitsje om tokens te krijen
  4. As it fersyk goed is, jout de Google API werom:
    • Tagongstoken wêrmei wy oanfragen kinne meitsje
    • De jildigensperioade fan dit token
    • Ferfarskje token nedich om it Access token te ferfarskjen.

Earst moatte jo nei de Google API-konsole gean: Credentials - Google API Console, Selektearje de winske applikaasje en meitsje yn 'e Credentials seksje in client OAuth identifier. Dêr (of letter, yn 'e eigenskippen fan' e oanmakke identifier) ​​moatte jo de adressen opjaan wêrnei trochferwizing is tastien. Yn ús gefal sille dit ferskate lokale host-yngongen wêze mei ferskate havens (sjoch hjirûnder).

Om it handiger te meitsjen om it skriptalgoritme te lêzen, kinne jo de earste stappen werjaan yn in aparte funksje dy't tagong sil weromjaan en tokens foar de applikaasje ferfarskje:

$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

Wy sette de Client ID en Client Secret krigen yn de OAuth client identifier eigenskippen, en de koade ferifiearder is in tekenrige fan 43 oant 128 tekens dy't willekeurich moatte wurde generearre út net-reservearre karakters: [AZ] / [az] / [0-9] / "-" / "." / "_" / "~".

Dizze koade sil dan wer ferstjoerd wurde. It elimineert de kwetsberens wêryn in oanfaller in antwurd kin ûnderskeppe dat weromjûn wurdt as in trochferwizing nei autorisaasje fan brûkers.
Jo kinne in koade ferifiearder stjoere yn it aktuele fersyk yn dúdlike tekst (wat it sinleas makket - dit is allinich geskikt foar systemen dy't SHA256 net stypje), of troch in hash te meitsjen mei it SHA256-algoritme, dat kodearre wurde moat yn BASE64Url (ferskillend fan Base64 troch twa tabelkarakters) en it fuortheljen fan de karakterline-einings: =.

Folgjende moatte wy begjinne te harkjen nei http op 'e lokale masine om in antwurd te ûntfangen nei autorisaasje, dat sil wurde weromjûn as in trochferwizing.

Bestjoerlike taken wurde útfierd op in spesjale tsjinner, wy kinne de mooglikheid net útslute dat ferskate behearders it skript tagelyk útfiere, dus it sil willekeurich in poarte selektearje foar de hjoeddeistige brûker, mar ik spesifisearre foarôf definieare poarten om't se moatte ek wurde tafoege as fertroud yn 'e API-konsole.

access_type=offline betsjut dat de applikaasje in ferrûne token op himsels kin bywurkje sûnder ynteraksje mei de brûker mei de browser,
response_type=koade stelt it formaat yn fan hoe't de koade weromjûn wurdt (in ferwizing nei de âlde autorisaasjemetoade, doe't de brûker de koade fan 'e browser nei it skript kopiearre),
omfang jout de omfang en soarte fan tagong oan. Se moatte skieden wurde troch spaasjes of %20 (neffens URL-kodearring). In list mei tagongsgebieten mei typen is hjir te sjen: OAuth 2.0 Scopes foar Google API's.

Nei ûntfangst fan de autorisaasjekoade sil de applikaasje in ticht berjocht weromjaan oan 'e browser, stopje mei harkjen nei de haven en stjoert in POST-fersyk om it token te krijen. Wy jouwe dêryn de earder oantsjutte id en geheim fan 'e konsole API oan, it adres wêrnei de brûker sil wurde omlaat en grant_type yn oerienstimming mei de protokol spesifikaasje.

As antwurd krije wy in Access token, syn jildigensperioade yn sekonden, en in Refresh token, wêrmei wy it Access token bywurkje kinne.

De applikaasje moat tokens opslaan op in feilich plak mei in lange houdbaarheid, dus oant wy de ûntfongen tagong ynlûke, sil de applikaasje it ferfarskingstoken net werombringe. Oan 'e ein haw ik in fersyk tafoege om it token yn te lûken; as de applikaasje net mei sukses foltôge is en it ferfarske token net weromjûn is, sil it de proseduere opnij begjinne (wy beskôgen it ûnfeilich om tokens lokaal op 'e terminal op te slaan, en wy dogge 't wol dingen komplisearje mei kryptografy of de browser faak iepenje).

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
}

Lykas jo al opfallen, wurdt Invoke-WebRequest brûkt by it ynlûken fan in token. Oars as Invoke-RestMethod, jout it de ûntfongen gegevens net werom yn in brûkber formaat en toant de status fan it fersyk.

Dêrnei freget it skript jo om de foar- en efternamme fan 'e brûker yn te fieren, en genereart in login + e-post.

Fersiken

De folgjende oanfragen sille wêze - earst moatte jo kontrolearje oft in brûker mei deselde oanmelding al bestiet om in beslút te krijen oer it meitsjen fan in nije of it ynskeakeljen fan de aktuele.

Ik besleat om alle oanfragen yn it formaat fan ien funksje te ymplementearjen mei in seleksje, mei help fan 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'])
      }
    }
  }
}

Yn elk fersyk moatte jo in autorisaasjekoptekst stjoere mei it tokentype en it Access token sels. Op it stuit is it tokentype altyd Bearer. Omdat wy moatte kontrolearje dat it token is net ferrûn en update it nei in oere út it momint dat it waard útjûn, Ik spesifisearre in fersyk foar in oare funksje dy't jout in Access token. Itselde stik koade is oan it begjin fan it skript by it ûntfangen fan it earste Access token:

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
}

Kontrolearje de oanmelding op bestean:

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
}

It e-post:$query-fersyk sil de API freegje om in brûker te sykjen mei krekt dy e-post, ynklusyf aliasen. Jo kinne ek jokerteken brûke: =, :, :{PREFIX}*.

Om gegevens te krijen, brûk de GET-oanfraachmetoade, om gegevens yn te foegjen (in akkount oanmeitsje of in lid taheakje oan in groep) - POST, om besteande gegevens te aktualisearjen - PUT, om in rekord te wiskjen (bygelyks in lid fan in groep) - DELETE.

It skript sil ek freegje om in telefoannûmer (in net falidearre tekenrige) en foar opname yn in regionale distribúsje groep. It beslút hokker organisatoaryske ienheid de brûker moat hawwe basearre op de selekteare Active Directory OU en komt mei in wachtwurd:

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"

En dan begjint er it akkount te manipulearjen:

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

De funksjes foar it bywurkjen en oanmeitsjen fan in akkount hawwe in ferlykbere syntaksis; net alle ekstra fjilden binne fereaske; yn 'e seksje mei telefoannûmers moatte jo in array opjaan dy't maksimaal ien record mei it nûmer en it type kin befetsje.

Om gjin flater te ûntfangen by it tafoegjen fan in brûker oan in groep, kinne wy ​​earst kontrolearje oft hy al lid is fan dizze groep troch in list mei groepsleden of komposysje te krijen fan de brûker sels.

It opfreegjen fan it groepslidmaatskip fan in spesifike brûker sil net rekursyf wêze en sil allinich direkt lidmaatskip sjen litte. It opnimmen fan in brûker yn in âldergroep dy't al in berngroep hat wêrfan de brûker lid is, sil slagje.

konklúzje

Alles wat oerbliuwt is de brûker it wachtwurd foar it nije akkount te stjoeren. Wy dogge dit fia SMS, en stjoere algemiene ynformaasje mei ynstruksjes en ynlogge nei in persoanlike e-mail, dat, tegearre mei in telefoannûmer, waard levere troch de werving ôfdieling. As alternatyf kinne jo jild besparje en jo wachtwurd stjoere nei in geheime telegramchat, dy't ek kin wurde beskôge as de twadde faktor (MacBooks sil in útsûndering wêze).

Tankewol foar it lêzen oant it ein. Ik sil bliid wêze om suggestjes te sjen foar it ferbetterjen fan de styl fan it skriuwen fan artikels en winskje dat jo minder flaters fange by it skriuwen fan skripts =)

List mei keppelings dy't tematysk nuttich kinne wêze of gewoan fragen beantwurdzje:

Boarne: www.habr.com

Add a comment