Google-käyttäjien luominen PowerShellistä API:n kautta

Hi!

Tässä artikkelissa kuvataan PowerShell-vuorovaikutuksen käyttöönotto Google-sovellusliittymän kanssa G Suite ‑käyttäjien manipuloimiseksi.

Käytämme organisaatiossamme useita sisäisiä ja pilvipalveluita. Pääsääntöisesti valtuutus niissä tulee Googlelle tai Active Directorylle, joiden välillä emme voi ylläpitää kopiota, joten kun uusi työntekijä lähtee, sinun on luotava / otettava tili käyttöön näissä kahdessa järjestelmässä. Prosessin automatisoimiseksi päätimme kirjoittaa skriptin, joka kerää tietoa ja lähettää sen molemmille palveluille.

Lupa

Vaatimuksia laadittaessa päätimme käyttää valtuuttamiseen oikeita ihmispäälliköitä, mikä yksinkertaistaa toimenpiteiden analysointia vahingossa tai tahallisesti tapahtuvien massiivisten muutosten sattuessa.

Google-sovellusliittymät käyttävät OAuth 2.0 -protokollaa todentamiseen ja valtuutukseen. Käyttötapaukset ja tarkemmat kuvaukset löydät täältä: OAuth 2.0:n käyttäminen Google-sovellusliittymien käyttämiseen.

Valitsin skriptin, jota käytetään valtuutukseen työpöytäsovelluksissa. Tarjolla on myös mahdollisuus käyttää palvelutiliä, joka ei vaadi käyttäjältä tarpeettomia liikkeitä.

Alla oleva kuva on kaaviomainen kuvaus valitusta skenaariosta Google-sivulta.

Google-käyttäjien luominen PowerShellistä API:n kautta

  1. Ensin lähetämme käyttäjän Google-tilin todennussivulle ja määritämme GET-parametrit:
    • sovelluksen tunnus
    • alueet, joihin sovellus tarvitsee pääsyn
    • osoite, johon käyttäjä ohjataan toimenpiteen suorittamisen jälkeen
    • tapa, jolla päivitämme tunnuksen
    • Turvakoodi
    • vahvistuskoodin lähetysmuoto

  2. Kun valtuutus on suoritettu, käyttäjä ohjataan ensimmäisessä pyynnössä määritetylle sivulle GET-parametrien välittämän virheen tai valtuutuskoodin kanssa.
  3. Sovelluksen (komentosarjan) on vastaanotettava nämä parametrit ja, jos se vastaanottaa koodin, tehtävä seuraava pyyntö tokenien hankkimiseksi
  4. Jos pyyntö on oikea, Google-sovellusliittymä palauttaa:
    • Käyttöoikeustunnus, jolla voimme tehdä pyyntöjä
    • Tämän tunnuksen voimassaoloaika
    • Päivitystunnus vaaditaan pääsytunnuksen päivittämiseen.

Ensin sinun on siirryttävä Google API -konsoliin: Tunnistetiedot – Google-sovellusliittymäkonsoli, valitse haluamasi sovellus ja luo Tunnistetiedot-osiossa asiakkaan OAuth-tunniste. Siellä (tai myöhemmin luodun tunnisteen ominaisuuksissa) sinun on määritettävä osoitteet, joihin uudelleenohjaus on sallittu. Meidän tapauksessamme nämä ovat useita localhost-merkintöjä eri porteilla (katso alla).

Jotta komentosarjaalgoritmin lukeminen olisi helpompaa, voit näyttää ensimmäiset vaiheet erillisessä funktiossa, joka palauttaa sovelluksen pääsy- ja päivitystunnukset:

$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

Asetamme OAuth-asiakastunnisteen ominaisuuksissa saadun asiakastunnuksen ja asiakassalaisuuden, ja koodin vahvistus on 43–128 merkin pituinen merkkijono, joka on luotava satunnaisesti varaamattomista merkeistä: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Tämä koodi lähetetään sitten uudelleen. Se poistaa haavoittuvuuden, jossa hyökkääjä voi siepata vastauksen, joka palautetaan uudelleenohjauksena käyttäjän valtuutuksen jälkeen.
Voit lähettää koodin varmentajan nykyiseen pyyntöön selkeänä tekstinä (mikä tekee siitä merkityksettömän - tämä sopii vain järjestelmille, jotka eivät tue SHA256:ta) tai luomalla hajautus SHA256-algoritmilla, joka on koodattava BASE64Url:ään (erilainen Base64:stä kahdella taulukon merkillä) ja poistamalla merkkirivien päätteet: =.

Seuraavaksi meidän on alettava kuunnella http:tä paikallisella koneella, jotta saamme valtuutuksen jälkeen vastauksen, joka palautetaan uudelleenohjauksena.

Ylläpitotehtävät suoritetaan erityisellä palvelimella, emme voi sulkea pois mahdollisuutta, että useat järjestelmänvalvojat suorittavat komentosarjan samanaikaisesti, joten se valitsee satunnaisesti portin nykyiselle käyttäjälle, mutta määritin ennalta määritetyt portit, koska ne on myös lisättävä luotettaviksi API-konsolissa.

access_type=offline tarkoittaa, että sovellus voi päivittää vanhentuneen tunnuksen itse ilman käyttäjän toimia selaimen kanssa,
vastaus_tyyppi=koodi määrittää muodon, jolla koodi palautetaan (viittaus vanhaan valtuutusmenetelmään, kun käyttäjä kopioi koodin selaimesta komentosarjaan),
laajuus ilmaisee käyttöoikeuden laajuuden ja tyypin. Ne on erotettava välilyönnillä tai %20 (URL-koodauksen mukaan). Luettelo pääsyalueista tyypeineen löytyy täältä: OAuth 2.0 -laajuudet Google-sovellusliittymille.

Saatuaan valtuutuskoodin sovellus palauttaa selaimeen sulkemisviestin, lopettaa portin kuuntelemisen ja lähettää POST-pyynnön tunnuksen hankkimiseksi. Ilmoitamme siihen aiemmin määritetyn konsolin API:n tunnuksen ja salaisuuden, osoitteen, johon käyttäjä uudelleenohjataan, ja grant_typen protokollamäärittelyn mukaisesti.

Vastauksena saamme pääsytunnuksen, sen voimassaoloajan sekunneissa ja päivitystunnuksen, jolla voimme päivittää pääsytunnuksen.

Sovelluksen tulee säilyttää tunnukset turvallisessa paikassa, jossa on pitkä säilyvyys, joten ennen kuin peruutamme saamamme käyttöoikeudet, sovellus ei palauta päivitystunnusta. Lopuksi lisäsin pyynnön tunnuksen peruuttamiseksi; jos hakemusta ei saatu päätökseen onnistuneesti eikä päivitystunnusta palautettu, se aloittaa toimenpiteen uudelleen (Pidimme, että tokenien tallentaminen paikallisesti terminaaliin ei ole turvallista, ja emme en halua monimutkaistaa asioita salauksella tai avata selainta usein).

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
}

Kuten olet jo huomannut, tunnuksen peruuttamisessa käytetään Invoke-WebRequestiä. Toisin kuin Invoke-RestMethod, se ei palauta vastaanotettuja tietoja käyttökelpoisessa muodossa ja näyttää pyynnön tilan.

Seuraavaksi komentosarja pyytää sinua syöttämään käyttäjän etu- ja sukunimen, mikä luo kirjautumistunnuksen + sähköpostiosoitteen.

pyynnöt

Seuraavat pyynnöt ovat - ensinnäkin sinun on tarkistettava, onko käyttäjä, jolla on sama sisäänkirjautuminen, jo olemassa, jotta saat päätöksen uuden luomisesta tai nykyisen sallimisesta.

Päätin toteuttaa kaikki pyynnöt yhden funktion muodossa valinnalla kytkimen avulla:

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

Jokaisessa pyynnössä sinun on lähetettävä valtuutusotsikko, joka sisältää tunnuksen tyypin ja itse käyttöoikeustunnuksen. Tällä hetkellä tunnuksen tyyppi on aina Bearer. Koska meidän on tarkistettava, että tunnus ei ole vanhentunut ja päivitettävä se tunnin kuluttua sen myöntämisestä, määritin pyynnön toiselle toiminnolle, joka palauttaa pääsytunnuksen. Sama koodinpätkä on skriptin alussa, kun vastaanotat ensimmäisen pääsytunnuksen:

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
}

Kirjautumistunnuksen olemassaolon tarkistaminen:

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
}

Sähköposti:$query-pyyntö pyytää sovellusliittymää etsimään käyttäjää, jolla on täsmälleen sama sähköpostiosoite, mukaan lukien aliakset. Voit myös käyttää jokerimerkkejä: =, :, :{PREFIX}*.

Saadaksesi tietoja, käytä GET-pyyntömenetelmää, tietojen lisääminen (tilin luominen tai jäsenen lisääminen ryhmään) - POST, olemassa olevien tietojen päivittäminen - PUT, tietueen poistaminen (esimerkiksi jäsen ryhmästä) - POISTAA.

Käsikirjoitus pyytää myös puhelinnumeroa (vahvistamaton merkkijono) ja sisällyttämistä alueelliseen jakeluryhmään. Se päättää, mikä organisaatioyksikkö käyttäjällä tulee olla valitun Active Directory OU:n perusteella ja keksii salasanan:

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"

Ja sitten hän alkaa manipuloida tiliä:

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

Tilin päivitys- ja luontitoiminnoilla on samanlainen syntaksi, kaikkia lisäkenttiä ei vaadita, puhelinnumerot-osiossa on määritettävä taulukko, joka voi sisältää enintään yhden tietueen numerolla ja sen tyypillä.

Jotta ei tulisi virheilmoitusta lisättäessä käyttäjää ryhmään, voimme ensin tarkistaa, onko hän jo tämän ryhmän jäsen hankkimalla käyttäjältä itseltään luettelon ryhmän jäsenistä tai kokoonpanosta.

Tietyn käyttäjän ryhmäjäsenyyden kyseleminen ei ole rekursiivista ja näyttää vain suoran jäsenyyden. Käyttäjän sisällyttäminen pääryhmään, jolla on jo aliryhmä, jonka jäsen käyttäjä on, onnistuu.

Johtopäätös

Jäljelle jää vain uuden tilin salasanan lähettäminen käyttäjälle. Teemme tämän tekstiviestillä ja lähetämme yleiset tiedot ohjeineen ja sisäänkirjautumisen henkilökohtaiseen sähköpostiin, jonka rekrytointiosasto antoi puhelinnumeron kanssa. Vaihtoehtoisesti voit säästää rahaa ja lähettää salasanasi salaiseen sähkekeskusteluun, jota voidaan pitää myös toisena tekijänä (MacBookit ovat poikkeus).

Kiitos, että luit loppuun asti. Otan mielelläni vastaan ​​ehdotuksia artikkelien kirjoitustyylin parantamiseksi ja toivon, että huomaat vähemmän virheitä kirjoitettaessa skriptejä =)

Luettelo linkeistä, jotka voivat olla temaattisesti hyödyllisiä tai vain vastata kysymyksiin:

Lähde: will.com

Lisää kommentti