Mencipta Pengguna Google daripada PowerShell melalui API

Hello!

Artikel ini akan menerangkan pelaksanaan interaksi PowerShell dengan API Google untuk memanipulasi pengguna G Suite.

Kami menggunakan beberapa perkhidmatan dalaman dan awan di seluruh organisasi. Untuk sebahagian besar, kebenaran di dalamnya datang kepada Google atau Active Directory, yang antaranya kami tidak dapat mengekalkan replika; oleh itu, apabila pekerja baharu keluar, anda perlu membuat/mendayakan akaun dalam kedua-dua sistem ini. Untuk mengautomasikan proses, kami memutuskan untuk menulis skrip yang mengumpul maklumat dan menghantarnya ke kedua-dua perkhidmatan.

Kebenaran

Semasa menyediakan keperluan, kami memutuskan untuk menggunakan pentadbir manusia sebenar untuk kebenaran; ini memudahkan analisis tindakan sekiranya berlaku perubahan besar-besaran yang tidak disengajakan atau disengajakan.

API Google menggunakan protokol OAuth 2.0 untuk pengesahan dan kebenaran. Kes penggunaan dan penerangan yang lebih terperinci boleh didapati di sini: Menggunakan OAuth 2.0 untuk Mengakses Google API.

Saya memilih skrip yang digunakan untuk kebenaran dalam aplikasi desktop. Terdapat juga pilihan untuk menggunakan akaun perkhidmatan, yang tidak memerlukan pergerakan yang tidak perlu daripada pengguna.

Gambar di bawah ialah penerangan skematik senario yang dipilih daripada halaman Google.

Mencipta Pengguna Google daripada PowerShell melalui API

  1. Mula-mula, kami menghantar pengguna ke halaman pengesahan Akaun Google, dengan menyatakan parameter GET:
    • id permohonan
    • kawasan yang aplikasi memerlukan akses
    • alamat di mana pengguna akan diubah hala selepas menyelesaikan prosedur
    • cara kami akan mengemas kini token
    • Kod keselamatan
    • format penghantaran kod pengesahan

  2. Selepas kebenaran selesai, pengguna akan diubah hala ke halaman yang dinyatakan dalam permintaan pertama, dengan ralat atau kod kebenaran yang diluluskan oleh parameter GET
  3. Aplikasi (skrip) perlu menerima parameter ini dan, jika menerima kod, buat permintaan berikut untuk mendapatkan token
  4. Jika permintaan itu betul, API Google mengembalikan:
    • Token akses yang dengannya kami boleh membuat permintaan
    • Tempoh sah token ini
    • Token muat semula diperlukan untuk memuat semula token Akses.

Mula-mula anda perlu pergi ke konsol API Google: Bukti kelayakan - Konsol API Google, pilih aplikasi yang dikehendaki dan dalam bahagian Bukti kelayakan buat pengecam OAuth klien. Di sana (atau kemudian, dalam sifat pengecam yang dibuat) ​​anda perlu menentukan alamat yang dibenarkan pengalihan. Dalam kes kami, ini akan menjadi beberapa entri localhost dengan port yang berbeza (lihat di bawah).

Untuk menjadikannya lebih mudah untuk membaca algoritma skrip, anda boleh memaparkan langkah pertama dalam fungsi berasingan yang akan mengembalikan token Akses dan muat semula untuk aplikasi:

$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

Kami menetapkan ID Pelanggan dan Rahsia Pelanggan yang diperoleh dalam sifat pengecam klien OAuth dan pengesah kod ialah rentetan 43 hingga 128 aksara yang mesti dijana secara rawak daripada aksara yang tidak disimpan: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Kod ini kemudiannya akan dihantar semula. Ia menghapuskan kelemahan di mana penyerang boleh memintas respons yang dikembalikan sebagai ubah hala selepas kebenaran pengguna.
Anda boleh menghantar pengesah kod dalam permintaan semasa dalam teks yang jelas (yang menjadikannya tidak bermakna - ini hanya sesuai untuk sistem yang tidak menyokong SHA256), atau dengan membuat cincang menggunakan algoritma SHA256, yang mesti dikodkan dalam BASE64Url (berbeza dari Base64 dengan dua aksara jadual) dan mengalih keluar pengakhiran baris aksara: =.

Seterusnya, kita perlu mula mendengar http pada mesin tempatan untuk menerima respons selepas kebenaran, yang akan dikembalikan sebagai ubah hala.

Tugas pentadbiran dilakukan pada pelayan khas, kami tidak boleh menolak kemungkinan bahawa beberapa pentadbir akan menjalankan skrip pada masa yang sama, jadi ia akan secara rawak memilih port untuk pengguna semasa, tetapi saya menetapkan port yang telah ditetapkan kerana ia juga mesti ditambah seperti yang dipercayai dalam konsol API.

access_type=luar talian bermakna aplikasi boleh mengemas kini token tamat tempoh sendiri tanpa interaksi pengguna dengan penyemak imbas,
response_type=code menetapkan format bagaimana kod itu akan dikembalikan (rujukan kepada kaedah kebenaran lama, apabila pengguna menyalin kod daripada penyemak imbas ke dalam skrip),
skop menunjukkan skop dan jenis akses. Ia mesti dipisahkan oleh ruang atau % 20 (mengikut Pengekodan URL). Senarai kawasan akses dengan jenis boleh dilihat di sini: Skop OAuth 2.0 untuk API Google.

Selepas menerima kod kebenaran, aplikasi akan mengembalikan mesej rapat kepada penyemak imbas, berhenti mendengar pada port dan menghantar permintaan POST untuk mendapatkan token. Kami menunjukkan di dalamnya id dan rahsia yang dinyatakan sebelum ini daripada API konsol, alamat yang pengguna akan diubah hala dan grant_type mengikut spesifikasi protokol.

Sebagai tindak balas, kami akan menerima token Akses, tempoh sahnya dalam beberapa saat dan token Muat Semula, yang dengannya kami boleh mengemas kini token Akses.

Aplikasi mesti menyimpan token di tempat yang selamat dengan jangka hayat yang panjang, jadi sehingga kami membatalkan akses yang diterima, token muat semula tidak akan dikembalikan kepada aplikasi. Pada akhirnya, saya menambah permintaan untuk membatalkan token; jika permohonan tidak berjaya diselesaikan dan token muat semula tidak dikembalikan, ia akan memulakan prosedur semula (kami menganggap tidak selamat untuk menyimpan token secara setempat pada terminal, dan kami tidak tidak mahu merumitkan perkara dengan kriptografi atau membuka penyemak imbas dengan kerap).

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
}

Seperti yang telah anda perhatikan, apabila membatalkan token, Invoke-WebRequest digunakan. Tidak seperti Invoke-RestMethod, ia tidak mengembalikan data yang diterima dalam format yang boleh digunakan dan menunjukkan status permintaan.

Seterusnya, skrip meminta anda memasukkan nama pertama dan nama keluarga pengguna, menghasilkan log masuk + e-mel.

permintaan

Permintaan seterusnya ialah - pertama sekali, anda perlu menyemak sama ada pengguna dengan log masuk yang sama sudah wujud untuk mendapatkan keputusan untuk membuat yang baharu atau mendayakan yang semasa.

Saya memutuskan untuk melaksanakan semua permintaan dalam format satu fungsi dengan pilihan, menggunakan suis:

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'])
      }
    }
  }
}

Dalam setiap permintaan, anda perlu menghantar pengepala Kebenaran yang mengandungi jenis token dan token Akses itu sendiri. Pada masa ini, jenis token sentiasa Pembawa. Kerana kita perlu menyemak bahawa token belum tamat tempoh dan mengemas kininya selepas sejam dari saat ia dikeluarkan, saya menyatakan permintaan untuk fungsi lain yang mengembalikan token Akses. Sekeping kod yang sama adalah pada permulaan skrip apabila menerima token Akses pertama:

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
}

Menyemak log masuk untuk kewujudan:

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
}

Permintaan e-mel:$query akan meminta API untuk mencari pengguna dengan e-mel itu, termasuk alias. Anda juga boleh menggunakan kad bebas: =, :, :{PREFIX}*.

Untuk mendapatkan data, gunakan kaedah permintaan GET, untuk memasukkan data (membuat akaun atau menambah ahli pada kumpulan) - POST, untuk mengemas kini data sedia ada - PUT, untuk memadam rekod (contohnya, ahli daripada kumpulan) - PADAM.

Skrip juga akan meminta nombor telefon (rentetan tidak sah) dan untuk dimasukkan dalam kumpulan pengedaran serantau. Ia memutuskan unit organisasi yang mana pengguna harus mempunyai berdasarkan OU Direktori Aktif yang dipilih dan menghasilkan kata laluan:

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"

Dan kemudian dia mula memanipulasi akaun:

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

Fungsi untuk mengemas kini dan mencipta akaun mempunyai sintaks yang serupa; tidak semua medan tambahan diperlukan; dalam bahagian dengan nombor telefon, anda perlu menentukan tatasusunan yang boleh mengandungi sehingga satu rekod dengan nombor dan jenisnya.

Untuk tidak menerima ralat semasa menambahkan pengguna pada kumpulan, kami boleh menyemak dahulu sama ada dia sudah menjadi ahli kumpulan ini dengan mendapatkan senarai ahli kumpulan atau gubahan daripada pengguna itu sendiri.

Menanyakan keahlian kumpulan pengguna tertentu tidak akan menjadi rekursif dan hanya akan menunjukkan keahlian langsung. Termasuk pengguna dalam kumpulan induk yang sudah mempunyai kumpulan anak yang menjadi ahli pengguna akan berjaya.

Kesimpulan

Yang tinggal hanyalah menghantar kata laluan kepada pengguna untuk akaun baharu. Kami melakukan ini melalui SMS, dan menghantar maklumat am dengan arahan dan log masuk ke e-mel peribadi, yang, bersama-sama dengan nombor telefon, disediakan oleh jabatan pengambilan. Sebagai alternatif, anda boleh menjimatkan wang dan menghantar kata laluan anda ke sembang telegram rahsia, yang juga boleh dianggap sebagai faktor kedua (MacBooks akan menjadi pengecualian).

Terima kasih kerana membaca sehingga habis. Saya akan gembira melihat cadangan untuk menambah baik gaya penulisan artikel dan berharap anda mendapat lebih sedikit ralat semasa menulis skrip =)

Senarai pautan yang mungkin berguna secara tematik atau hanya menjawab soalan:

Sumber: www.habr.com

Tambah komen