Kreante Google-Uzantojn de PowerShell per API

Saluton!

Ĉi tiu artikolo priskribos la efektivigon de PowerShell-interago kun la API de Google por manipuli uzantojn de G Suite.

Ni uzas plurajn internajn kaj nubajn servojn tra la organizo. Plejparte, rajtigo en ili dependas de Google aŭ Active Directory, inter kiuj ni ne povas konservi kopion; sekve, kiam nova dungito foriras, vi devas krei/aktivigi konton en ĉi tiuj du sistemoj. Por aŭtomatigi la procezon, ni decidis skribi skripton, kiu kolektas informojn kaj sendas ĝin al ambaŭ servoj.

Rajtigo

Dum ellaboro de la postuloj, ni decidis uzi verajn homajn administrantojn por rajtigo; tio simpligas la analizon de agoj en kazo de hazardaj aŭ intencitaj amasaj ŝanĝoj.

Google-APIoj uzas la protokolon OAuth 2.0 por aŭtentikigo kaj rajtigo. Uzkazoj kaj pli detalaj priskriboj troveblas ĉi tie: Uzante OAuth 2.0 por Aliri Guglo-APIojn.

Mi elektis la skripton, kiu estas uzata por rajtigo en labortablaj aplikaĵoj. Ankaŭ ekzistas eblo uzi servokonton, kiu ne postulas nenecesajn movojn de la uzanto.

La suba bildo estas skema priskribo de la elektita scenaro el la Guglo-paĝo.

Kreante Google-Uzantojn de PowerShell per API

  1. Unue, ni sendas la uzanton al la paĝo de aŭtentikiga konto de Google, specifante GET-parametrojn:
    • aplika id
    • areoj al kiuj la aplikaĵo bezonas aliron
    • la adreso al kiu la uzanto estos alidirektita post kompletigo de la proceduro
    • kiel ni ĝisdatigos la ĵetonon
    • Sekureca Kodo
    • konfirmkodo transdono formato

  2. Post kiam rajtigo estas kompletigita, la uzanto estos redirektita al la paĝo specifita en la unua peto, kun eraro aŭ rajtigokodo pasita de GET-parametroj
  3. La aplikaĵo (skripto) devos ricevi ĉi tiujn parametrojn kaj, se ricevite la kodon, faru la sekvan peton por akiri ĵetonojn
  4. Se la peto estas ĝusta, la Google API revenas:
    • Alirĵetono, per kiu ni povas fari petojn
    • La validecperiodo de ĉi tiu ĵetono
    • Refreŝiga ĵetono necesa por refreŝigi la Access-ĵetonon.

Unue vi devas iri al la Google API-konzolo: Akreditaĵoj - Google API-Konzolo, elektu la deziratan aplikaĵon kaj en la sekcio Akreditaĵoj kreu klientan OAuth-identigilon. Tie (aŭ poste, en la propraĵoj de la kreita identigilo) vi devas specifi la adresojn al kiuj alidirektado estas permesita. En nia kazo, ĉi tiuj estos pluraj lokaj gastigaj eniroj kun malsamaj havenoj (vidu sube).

Por pli konvene legi la skripto-algoritmon, vi povas montri la unuajn paŝojn en aparta funkcio, kiu redonos Aliron kaj refreŝigos tokojn por la aplikaĵo:

$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

Ni fiksas la Klientidentigilon kaj Klientsekreton akiritan en la OAuth-klientidentigilo-propraĵoj, kaj la koda kontrolilo estas ĉeno de 43 ĝis 128 signoj, kiuj devas esti hazarde generitaj el nerezervitaj signoj: [AZ] / [az] / [0-9] / "-" / "." / "_" / "~".

Ĉi tiu kodo tiam estos transdonita denove. Ĝi forigas la vundeblecon, en kiu atakanto povus kapti respondon resenditan kiel alidirektilon post rajtigo de la uzanto.
Vi povas sendi kodkontrolilon en la nuna peto en klara teksto (kio sensignifas - tio taŭgas nur por sistemoj, kiuj ne subtenas SHA256), aŭ kreante haŝon per la SHA256-algoritmo, kiu devas esti kodita en BASE64Url (malsama). el Bazo64 per du tabelaj signoj) kaj forigante la signajn liniofinaĵojn: =.

Poste, ni devas komenci aŭskulti http sur la loka maŝino por ricevi respondon post rajtigo, kiu estos resendita kiel alidirektilo.

Administraj taskoj estas plenumitaj sur speciala servilo, ni ne povas ekskludi la eblecon, ke pluraj administrantoj rulu la skripton samtempe, do ĝi hazarde elektos pordon por la nuna uzanto, sed mi specifis antaŭdifinitajn pordojn ĉar ili ankaŭ devas esti aldonitaj kiel fidindaj en la API-konzolo.

access_type=senrete signifas, ke la aplikaĵo povas ĝisdatigi eksvalidiĝintan ĵetonon memstare sen uzantinterago kun la retumilo,
respondo_tipo=kodo fiksas la formaton de kiel la kodo estos resendita (referenco al la malnova rajtiga metodo, kiam la uzanto kopiis la kodon de la retumilo en la skripton),
medio indikas la amplekson kaj tipon de aliro. Ili devas esti apartigitaj per spacoj aŭ %20 (laŭ URL-kodado). Listo de alirregionoj kun tipoj povas esti vidita ĉi tie: OAuth 2.0 Amplekso por Guglo-APIoj.

Post ricevi la rajtigan kodon, la aplikaĵo resendos proksiman mesaĝon al la retumilo, ĉesos aŭskulti la havenon kaj sendos POST-peton por akiri la ĵetonon. Ni indikas en ĝi la antaŭe specifitan identigilon kaj sekreton de la konzola API, la adreson al kiu la uzanto estos redirektita kaj grant_type laŭ la protokolo-specifo.

Responde, ni ricevos Alir-ĵetonon, ĝian validecperiodon en sekundoj, kaj Refresh-ĵetonon, per kiu ni povas ĝisdatigi la Access-ĵetonon.

La aplikaĵo devas stoki ĵetonojn en sekura loko kun longa konservodaŭro, do ĝis ni nuligos la ricevitan aliron, la aplikaĵo ne resendos la refreŝigan ĵetonon. Fine, mi aldonis peton por revoki la ĵetonon; se la aplikaĵo ne estis kompletigita sukcese kaj la refreŝiga ĵetono ne estis resendita, ĝi komencos la proceduron denove (ni konsideris nesekura stoki ĵetonojn loke sur la terminalo, kaj ni ne ne volas kompliki aferojn per kripto aŭ malfermi la retumilon ofte).

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
}

Kiel vi jam rimarkis, kiam oni revokas ĵetonon, Invoke-WebRequest estas uzata. Male al Invoke-RestMethod, ĝi ne resendas la ricevitajn datumojn en uzebla formato kaj montras la staton de la peto.

Poste, la skripto petas vin enigi la nomon kaj familinomon de la uzanto, generante ensaluton + retpoŝton.

Petoj

La sekvaj petoj estos - unue vi devas kontroli ĉu uzanto kun la sama ensaluto jam ekzistas por akiri decidon pri krei novan aŭ ebligi la nunan.

Mi decidis efektivigi ĉiujn petojn en la formato de unu funkcio kun elekto, uzante ŝaltilon:

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'])
      }
    }
  }
}

En ĉiu peto, vi devas sendi Rajtigan kaplinion enhavantan la ĵetonan tipon kaj la Alir-ĵetonon mem. Nuntempe, la ĵetono-tipo ĉiam estas Bearer. Ĉar ni devas kontroli, ke la ĵetono ne eksvalidiĝis kaj ĝisdatigi ĝin post unu horo de la momento, kiam ĝi estis elsendita, mi specifis peton por alia funkcio, kiu resendas Access-ĵetonon. La sama kodo estas komence de la skripto kiam oni ricevas la unuan Alir-ĵetonon:

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
}

Kontrolante la ensaluton por ekzisto:

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
}

La retpoŝto:$query peto petos la API serĉi uzanton kun ĝuste tiu retpoŝto, inkluzive de kaŝnomoj. Vi ankaŭ povas uzi ĵokeron: =, :, :{PREFIXSO}*.

Por akiri datumojn, uzu la metodon de peto GET, por enmeti datumojn (krei konton aŭ aldoni membron al grupo) - POST, por ĝisdatigi ekzistantajn datumojn - PUT, por forigi rekordon (ekzemple membro de grupo) - FORIGI.

La skripto ankaŭ petos telefonnumeron (nevalidigitan ĉenon) kaj inkluziviĝon en regiona distribua grupo. Ĝi decidas kiun organizan unuon la uzanto devus havi surbaze de la elektita Active Directory OU kaj venas kun pasvorto:

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"

Kaj tiam li komencas manipuli la konton:

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

La funkcioj por ĝisdatigi kaj krei konton havas similan sintakson; ne ĉiuj aldonaj kampoj estas postulataj; en la sekcio kun telefonnumeroj, vi devas specifi tabelon, kiu povas enhavi ĝis unu rekordon kun la nombro kaj ĝia tipo.

Por ne ricevi eraron aldonante uzanton al grupo, ni unue povas kontroli ĉu li jam estas membro de tiu ĉi grupo ricevante liston de grupanoj aŭ komponadon de la uzanto mem.

Demandi la grupan membrecon de specifa uzanto ne estos rekursiva kaj nur montros rektan membrecon. Inkluzivi de uzanto en gepatra grupo kiu jam havas infangrupon, kiun la uzanto estas membro, sukcesos.

konkludo

Restas nur sendi al la uzanto la pasvorton por la nova konto. Ni faras tion per SMS, kaj sendas ĝeneralajn informojn kun instrukcioj kaj ensalutu al persona retpoŝto, kiu, kune kun telefonnumero, estis provizita de la varba sekcio. Kiel alternativo, vi povas ŝpari monon kaj sendi vian pasvorton al sekreta telegrama babilejo, kiu ankaŭ povas esti konsiderata la dua faktoro (MacBooks estos escepto).

Dankon pro legi ĝis la fino. Mi ĝojos vidi sugestojn por plibonigi la stilon verki artikolojn kaj deziras ke vi kaptu malpli da eraroj dum verkado de skriptoj =)

Listo de ligiloj kiuj povas esti teme utilaj aŭ simple respondi demandojn:

fonto: www.habr.com

Aldoni komenton