Krijimi i përdoruesve Google nga PowerShell përmes API

đŸ„‡Si tĂ« arrish nĂ« qiell dhe tĂ« bĂ«hesh pilot | ProHoster

Në këtë artikull do të përshkruhet implementimi i ndërveprimit të PowerShell me Google API për të kryer manipulime me përdoruesit e G Suite.

Në organizatën tonë ne përdorim disa shërbime të brendshme dhe cloud. Kryesisht, autorizimi në to përfshin Google ose Active Directory, midis të cilëve nuk mund të mbajmë një replikë, prandaj, kur një punonjës i ri hyn, është e nevojshme të krijohet/aktivizohet një llogari në këto dy sisteme. Për të automatizuar këtë proces, ne vendosëm të shkruajmë një skenar që mbledh informacionin dhe e dërgon në të dy shërbimet.

Autorizimi

Duke përpiluar kërkesat, ne vendosëm të përdorim për autorizim njerëz realë administratori, kjo e thjeshton analizimin e veprimeve gjatë ndryshimeve masive të rastësishme ose të qëllimshme.

Për autentifikim dhe autorizim, Google API përdor protokollin OAuth 2.0. Scenarët e përdorimit dhe përshkrimi më i detajuar mund të shihen këtu: Përdorimi i OAuth 2.0 për të aksesuar Google API.

Unë zgjodha skenarin që përdoret gjatë autorizimit në aplikacione desktop. Ka gjithashtu një mundësi për të përdorur një llogari shërbimi, e cila nuk kërkon lëvizje të tepërta nga përdoruesi.

Imazhi më poshtë është një përshkrim skematik i skenarit të zgjedhur nga faqja e Google.

Krijimi i përdoruesve Google nga PowerShell përmes API

  1. Fillimisht ne e dërgojmë përdoruesin në faqen e autentifikimit në llogarinë Google, duke treguar parametrat GET:
    • identifikuesin e aplikacionit
    • fushat pĂ«r tĂ« cilat aplikacioni ka nevojĂ« pĂ«r akses
    • adresĂ«n nĂ« tĂ« cilĂ«n pĂ«rdoruesi do tĂ« ridrejtohet pas pĂ«rfundimit tĂ« procedurĂ«s
    • metodĂ«n me tĂ« cilĂ«n do tĂ« azhurnojmĂ« tokenin
    • kodin e verifikimit
    • formatin e transmetimit tĂ« kodit tĂ« verifikimit

  2. Pas përfundimit të autorizimit, përdoruesi do të ridrejtohet në faqen e përcaktuar në kërkesën e parë, me një gabim ose kodin e autorizimit, të dërguara si parametra GET
  3. Aplikacioni (skenari) do të duhet të marrë këta parametra dhe, në rast se merr kodin, të kryejë kërkesën e mëtejshme për të marrë tokenet
  4. Me një kërkesë të saktë, Google API kthen:
    • Tokenin e aksesit, me tĂ« cilin mund tĂ« bĂ«jmĂ« kĂ«rkesa
    • KohĂ«n e skadencĂ«s sĂ« kĂ«tij tokeni
    • Tokenin e rifreskimit, i nevojshĂ«m pĂ«r rinovimin e Tokenit tĂ« aksesit.

Fillimisht duhet tĂ« shkojmĂ« nĂ« konsolĂ«n Google API: AftĂ«sitĂ« — Google API Console, zgjidhni aplikacionin e nevojshĂ«m dhe nĂ« seksionin Credentials krijoni njĂ« identifikues OAuth klienti. Atje (ose mĂ« vonĂ«, nĂ« pronat e identifikuesit tĂ« krijuar) duhet tĂ« specifikoni adresat, nĂ« tĂ« cilat lejohet ridrejtimi. NĂ« rastin tonĂ«, kjo do tĂ« jetĂ« disa regjistra localhost me porte tĂ« ndryshme (shih mĂ« poshtĂ«).

Për të lehtësuar leximin e algoritmit të skriptit, mund të nxirrni hapat e parë në një funksion të veçantë, i cili do të kthejë Access dhe refresh token për aplikacionin:

$client_secret = 'Sekreti ynë i klientit'
$client_id = 'ID e klientit tonë'
function Get-GoogleAuthToken {
  if (-not [System.Net.HttpListener]::IsSupported) {
    "HttpListener nuk mbështetet."
    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 "Niseni shfletuesin..."
  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 {
    "Nuk arriti të nisë dëgjuesin."
    exit 1
  }
  while (($code -eq $null)) {
    $context = $listener.GetContext()
    Write-Host "Konekcioni u pranua" -f 'mag'
    $url = $context.Request.RawUrl
    $code = $url.split('?')[1].split('=')[1].split('&')[0]
    if ($url.split('?')[1].split('=')[0] -eq 'error') {
      Write-Host "Gabim!"$code -f 'red'
      $buffer = [System.Text.Encoding]::UTF8.GetBytes("Gabim!"+$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("Tani mund të mbyllni këtë skedë të shfletuesit.")
    $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

Ne caktojmĂ« Client ID dhe Client Secret, tĂ« marra nĂ« pronat e identifikuesit klient OAuth, dhe code verifier – kjo Ă«shtĂ« njĂ« varg me gjatĂ«si nga 43 deri nĂ« 128 karaktere, i cili duhet tĂ« gjenerohet rastĂ«sisht nga karaktere tĂ« pavaruara: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".

Më pas ky kod do të përcillet përsëri. Ai eliminohet vulnerabilitetin, ku një sulmues mund të kapë përgjigjen, e cila kthehet me ridrejtim pas autorizimit të përdoruesit.
Dërgimi i code verifier në kërkesën aktuale mund të bëhet në formë të hapur (çka e bën atë të panevojshëm - kjo i përshtatet vetëm sistemeve që nuk mbështesin SHA256), ose duke krijuar një hash sipas algoritmit SHA256, i cili duhet të kodifikohet në BASE64Url (dallon nga Base64 me dy simbole të ndryshme) dhe të hiqet simboli i përfundimit të rreshtit: =.

Më pas, na nevojitet të fillojmë të dëgjojmë http në makinën lokale, për të marrë përgjigjen pas autorizimit, e cila do të kthehet përmes një ridirektimi.

Detyrat administrative realizohen nĂ« njĂ« сДрĐČДр tĂ« veçantĂ«, nuk mund tĂ« pĂ«rjashtojmĂ« mundĂ«sinĂ« qĂ« disa administratorĂ«t tĂ« fillojnĂ« skenarin nĂ« tĂ« njĂ«jtĂ«n kohĂ«, ndaj ai do tĂ« zgjedhĂ« rastĂ«sisht njĂ« port pĂ«r pĂ«rdoruesin aktual, por unĂ« kam caktuar mĂ« parĂ« porte tĂ« pĂ«rcaktuara, pasi ato duhet gjithashtu tĂ« shtohen si tĂ« besuara nĂ« konsolĂ«n e API-t.

access_type=offline do të thotë që aplikacioni mund të freskojë tokenin e skaduar vetë pa ndërveprimin e përdoruesit me shfletuesin,
response_type=code përcakton formatin se si do të kthehet kodi (referencë ndaj mënyrës së vjetër të autorizimit, kur përdoruesi kopjon dhe ngjit kode nga shfletuesi në skenar),
scope tregon fushat dhe llojin e aksesit. Ato duhet të ndahen me hapësira ose (siparo URL Encoding). Lista e fushave të aksesit me llojet mund të shihet këtu: OAuth 2.0 Scopes për Google APIs.

Pas marrjes së kodit të autorizimit, aplikacioni do të kthejë në shfletues një mesazh për mbylljen, do të ndalojë dëgjimin e portit dhe do të dërgojë një kërkesë POST për të marrë tokenin. Ne e caktuar në të identifikimin dhe sekretin e vendosur më parë nga konsola e API-t, adresën në të cilën do të ridrejtohet përdoruesi dhe grant_type sipas specifikimit të protokollit.

Në përgjigje, ne do të marrim një Access token, kohën e tij të vlefshmërisë në sekonda dhe një Refresh token, me të cilin mund të përditësojmë Access token.

Aplikacioni duhet të ruajë tokenet në një vend të sigurt me një afat shëndetësor të gjatë, prandaj, derisa ne të tërheqim aksesin e marrë, aplikacioni nuk do të marrë një refresh token. Në fund, kam shtuar një kërkesë për të tërhequr tokenin, nëse aplikacioni përfundoi pa sukses dhe refresh token nuk u kthye, ai do të fillojë procesin nga e para (ne e konsideruam të pasigurt të ruajmë tokenet lokal në terminal, dhe nuk dëshirojmë ta komplikesojmë me kriptografi ose të hapim shpesh shfletuesin).

bëni {
  $token_result = Merr-GoogleAuthToken
  $token = $token_result.access_token
  nëse ($token_result.refresh_token -eq $null) {
    Write-Host ("Seanca nuk është shuar. Po revokoj tokenin...")
    Invoke-WebRequest -Uri ("https://accounts.google.com/o/oauth2/revoke?token="+$token)
  }
} ndërsa ($token_result.refresh_token -eq $null)
$refresh_token = $token_result.refresh_token
$minute = ([int]("{0:mm}" -f ([timespan]::fromseconds($token_result.expires_in))))+((Merr-date).Minute)-2
nëse ($minute -lt 0) {$minute += 60}
ndryshe nëse ($minute -gt 59) {$minute -=60}
$token_expire = @{
  orë = ([int]("{0:hh}" -f ([timespan]::fromseconds($token_result.expires_in))))+((Merr-date).Hour)
  minut = $minute
}

Siç e keni vënë re, kur revokohet tokeni, përdoret Invoke-WebRequest. Ndryshe nga Invoke-RestMethod, ai nuk kthen të dhënat e marra në një format të përshtatshëm për përdorim dhe tregon statusin e kërkesës.

Më pas skripti do të kërkojë të hyni emrin dhe mbiemrin e përdoruesit, duke gjeneruar login + email.

Kërkesat

KĂ«rkesat e ardhshme do tĂ« jenĂ« – sĂ« pari duhet tĂ« kontrollohet nĂ«se ekziston tashmĂ« njĂ« pĂ«rdorues me kĂ«tĂ« login pĂ«r tĂ« marrĂ« vendimin mbi formimin e njĂ« tĂ« ri ose pĂ«rfshirjen e aktualit.

Vendosa të realizoj të gjitha kërkesat në formatin e një funksioni me përzgjedhje, duke përdorur 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'])
      }
    }
  }
}

Çdo kĂ«rkesĂ« duhet tĂ« dĂ«rgojĂ« njĂ« titull Authorization, i cili pĂ«rmban llojin e tokenit dhe vetĂ« tokenin e Qasjes. Aktualisht, lloji i tokenit Ă«shtĂ« gjithmonĂ« Bearer. Duke qenĂ« se na nevojitet tĂ« kontrollojmĂ« qĂ« tokeni tĂ« mos jetĂ« i skaduar dhe ta azhurnojmĂ« pas njĂ« ore nga lĂ«shimi, kam parashikuar njĂ« kĂ«rkesĂ« pĂ«r njĂ« funksion tjetĂ«r, i cili kthen tokenin e Qasjes. Ky kod i njĂ«jtĂ« ndodhet nĂ« fillim tĂ« skriptit kur marrim tokenin e parĂ« tĂ« Qasjes:

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 i skaduar. Po e rifreskoj..."
    $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
}

Kontrollimi i ekzistencës së loginit:

function Check_Google {
  $query = (GoogleQuery 'SearchAccount' $username)
  if ($query.users -ne $null) {
    $user = $query.users[0]
    Write-Host $user.name.fullName' - '$user.PrimaryEmail' - pezulluar: '$user.Suspended
    $GAresult = $user
  }
  if ($GAresult) {
      $return = $GAresult
  } else {$return = 'gg'}
  return $return
}

Kërkesa e emailit: $query do të kërkojë API-në për të gjetur një përdorues me atë email, duke përfshirë aliasët. Po ashtu mund të përdoren wildcard: =, :, :{PREFIX}*.

PĂ«r tĂ« marrĂ« tĂ« dhĂ«nat pĂ«rdoret metoda GET, pĂ«r tĂ« futur tĂ« dhĂ«na (krijimi i llogarisĂ« ose shtimi i njĂ« anĂ«tari nĂ« grup) – POST, pĂ«r tĂ« pĂ«rditĂ«suar tĂ« dhĂ«na ekzistuese – PUT, pĂ«r tĂ« fshirĂ« njĂ« rekord (p.sh., njĂ« anĂ«tar nga grupi) – DELETE.

Skedari gjithashtu do të kërkojë numrin e telefonit (string pa validim) dhe nëse të përfshihet në grupin rajonal të shpërndarjes. Ai përcakton se cila njësinë organizative duhet të ketë përdoruesi në bazë të OU të zgjedhur në Active Directory dhe shpik një fjalëkalim:

do {
  $phone = Read-Host "Telefoni nĂ« formatin +7хххххххх"
} while (-not $phone)
do {
    $moscow = Read-Host "Në zyrën e Moskës? (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 "Do të krijohet në /Team delivery"
    $orgunit = "/Team delivery"
}
$Password = -join (48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba"

Dhe më pas fillon manipulimet me llogarinë:

$query = @{
  email = $email
  givenName = $firstname
  familyName = $lastname
  password = $password
  phone = $phone
  orgunit = $orgunit
}
if ($GMailExist) {
  Write-Host "Duke nisur ndryshimin e llogarisë" -f mag
  (GoogleQuery 'UpdateAccount' $query) | fl
  write-host "Mos harro të kontrollosh grupet për $Username të aktivizuar në Google."
} else {
  Write-Host "Duke nisur krijimin e llogarisë" -f mag
  (GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
  write-host "Duke shtuar në grupin moscowoffice"
  $query = @{
    groupkey = 'moscowoffice@rocketguys.com'
    email = $email
  }
  (GoogleQuery 'AddMember' $query) | fl
}

Funksionet e përditësimit dhe krijimit të llogarive kanë një sintaksë të ngjashme, jo të gjitha fushat shtesë janë të detyrueshme, në seksionin me numrat e telefonit duhet të specifikohet një array që mund të përmbajë nga një regjistër me numrin dhe tipin e tij.

Për të shmangur një gabim gjatë shtimit të përdoruesit në grup, paraprakisht mund të kontrollojmë nëse ai tashmë është në këtë grup, duke marrë listën e anëtarëve të grupit ose përbërjen e vetë përdoruesit.

Kërkesa për përbërjen e grupeve të një përdoruesi të caktuar nuk do të jetë rekurzive dhe do të tregojë vetëm anëtarësimin e drejtpërdrejtë. Inkludimi i përdoruesit në grupin prind, në të cilin tashmë është një grup i fëmijës, të cilit përdoruesi i përket, do të jetë i suksesshëm.

Përfundim

Kështu mbetet të dërgojmë përdoruesit fjalëkalimin e llogarisë së re. Ne e bëjmë këtë përmes SMS, ndërsa informacioni i përgjithshëm me udhëzime dhe login e dërgojmë në postën personale, të cilën, së bashku me numrin e telefonit, e ofroi departamenti i rekrutimit. Si një alternativë, mund të kursejmë disa para dhe ta dërgojmë fjalëkalimin në një bisedë sekrete në Telegram, e cila mund të konsiderohet gjithashtu si faktori i dytë (përjashtime do të jenë makbukët).

Faleminderit që e lexuat deri në fund. Do të isha i lumtur të shihja sugjerime për përmirësimin e stilit të shkrimit të artikujve dhe ju uroj që të okoni sa më pak gabime gjatë shkruajtjes së skenarëve =)

Lista e lidhjeve që mund të jenë tematikisht të dobishme ose thjesht të përgjigjen pyetjeve që mund të kenë lindur:

Burimi: habr.com

Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster