Opprette Google-brukere fra PowerShell via API

Hei!

Denne artikkelen vil beskrive implementeringen av PowerShell-interaksjon med Google API for å manipulere G Suite-brukere.

Vi bruker flere interne og skytjenester på tvers av organisasjonen. For det meste kommer autorisasjonen i dem ned til Google eller Active Directory, mellom hvilke vi ikke kan opprettholde en replika; følgelig, når en ny ansatt slutter, må du 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.

Autorisasjon

Da vi utarbeidet kravene, bestemte vi oss for å bruke ekte menneskelige administratorer for autorisasjon; dette forenkler analysen av handlinger ved tilfeldige eller tilsiktede massive endringer.

Google APIer bruker OAuth 2.0-protokollen for autentisering og autorisasjon. Brukstilfeller og mer detaljerte beskrivelser finner du her: Bruker OAuth 2.0 for å få tilgang til Google APIer.

Jeg valgte skriptet som brukes for autorisasjon i skrivebordsapplikasjoner. Det er også mulighet for å bruke en tjenestekonto, som ikke krever unødvendige bevegelser fra brukeren.

Bildet nedenfor er en skjematisk beskrivelse av det valgte scenariet fra Google-siden.

Opprette Google-brukere fra PowerShell via API

  1. Først sender vi brukeren til Google-kontoautentiseringssiden, og spesifiserer GET-parametere:
    • applikasjons-ID
    • områder som applikasjonen trenger tilgang til
    • adressen som brukeren vil bli omdirigert til etter å ha fullført prosedyren
    • måten vi oppdaterer tokenet på
    • Sikkerhetskode
    • overføringsformat for bekreftelseskode

  2. Etter at autorisasjonen er fullført, vil brukeren bli omdirigert til siden spesifisert i den første forespørselen, med en feil eller autorisasjonskode sendt av GET-parametere
  3. Applikasjonen (skriptet) må motta disse parameterne og, hvis den mottas koden, sende følgende forespørsel for å få tokens
  4. Hvis forespørselen er riktig, returnerer Google API:
    • Tilgangstoken som vi kan sende forespørsler med
    • Gyldighetsperioden for dette tokenet
    • Oppdater token kreves for å oppdatere tilgangstoken.

Først må du gå til Google API-konsollen: Påloggingsinformasjon – Google API-konsoll, velg ønsket applikasjon og opprett en klient-OAuth-identifikator i delen påloggingsinformasjon. Der (eller senere, i egenskapene til den opprettede identifikatoren) må du spesifisere adressene som omdirigering er tillatt. I vårt tilfelle vil dette være flere lokale vertsoppføringer med forskjellige porter (se nedenfor).

For å gjøre det mer praktisk å lese skriptalgoritmen, kan du vise de første trinnene i en egen funksjon som vil returnere Access og oppdatere tokens for applikasjonen:

$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

Vi angir klient-IDen og klienthemmeligheten innhentet i OAuth-klientidentifikatoregenskapene, og kodebekreftelsen er en streng på 43 til 128 tegn som må genereres tilfeldig fra ureserverte tegn: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Denne koden vil da bli overført igjen. Det eliminerer sårbarheten der en angriper kan avskjære et svar som returneres som en omdirigering etter brukerautorisasjon.
Du kan sende en kodeverifikator i den gjeldende forespørselen i klartekst (noe som gjør det meningsløst - dette er kun egnet for systemer som ikke støtter SHA256), eller ved å lage en hash ved hjelp av SHA256-algoritmen, som må kodes i BASE64Url (forskjellig fra Base64 med to tabelltegn) og fjerner tegnlinjeavslutningene: =.

Deretter må vi begynne å lytte til http på den lokale maskinen for å motta et svar etter autorisasjon, som vil bli returnert som en omdirigering.

Administrative oppgaver utføres på en spesiell server, vi kan ikke utelukke muligheten for at flere administratorer vil kjøre skriptet samtidig, så det vil tilfeldig velge en port for gjeldende bruker, men jeg spesifiserte forhåndsdefinerte porter pga. de må også legges til som klarert i API-konsollen.

access_type=frakoblet betyr at applikasjonen kan oppdatere et utløpt token på egen hånd uten brukerinteraksjon med nettleseren,
response_type=kode angir formatet for hvordan koden skal returneres (en referanse til den gamle autorisasjonsmetoden, da brukeren kopierte koden fra nettleseren til skriptet),
omfang angir omfang og type tilgang. De må være atskilt med mellomrom eller %20 (i henhold til URL-koding). En liste over tilgangsområder med typer kan ses her: OAuth 2.0 Scopes for Google APIer.

Etter å ha mottatt autorisasjonskoden, vil applikasjonen returnere en lukkemelding til nettleseren, slutte å lytte på porten og sende en POST-forespørsel for å få tokenet. Vi angir i den den tidligere spesifiserte IDen og hemmeligheten fra konsollens API, adressen som brukeren vil bli omdirigert til og grant_type i samsvar med protokollspesifikasjonen.

Som svar vil vi motta et Access-token, dets gyldighetsperiode i sekunder, og et Refresh-token, som vi kan oppdatere Access-tokenet med.

Applikasjonen må oppbevare tokens på et sikkert sted med lang holdbarhet, så inntil vi tilbakekaller den mottatte tilgangen, vil ikke applikasjonen returnere oppdateringstokenet. På slutten la jeg til en forespørsel om å tilbakekalle tokenet; hvis applikasjonen ikke ble fullført og oppdateringstokenet ikke ble returnert, vil det starte prosedyren på nytt (vi anså det som utrygt å lagre tokens lokalt på terminalen, og vi gjør det ikke ønsker ikke å komplisere ting med kryptografi eller åpne nettleseren 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
}

Som du allerede har lagt merke til, når du tilbakekaller et token, brukes Invoke-WebRequest. I motsetning til Invoke-RestMethod, returnerer den ikke de mottatte dataene i et brukbart format og viser statusen til forespørselen.

Deretter ber skriptet deg om å skrive inn brukerens for- og etternavn, og genererer en pålogging + e-post.

forespørsler

De neste forespørslene vil være - først og fremst må du sjekke om en bruker med samme pålogging allerede eksisterer for å få en beslutning om å opprette en ny eller aktivere den nåværende.

Jeg bestemte meg for å implementere alle forespørsler i formatet til én funksjon med et utvalg, ved å bruke bryteren:

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

I hver forespørsel må du sende en autorisasjonsoverskrift som inneholder tokentypen og selve Access-tokenet. For øyeblikket er tokentypen alltid bærer. Fordi vi må sjekke at tokenet ikke har utløpt og oppdatere det etter en time fra det ble utstedt, jeg spesifiserte en forespørsel om en annen funksjon som returnerer et Access token. Den samme kodebiten er i begynnelsen av skriptet når du mottar 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 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
}

Sjekker påloggingen for eksistens:

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
}

Email:$query-forespørselen vil be API-et om å se etter en bruker med akkurat den e-postadressen, inkludert aliaser. Du kan også bruke jokertegn: =, :, :{PREFIX}*.

For å få data, bruk GET-forespørselsmetoden, for å sette inn data (opprette en konto eller legge til et medlem i en gruppe) - POST, for å oppdatere eksisterende data - PUT, for å slette en post (for eksempel et medlem fra en gruppe) - SLETT.

Skriptet vil også be om et telefonnummer (en uvalidert streng) og om å bli inkludert i en regional distribusjonsgruppe. Den bestemmer hvilken organisasjonsenhet brukeren skal ha basert på den valgte Active Directory OU og kommer opp med et passord:

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"

Og så begynner han å manipulere kontoen:

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

Funksjonene for å oppdatere og opprette en konto har en lignende syntaks; ikke alle tilleggsfelt er påkrevd; i seksjonen med telefonnumre må du spesifisere en matrise som kan inneholde opptil én post med nummeret og dets type.

For ikke å motta en feilmelding når du legger til en bruker i en gruppe, kan vi først sjekke om han allerede er medlem av denne gruppen ved å få en liste over gruppemedlemmer eller sammensetning fra brukeren selv.

Å spørre om gruppemedlemskapet til en spesifikk bruker vil ikke være rekursivt og vil bare vise direkte medlemskap. Å inkludere en bruker i en overordnet gruppe som allerede har en undergruppe som brukeren er medlem av, vil lykkes.

Konklusjon

Alt som gjenstår er å sende brukeren passordet til den nye kontoen. Vi gjør dette på SMS, og sender generell informasjon med instruksjoner og pålogging til en personlig epost, som sammen med telefonnummer er oppgitt av rekrutteringsavdelingen. Som et alternativ kan du spare penger og sende passordet ditt til en hemmelig telegramchat, som også kan betraktes som den andre faktoren (MacBooks vil være et unntak).

Takk for at du leste til slutten. Jeg vil gjerne se forslag for å forbedre stilen for å skrive artikler og ønsker at du skal få færre feil når du skriver manus =)

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

Kilde: www.habr.com

Legg til en kommentar