Oi
Este artigo descreve como o PowerShell interage com a API do Google para manipular os usuários do G Suite.
Na organização, usamos vários serviços internos e em nuvem. Na maioria das vezes, a autorização neles se resume ao Google ou ao Active Directory, entre os quais não podemos manter uma réplica, respectivamente, quando um novo funcionário é liberado, você precisa criar / ativar uma conta nesses dois sistemas. Para automatizar o processo, decidimos escrever um script que coleta informações e as envia para os dois serviços.
Entrar
Ao compor os requisitos, decidimos usar pessoas reais como administradores para autorização, simplificando a análise de ações em caso de mudanças maciças acidentais ou intencionais.
As APIs do Google usam o protocolo OAuth 2.0 para autenticação e autorização. Casos de uso e uma descrição mais detalhada podem ser encontrados aqui:
Uso do OAuth 2.0 para acessar APIs do Google .
Eu escolhi o script usado para autorização em aplicativos da área de trabalho. Há também uma opção para usar uma conta de serviço que não requer movimentos desnecessários do usuário.
A imagem abaixo é uma descrição esquemática do cenário selecionado na página do Google.

- Primeiro, enviamos o usuário para a página de autenticação na conta do Google, indicando os parâmetros GET:
- ID do aplicativo
- áreas às quais o aplicativo precisa acessar
- endereço para o qual o usuário será redirecionado após a conclusão do procedimento
- a maneira como atualizaremos o token
- código de verificação
- formato de transmissão do código de verificação
- Após a conclusão da autorização, o usuário será redirecionado para a página especificada na primeira solicitação, com um código de erro ou autorização transmitido pelos parâmetros GET
- O aplicativo (script) precisará obter esses parâmetros e, se o código for recebido, execute a seguinte solicitação de tokens
- Se a solicitação estiver correta, a API do Google retornará:
- Token de acesso com o qual podemos fazer solicitações
- Validade deste token
- Atualize o token necessário para atualizar o token do Access.
Primeiro, você precisa acessar o console da API do Google:
Credenciais - Google API Console , selecione o aplicativo desejado e crie o identificador OAuth do cliente na seção Credenciais. No mesmo local (ou posterior, nas propriedades do identificador criado), você precisa especificar os endereços para os quais o redirecionamento é permitido. No nosso caso, haverá várias entradas de host local com portas diferentes (veja abaixo).
Para facilitar a leitura do algoritmo de script, você pode gerar as primeiras etapas em uma função separada que retornará o Access e atualizará os tokens para o aplicativo:
$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
Definimos o ID do cliente e o Segredo do cliente obtidos nas propriedades do identificador de cliente OAuth, e o verificador de código é uma sequência de 43 a 128 caracteres, que deve ser gerada aleatoriamente a partir de caracteres não reservados: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".
Além disso, este código será retransmitido. Elimina a vulnerabilidade na qual um invasor pode interceptar uma resposta que retornou um redirecionamento após a autorização do usuário.
Você pode enviar o verificador de código na solicitação atual de forma clara (o que o torna inútil - isso é adequado apenas para sistemas que não suportam SHA256) ou criando um hash SHA256 que precisa ser codificado em BASE64Url (difere de Base64 em dois caracteres da tabela) e exclui o caractere fim de linha: =.
Em seguida, precisamos começar a ouvir http na máquina local para obter uma resposta após a autorização, que retornará como um redirecionamento.
As tarefas administrativas são executadas em um servidor especial, não podemos excluir a possibilidade de vários administradores executarem o script ao mesmo tempo, portanto ele selecionará aleatoriamente uma porta para o usuário atual, mas especifiquei portas predefinidas, porque eles também devem ser adicionados como confiáveis no console da API.
access_type = offline significa que o aplicativo pode atualizar o token expirado independentemente, sem interação do usuário com o navegador,
response_type = code define o formato de como o código retornará (consultando o antigo método de autorização quando o usuário copiou o código do navegador para o script),
scope indica o
escopo e o tipo de acesso. Eles devem ser separados por espaços ou% 20 (de acordo com a codificação de URL). Uma lista de áreas de acesso com tipos pode ser vista aqui:
escopos OAuth 2.0 para APIs do Google .
Depois de receber o código de autorização, o aplicativo retornará uma mensagem de fechamento para o navegador, parará de ouvir a porta e enviará uma solicitação POST para receber o token. Indicamos nele o ID e o segredo anteriormente definidos da API do console, o endereço para o qual o usuário será redirecionado e o grant_type de acordo com a especificação do protocolo.
Em resposta, obteremos um token do Access, sua duração em segundos e um token de atualização, com o qual podemos atualizar o token do Access.
O aplicativo deve armazenar tokens em um local seguro e com uma vida útil longa, portanto, até revogar o acesso obtido, o token de atualização não retornará ao aplicativo. No final, adicionei uma solicitação para revogar o token, se o aplicativo não tiver sido concluído com êxito e o token de atualização não retornar, ele iniciará o procedimento novamente (consideramos inseguro armazenar tokens localmente no terminal, mas não queremos complicar a criptografia ou abrir frequentemente o 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 você deve ter notado, ao invocar um token, Invoke-WebRequest é usado. Ao contrário de Invoke-RestMethod, ele não retorna os dados recebidos em um formato conveniente para uso e mostra o status da solicitação.
Em seguida, o script solicitará que você digite o nome e o sobrenome do usuário, gerando um nome de usuário + email.
Inquéritos
A seguir, serão apresentadas as solicitações - primeiro, você precisa verificar se o usuário já existe com esse login para obter uma decisão de formar um novo ou ativar o atual.
Decidi implementar todas as solicitações no formato de uma única função com uma seleção usando o 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']) } } } }
Em cada solicitação, você precisa enviar um cabeçalho de autorização contendo o tipo de token e o próprio token de acesso. No momento, o tipo de token é sempre Portador. Porque precisamos verificar se o token não expirou e atualizá-lo após uma hora a partir do momento em que foi emitido, indiquei uma solicitação para outra função que retorna um token do Access. O mesmo trecho de código está no início do script ao receber o primeiro token do Access:
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 }
Verificando a existência do login:
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 }
Solicitação de email: $ query solicitará à API que procure o usuário com este email, incluindo aliases. Você também pode usar curinga:
=,:,: {PREFIX} * .
Para obter dados, o método de solicitação GET é usado, para inserir dados (criar uma conta ou adicionar um membro a um grupo) - POST, atualizar dados existentes - PUT, excluir uma entrada (por exemplo, um participante de um grupo) - DELETE.
O script também solicitará um número de telefone (uma sequência inválida) e o incluirá em um grupo de distribuição regional. Ele decide qual unidade organizacional o usuário deve ter com base na UO do Active Directory selecionada e criará uma senha:
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"
E então começa a manipular a conta:
$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 }
As funções de atualização e criação de uma conta têm a mesma sintaxe, nem todos os campos adicionais são necessários. Na seção com números de telefone, é necessário especificar uma matriz que possa conter de um registro com um número e seu tipo.
Para não ocorrer um erro ao adicionar um usuário a um grupo, primeiro podemos verificar se ele já está nesse grupo recebendo uma lista de membros ou composição do grupo do próprio usuário.
Uma solicitação para a composição de grupos de um usuário específico não será recursiva e mostrará apenas associação direta. A inclusão do usuário no grupo pai, no qual o grupo subsidiário do qual o usuário é membro, já é bem-sucedida.
Conclusão
Resta enviar ao usuário a senha da nova conta. Fazemos isso por SMS e enviamos informações gerais com instruções e login para correio pessoal, que, juntamente com o número de telefone, foi fornecido pelo departamento de seleção de pessoal. Como alternativa, você pode economizar dinheiro e enviar uma senha para um bate-papo secreto por telegrama, que também pode ser considerado o segundo fator (os macbooks serão uma exceção).
Obrigado por ler até o fim. Ficarei feliz em ver sugestões para melhorar o estilo de redação dos artigos e desejo que você consiga menos erros ao escrever scripts =)
Lista de links que podem ser úteis em termos temáticos ou apenas responder às suas perguntas: