API aracılığıyla PowerShell'den Google Kullanıcıları Oluşturma

Merhaba!

Bu makalede, G Suite kullanıcılarını yönetmek için Google API ile PowerShell etkileşiminin uygulanması açıklanacaktır.

Kuruluş genelinde çeşitli dahili ve bulut hizmetleri kullanıyoruz. Çoğunlukla, bunlarda yetkilendirme Google veya Active Directory ile ilgilidir ve bunların arasında bir kopya bulunduramayız; buna göre, yeni bir çalışan ayrıldığında, bu iki sistemde bir hesap oluşturmanız/etkinleştirmeniz gerekir. Süreci otomatikleştirmek için bilgileri toplayan ve her iki hizmete de gönderen bir komut dosyası yazmaya karar verdik.

Yetki

Gereksinimleri hazırlarken yetkilendirme için gerçek insan yöneticileri kullanmaya karar verdik; bu, kazara veya kasıtlı büyük değişiklikler durumunda eylemlerin analizini basitleştirir.

Google API'leri, kimlik doğrulama ve yetkilendirme için OAuth 2.0 protokolünü kullanır. Kullanım senaryolarını ve daha ayrıntılı açıklamaları burada bulabilirsiniz: Google API'lerine Erişmek için OAuth 2.0'ı Kullanma.

Masaüstü uygulamalarında yetkilendirme için kullanılan scripti seçtim. Kullanıcının gereksiz hareketlerini gerektirmeyen bir hizmet hesabı kullanma seçeneği de vardır.

Aşağıdaki resim, Google sayfasından seçilen senaryonun şematik açıklamasıdır.

API aracılığıyla PowerShell'den Google Kullanıcıları Oluşturma

  1. Öncelikle kullanıcıyı GET parametrelerini belirterek Google Hesabı kimlik doğrulama sayfasına göndeririz:
    • Uygulama Kimliği
    • uygulamanın erişmesi gereken alanlar
    • prosedürü tamamladıktan sonra kullanıcının yönlendirileceği adres
    • jetonu güncelleme şeklimiz
    • Güvenlik Kodu
    • doğrulama kodu iletim formatı

  2. Yetkilendirme tamamlandıktan sonra kullanıcı, GET parametrelerinin ilettiği hata veya yetkilendirme kodu ile ilk istekte belirtilen sayfaya yönlendirilecektir.
  3. Uygulamanın (komut dosyası) bu parametreleri alması ve kodu almışsa belirteçleri almak için aşağıdaki isteği yapması gerekecektir.
  4. İstek doğruysa Google API şunu döndürür:
    • İstekte bulunabileceğimiz erişim belirteci
    • Bu jetonun geçerlilik süresi
    • Erişim belirtecini yenilemek için belirteci yenilemek gerekir.

Öncelikle Google API konsoluna gitmeniz gerekiyor: Kimlik Bilgileri - Google API Konsolu, istediğiniz uygulamayı seçin ve Kimlik Bilgileri bölümünde bir istemci OAuth tanımlayıcısı oluşturun. Orada (veya daha sonra oluşturulan tanımlayıcının özelliklerinde), yeniden yönlendirmeye izin verilen adresleri belirtmeniz gerekir. Bizim durumumuzda bunlar farklı bağlantı noktalarına sahip birkaç localhost girişi olacaktır (aşağıya bakın).

Komut dosyası algoritmasını okumayı daha kolay hale getirmek için, ilk adımları Erişimi döndürecek ve uygulama için belirteçleri yenileyecek ayrı bir işlevde görüntüleyebilirsiniz:

$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

OAuth istemci tanımlayıcı özelliklerinde elde edilen İstemci Kimliğini ve İstemci Sırrını belirledik ve kod doğrulayıcı, ayrılmamış karakterlerden rastgele oluşturulması gereken 43 ila 128 karakterlik bir dizedir: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Bu kod daha sonra tekrar iletilecektir. Bir saldırganın, kullanıcı yetkilendirmesinden sonra yönlendirme olarak döndürülen yanıtı engelleyebileceği güvenlik açığını ortadan kaldırır.
Mevcut istekte bir kod doğrulayıcıyı açık metin olarak gönderebilirsiniz (bu onu anlamsız kılar - bu yalnızca SHA256'yı desteklemeyen sistemler için uygundur) veya BASE256Url'de kodlanması gereken SHA64 algoritmasını kullanarak bir karma oluşturarak (farklı) Base64'ten iki tablo karakteri kadar) ve karakter satırı sonlarının kaldırılması: =.

Daha sonra, yetkilendirme sonrasında yönlendirme olarak döndürülecek bir yanıt alabilmek için yerel makinede http'yi dinlemeye başlamamız gerekiyor.

Yönetim görevleri özel bir sunucuda gerçekleştirilir, birkaç yöneticinin betiği aynı anda çalıştırma olasılığını göz ardı edemeyiz, bu nedenle mevcut kullanıcı için rastgele bir bağlantı noktası seçecektir, ancak önceden tanımlanmış bağlantı noktalarını belirttim çünkü bunların ayrıca API konsoluna güvenilir olarak eklenmesi gerekir.

access_type=çevrimdışı uygulamanın, süresi dolmuş bir jetonu tarayıcıyla kullanıcı etkileşimi olmadan kendi başına güncelleyebileceği anlamına gelir,
answer_type=kod kodun nasıl döndürüleceğine ilişkin formatı ayarlar (kullanıcının kodu tarayıcıdan betiğe kopyaladığı eski yetkilendirme yöntemine bir referans),
kapsam Erişimin kapsamını ve türünü belirtir. Boşluk veya %20 (URL Kodlamasına göre) ile ayrılmalıdır. Türleri olan erişim alanlarının listesini burada görebilirsiniz: Google API'leri için OAuth 2.0 Kapsamları.

Yetkilendirme kodunu aldıktan sonra uygulama tarayıcıya bir kapatma mesajı gönderecek, bağlantı noktasını dinlemeyi bırakacak ve belirteci almak için bir POST isteği gönderecektir. Konsol API'sinden önceden belirtilen kimliği ve sırrı, kullanıcının yönlendirileceği adresi ve protokol spesifikasyonuna uygun olarak grant_type'ı belirtiyoruz.

Yanıt olarak, bir Erişim jetonu, saniye cinsinden geçerlilik süresi ve Erişim jetonunu güncelleyebileceğimiz bir Yenileme jetonu alacağız.

Uygulamanın jetonları uzun raf ömrüne sahip güvenli bir yerde saklaması gerekir; bu nedenle, biz alınan erişimi iptal edene kadar uygulama yenileme jetonunu iade etmeyecektir. Sonunda, belirteci iptal etmek için bir istek ekledim; eğer uygulama başarılı bir şekilde tamamlanmadıysa ve yenileme belirteci döndürülmediyse, prosedürü yeniden başlatacaktır (belirteçleri terminalde yerel olarak saklamanın güvenli olmadığını düşündük ve kriptografiyle işleri karmaşık hale getirmek veya tarayıcıyı sık sık açmak istemiyorum).

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
}

Daha önce fark ettiğiniz gibi, bir jetonu iptal ederken Invoke-WebRequest kullanılır. Invoke-RestMethod'dan farklı olarak alınan verileri kullanılabilir bir formatta döndürmez ve isteğin durumunu gösterir.

Daha sonra, komut dosyası sizden kullanıcının adını ve soyadını girerek bir oturum açma + e-posta oluşturmanızı ister.

istekler

Sonraki istekler şu şekilde olacaktır: Her şeyden önce, yeni bir tane oluşturma veya mevcut olanı etkinleştirme konusunda bir karar almak için aynı oturum açma bilgilerine sahip bir kullanıcının zaten mevcut olup olmadığını kontrol etmeniz gerekir.

Anahtarı kullanarak tüm istekleri tek bir işlev biçiminde bir seçimle uygulamaya karar verdim:

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

Her istekte, belirteç türünü ve Erişim belirtecinin kendisini içeren bir Yetkilendirme başlığı göndermeniz gerekir. Şu anda token türü her zaman Taşıyıcıdır. Çünkü belirtecin süresinin dolmadığını kontrol etmemiz ve verildiği andan itibaren bir saat sonra güncellememiz gerekiyor, Erişim belirteci döndüren başka bir işlev için istek belirttim. İlk Erişim belirtecini alırken aynı kod parçası betiğin başında bulunur:

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
}

Giriş bilgilerinin varlığı kontrol ediliyor:

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 isteği, API'den takma adlar da dahil olmak üzere tam olarak bu e-postaya sahip bir kullanıcıyı aramasını isteyecektir. Joker karakter de kullanabilirsiniz: =, :, :{PREFIX}*.

Veri elde etmek için, veri eklemek için (bir hesap oluşturmak veya bir gruba üye eklemek) GET istek yöntemini kullanın - POST, mevcut verileri güncellemek için - PUT, bir kaydı silmek için (örneğin, gruptaki bir üye) - SİLMEK.

Komut dosyası ayrıca bir telefon numarası (doğrulanmamış bir dize) ve bölgesel bir dağıtım grubuna dahil edilmesini isteyecektir. Seçilen Active Directory OU'suna göre kullanıcının hangi kuruluş birimine sahip olması gerektiğine karar verir ve bir parola üretir:

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"

Ve sonra hesabı manipüle etmeye başlıyor:

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

Hesap güncelleme ve oluşturma işlevleri benzer bir sözdizimine sahiptir; tüm ek alanlar gerekli değildir; telefon numaralarının bulunduğu bölümde, numara ve türüyle birlikte en fazla bir kayıt içerebilecek bir dizi belirtmeniz gerekir.

Bir kullanıcıyı bir gruba eklerken hata almamak için öncelikle kullanıcının kendisinden grup üyeleri veya kompozisyon listesi alarak bu grubun zaten üyesi olup olmadığını kontrol edebiliriz.

Belirli bir kullanıcının grup üyeliğini sorgulamak yinelemeli olmayacak ve yalnızca doğrudan üyeliği gösterecektir. Kullanıcının halihazırda üyesi olduğu bir alt gruba sahip olan bir üst gruba dahil edilmesi başarılı olacaktır.

Sonuç

Geriye kalan tek şey kullanıcıya yeni hesabın şifresini göndermektir. Bunu SMS yoluyla yapıyoruz ve talimatlarla birlikte genel bilgileri ve işe alım departmanı tarafından telefon numarasıyla birlikte sağlanan kişisel bir e-postaya giriş yapıyoruz. Alternatif olarak, paradan tasarruf edebilir ve şifrenizi gizli bir telgraf sohbetine gönderebilirsiniz ki bu da ikinci faktör olarak kabul edilebilir (MacBook'lar bir istisna olacaktır).

Sonuna kadar okuduğunuz için teşekkür ederiz. Makale yazma tarzınızı geliştirmeye yönelik önerileri görmekten memnuniyet duyacağım ve senaryo yazarken daha az hata yakalamanızı diliyorum =)

Tematik olarak yararlı olabilecek veya yalnızca soruları yanıtlayabilecek bağlantıların listesi:

Kaynak: habr.com

Yorum ekle