Oppretting av Google-brukere fra PowerShell via API

Hei!

I denne artikkelen vil implementeringen av PowerShell-interaksjon med Google API for å utføre manipulasjoner med G Suite-brukere bli beskrevet.

I organisasjonen bruker vi flere interne og skybaserte tjenester. For det meste er autentisering i dem knyttet til Google eller Active Directory, mellom hvilke vi ikke kan opprettholde en replika. Dermed, ved oppstart av en ny ansatt, må vi opprette/aktivere en konto i disse to systemene. For å automatisere prosessen bestemte vi oss for å skrive et skript som samler informasjon og sender den til begge tjenestene.

Autorisering

Mens vi formulerte kravene, bestemte vi oss for å bruke reelle administratorer for autentisering, noe som forenkler analysen av handlingene ved utilsiktede eller hensiktige omfattende endringer.

For autentisering og autorisasjon bruker Google API protokollen OAuth 2.0. Bruksområder og en mer detaljert beskrivelse kan sees her: Bruk av OAuth 2.0 for tilgang til Google API-er.

Jeg valgte et scenario som brukes under autentisering i skrivebordsapplikasjoner. Det finnes også en mulighet for å bruke en tjenestekonto, som ikke krever unødvendige handlinger fra brukeren.

Bildet under er en skematisk beskrivelse av det valgte scenariet fra Google-siden.

Oppretting av Google-brukere fra PowerShell via API

  1. Først sender vi brukeren til autentiseringssiden for Google-kontoen, idet vi angir GET-parametrene:
    • applikasjonsidentifikatoren
    • områdene som applikasjonen trenger tilgang til
    • adressen brukeren vil bli omdirigert til etter at prosedyren er fullført
    • metoden vi vil bruke for å oppdatere tokenet
    • verifikasjonskoden
    • formatet for overføring av verifikasjonskoden

  2. Etter å ha fullført autentiseringen vil brukeren bli omdirigert til den angitte siden fra første forespørsel, med feil eller autorisasjonskode sendt som GET-parametre
  3. Applikasjonen (skriptet) må hente disse parameterne, og i tilfelle av en mottatt kode, utføre neste forespørsel for å hente tokens
  4. Ved en korrekt forespørsel returnerer Google API:
    • Access-tokenet vi kan bruke for forespørslene
    • Gyldighetstiden for dette tokenet
    • Refresh-tokenet som er nødvendig for å oppdatere Access-tokenet.

Først må vi gå til Google API-konsollen: Credentials — Google API Console, velg den nødvendige applikasjonen og opprett en OAuth-klient-ID i delen Credentials. Her (eller senere, i egenskapene for den opprettede ID-en) må adressene som er tillatt for omdirigering spesifiseres. I vårt tilfelle vil dette være flere oppføringer for localhost med forskjellige porter (se videre).

For å gjøre det lettere å lese algoritmen i skriptet, kan man flytte de første trinnene til en egen funksjon som returnerer Access- og refresh-tokenene for applikasjonen:

$client_secret = 'Vår Klienthemmelighet'
$client_id = 'Vår Klient-ID'
function Get-GoogleAuthToken {
  if (-not [System.Net.HttpListener]::IsSupported) {
    "HttpListener stø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 nettleseren..."
  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 {
    "Kan ikke starte lytteren."
    exit 1
  }
  while (($code -eq $null)) {
    $context = $listener.GetContext()
    Write-Host "Tilslutning akseptert" -f 'mag'
    $url = $context.Request.RawUrl
    $code = $url.split('?')[1].split('=')[1].split('&')[0]
    if ($url.split('?')[1].split('=')[0] -eq 'error') {
      Write-Host "Feil!"$code -f 'red'
      $buffer = [System.Text.Encoding]::UTF8.GetBytes("Feil!"+$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("Nå kan du lukke denne nettleserfanen.")
    $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 setter Client ID og Client Secret, som er hentet fra egenskapene til OAuth-klient-ID-en, og code verifier – dette er en streng som er mellom 43 og 128 tegn lang, og den må genereres tilfeldig fra ikke-reserverte tegn: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".

Deretter vil denne koden bli sendt på nytt. Den utelukker sårbarheten hvor en angriper kan fange svaret som returneres ved omdirigering etter autentisering av brukeren.
Det er mulig å sende code verifier i den nåværende forespørselen i klartekst (som gjør det meningsløst – dette er kun tilpasset systemer som ikke støtter SHA256), eller ved å opprette en hash med SHA256-algoritmen som må kodes i BASE64Url (som skiller seg fra Base64 med to tegn) og fjerne linjeslutttegn: =.

Neste steg er å begynne å lytte til http på den lokale maskinen for å motta svaret etter autentisering, som vil bli returnert som en omdirigering.

Administrative oppgaver utføres på en spesialserver, vi kan ikke utelukke muligheten for at flere administratorer samtidig kjører skriptet, derfor vil det tilfeldig velge en port for den nåværende brukeren, men jeg har spesifisert forhåndsdefinerte porter, da disse også må legges til som pålitelige i API-konsollen.

access_type=offline betyr at applikasjonen kan oppdatere utløpte tokens selvstendig uten brukerinteraksjon med nettleseren.
response_type=code bestemmer formatet på hvordan koden vil bli returnert (henvisning til den gamle metoden for autentisering, der brukeren kopierte koden fra nettleseren til skriptet),
scope angir tilgjengeområda og tilgjenge-type. Desse må delast med mellomrom eller (i samsvar med URL-koding). Du kan sjå lista over tilgjengeområda med typar her: OAuth 2.0 Scopes for Google APIs.

Etter å ha mottatt autorisasjonskoden, vil applikasjonen returnere en melding til nettleseren om at den avsluttes, slutte å lytte til porten, og sende en POST-forespørsel for å motta tokenet. Vi spesifiserer de tidligere angitte id-ene og hemmeligheten fra API-konsollen, adressen dit brukeren vil bli omdirigert og grant_type i henhold til protokollens spesifikasjoner.

Som svar vil vi motta Access-tokenet, dets gyldighetstid i sekunder og Refresh-tokenet, som vi kan bruke til å oppdatere Access-tokenet.

Applikasjonen må oppbevare tokens på et sikkert sted med lang holdbarhet, derfor, så lenge vi ikke tilbakekaller den mottatte tilgangen, vil applikasjonen ikke motta refresh-tokenet. Til slutt la jeg til en forespørsel om tilbakekalling av tokenet hvis applikasjonen ikke ble avsluttet vellykket og refresh-tokenet ikke ble returnert, da vil det starte prosedyren på nytt (vi anså det som usikkert å lagre tokens lokalt på terminalen, og ønsker ikke å komplisere det med kryptografi eller ofte åpne nettleseren).

do {
  $token_result = Get-GoogleAuthToken
  $token = $token_result.access_token
  if ($token_result.refresh_token -eq $null) {
    Write-Host ("Økt er ikke ødelagt. Opphever 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
}

Som du allerede har merket, brukes Invoke-WebRequest ved oppheving av token. I motsetning til Invoke-RestMethod, returnerer det ikke dataene i et brukervennlig format, og viser statusen for forespørselen.

Deretter vil skriptet be om å skrive inn brukerens fornavn og etternavn, og generere brukernavn + e-post.

Forespørsel

Det neste vil være forespørslene – først må vi sjekke om det allerede finnes en bruker med det brukernavnet for å bestemme om vi skal opprette en ny eller bruke den eksisterende.

Jeg har besluttet å implementere alle forespørslene i formatet av en enkelt funksjon med valg, ved hjelp av 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'])
      }
    }
  }
}

Hvert forespørsel må sende en Authorization-header som inneholder token-type og selve Access-tokenet. Per nå er token-type alltid Bearer. Siden vi må sjekke at tokenet ikke er utløpt og oppdatere det etter en time fra utstedelsen, har jeg spesifisert en forespørsel til en annen funksjon som returnerer Access-tokenet. Denne delen av koden finnes også i starten av skriptet når vi henter det første Access-tokenet:

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 utløpt. Oppdaterer..."
    $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
}

Sjekk av at brukernavnet eksisterer:

function Check_Google {
  $query = (GoogleQuery 'SearchAccount' $username)
  if ($query.users -ne $null) {
    $user = $query.users[0]
    Write-Host $user.name.fullName' - '$user.PrimaryEmail' - suspenderte: '$user.Suspended
    $GAresult = $user
  }
  if ($GAresult) {
      $return = $GAresult
  } else {$return = 'gg'}
  return $return
}

Forespørselen om email: $query ber API-en om å lete etter en bruker med denne emailen, inkludert eventuelle aliaser. Du kan også bruke wildcard: =, :, :{PREFIX}*.

For å hente data brukes GET-metoden, for å sette inn data (opprettelse av konto eller legge til deltaker i gruppe) – POST, for å oppdatere eksisterende data – PUT, for å slette en post (for eksempel en deltaker fra en gruppe) – DELETE.

Skriptet vil også spørre om telefonnummer (en ugyldigert streng) og om å bli med i den regionale distribusjonsgruppen. Det avgjør hvilken organisatorisk enhet brukeren skal ha basert på den valgte OU i Active Directory og lager et passord:

do {
  $phone = Read-Host "Telefon i format +7xxxxxxxxxx"
} while (-not $phone)
do {
    $moscow = Read-Host "I Moskva-kontoret? (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 "Vil bli opprettet i /Team delivery"
    $orgunit = "/Team delivery"
}
$Password = -join (48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba"

Og nå begynner det å manipulere kontoen:

$query = @{
  email = $email
  givenName = $firstname
  familyName = $lastname
  password = $password
  phone = $phone
  orgunit = $orgunit
}
if ($GMailExist) {
  Write-Host "Starter endringen av kontoen" -f mag
  (GoogleQuery 'UpdateAccount' $query) | fl
  write-host "Ikke glem å sjekke gruppene for $Username i Google."
} else {
  Write-Host "Starter opprettelsen av kontoen" -f mag
  (GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
  write-host "Legger til i gruppen moscowoffice"
  $query = @{
    groupkey = 'moscowoffice@rocketguys.com'
    email = $email
  }
  (GoogleQuery 'AddMember' $query) | fl
}

Funksjonene for oppdatering og oppretting av kontoer har lignende syntaks, ikke alle tilleggsfeltene er obligatoriske, i delen med telefonnumre må vi angi et array som kan inneholde minst én oppføring med nummeret og typen.

For å unngå feil når vi legger til en bruker i en gruppe, kan vi på forhånd sjekke om vedkommende allerede er i denne gruppen, ved å hente ut medlemslisten eller sammensetningen til brukeren selv.

Forespørselen om medlemskapet til en bestemt bruker vil ikke være rekurserende og vil vise kun direkte medlemskap. Å inkludere en bruker i en overordnet gruppe der en underordnet gruppe allerede tilhører, og brukeren er medlem av den, vil være vellykket.

Konklusjon

Det gjenstår å sende brukeren passordet til den nye kontoen. Vi gjør dette via SMS, mens generell informasjon med instruksjoner og brukernavn sendes til den personlige e-posten som ble gitt av bemanningsavdelingen, sammen med telefonnummeret. Som et alternativ kan vi spare penger og sende passordet i en hemmelig Telegram-chat, noe som også kan betraktes som en to-faktor (unntakene vil være MacBook-er).

Takk for at du leste helt til slutt. Jeg ser frem til forslag til forbedring av skrive-stilen og ønsker deg å unngå flest mulig feil når du skriver skripter =)

Liste over lenker som kan være tematisk nyttige eller bare svare på oppståtte spørsmål:

Kilde: habr.com

Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere 🔥 Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster