Membuat Pengguna Google dari PowerShell melalui API

Hi!

Artikel ini akan menjelaskan penerapan interaksi PowerShell dengan Google API untuk memanipulasi pengguna G Suite.

Kami menggunakan beberapa layanan internal dan cloud di seluruh organisasi. Sebagian besar, otorisasi di dalamnya berasal dari Google atau Direktori Aktif, yang replikanya tidak dapat kami simpan; oleh karena itu, ketika karyawan baru keluar, Anda perlu membuat/mengaktifkan akun di kedua sistem ini. Untuk mengotomatiskan proses, kami memutuskan untuk menulis skrip yang mengumpulkan informasi dan mengirimkannya ke kedua layanan.

Otorisasi

Saat menyusun persyaratan, kami memutuskan untuk menggunakan administrator manusia sungguhan untuk otorisasi; hal ini menyederhanakan analisis tindakan jika terjadi perubahan besar yang disengaja atau tidak disengaja.

Google API menggunakan protokol OAuth 2.0 untuk autentikasi dan otorisasi. Kasus penggunaan dan deskripsi lebih rinci dapat ditemukan di sini: Menggunakan OAuth 2.0 untuk Mengakses Google API.

Saya memilih skrip yang digunakan untuk otorisasi di aplikasi desktop. Ada juga opsi untuk menggunakan akun layanan, yang tidak memerlukan pergerakan yang tidak perlu dari pengguna.

Gambar di bawah ini adalah gambaran skema skenario yang dipilih dari halaman Google.

Membuat Pengguna Google dari PowerShell melalui API

  1. Pertama, kami mengirim pengguna ke halaman otentikasi Akun Google, menentukan parameter GET:
    • ID aplikasi
    • area yang memerlukan akses oleh aplikasi
    • alamat tujuan pengguna akan diarahkan setelah menyelesaikan prosedur
    • cara kami memperbarui token
    • Kode keamanan
    • format transmisi kode verifikasi

  2. Setelah otorisasi selesai, pengguna akan diarahkan ke halaman yang ditentukan dalam permintaan pertama, dengan kode kesalahan atau otorisasi yang diteruskan oleh parameter GET
  3. Aplikasi (skrip) perlu menerima parameter ini dan, jika menerima kode, membuat permintaan berikut untuk mendapatkan token
  4. Jika permintaannya benar, Google API akan mengembalikan:
    • Akses token yang dapat digunakan untuk membuat permintaan
    • Masa berlaku token ini
    • Token penyegaran diperlukan untuk menyegarkan token Akses.

Pertama, Anda harus pergi ke konsol Google API: Kredensial - Konsol Google API, pilih aplikasi yang diinginkan dan di bagian Kredensial buat pengidentifikasi OAuth klien. Di sana (atau lebih baru, di properti pengidentifikasi yang dibuat) Anda perlu menentukan alamat tujuan pengalihan yang diizinkan. Dalam kasus kami, ini akan berupa beberapa entri localhost dengan port berbeda (lihat di bawah).

Agar lebih mudah membaca algoritme skrip, Anda dapat menampilkan langkah pertama dalam fungsi terpisah yang akan mengembalikan Access dan menyegarkan token 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 Klien dan Rahasia Klien yang diperoleh di properti pengidentifikasi klien OAuth, dan pemverifikasi kode adalah string 43 hingga 128 karakter yang harus dibuat secara acak dari karakter yang tidak direservasi: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Kode ini kemudian akan dikirimkan kembali. Ini menghilangkan kerentanan di mana penyerang dapat mencegat respons yang dikembalikan sebagai pengalihan setelah otorisasi pengguna.
Anda dapat mengirim pemverifikasi kode dalam permintaan saat ini dalam teks yang jelas (yang membuatnya tidak berarti - ini hanya cocok untuk sistem yang tidak mendukung SHA256), atau dengan membuat hash menggunakan algoritma SHA256, yang harus dikodekan dalam BASE64Url (berbeda dari Base64 dengan dua karakter tabel) dan menghilangkan akhiran baris karakter: =.

Selanjutnya, kita perlu mulai mendengarkan http di mesin lokal untuk menerima respons setelah otorisasi, yang akan dikembalikan sebagai pengalihan.

Tugas administratif dilakukan di server khusus, kami tidak dapat mengesampingkan kemungkinan bahwa beberapa administrator akan menjalankan skrip secara bersamaan, sehingga akan secara acak memilih port untuk pengguna saat ini, tetapi saya menentukan port yang telah ditentukan karena mereka juga harus ditambahkan sebagai tepercaya di konsol API.

access_type=luring berarti aplikasi dapat memperbarui sendiri token yang kedaluwarsa tanpa interaksi pengguna dengan browser,
response_type=kode menetapkan format bagaimana kode akan dikembalikan (referensi ke metode otorisasi lama, ketika pengguna menyalin kode dari browser ke dalam skrip),
cakupan menunjukkan ruang lingkup dan jenis akses. Mereka harus dipisahkan dengan spasi atau %20 (menurut Pengkodean URL). Daftar area akses beserta tipenya dapat dilihat di sini: Cakupan OAuth 2.0 untuk Google API.

Setelah menerima kode otorisasi, aplikasi akan mengembalikan pesan tutup ke browser, berhenti mendengarkan port dan mengirim permintaan POST untuk mendapatkan token. Kami menunjukkan di dalamnya id dan rahasia yang ditentukan sebelumnya dari API konsol, alamat tujuan pengalihan pengguna dan grant_type sesuai dengan spesifikasi protokol.

Sebagai tanggapan, kami akan menerima token Akses, masa berlakunya dalam hitungan detik, dan token Penyegaran, yang dengannya kami dapat memperbarui token Akses.

Aplikasi harus menyimpan token di tempat yang aman dengan umur simpan yang lama, sehingga sampai kami mencabut akses yang diterima, aplikasi tidak akan mengembalikan token penyegaran. Pada akhirnya, saya menambahkan permintaan untuk mencabut token; jika aplikasi tidak berhasil diselesaikan dan token penyegaran tidak dikembalikan, prosedur akan dimulai lagi (kami menganggap tidak aman untuk menyimpan token secara lokal di terminal, dan kami tidak melakukannya Saya tidak ingin mempersulit kriptografi atau sering membuka browser).

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 ketahui, saat mencabut token, Invoke-WebRequest digunakan. Tidak seperti Invoke-RestMethod, ini tidak mengembalikan data yang diterima dalam format yang dapat digunakan dan menunjukkan status permintaan.

Selanjutnya, skrip meminta Anda memasukkan nama depan dan belakang pengguna, menghasilkan login + email.

Pertanyaan

Permintaan selanjutnya adalah - pertama-tama, Anda perlu memeriksa apakah pengguna dengan login yang sama sudah ada untuk mendapatkan keputusan untuk membuat yang baru atau mengaktifkan yang sekarang.

Saya memutuskan untuk mengimplementasikan semua permintaan dalam format satu fungsi dengan pilihan, menggunakan 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'])
      }
    }
  }
}

Dalam setiap permintaan, Anda perlu mengirimkan header Otorisasi yang berisi jenis token dan token Akses itu sendiri. Saat ini, jenis token selalu Pembawa. Karena kita perlu memeriksa apakah token tersebut belum kedaluwarsa dan memperbaruinya setelah satu jam sejak dikeluarkan, saya menetapkan permintaan untuk fungsi lain yang mengembalikan token Akses. Potongan kode yang sama ada di awal skrip saat 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
}

Memeriksa keberadaan login:

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 email:$query akan meminta API untuk mencari pengguna dengan email yang sama persis, termasuk alias. Anda juga dapat menggunakan karakter pengganti: =, :, :{Awalan}*.

Untuk memperoleh data, gunakan metode permintaan GET, untuk memasukkan data (membuat akun atau menambahkan anggota ke grup) - POST, untuk memperbarui data yang ada - PUT, untuk menghapus catatan (misalnya anggota dari grup) - MENGHAPUS.

Skrip juga akan meminta nomor telepon (string yang tidak divalidasi) dan dimasukkan ke dalam grup distribusi regional. Ini memutuskan unit organisasi mana yang harus dimiliki pengguna berdasarkan OU Direktori Aktif yang dipilih dan memberikan kata sandi:

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 mulai memanipulasi akun tersebut:

$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 memperbarui dan membuat akun memiliki sintaks yang serupa; tidak semua bidang tambahan diperlukan; di bagian dengan nomor telepon, Anda perlu menentukan array yang dapat berisi hingga satu catatan dengan nomor dan jenisnya.

Agar tidak terjadi kesalahan saat menambahkan pengguna ke grup, pertama-tama kita dapat memeriksa apakah dia sudah menjadi anggota grup ini dengan mendapatkan daftar anggota atau komposisi grup dari pengguna itu sendiri.

Menanyakan keanggotaan grup pengguna tertentu tidak akan bersifat rekursif dan hanya akan menampilkan keanggotaan langsung. Menyertakan pengguna dalam grup induk yang telah memiliki grup anak tempat pengguna tersebut menjadi anggotanya akan berhasil.

Kesimpulan

Yang tersisa hanyalah mengirimkan kata sandi untuk akun baru kepada pengguna. Kami melakukan ini melalui SMS, dan mengirimkan informasi umum dengan instruksi dan login ke email pribadi, yang, bersama dengan nomor telepon, disediakan oleh departemen perekrutan. Sebagai alternatif, Anda dapat menghemat uang dan mengirimkan kata sandi Anda ke obrolan telegram rahasia, yang juga dapat dianggap sebagai faktor kedua (MacBook akan menjadi pengecualian).

Terima kasih telah membaca sampai akhir. Saya akan senang melihat saran untuk meningkatkan gaya penulisan artikel dan berharap Anda lebih sedikit menemukan kesalahan saat menulis skrip =)

Daftar tautan yang mungkin berguna secara tematis atau sekadar menjawab pertanyaan:

Sumber: www.habr.com

Tambah komentar