Hola
Este artículo describirá cómo PowerShell interactúa con la API de Google para manipular a los usuarios de G Suite.
En la organización, utilizamos varios servicios internos y en la nube. 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, respectivamente, cuando se libera a un nuevo empleado, debe 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.
Iniciar sesión
Al componer los requisitos, decidimos utilizar personas reales como administradores para la autorización, esto simplifica el análisis de 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 una descripción más detallada se pueden encontrar aquí:
Uso de OAuth 2.0 para acceder a las API de Google .
Elegí el script que se usa para la autorización en aplicaciones de escritorio. También hay una opción para usar una cuenta de servicio que no requiere movimientos innecesarios del usuario.
La imagen a continuación es una descripción esquemática del escenario seleccionado de la página de Google.

- Primero, enviamos al usuario a la página de autenticación en la cuenta de Google, indicando los parámetros GET:
- ID de la aplicación
- áreas a las que la aplicación necesita acceso
- dirección a la que se redirigirá al usuario después de completar el procedimiento
- la forma en que actualizaremos el token
- código de verificación
- formato de transmisión del código de verificación
- Una vez completada la autorización, el usuario será redirigido a la página especificada en la primera solicitud, con un error o código de autorización transmitido por los parámetros GET
- La aplicación (script) necesitará obtener estos parámetros y, si se recibe el código, ejecutar la siguiente solicitud de tokens
- Si la solicitud es correcta, la API de Google devuelve:
- Token de acceso con el que podemos realizar solicitudes
- Validez de este token
- Se necesita el token de actualización para actualizar el token de acceso.
Primero debe ir a la consola API de Google:
Credenciales - Consola API de Google , seleccione la aplicación que necesita y en la sección Credenciales cree el identificador OAuth del cliente. En el mismo lugar (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 host local con diferentes puertos (ver más abajo).
Para facilitar la lectura del algoritmo de script, puede generar los primeros pasos en una función separada que devolverá Access y actualizará tokens 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
Establecemos el ID de cliente y el secreto de cliente obtenidos en las propiedades del identificador de cliente OAuth, y el verificador de código es una cadena de 43 a 128 caracteres de longitud, que debe generarse aleatoriamente a partir de caracteres no reservados: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".
Además este código será retransmitido. Elimina la vulnerabilidad en la que un atacante podría interceptar la respuesta que se devolvió como una redirección después de la autorización del usuario.
Puede enviar el verificador de código en la solicitud actual de forma clara (lo que lo hace inútil; esto es adecuado solo para sistemas que no admiten SHA256), o creando un hash SHA256 que debe codificarse en BASE64Url (difiere de Base64 en dos caracteres de la tabla) y eliminar el carácter fin de línea: =.
Luego, debemos comenzar a escuchar http en la máquina local para obtener una respuesta después de la autorización, que regresará como una redirección.
Las tareas administrativas se realizan en un servidor especial, no podemos excluir 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 = offline significa que la aplicación puede actualizar el token caducado independientemente sin la interacción del usuario con el navegador,
response_type = code establece el formato de cómo se devolverá el código (refiriéndose al antiguo método de autorización cuando el usuario copió el código del navegador al script),
alcance indica el
alcance y el tipo de acceso. Deben estar separados por espacios o% 20 (según la codificación de URL). Puede ver una lista de áreas de acceso con tipos aquí:
OAuth 2.0 Scopes 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 el puerto y enviará una solicitud POST para recibir el token. Indicamos en él la identificación y el secreto previamente establecidos de la API de la consola, la dirección a la que se redirigirá al usuario y grant_type de acuerdo con la especificación del protocolo.
En respuesta, obtendremos un token de acceso, su duración 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 con una larga vida útil, por lo que hasta que revoquemos el acceso recibido, el token de actualización no se devolverá a la aplicación. Al final, agregué una solicitud para revocar el token, si la aplicación no se completó con éxito y el token de actualización no regresó, comenzará el procedimiento nuevamente (consideramos que no es seguro almacenar tokens localmente en el terminal, pero no queremos complicar la criptografía o, a menudo, abrir el navegador).
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 habrás notado, cuando invocas un token, se usa Invoke-WebRequest. A diferencia de Invoke-RestMethod, no devuelve los datos recibidos en un formato conveniente para su uso y muestra el estado de la solicitud.
A continuación, el script le pedirá que ingrese el nombre y apellido del usuario, generando un nombre de usuario + correo electrónico.
Consultas
Las siguientes serán las solicitudes: en primer lugar, debe verificar si el usuario ya existe con dicho inicio de sesión para tomar la decisión de formar uno nuevo o activar el actual.
Decidí implementar todas las solicitudes en el formato de una sola 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. 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ó, indiqué una solicitud para otra función que devuelve un token de acceso. El mismo fragmento de código se encuentra al comienzo del script al recibir 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 }
Solicitud de correo electrónico: $ query le pedirá a la API que busque un usuario con exactamente ese correo electrónico, incluidos los alias que se encontrarán. También puede usar comodines:
=,:,: {PREFIX} * .
Para obtener datos, se utiliza el método de solicitud GET, para insertar datos (crear una cuenta o agregar un miembro a un grupo) - POST, para actualizar datos existentes - PUT, para eliminar una entrada (por ejemplo, un participante de un grupo) - DELETE.
El script también solicitará un número de teléfono (una cadena no válida) y lo incluirá 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 obtendrá 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 = 'moscowoffice@rocketguys.com' email = $email } (GoogleQuery 'AddMember' $query) | fl }
Las funciones de actualización y creación de una cuenta tienen la misma sintaxis, no se requieren todos los campos adicionales, en la sección con números de teléfono debe especificar una matriz que puede contener de un registro con un número y su tipo.
Para no obtener un error al agregar un usuario a un grupo, primero podemos verificar si ya está en este grupo al recibir una lista de miembros del grupo o la composición del propio usuario.
Una solicitud para la composición de grupos de un usuario específico no será recursiva y solo mostrará membresía directa. La inclusión del usuario en el grupo principal, en el que el grupo subsidiario del que el usuario es miembro, ya es exitosa.
Conclusión
Queda por enviar al usuario la contraseña de la nueva cuenta. Hacemos esto por SMS y enviamos información general con instrucciones e inicio de sesión al correo personal, que, junto con el número de teléfono, fue proporcionado por el departamento de selección de personal. Como alternativa, puede ahorrar dinero y enviar una contraseña a un chat secreto de telegramas, que también puede considerarse el segundo factor (las macbooks serán una excepción).
Gracias por leer hasta el final. Estaré encantado de ver sugerencias para mejorar el estilo de escritura de los artículos y deseo que detecte menos errores al escribir guiones =)
Lista de enlaces que pueden ser temáticamente útiles o simplemente responder a sus preguntas: