Créez des utilisateurs Google à partir de PowerShell via l'API

Salut

Cet article décrit comment PowerShell interagit avec l'API Google pour manipuler les utilisateurs de G Suite.

Dans l'organisation, nous utilisons plusieurs services internes et cloud. Pour la plupart, l'autorisation y relative revient à Google ou à Active Directory, entre lesquels nous ne pouvons pas conserver de réplique, respectivement, lorsqu'un nouvel employé est libéré, vous devez créer / activer un compte dans ces deux systèmes. Pour automatiser le processus, nous avons décidé d'écrire un script qui collecte des informations et les envoie aux deux services.

Se connecter


Composant les exigences, nous avons décidé d'utiliser des personnes réelles comme administrateurs pour l'autorisation, ce qui simplifie l'analyse des actions en cas de changements massifs accidentels ou intentionnels.

Les API Google utilisent le protocole OAuth 2.0 pour l'authentification et l'autorisation. Des cas d'utilisation et une description plus détaillée peuvent être trouvés ici: Utilisation d'OAuth 2.0 pour accéder aux API Google .

J'ai choisi le script utilisé pour l'autorisation dans les applications de bureau. Il existe également une option pour utiliser un compte de service qui ne nécessite pas de mouvements inutiles de la part de l'utilisateur.

L'image ci-dessous est une description schématique du scénario sélectionné à partir de la page Google.



  1. Tout d'abord, nous envoyons l'utilisateur à la page d'authentification du compte Google, en indiquant les paramètres GET:
    • identifiant d'application
    • les zones auxquelles l'application doit accéder
    • adresse vers laquelle l'utilisateur sera redirigé une fois la procédure terminée
    • la façon dont nous mettrons à jour le jeton
    • code de vérification
    • format de transmission du code de vérification

  2. Une fois l'autorisation terminée, l'utilisateur sera redirigé vers la page spécifiée dans la première demande, avec un code d'erreur ou d'autorisation transmis par les paramètres GET
  3. L'application (script) devra obtenir ces paramètres et, si le code est reçu, exécuter la demande de jetons suivante
  4. Si la demande est correcte, l'API Google renvoie:

    • Jeton d'accès avec lequel nous pouvons faire des demandes
    • Validité de ce jeton
    • Actualiser le jeton nécessaire pour mettre à jour le jeton d'accès.

Vous devez d'abord accéder à la console Google API: Credentials - Google API Console , sélectionnez l'application souhaitée et créez l'identifiant OAuth du client dans la section Credentials. Au même endroit (ou plus tard, dans les propriétés de l'identifiant créé), vous devez spécifier les adresses vers lesquelles la redirection est autorisée. Dans notre cas, il s'agira de plusieurs entrées d'hôte local avec différents ports (voir ci-dessous).

Pour faciliter la lecture de l'algorithme de script, vous pouvez générer les premières étapes dans une fonction distincte qui renverra des jetons d'accès et d'actualisation pour l'application:

$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 

Nous définissons l'ID client et le secret client obtenus dans les propriétés de l'identifiant client OAuth, et le vérificateur de code est une chaîne de 43 à 128 caractères, qui doit être générée de manière aléatoire à partir de caractères non réservés: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

De plus, ce code sera retransmis. Il élimine la vulnérabilité dans laquelle un attaquant pourrait intercepter la réponse renvoyée sous forme de redirection après l'autorisation de l'utilisateur.
Vous pouvez envoyer le vérificateur de code dans la demande actuelle sous une forme claire (ce qui le rend inutile - cela ne convient qu'aux systèmes qui ne prennent pas en charge SHA256), ou en créant un hachage SHA256 qui doit être codé en BASE64Url (diffère de Base64 en deux caractères du tableau) et supprimez le caractère fin de ligne: =.

Ensuite, nous devons commencer à écouter http sur la machine locale pour obtenir une réponse après autorisation, qui sera renvoyée sous forme de redirection.

Les tâches administratives sont effectuées sur un serveur spécial, nous ne pouvons pas exclure la possibilité que plusieurs administrateurs exécutent le script en même temps, donc il sélectionnera au hasard un port pour l'utilisateur actuel, mais j'ai spécifié des ports prédéfinis, car ils doivent également être ajoutés comme approuvés dans la console API.

access_type = offline signifie que l'application peut mettre à jour le jeton expiré indépendamment sans interaction de l'utilisateur avec le navigateur,
response_type = code définit le format de retour du code (se référant à l'ancienne méthode d'autorisation lorsque l'utilisateur a copié le code du navigateur vers le script),
scope indique la portée et le type d'accès. Ils doivent être séparés par des espaces ou% 20 (selon l'encodage URL). Une liste des zones d'accès avec des types peut être consultée ici: OAuth 2.0 Scopes for Google APIs .

Après avoir reçu le code d'autorisation, l'application renverra un message de fermeture au navigateur, cessera d'écouter le port et enverra une demande POST pour recevoir le jeton. Nous y indiquons l'identifiant et le secret précédemment définis dans l'API de la console, l'adresse vers laquelle l'utilisateur sera redirigé et grant_type conformément à la spécification du protocole.

En réponse, nous obtiendrons un jeton d'accès, sa durée en secondes et un jeton d'actualisation, avec lesquels nous pourrons mettre à jour le jeton d'accès.

L'application doit stocker les jetons dans un endroit sûr avec une longue durée de conservation, donc jusqu'à ce que nous révoquions l'accès reçu, le jeton d'actualisation ne sera pas retourné à l'application. À la fin, j'ai ajouté une demande de révocation du jeton.Si l'application ne s'est pas terminée avec succès et que le jeton d'actualisation n'est pas revenu, il recommencera la procédure (nous avons considéré qu'il n'était pas sûr de stocker les jetons localement sur le terminal, mais nous ne voulons pas compliquer la cryptographie ou souvent ouvrir le navigateur).

 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 } 

Comme vous l'avez peut-être remarqué, lors de l'appel d'un jeton, Invoke-WebRequest est utilisé. Contrairement à Invoke-RestMethod, il ne renvoie pas les données reçues dans un format pratique à utiliser et affiche l'état de la demande.

Ensuite, le script vous demandera d'entrer le prénom et le nom de l'utilisateur, générant un nom d'utilisateur + e-mail.

Demandes


Voici les demandes - tout d'abord, vous devez vérifier si l'utilisateur existe déjà avec une telle connexion pour obtenir une décision sur la création d'une nouvelle connexion ou l'activation de la connexion actuelle.

J'ai décidé d'implémenter toutes les requêtes au format d'une seule fonction avec une sélection via 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']) } } } } 

Dans chaque demande, vous devez envoyer un en-tête d'autorisation contenant le type de jeton et le jeton d'accès lui-même. Pour le moment, le type de jeton est toujours porteur. Parce que nous devons vérifier que le jeton n'est pas expiré et le mettre à jour une heure après son émission, j'ai indiqué une demande pour une autre fonction qui renvoie un jeton d'accès. Le même morceau de code se trouve au début du script lors de la réception du premier jeton 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 } 

Vérification de la connexion pour l'existence:

 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 } 

Demande par e-mail: $ query demandera à l'API de rechercher un utilisateur avec exactement cet e-mail, y compris les alias. Vous pouvez également utiliser des caractères génériques: =,:,: {PREFIX} * .

Pour obtenir des données, la méthode de demande GET est utilisée, pour insérer des données (créer un compte ou ajouter un membre à un groupe) - POST, pour mettre à jour des données existantes - PUT, pour supprimer une entrée (par exemple, un participant d'un groupe) - SUPPRIMER.

Le script demandera également un numéro de téléphone (une chaîne non valide) et l'inclura dans un groupe de distribution régional. Il décide de l'unité organisationnelle que l'utilisateur doit avoir en fonction de l'unité d'organisation Active Directory sélectionnée et propose un mot de passe:

 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" 

Et puis commence à manipuler le 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 = 'moscowoffice@rocketguys.com' email = $email } (GoogleQuery 'AddMember' $query) | fl } 

Les fonctions de mise à jour et de création d'un compte ont la même syntaxe, tous les champs supplémentaires ne sont pas requis, dans la section avec les numéros de téléphone, vous devez spécifier un tableau qui peut contenir à partir d'un enregistrement avec un numéro et son type.

Afin de ne pas obtenir d'erreur lors de l'ajout d'un utilisateur à un groupe, nous pouvons d'abord vérifier s'il est déjà dans ce groupe en recevant une liste des membres du groupe ou de la composition de l'utilisateur lui-même.

Une demande de composition de groupes d'un utilisateur spécifique ne sera pas récursive et ne montrera que l'adhésion directe. L'inclusion de l'utilisateur dans le groupe parent, dans lequel le groupe subsidiaire dont l'utilisateur est membre, est déjà réussie.

Conclusion


Reste à envoyer à l'utilisateur le mot de passe du nouveau compte. Nous le faisons par SMS et envoyons des informations générales avec des instructions et une connexion à un courrier personnel qui, avec le numéro de téléphone, a été fourni par le service de sélection du personnel. Comme alternative, vous pouvez économiser de l'argent et envoyer un mot de passe à un chat télégramme secret, qui peut également être considéré comme le deuxième facteur (les macbooks seront une exception).

Merci d'avoir lu jusqu'au bout. Je serai heureux de voir des suggestions pour améliorer le style d'écriture des articles et je souhaite que vous rattrapiez moins d'erreurs lors de l'écriture de scripts =)

Liste de liens qui peuvent être utiles par thème ou simplement répondre à vos questions:

Source: https://habr.com/ru/post/fr468969/


All Articles