Creació d'usuaris de Google des de PowerShell mitjançant API

Hi!

En aquest article es descriu la implementació de la interacció de PowerShell amb l'API de Google per manipular els usuaris de G Suite.

Utilitzem diversos serveis interns i al núvol a tota l'organització. En la seva majoria, l'autorització es redueix a Google o Active Directory, entre els quals no podem mantenir una rèplica; per tant, quan un nou empleat marxa, cal crear/habilitar un compte en aquests dos sistemes. Per automatitzar el procés, hem decidit escriure un script que reculli informació i l'enviï als dos serveis.

Autorització

A l'hora d'elaborar els requisits, hem decidit utilitzar administradors humans reals per a l'autorització; això simplifica l'anàlisi de les accions en cas de canvis massius accidentals o intencionats.

Les API de Google utilitzen el protocol OAuth 2.0 per a l'autenticació i l'autorització. Podeu trobar casos d'ús i descripcions més detallades aquí: Ús d'OAuth 2.0 per accedir a les API de Google.

Vaig triar l'script que s'utilitza per a l'autorització a les aplicacions d'escriptori. També hi ha una opció per utilitzar un compte de servei, que no requereix moviments innecessaris de l'usuari.

La imatge següent és una descripció esquemàtica de l'escenari seleccionat a la pàgina de Google.

Creació d'usuaris de Google des de PowerShell mitjançant API

  1. En primer lloc, enviem l'usuari a la pàgina d'autenticació del compte de Google, especificant els paràmetres GET:
    • identificador de l'aplicació
    • àrees a les quals l'aplicació necessita accedir
    • l'adreça a la qual serà redirigit l'usuari un cop finalitzat el tràmit
    • la manera com actualitzarem el testimoni
    • Codi de seguretat
    • format de transmissió del codi de verificació

  2. Un cop completada l'autorització, l'usuari serà redirigit a la pàgina especificada a la primera sol·licitud, amb un error o codi d'autorització passat pels paràmetres GET
  3. L'aplicació (script) haurà de rebre aquests paràmetres i, si ha rebut el codi, realitzar la següent sol·licitud per obtenir fitxes
  4. Si la sol·licitud és correcta, l'API de Google retorna:
    • Token d'accés amb el qual podem fer peticions
    • El període de validesa d'aquest testimoni
    • Es requereix un testimoni d'actualització per actualitzar el testimoni d'accés.

Primer heu d'anar a la consola de l'API de Google: Credencials - Consola de l'API de Google, seleccioneu l'aplicació desitjada i, a la secció Credencials, creeu un identificador d'OAuth de client. Allà (o més endavant, a les propietats de l'identificador creat) cal especificar les adreces a les quals es permet la redirecció. En el nostre cas, seran diverses entrades localhost amb diferents ports (vegeu més avall).

Per facilitar la lectura de l'algorisme de l'script, podeu mostrar els primers passos en una funció independent que retornarà els testimonis d'accés i d'actualització de l'aplicació:

$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

Establem l'identificador de client i el secret de client obtinguts a les propietats d'identificador de client d'OAuth i el verificador de codi és una cadena de 43 a 128 caràcters que s'ha de generar aleatòriament a partir de caràcters no reservats: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Aquest codi es tornarà a transmetre. Elimina la vulnerabilitat en què un atacant podria interceptar una resposta retornada com a redirecció després de l'autorització de l'usuari.
Podeu enviar un verificador de codi a la sol·licitud actual en text clar (la qual cosa fa que no tingui sentit; això només és adequat per a sistemes que no admeten SHA256), o bé creant un hash mitjançant l'algorisme SHA256, que s'ha de codificar a BASE64Url (diferent de Base64 per dos caràcters de taula) i eliminant les terminacions de línia de caràcters: =.

A continuació, hem de començar a escoltar http a la màquina local per rebre una resposta després de l'autorització, que es retornarà com a redirecció.

Les tasques administratives es realitzen en un servidor especial, no podem descartar la possibilitat que diversos administradors executin l'script al mateix temps, de manera que seleccionarà aleatòriament un port per a l'usuari actual, però he especificat ports predefinits perquè també s'han d'afegir com a de confiança a la consola de l'API.

access_type=fora de línia significa que l'aplicació pot actualitzar per si mateixa un testimoni caducat sense la interacció de l'usuari amb el navegador,
resposta_tipus=codi estableix el format de com es retornarà el codi (una referència a l'antic mètode d'autorització, quan l'usuari va copiar el codi del navegador a l'script),
abast indica l'abast i el tipus d'accés. Han d'estar separats per espais o %20 (segons la codificació d'URL). Aquí es pot veure una llista d'àrees d'accés amb tipus: Àmbits OAuth 2.0 per a les API de Google.

Després de rebre el codi d'autorització, l'aplicació retornarà un missatge de tancament al navegador, deixarà d'escoltar el port i enviarà una sol·licitud POST per obtenir el testimoni. Hi indiquem l'identificador i el secret prèviament especificats de l'API de la consola, l'adreça a la qual es redirigirà l'usuari i grant_type d'acord amb l'especificació del protocol.

En resposta, rebrem un testimoni d'accés, el seu període de validesa en segons i un testimoni d'actualització, amb el qual podem actualitzar el testimoni d'accés.

L'aplicació ha d'emmagatzemar els testimonis en un lloc segur amb una llarga vida útil, de manera que fins que no revoquem l'accés rebut, l'aplicació no retornarà el testimoni d'actualització. Al final, vaig afegir una sol·licitud per revocar el testimoni; si l'aplicació no s'ha completat correctament i no es va retornar el testimoni d'actualització, tornarà a iniciar el procediment (considerem que no era segur emmagatzemar fitxes localment al terminal i no No vull complicar les coses amb la criptografia ni obrir el navegador amb freqüència).

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
}

Com ja heu vist, en revocar un testimoni, s'utilitza Invoke-WebRequest. A diferència d'Invoke-RestMethod, no retorna les dades rebudes en un format utilitzable i mostra l'estat de la sol·licitud.

A continuació, l'script us demana que introduïu el nom i cognoms de l'usuari, generant un inici de sessió + correu electrònic.

Sol·licituds

Les següents sol·licituds seran: en primer lloc, cal comprovar si ja existeix un usuari amb el mateix inici de sessió per obtenir una decisió sobre crear-ne un de nou o habilitar l'actual.

Vaig decidir implementar totes les sol·licituds en el format d'una funció amb una selecció, utilitzant 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'])
      }
    }
  }
}

En cada sol·licitud, heu d'enviar una capçalera d'autorització que contingui el tipus de testimoni i el propi testimoni d'accés. Actualment, el tipus de testimoni sempre és Bearer. Perquè hem de comprovar que el testimoni no ha caducat i actualitzar-lo després d'una hora des del moment en què es va emetre, he especificat una sol·licitud per a una altra funció que retorna un testimoni d'accés. El mateix fragment de codi es troba al principi de l'script quan es reb el primer testimoni d'accés:

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
}

Comprovant l'existència de l'inici de sessió:

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 sol·licitud email:$query demanarà a l'API que cerqui un usuari amb exactament aquest correu electrònic, inclosos els àlies. També podeu utilitzar el comodí: =, :, :{PREFIX}*.

Per obtenir dades, utilitzeu el mètode de sol·licitud GET, per inserir dades (crear un compte o afegir un membre a un grup) - POST, per actualitzar les dades existents - PUT, per eliminar un registre (per exemple, un membre d'un grup) - ELIMINAR.

L'script també demanarà un número de telèfon (una cadena no validada) i la inclusió en un grup de distribució regional. Decideix quina unitat organitzativa ha de tenir l'usuari en funció de la OU d'Active Directory seleccionada i ofereix una contrasenya:

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"

I llavors comença a manipular el compte:

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

Les funcions per actualitzar i crear un compte tenen una sintaxi semblant; no tots els camps addicionals són obligatoris; a la secció amb números de telèfon, cal especificar una matriu que pot contenir fins a un registre amb el número i el seu tipus.

Per no rebre cap error en afegir un usuari a un grup, primer podem comprovar si ja és membre d'aquest grup obtenint una llista de membres del grup o composició del mateix usuari.

Consultar la pertinença al grup d'un usuari específic no serà recursiu i només mostrarà la pertinença directa. La inclusió d'un usuari en un grup principal que ja té un grup secundari del qual l'usuari és membre tindrà èxit.

Conclusió

Només queda enviar a l'usuari la contrasenya del nou compte. Ho fem per SMS i enviem informació general amb instruccions i inici de sessió a un correu electrònic personal, que, juntament amb un número de telèfon, ens va proporcionar el departament de contractació. Com a alternativa, podeu estalviar diners i enviar la vostra contrasenya a un xat de telegrama secret, que també es pot considerar el segon factor (MacBooks serà una excepció).

Gràcies per llegir fins al final. Estaré encantat de veure suggeriments per millorar l'estil d'escriure articles i desitjar-vos que detecteu menys errors en escriure guions =)

Llista d'enllaços que poden ser útils temàticament o simplement respondre a preguntes:

Font: www.habr.com

Afegeix comentari