Google-gebruikers maken vanuit PowerShell via API

Hey there!

Dit artikel beschrijft de implementatie van PowerShell-interactie met de Google API om G Suite-gebruikers te manipuleren.

We maken gebruik van verschillende interne en clouddiensten binnen de organisatie. De autorisatie daarin komt voor het grootste deel neer op Google of Active Directory, waartussen we geen replica kunnen onderhouden; daarom moet u, wanneer een nieuwe medewerker vertrekt, een account in deze twee systemen aanmaken/activeren. Om het proces te automatiseren, hebben we besloten een script te schrijven dat informatie verzamelt en naar beide diensten verzendt.

Machtiging

Bij het opstellen van de eisen hebben we besloten om echte menselijke beheerders in te zetten voor de autorisatie; dit vereenvoudigt de analyse van acties in het geval van toevallige of opzettelijke grote veranderingen.

Google API's gebruiken het OAuth 2.0-protocol voor authenticatie en autorisatie. Gebruiksscenario's en meer gedetailleerde beschrijvingen zijn hier te vinden: OAuth 2.0 gebruiken om toegang te krijgen tot Google API's.

Ik heb gekozen voor het script dat wordt gebruikt voor autorisatie in desktopapplicaties. Ook is er de mogelijkheid om gebruik te maken van een serviceaccount, waarbij geen onnodige bewegingen van de gebruiker nodig zijn.

De onderstaande afbeelding is een schematische beschrijving van het geselecteerde scenario van de Google-pagina.

Google-gebruikers maken vanuit PowerShell via API

  1. Eerst sturen we de gebruiker naar de authenticatiepagina van het Google-account, met vermelding van GET-parameters:
    • Applicatie ID
    • gebieden waartoe de applicatie toegang nodig heeft
    • het adres waarnaar de gebruiker wordt doorgestuurd na voltooiing van de procedure
    • de manier waarop we het token zullen bijwerken
    • Beveiligingscode
    • transmissieformaat voor verificatiecode

  2. Nadat de autorisatie is voltooid, wordt de gebruiker doorgestuurd naar de pagina die is opgegeven in het eerste verzoek, met een fout- of autorisatiecode die wordt doorgegeven door GET-parameters
  3. De applicatie (script) zal deze parameters moeten ontvangen en, indien de code ontvangen is, het volgende verzoek doen om tokens te verkrijgen
  4. Als het verzoek correct is, retourneert de Google API:
    • Toegangstoken waarmee wij verzoeken kunnen doen
    • De geldigheidsduur van dit token
    • Vernieuwingstoken vereist om het toegangstoken te vernieuwen.

Eerst moet je naar de Google API-console gaan: Inloggegevens - Google API-console, selecteer de gewenste applicatie en maak in de sectie Credentials een OAuth-client-ID aan. Daar (of later, in de eigenschappen van de gemaakte ID) moet u de adressen opgeven waarnaar omleiding is toegestaan. In ons geval zijn dit verschillende localhost-vermeldingen met verschillende poorten (zie hieronder).

Om het gemakkelijker te maken om het scriptalgoritme te lezen, kunt u de eerste stappen weergeven in een aparte functie die toegangstokens en vernieuwingstokens voor de toepassing retourneert:

$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

We stellen de client-ID en het clientgeheim in die zijn verkregen in de OAuth-client-ID-eigenschappen, en de codeverificatie is een reeks van 43 tot 128 tekens die willekeurig moet worden gegenereerd uit niet-gereserveerde tekens: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Deze code wordt dan opnieuw verzonden. Het elimineert de kwetsbaarheid waarbij een aanvaller een reactie kan onderscheppen die als omleiding wordt geretourneerd na autorisatie van de gebruiker.
U kunt een codeverificatie in het huidige verzoek in duidelijke tekst sturen (wat het betekenisloos maakt - dit is alleen geschikt voor systemen die SHA256 niet ondersteunen), of door een hash te maken met behulp van het SHA256-algoritme, dat moet worden gecodeerd in BASE64Url (verschillende uit Base64 door twee tabeltekens) en het verwijderen van de tekenregeluitgangen: =.

Vervolgens moeten we gaan luisteren naar http op de lokale machine om na autorisatie een antwoord te ontvangen, dat als een omleiding zal worden geretourneerd.

Administratieve taken worden uitgevoerd op een speciale server. We kunnen de mogelijkheid niet uitsluiten dat meerdere beheerders het script tegelijkertijd uitvoeren, dus het zal willekeurig een poort selecteren voor de huidige gebruiker, maar ik heb vooraf gedefinieerde poorten opgegeven omdat ze moeten ook als vertrouwd worden toegevoegd in de API-console.

toegangs_type=offline betekent dat de applicatie een verlopen token zelfstandig kan bijwerken zonder gebruikersinteractie met de browser,
respons_type=code stelt het formaat in van hoe de code wordt geretourneerd (een verwijzing naar de oude autorisatiemethode, toen de gebruiker de code van de browser naar het script kopieerde),
omvang geeft de omvang en het type toegang aan. Ze moeten worden gescheiden door spaties of %20 (volgens URL-codering). Een lijst met toegangsgebieden met typen vindt u hier: OAuth 2.0 Scopes voor Google API's.

Na ontvangst van de autorisatiecode stuurt de applicatie een sluitbericht naar de browser, stopt met luisteren op de poort en verzendt een POST-verzoek om het token te verkrijgen. We geven daarin de eerder opgegeven ID en het geheim van de console-API aan, het adres waarnaar de gebruiker wordt doorgestuurd en Grant_type in overeenstemming met de protocolspecificatie.

Als reactie hierop ontvangen we een Access token, de geldigheidsduur ervan in seconden, en een Refresh token, waarmee we het Access token kunnen bijwerken.

De applicatie moet tokens opslaan op een veilige plaats met een lange houdbaarheid, dus totdat we de ontvangen toegang intrekken, zal de applicatie het vernieuwingstoken niet retourneren. Aan het einde heb ik een verzoek toegevoegd om het token in te trekken; als de applicatie niet succesvol is voltooid en het vernieuwingstoken niet is geretourneerd, wordt de procedure opnieuw gestart (we vonden het onveilig om tokens lokaal op de terminal op te slaan, en dat doen we niet Ik wil de zaken niet ingewikkeld maken met cryptografie of de browser regelmatig openen).

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
}

Zoals je al hebt gemerkt, wordt bij het intrekken van een token Invoke-WebRequest gebruikt. In tegenstelling tot Invoke-RestMethod retourneert het de ontvangen gegevens niet in een bruikbaar formaat en wordt de status van het verzoek weergegeven.

Vervolgens vraagt ​​het script u om de voor- en achternaam van de gebruiker in te voeren, waardoor een login + e-mailadres wordt gegenereerd.

verzoeken

De volgende verzoeken zijn: allereerst moet u controleren of er al een gebruiker met dezelfde login bestaat om een ​​beslissing te krijgen over het maken van een nieuwe of het inschakelen van de huidige.

Ik besloot om alle verzoeken in het formaat van één functie met een selectie te implementeren, met behulp van 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'])
      }
    }
  }
}

Bij elk verzoek moet u een autorisatieheader verzenden met daarin het tokentype en het toegangstoken zelf. Momenteel is het tokentype altijd Bearer. Omdat we moeten controleren of het token niet is verlopen en het bijwerken na een uur vanaf het moment dat het is uitgegeven. Ik heb een verzoek opgegeven voor een andere functie die een toegangstoken retourneert. Hetzelfde stukje code staat aan het begin van het script bij ontvangst van het eerste toegangstoken:

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
}

Controleren of de login bestaat:

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
}

Het email:$query-verzoek vraagt ​​de API om te zoeken naar een gebruiker met precies dat e-mailadres, inclusief aliassen. U kunt ook een jokerteken gebruiken: =, :, :{PREFIX}*.

Om gegevens te verkrijgen, gebruikt u de GET-verzoekmethode om gegevens in te voegen (een account aanmaken of een lid aan een groep toevoegen) - POST, om bestaande gegevens bij te werken - PUT, om een ​​record te verwijderen (bijvoorbeeld een lid uit een groep) - VERWIJDEREN.

Het script vraagt ​​ook om een ​​telefoonnummer (een niet-gevalideerde string) en om opname in een regionale distributiegroep. Het beslist welke organisatie-eenheid de gebruiker moet hebben op basis van de geselecteerde Active Directory-OE en komt met een wachtwoord:

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"

En dan begint hij het account te manipuleren:

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

De functies voor het bijwerken en aanmaken van een account hebben een vergelijkbare syntaxis; niet alle extra velden zijn vereist; in het gedeelte met telefoonnummers moet u een array opgeven die maximaal één record kan bevatten met het nummer en het type ervan.

Om geen foutmelding te krijgen bij het toevoegen van een gebruiker aan een groep, kunnen we eerst controleren of hij al lid is van deze groep door van de gebruiker zelf een lijst met groepsleden of samenstelling op te vragen.

Het opvragen van het groepslidmaatschap van een specifieke gebruiker zal niet recursief zijn en zal alleen direct lidmaatschap tonen. Het opnemen van een gebruiker in een bovenliggende groep die al een onderliggende groep heeft waarvan de gebruiker lid is, zal lukken.

Conclusie

Het enige dat overblijft is om de gebruiker het wachtwoord voor het nieuwe account te sturen. We doen dit via sms en sturen algemene informatie met instructies en inloggen op een persoonlijke e-mail, die samen met een telefoonnummer is verstrekt door de recruitmentafdeling. Als alternatief kunt u geld besparen en uw wachtwoord naar een geheime telegramchat sturen, wat ook als de tweede factor kan worden beschouwd (MacBooks vormen een uitzondering).

Bedankt voor het lezen tot het einde. Ik zal blij zijn om suggesties te zien voor het verbeteren van de stijl van het schrijven van artikelen en ik wens dat je minder fouten ondervindt bij het schrijven van scripts =)

Lijst met links die thematisch nuttig kunnen zijn of eenvoudigweg vragen kunnen beantwoorden:

Bron: www.habr.com

Voeg een reactie