Skapa Google-användare från PowerShell via API

Hälsningar!

Den här artikeln kommer att beskriva implementeringen av PowerShell-interaktion med Googles API för att manipulera G Suite-användare.

Vi använder flera interna och molntjänster över hela organisationen. För det mesta kommer auktoriseringen i dem ner på Google eller Active Directory, mellan vilka vi inte kan upprätthålla en replik; följaktligen, när en ny anställd slutar, måste du skapa/aktivera ett konto i dessa två system. För att automatisera processen bestämde vi oss för att skriva ett skript som samlar in information och skickar den till båda tjänsterna.

Bemyndigande

När vi utarbetade kraven bestämde vi oss för att använda riktiga mänskliga administratörer för auktorisering, detta förenklar analysen av åtgärder vid oavsiktliga eller avsiktliga stora förändringar.

Googles API:er använder OAuth 2.0-protokollet för autentisering och auktorisering. Användningsfall och mer detaljerade beskrivningar finns här: Använder OAuth 2.0 för att komma åt Googles API:er.

Jag valde skriptet som används för auktorisering i skrivbordsapplikationer. Det finns också en möjlighet att använda ett tjänstekonto, vilket inte kräver onödiga rörelser från användaren.

Bilden nedan är en schematisk beskrivning av det valda scenariot från Google-sidan.

Skapa Google-användare från PowerShell via API

  1. Först skickar vi användaren till sidan för Google-kontoautentisering och anger GET-parametrar:
    • ansöknings-ID
    • områden som applikationen behöver åtkomst till
    • adressen som användaren kommer att omdirigeras till efter att ha slutfört proceduren
    • hur vi kommer att uppdatera token
    • Säkerhetskod
    • överföringsformat för verifieringskod

  2. Efter att auktoriseringen är slutförd kommer användaren att omdirigeras till sidan som anges i den första begäran, med ett fel eller auktoriseringskod som skickas av GET-parametrar
  3. Applikationen (skriptet) kommer att behöva ta emot dessa parametrar och, om den får koden, göra följande begäran för att få tokens
  4. Om begäran är korrekt returnerar Googles API:
    • Åtkomsttoken som vi kan göra förfrågningar med
    • Giltighetsperioden för denna token
    • Uppdatera token krävs för att uppdatera åtkomsttoken.

Först måste du gå till Google API-konsolen: Användaruppgifter – Google API-konsol, välj önskat program och skapa en klient-OAuth-identifierare i avsnittet Inloggningsuppgifter. Där (eller senare, i egenskaperna för den skapade identifieraren) måste du ange adresserna till vilka omdirigering är tillåten. I vårt fall kommer dessa att vara flera lokala värdposter med olika portar (se nedan).

För att göra det bekvämare att läsa skriptalgoritmen kan du visa de första stegen i en separat funktion som kommer att returnera Access och uppdatera tokens för applikationen:

$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

Vi ställer in klient-ID och klienthemlighet som erhållits i OAuth-klientidentifierarens egenskaper, och kodverifieraren är en sträng på 43 till 128 tecken som måste genereras slumpmässigt från oreserverade tecken: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Denna kod kommer sedan att sändas igen. Det eliminerar sårbarheten där en angripare kan fånga upp ett svar som returneras som en omdirigering efter användarauktorisering.
Du kan skicka en kodverifierare i den aktuella förfrågan i klartext (vilket gör det meningslöst - detta är endast lämpligt för system som inte stöder SHA256), eller genom att skapa en hash med SHA256-algoritmen, som måste kodas i BASE64Url (avvikande från Base64 med två tabelltecken) och ta bort teckenradsändelserna: =.

Därefter måste vi börja lyssna på http på den lokala maskinen för att få ett svar efter auktorisering, som kommer att returneras som en omdirigering.

Administrativa uppgifter utförs på en speciell server, vi kan inte utesluta möjligheten att flera administratörer kommer att köra skriptet samtidigt, så det kommer slumpmässigt välja en port för den aktuella användaren, men jag angav fördefinierade portar eftersom de måste också läggas till som betrodda i API-konsolen.

access_type=offline betyder att applikationen kan uppdatera en utgången token på egen hand utan användarinteraktion med webbläsaren,
response_type=kod ställer in formatet för hur koden kommer att returneras (en referens till den gamla auktoriseringsmetoden, när användaren kopierade koden från webbläsaren till skriptet),
omfattning anger omfattning och typ av åtkomst. De måste separeras med mellanslag eller %20 (enligt URL-kodning). En lista över åtkomstområden med typer kan ses här: OAuth 2.0 Scopes för Google API:er.

Efter att ha mottagit auktoriseringskoden kommer applikationen att returnera ett stängningsmeddelande till webbläsaren, sluta lyssna på porten och skicka en POST-förfrågan för att erhålla token. Vi anger i den det tidigare angivna ID:t och hemligheten från konsolens API, adressen till vilken användaren kommer att omdirigeras och grant_type i enlighet med protokollspecifikationen.

Som svar kommer vi att få en Access-token, dess giltighetstid i sekunder och en Refresh-token, med vilken vi kan uppdatera Access-token.

Applikationen måste förvara tokens på en säker plats med lång hållbarhetstid, så tills vi återkallar den mottagna åtkomsten kommer applikationen inte att returnera uppdateringstoken. I slutet lade jag till en begäran om att återkalla token; om applikationen inte slutfördes framgångsrikt och uppdateringstoken inte returnerades kommer den att starta proceduren igen (vi ansåg att det var osäkert att lagra tokens lokalt på terminalen, och vi gör det vill inte komplicera saker med kryptografi eller öppna webbläsaren ofta).

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
}

Som du redan märkt, när du återkallar en token, används Invoke-WebRequest. Till skillnad från Invoke-RestMethod returnerar den inte mottagna data i ett användbart format och visar status för begäran.

Därefter ber skriptet dig att ange användarens för- och efternamn, vilket genererar en inloggning + e-post.

förfrågningar

Nästa förfrågningar blir - först och främst måste du kontrollera om en användare med samma inloggning redan finns för att få ett beslut om att skapa en ny eller aktivera den nuvarande.

Jag bestämde mig för att implementera alla förfrågningar i formatet av en funktion med ett urval, med hjälp av 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'])
      }
    }
  }
}

I varje begäran måste du skicka en auktoriseringsrubrik som innehåller tokentypen och själva åtkomsttokenen. För närvarande är tokentypen alltid Bärare. Därför att vi måste kontrollera att token inte har gått ut och uppdatera den efter en timme från det att den utfärdades, jag angav en begäran om en annan funktion som returnerar en Access-token. Samma kodbit finns i början av skriptet när du tar emot den första åtkomsttoken:

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
}

Kontrollera att inloggningen finns:

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
}

Email:$query-begäran kommer att be API:et leta efter en användare med exakt den e-postadressen, inklusive alias. Du kan också använda jokertecken: =, :, :{PREFIX}*.

För att erhålla data, använd metoden GET-begäran, för att infoga data (skapa ett konto eller lägga till en medlem i en grupp) - POST, för att uppdatera befintliga data - PUT, för att radera en post (till exempel en medlem från en grupp) - RADERA.

Skriptet kommer också att fråga efter ett telefonnummer (en ovaliderad sträng) och för inkludering i en regional distributionsgrupp. Den bestämmer vilken organisationsenhet användaren ska ha baserat på den valda Active Directory OU-enheten och kommer med ett lösenord:

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"

Och sedan börjar han manipulera kontot:

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

Funktionerna för att uppdatera och skapa ett konto har en liknande syntax; inte alla ytterligare fält krävs; i avsnittet med telefonnummer måste du ange en array som kan innehålla upp till en post med numret och dess typ.

För att inte få ett felmeddelande när du lägger till en användare i en grupp kan vi först kontrollera om han redan är medlem i denna grupp genom att få en lista över gruppmedlemmar eller sammansättning från användaren själv.

Att fråga efter gruppmedlemskap för en specifik användare kommer inte att vara rekursivt och visar bara direkt medlemskap. Att inkludera en användare i en överordnad grupp som redan har en underordnad grupp som användaren är medlem i kommer att lyckas.

Slutsats

Allt som återstår är att skicka användaren lösenordet för det nya kontot. Vi gör detta via SMS, och skickar allmän information med instruktioner och inloggning till ett personligt mejl, som tillsammans med ett telefonnummer lämnats av rekryteringsavdelningen. Som ett alternativ kan du spara pengar och skicka ditt lösenord till en hemlig telegramchatt, vilket också kan betraktas som den andra faktorn (MacBooks kommer att vara ett undantag).

Tack för att du läste till slutet. Jag kommer gärna se förslag för att förbättra stilen att skriva artiklar och önskar att du ska få färre fel när du skriver manus =)

Lista över länkar som kan vara tematiskt användbara eller helt enkelt svara på frågor:

Källa: will.com

Lägg en kommentar