Crear usuarios de Google desde PowerShell a través de API

Hi!

Este artículo describirá la implementación de la interacción de PowerShell con la API de Google para manipular a los usuarios de G Suite.

Utilizamos varios servicios internos y en la nube en toda la organización. En su mayor parte, la autorización en ellos se reduce a Google o Active Directory, entre los cuales no podemos mantener una réplica, por lo que cuando un nuevo empleado se va, es necesario crear/habilitar una cuenta en estos dos sistemas. Para automatizar el proceso, decidimos escribir un script que recopile información y la envíe a ambos servicios.

Autorización

Al elaborar los requisitos, decidimos utilizar administradores humanos reales para la autorización, lo que simplifica el análisis de las acciones en caso de cambios masivos accidentales o intencionales.

Las API de Google utilizan el protocolo OAuth 2.0 para autenticación y autorización. Los casos de uso y descripciones más detalladas se pueden encontrar aquí: Uso de OAuth 2.0 para acceder a las API de Google.

Elegí el script que se utiliza para la autorización en aplicaciones de escritorio. También existe la opción de utilizar una cuenta de servicio, que no requiere movimientos innecesarios por parte del usuario.

La siguiente imagen es una descripción esquemática del escenario seleccionado en la página de Google.

Crear usuarios de Google desde PowerShell a través de API

  1. Primero, enviamos al usuario a la página de autenticación de la cuenta de Google, especificando los parámetros GET:
    • ID de aplicación
    • áreas a las que la aplicación necesita acceso
    • la dirección a la que el usuario será redirigido después de completar el procedimiento
    • la forma en que actualizaremos el token
    • Código de seguridad
    • formato de transmisión del código de verificación

  2. Una vez completada la autorización, el usuario será redirigido a la página especificada en la primera solicitud, con un código de error o de autorización pasado por los parámetros GET.
  3. La aplicación (script) deberá recibir estos parámetros y, si recibe el código, realizar la siguiente solicitud para obtener tokens
  4. Si la solicitud es correcta, la API de Google devuelve:
    • Token de acceso con el que podremos realizar solicitudes
    • El período de validez de este token.
    • Token de actualización necesario para actualizar el token de acceso.

Primero debes ir a la consola API de Google: Credenciales: Consola API de Google, seleccione la aplicación deseada y en la sección Credenciales cree un identificador OAuth de cliente. Allí (o más tarde, en las propiedades del identificador creado) debe especificar las direcciones a las que se permite la redirección. En nuestro caso, serán varias entradas de localhost con diferentes puertos (ver más abajo).

Para que sea más conveniente leer el algoritmo del script, puede mostrar los primeros pasos en una función separada que devolverá tokens de acceso y actualización para la aplicación:

$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

Configuramos el ID de cliente y el Secreto de cliente obtenidos en las propiedades del identificador de cliente de OAuth, y el código verificador es una cadena de 43 a 128 caracteres que debe generarse aleatoriamente a partir de caracteres no reservados: [AZ] / [az] / [0-9] / "-" / "." / "_" / "~".

Este código se transmitirá nuevamente. Elimina la vulnerabilidad en la que un atacante podría interceptar una respuesta devuelta como redirección después de la autorización del usuario.
Puede enviar un verificador de código en la solicitud actual en texto claro (lo que lo hace sin sentido; esto solo es adecuado para sistemas que no admiten SHA256), o creando un hash usando el algoritmo SHA256, que debe estar codificado en BASE64Url (diferente de Base64 por dos caracteres de la tabla) y eliminando los finales de línea de caracteres: =.

A continuación, debemos comenzar a escuchar http en la máquina local para recibir una respuesta después de la autorización, que se devolverá como una redirección.

Las tareas administrativas se realizan en un servidor especial, no podemos descartar la posibilidad de que varios administradores ejecuten el script al mismo tiempo, por lo que seleccionará aleatoriamente un puerto para el usuario actual, pero especifiqué puertos predefinidos porque también deben agregarse como confiables en la consola API.

access_type=sin conexión significa que la aplicación puede actualizar un token caducado por sí sola sin interacción del usuario con el navegador,
tipo_respuesta=código establece el formato de cómo se devolverá el código (una referencia al antiguo método de autorización, cuando el usuario copiaba el código del navegador al script),
alcance Indica el alcance y tipo de acceso. Deben estar separados por espacios o %20 (según codificación de URL). Aquí se puede ver una lista de áreas de acceso con tipos: Ámbitos de OAuth 2.0 para las API de Google.

Después de recibir el código de autorización, la aplicación devolverá un mensaje de cierre al navegador, dejará de escuchar en el puerto y enviará una solicitud POST para obtener el token. En él indicamos el id y el secreto previamente especificados de la API de la consola, la dirección a la que será redirigido el usuario y Grant_type de acuerdo con la especificación del protocolo.

Como respuesta recibiremos un token de acceso, su período de validez en segundos y un token de actualización, con el que podremos actualizar el token de acceso.

La aplicación debe almacenar los tokens en un lugar seguro y con una larga vida útil, por lo que hasta que no revoquemos el acceso recibido, la aplicación no devolverá el token de actualización. Al final, agregué una solicitud para revocar el token; si la solicitud no se completó exitosamente y no se devolvió el token de actualización, iniciará el procedimiento nuevamente (consideramos que no era seguro almacenar tokens localmente en el terminal y no No quiero complicar las cosas con la criptografía ni abrir el navegador con frecuencia).

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
}

Como ya habrás notado, al revocar un token, se utiliza Invoke-WebRequest. A diferencia de Invoke-RestMethod, no devuelve los datos recibidos en un formato utilizable y muestra el estado de la solicitud.

A continuación, el script le pide que ingrese el nombre y apellido del usuario, generando un inicio de sesión + correo electrónico.

solicitudes

Las siguientes solicitudes serán: en primer lugar, debe verificar si ya existe un usuario con el mismo inicio de sesión para poder tomar una decisión sobre la creación de uno nuevo o habilitar el actual.

Decidí implementar todas las solicitudes en el formato de una función con una selección, usando el interruptor:

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 solicitud, debe enviar un encabezado de Autorización que contenga el tipo de token y el token de acceso en sí. Actualmente, el tipo de token es siempre Portador. Porque Necesitamos verificar que el token no haya caducado y actualizarlo después de una hora desde el momento en que se emitió. Especifiqué una solicitud para otra función que devuelve un token de acceso. El mismo fragmento de código se encuentra al comienzo del script cuando se recibe el primer token de acceso:

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
}

Comprobando la existencia del inicio de sesión:

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 solicitud email:$query le pedirá a la API que busque un usuario con exactamente ese correo electrónico, incluidos los alias. También puedes utilizar comodines: =, :, :{PREFIJO}*.

Para obtener datos, utilice el método de solicitud GET, para insertar datos (creando una cuenta o agregando un miembro a un grupo) - POST, para actualizar datos existentes - PUT, para eliminar un registro (por ejemplo, un miembro de un grupo) - BORRAR.

El script también solicitará un número de teléfono (una cadena no validada) y su inclusión en un grupo de distribución regional. Decide qué unidad organizativa debe tener el usuario en función de la unidad organizativa de Active Directory seleccionada y genera una contraseña:

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"

Y luego comienza a manipular la cuenta:

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

Las funciones para actualizar y crear una cuenta tienen una sintaxis similar, no todos los campos adicionales son obligatorios, en la sección con números de teléfono es necesario especificar una matriz que puede contener hasta un registro con el número y su tipo.

Para no recibir un error al agregar un usuario a un grupo, primero podemos verificar si ya es miembro de este grupo obteniendo una lista de miembros del grupo o la composición del propio usuario.

Consultar la membresía del grupo de un usuario específico no será recursivo y solo mostrará la membresía directa. La inclusión de un usuario en un grupo principal que ya tiene un grupo secundario del que el usuario es miembro se realizará correctamente.

Conclusión

Todo lo que queda es enviar al usuario la contraseña de la nueva cuenta. Hacemos esto a través de SMS y enviamos información general con instrucciones e inicio de sesión a un correo electrónico personal que, junto con un número de teléfono, fue proporcionado por el departamento de contratación. Como alternativa, puedes ahorrar dinero y enviar tu contraseña a un chat secreto de Telegram, lo que también puede considerarse el segundo factor (los MacBooks serán una excepción).

Gracias por leer hasta el final. Estaré encantado de ver sugerencias para mejorar el estilo de redacción de artículos y deseo que detecte menos errores al escribir guiones =)

Lista de enlaces que pueden ser útiles temáticamente o simplemente responder preguntas:

Fuente: habr.com

Añadir un comentario