Oprettelse af Google-brugere fra PowerShell via API

Hi!

Denne artikel vil beskrive implementeringen af ​​PowerShell-interaktion med Google API for at manipulere G Suite-brugere.

Vi bruger flere interne og cloud-tjenester på tværs af organisationen. For det meste kommer autorisation i dem ned til Google eller Active Directory, mellem hvilke vi ikke kan opretholde en replika; derfor skal du, når en ny medarbejder forlader, oprette/aktivere en konto i disse to systemer. For at automatisere processen besluttede vi at skrive et script, der indsamler oplysninger og sender dem til begge tjenester.

Tilladelse

Ved udarbejdelsen af ​​kravene besluttede vi at bruge rigtige menneskelige administratorer til autorisation; dette forenkler analysen af ​​handlinger i tilfælde af utilsigtede eller bevidste massive ændringer.

Google API'er bruger OAuth 2.0-protokollen til godkendelse og godkendelse. Use cases og mere detaljerede beskrivelser kan findes her: Brug af OAuth 2.0 til at få adgang til Google API'er.

Jeg valgte det script, der bruges til godkendelse i desktop-applikationer. Der er også mulighed for at bruge en servicekonto, som ikke kræver unødvendige bevægelser fra brugeren.

Billedet nedenfor er en skematisk beskrivelse af det valgte scenarie fra Google-siden.

Oprettelse af Google-brugere fra PowerShell via API

  1. Først sender vi brugeren til Google-kontogodkendelsessiden med angivelse af GET-parametre:
    • ansøgnings-id
    • områder, som applikationen skal have adgang til
    • den adresse, som brugeren vil blive omdirigeret til efter at have gennemført proceduren
    • måden vi opdaterer tokenet på
    • Sikkerhedskode
    • verifikationskode transmissionsformat

  2. Efter at godkendelsen er fuldført, vil brugeren blive omdirigeret til den side, der er angivet i den første anmodning, med en fejl eller autorisationskode videregivet af GET-parametre
  3. Applikationen (scriptet) skal modtage disse parametre og, hvis den modtages koden, skal du foretage følgende anmodning om at få tokens
  4. Hvis anmodningen er korrekt, returnerer Google API:
    • Adgangstoken, som vi kan fremsætte anmodninger med
    • Gyldighedsperioden for dette token
    • Opdater token påkrævet for at opdatere adgangstoken.

Først skal du gå til Google API-konsollen: Oplysninger - Google API-konsol, vælg den ønskede applikation og opret en klient-OAuth-identifikator i sektionen Credentials. Der (eller senere, i egenskaberne for den oprettede identifikator) skal du angive de adresser, hvortil omdirigering er tilladt. I vores tilfælde vil disse være flere lokale værtsindgange med forskellige porte (se nedenfor).

For at gøre det mere bekvemt at læse scriptalgoritmen kan du vise de første trin i en separat funktion, der returnerer Access og opdaterer tokens for 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 indstiller klient-id'et og klienthemmeligheden opnået i egenskaberne for OAuth-klient-id, og kodebekræftelsen er en streng på 43 til 128 tegn, der skal genereres tilfældigt fra ureserverede tegn: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Denne kode vil derefter blive transmitteret igen. Det eliminerer den sårbarhed, hvor en hacker kan opsnappe et svar, der returneres som en omdirigering efter brugerautorisation.
Du kan sende en kodeverifikator i den aktuelle anmodning i klartekst (hvilket gør det meningsløst - dette er kun egnet til systemer, der ikke understøtter SHA256), eller ved at oprette en hash ved hjælp af SHA256-algoritmen, som skal kodes i BASE64Url (afvigende fra Base64 med to tabeltegn) og fjernelse af tegnlinjeafslutningerne: =.

Dernæst skal vi begynde at lytte til http på den lokale maskine for at modtage et svar efter godkendelse, som vil blive returneret som en omdirigering.

Administrative opgaver udføres på en speciel server, vi kan ikke udelukke muligheden for at flere administratorer kører scriptet på samme tid, så det vil tilfældigt vælge en port for den aktuelle bruger, men jeg har angivet foruddefinerede porte pga. de skal også tilføjes som betroede i API-konsollen.

access_type=offline betyder, at applikationen kan opdatere et udløbet token på egen hånd uden brugerinteraktion med browseren,
response_type=kode indstiller formatet for, hvordan koden returneres (en reference til den gamle godkendelsesmetode, da brugeren kopierede koden fra browseren til scriptet),
rækkevidde angiver omfanget og typen af ​​adgang. De skal adskilles med mellemrum eller %20 (i henhold til URL-kodning). En liste over adgangsområder med typer kan ses her: OAuth 2.0 Scopes til Google API'er.

Efter at have modtaget autorisationskoden, vil applikationen returnere en lukkemeddelelse til browseren, stoppe med at lytte på porten og sende en POST-anmodning for at få tokenet. Vi angiver i den det tidligere specificerede id og hemmelighed fra konsol-API'en, adressen, som brugeren vil blive omdirigeret til og grant_type i overensstemmelse med protokolspecifikationen.

Som svar vil vi modtage et Access-token, dets gyldighedsperiode i sekunder og et Refresh-token, som vi kan opdatere Access-tokenet med.

Applikationen skal opbevare tokens et sikkert sted med lang holdbarhed, så indtil vi tilbagekalder den modtagne adgang, returneres refresh token ikke til applikationen. Til sidst tilføjede jeg en anmodning om at tilbagekalde tokenet; hvis applikationen ikke blev fuldført korrekt, og opdateringstokenet ikke blev returneret, vil det starte proceduren igen (vi anså det for usikkert at gemme tokens lokalt på terminalen, og det gør vi ikke ønsker ikke at komplicere ting med kryptografi eller åbne browseren ofte).

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 allerede har bemærket, bruges Invoke-WebRequest, når du tilbagekalder et token. I modsætning til Invoke-RestMethod returnerer den ikke de modtagne data i et brugbart format og viser status for anmodningen.

Derefter beder scriptet dig om at indtaste brugerens for- og efternavn, hvilket genererer et login + e-mail.

anmodninger

De næste anmodninger bliver - først og fremmest skal du kontrollere, om der allerede findes en bruger med samme login for at få en beslutning om at oprette en ny eller aktivere den nuværende.

Jeg besluttede at implementere alle anmodninger i formatet af én funktion med et udvalg ved hjælp af 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 hver anmodning skal du sende en autorisationsheader indeholdende tokentypen og selve adgangstokenet. I øjeblikket er tokentypen altid bærer. Fordi vi skal kontrollere, at tokenet ikke er udløbet og opdatere det efter en time fra det øjeblik, det blev udstedt, jeg specificerede en anmodning om en anden funktion, der returnerer et Access-token. Det samme stykke kode er i begyndelsen af ​​scriptet, når du modtager det første adgangstoken:

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
}

Kontrol af login for eksistens:

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-anmodningen vil bede API'et om at lede efter en bruger med præcis den e-mail, inklusive aliaser. Du kan også bruge jokertegn: =, :, :{PREFIX}*.

For at få data, brug GET-anmodningsmetoden, til at indsætte data (oprettelse af en konto eller tilføjelse af et medlem til en gruppe) - POST, for at opdatere eksisterende data - PUT, for at slette en post (f.eks. et medlem fra en gruppe) - SLET.

Scriptet vil også bede om et telefonnummer (en uvalideret streng) og om optagelse i en regional distributionsgruppe. Den bestemmer hvilken organisationsenhed brugeren skal have baseret på den valgte Active Directory OU og kommer med en adgangskode:

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"

Og så begynder han at manipulere kontoen:

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

Funktionerne til opdatering og oprettelse af en konto har en lignende syntaks; ikke alle ekstra felter er påkrævet; i afsnittet med telefonnumre skal du angive et array, der kan indeholde op til én post med nummeret og dets type.

For ikke at modtage en fejl, når du tilføjer en bruger til en gruppe, kan vi først kontrollere, om han allerede er medlem af denne gruppe, ved at få en liste over gruppemedlemmer eller sammensætning fra brugeren selv.

Forespørgsel om gruppemedlemskab for en specifik bruger vil ikke være rekursivt og vil kun vise direkte medlemskab. Det lykkes at inkludere en bruger i en overordnet gruppe, der allerede har en undergruppe, som brugeren er medlem af.

Konklusion

Det eneste, der er tilbage, er at sende brugeren adgangskoden til den nye konto. Det gør vi via SMS, og sender generel information med instruktioner og login til en personlig mail, som sammen med et telefonnummer er oplyst af rekrutteringsafdelingen. Som et alternativ kan du spare penge og sende din adgangskode til en hemmelig telegramchat, som også kan betragtes som den anden faktor (MacBooks vil være en undtagelse).

Tak fordi du læste med til slutningen. Jeg vil være glad for at se forslag til forbedring af stilen til at skrive artikler og ønsker, at du fanger færre fejl, når du skriver scripts =)

Liste over links, der kan være tematisk nyttige eller blot besvare spørgsmål:

Kilde: www.habr.com

Tilføj en kommentar