Skep Google-gebruikers vanaf PowerShell via API

Привет!

Hierdie artikel sal beskryf hoe PowerShell met die Google API omgaan om G Suite-gebruikers te manipuleer.

In die organisasie gebruik ons ​​verskeie interne en wolkdienste. Magtiging in hulle kom meestal neer op Google of Active Directory, waartussen ons nie onderskeidelik 'n replika kan onderhou nie, wanneer 'n nuwe werknemer vertrek, moet u 'n rekening in hierdie twee stelsels skep / aktiveer. Om die proses te outomatiseer, het ons besluit om 'n skrif te skryf wat inligting insamel en dit na beide dienste stuur.

magtiging

Toe ons die vereistes saamstel, het ons besluit om regte menslike administrateurs vir magtiging te gebruik, dit vergemaklik die ontleding van aksies in geval van toevallige of opsetlike massiewe veranderinge.

Google API's gebruik die OAuth 2.0-protokol vir stawing en magtiging. Gebruikscenario's en 'n meer gedetailleerde beskrywing kan hier gevind word: Gebruik OAuth 2.0 om toegang tot Google API's te kry.

Ek het die skrif gekies wat vir magtiging in rekenaartoepassings gebruik word. Daar is ook 'n opsie om 'n diensrekening te gebruik wat nie onnodige bewegings van die gebruiker vereis nie.

Die prentjie hieronder is 'n skematiese beskrywing van die geselekteerde scenario vanaf die Google-bladsy.

Skep Google-gebruikers vanaf PowerShell via API

  1. Eerstens stuur ons die gebruiker na die Google-rekeningstawingbladsy, met die GET-parameters:
    • aansoek ID
    • areas waartoe die toepassing toegang benodig
    • die adres waarheen die gebruiker herlei sal word nadat die prosedure voltooi is
    • die manier waarop ons die teken sal opdateer
    • Sekuriteitskode
    • verifikasie kode oordrag formaat

  2. Nadat magtiging voltooi is, sal die gebruiker herlei word na die bladsy gespesifiseer in die eerste versoek, met 'n fout of magtigingskode wat deur GET-parameters geslaag is
  3. Die toepassing (skrip) sal hierdie parameters moet kry en, indien die kode ontvang word, die volgende versoek uitvoer om tokens te kry
  4. Wanneer dit korrek versoek word, gee die Google API terug:
    • Toegangstoken waarmee ons navrae kan maak
    • Die vervaldatum van hierdie teken
    • Herlaai-token word vereis om die toegang-token te verfris.

Eerstens moet jy na die Google API-konsole gaan: Geloofsbriewe - Google API-konsole, kies die gewenste toepassing en skep 'n OAuth-kliënt-ID in die Geloofsbriewe-afdeling. Op dieselfde plek (of later, in die eienskappe van die geskepte identifiseerder), moet u die adresse spesifiseer waarna herleiding toegelaat word. In ons geval sal dit verskeie plaaslike gasheer-inskrywings wees met verskillende poorte (sien hieronder).

Om dit makliker te maak om die skripalgoritme te lees, kan jy die eerste stappe in 'n aparte funksie uitvoer wat Toegang sal gee en tokens vir die toepassing sal verfris:

$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

Ons stel die kliënt-ID en kliëntgeheim wat in die OAuth-kliënt-ID-eienskappe verkry is, en die kodeverifiërer is 'n string van 43 tot 128 karakters wat lukraak gegenereer moet word uit ongereserveerde karakters: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Dan sal hierdie kode weer versend word. Dit skakel 'n kwesbaarheid uit waarin 'n aanvaller die reaksie wat deur die herleiding teruggestuur word, kan onderskep nadat die gebruiker gemagtig is.
U kan die kodeverifieerder in die huidige versoek in duidelike teks stuur (wat dit betekenisloos maak - dit is slegs geskik vir stelsels wat nie SHA256 ondersteun nie), of deur 'n hash te skep met die SHA256-algoritme, wat in BASE64Url geënkodeer moet word (anders as Base64 deur twee tabelkarakters) en verwyder die karakterreëleindes: =.

Vervolgens moet ons na http op die plaaslike masjien begin luister om 'n antwoord te ontvang na magtiging, wat deur 'n herleiding teruggestuur sal word.

Administratiewe take word uitgevoer op 'n toegewyde bediener, ons kan nie die moontlikheid uitsluit dat verskeie administrateurs die skrip op dieselfde tyd sal laat loop nie, so dit sal willekeurig 'n poort vir die huidige gebruiker kies, maar ek het vooraf gedefinieerde poorte gespesifiseer, want. hulle moet ook bygevoeg word as vertrou in die API-konsole.

access_type=vanlyn beteken dat die toepassing 'n vervalde token op sy eie kan hernu sonder gebruikersinteraksie met die blaaier,
response_type=kode stel die formaat van hoe die kode teruggestuur sal word ('n verwysing na die ou magtigingsmetode, wanneer die gebruiker die kode van die blaaier na die skrif gekopieer het),
omvang spesifiseer omvang en tipe toegang. Hulle moet geskei word deur spasies of %20 (volgens URL-kodering). Die lys van toegangsareas met tipes kan hier gesien word: OAuth 2.0-bestekke vir Google API's.

Nadat die magtigingskode ontvang is, sal die toepassing 'n beslote boodskap aan die blaaier stuur, ophou luister op die poort en 'n POST-versoek stuur om die teken te ontvang. Ons spesifiseer daarin die voorheen gestelde ID en geheim van die konsole API, die adres waarna die gebruiker herlei sal word en die grant_type in ooreenstemming met die protokol spesifikasie.

In reaksie hierop sal ons 'n Toegang-token ontvang, die geldigheidstydperk daarvan in sekondes, en 'n Refresh-token, waarmee ons die Toegang-token kan opdateer.

Die toepassing moet die tokens op 'n veilige plek met 'n lang raklewe stoor, so totdat ons die toegang wat ontvang is herroep, sal die herlaaitoken nie na die toepassing teruggestuur word nie. Aan die einde het ek 'n versoek bygevoeg om die teken te herroep, as die aansoek nie suksesvol voltooi is nie en die herlaaitoken nie teruggekeer het nie, sal dit die prosedure weer begin (ons het dit as onveilig beskou om tokens plaaslik op die terminaal te stoor, en ons doen dit wil dit nie met kriptografie kompliseer of die blaaier gereeld oopmaak nie).

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
}

Soos u reeds opgemerk het, word Invoke-WebRequest gebruik wanneer u 'n teken herroep. Anders as Invoke-RestMethod, gee dit nie die ontvangde data in 'n bruikbare formaat terug en wys die status van die versoek nie.

Vervolgens vra die skrip om die gebruiker se voornaam en van in te voer, wat 'n aanmelding + e-pos genereer.

Versoeke

Die volgende versoeke sal wees - eerstens moet u kyk of daar reeds 'n gebruiker met so 'n aanmelding is om 'n besluit te kry oor die vorming van 'n nuwe een of die insluiting van die huidige een.

Ek het besluit om alle versoeke in die formaat van 'n enkele funksie te implementeer met 'n keuse deur skakelaar te gebruik:

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

In elke versoek moet jy 'n magtigingsopskrif stuur wat die tokentipe en die Access-token self bevat. Tans is die tekentipe altyd Draer. Omdat ons moet seker maak dat die token nie verval het nie en dit bywerk na 'n uur vanaf die oomblik van uitreiking, ek het 'n versoek gespesifiseer vir 'n ander funksie wat 'n Access token terugstuur. Dieselfde stukkie kode is aan die begin van die skrif wanneer die eerste toegangsteken ontvang word:

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
}

Kontroleer die aanmelding vir bestaan:

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
}

Die e-pos:$query-versoek sal die API vra om 'n gebruiker te soek met presies daardie e-pos, insluitend aliasse. Jy kan ook wildcard gebruik: =, :, :{PREFIX}*.

Om data te verkry, word die GET-versoekmetode gebruik, om data in te voeg (skep 'n rekening of voeg 'n lid by 'n groep) - POST, om bestaande data op te dateer - PUT, om 'n rekord uit te vee (byvoorbeeld 'n lid van 'n groep) - VEEL.

Die skrif sal ook vra vir 'n telefoonnommer (nie-gevalideerde string) en vir insluiting in 'n streekverspreidingsgroep. Dit besluit watter organisatoriese eenheid die gebruiker moet hê gebaseer op die geselekteerde Active Directory OE en sal met 'n wagwoord vorendag kom:

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 begin om die rekening te manipuleer:

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

Die opdaterings- en rekeningskeppingsfunksies het 'n soortgelyke sintaksis, nie alle bykomende velde word vereis nie, in die afdeling met telefoonnommers moet jy 'n skikking spesifiseer wat ten minste een rekord met 'n nommer en die tipe kan bevat.

Om nie 'n fout te kry wanneer 'n gebruiker by 'n groep gevoeg word nie, kan ons eers kyk of hy reeds in hierdie groep is deur 'n lys van groeplede of die samestelling van die gebruiker self te kry.

Om navraag te doen oor die groeplidmaatskap van 'n spesifieke gebruiker sal nie rekursief wees nie en sal slegs onmiddellike lidmaatskap wys. Dit sal suksesvol wees om 'n gebruiker by 'n ouergroep in te sluit wat reeds 'n kindergroep het waarvan die gebruiker 'n lid is.

Gevolgtrekking

Dit bly om die gebruiker 'n wagwoord vir 'n nuwe rekening te stuur. Ons doen dit per SMS, en stuur algemene inligting met instruksies en aanmelding na persoonlike pos, wat saam met 'n telefoonnommer deur die werwingsafdeling verskaf is. As alternatief kan u geld spaar en die wagwoord na die geheime telegramklets stuur, wat ook as die tweede faktor beskou kan word (macbooks sal 'n uitsondering wees).

Dankie dat jy tot die einde gelees het. Ek sal bly wees om voorstelle te sien vir die verbetering van die styl van die skryf van artikels en ek wens dat u minder foute opvang wanneer u skrifte skryf =)

Lys van skakels wat tematies nuttig kan wees of net vrae beantwoord:

Bron: will.com

Voeg 'n opmerking