Tạo người dùng Google từ PowerShell thông qua API

Hi!

Bài viết này sẽ mô tả cách triển khai tương tác PowerShell với API Google để thao túng người dùng G Suite.

Chúng tôi sử dụng một số dịch vụ nội bộ và đám mây trong toàn tổ chức. Phần lớn, việc ủy ​​quyền trong đó thuộc về Google hoặc Active Directory, giữa chúng tôi không thể duy trì một bản sao; do đó, khi nhân viên mới rời đi, bạn cần tạo/kích hoạt tài khoản trong hai hệ thống này. Để tự động hóa quy trình, chúng tôi quyết định viết một tập lệnh thu thập thông tin và gửi nó đến cả hai dịch vụ.

Ủy quyền

Khi đưa ra các yêu cầu, chúng tôi quyết định sử dụng quản trị viên thực sự là con người để ủy quyền; điều này giúp đơn giản hóa việc phân tích các hành động trong trường hợp có những thay đổi lớn do vô tình hoặc cố ý.

API Google sử dụng giao thức OAuth 2.0 để xác thực và ủy quyền. Các trường hợp sử dụng và mô tả chi tiết hơn có thể được tìm thấy ở đây: Sử dụng OAuth 2.0 để truy cập các API của Google.

Tôi đã chọn tập lệnh được sử dụng để ủy quyền trong các ứng dụng máy tính để bàn. Ngoài ra còn có một tùy chọn để sử dụng tài khoản dịch vụ mà không yêu cầu người dùng thực hiện các chuyển động không cần thiết.

Hình ảnh bên dưới là mô tả sơ đồ về kịch bản đã chọn từ trang Google.

Tạo người dùng Google từ PowerShell thông qua API

  1. Đầu tiên, chúng tôi gửi người dùng đến trang xác thực Tài khoản Google, chỉ định tham số GET:
    • id ứng dụng
    • các khu vực mà ứng dụng cần truy cập
    • địa chỉ mà người dùng sẽ được chuyển hướng đến sau khi hoàn tất thủ tục
    • cách chúng tôi sẽ cập nhật mã thông báo
    • Mã bảo mật
    • định dạng truyền mã xác minh

  2. Sau khi ủy quyền hoàn tất, người dùng sẽ được chuyển hướng đến trang được chỉ định trong yêu cầu đầu tiên, với lỗi hoặc mã ủy quyền được truyền bởi tham số GET
  3. Ứng dụng (tập lệnh) sẽ cần nhận các tham số này và nếu nhận được mã, hãy đưa ra yêu cầu sau để nhận mã thông báo
  4. Nếu yêu cầu đúng, API Google sẽ trả về:
    • Mã thông báo truy cập mà chúng tôi có thể thực hiện yêu cầu
    • Thời hạn hiệu lực của mã thông báo này
    • Cần làm mới mã thông báo để làm mới mã thông báo Truy cập.

Trước tiên, bạn cần truy cập bảng điều khiển Google API: Thông tin xác thực - Bảng điều khiển API của Google, chọn ứng dụng mong muốn và trong phần Thông tin xác thực, hãy tạo mã định danh OAuth của ứng dụng khách. Ở đó (hoặc sau này, trong thuộc tính của mã định danh đã tạo), ​​bạn cần chỉ định các địa chỉ được phép chuyển hướng. Trong trường hợp của chúng tôi, đây sẽ là một số mục localhost với các cổng khác nhau (xem bên dưới).

Để thuận tiện hơn khi đọc thuật toán tập lệnh, bạn có thể hiển thị các bước đầu tiên trong một hàm riêng biệt sẽ trả về mã thông báo Truy cập và làm mới cho ứng dụng:

$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

Chúng tôi đặt ID khách hàng và Bí mật khách hàng thu được trong thuộc tính định danh khách hàng OAuth và trình xác minh mã là một chuỗi từ 43 đến 128 ký tự phải được tạo ngẫu nhiên từ các ký tự không được đặt trước: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

Mã này sau đó sẽ được truyền lại. Nó loại bỏ lỗ hổng trong đó kẻ tấn công có thể chặn phản hồi được trả về dưới dạng chuyển hướng sau khi người dùng ủy quyền.
Bạn có thể gửi trình xác minh mã trong yêu cầu hiện tại ở dạng văn bản rõ ràng (điều này khiến nó vô nghĩa - điều này chỉ phù hợp với các hệ thống không hỗ trợ SHA256) hoặc bằng cách tạo hàm băm bằng thuật toán SHA256, phải được mã hóa trong BASE64Url (khác nhau từ Base64 bằng hai ký tự bảng) và xóa phần cuối dòng ký tự: =.

Tiếp theo, chúng ta cần bắt đầu nghe http trên máy cục bộ để nhận được phản hồi sau khi ủy quyền, phản hồi này sẽ được trả về dưới dạng chuyển hướng.

Các tác vụ quản trị được thực hiện trên một máy chủ đặc biệt, chúng tôi không thể loại trừ khả năng một số quản trị viên sẽ chạy tập lệnh cùng lúc, do đó nó sẽ chọn ngẫu nhiên một cổng cho người dùng hiện tại, nhưng tôi đã chỉ định các cổng được xác định trước vì chúng cũng phải được thêm dưới dạng đáng tin cậy trong bảng điều khiển API.

access_type = ngoại tuyến có nghĩa là ứng dụng có thể tự cập nhật mã thông báo đã hết hạn mà không cần người dùng tương tác với trình duyệt,
response_type = code đặt định dạng về cách trả về mã (tham chiếu đến phương thức ủy quyền cũ, khi người dùng sao chép mã từ trình duyệt vào tập lệnh),
phạm vi cho biết phạm vi và loại quyền truy cập. Chúng phải được phân tách bằng dấu cách hoặc %20 (theo Mã hóa URL). Bạn có thể xem danh sách các khu vực truy cập với các loại tại đây: Phạm vi OAuth 2.0 cho API Google.

Sau khi nhận được mã ủy quyền, ứng dụng sẽ trả về thông báo đóng cho trình duyệt, ngừng nghe trên cổng và gửi yêu cầu POST để lấy mã thông báo. Chúng tôi chỉ ra trong đó id và bí mật được chỉ định trước đó từ API bảng điều khiển, địa chỉ mà người dùng sẽ được chuyển hướng và Grant_type theo đặc tả giao thức.

Đáp lại, chúng tôi sẽ nhận được mã thông báo Access, thời hạn hiệu lực của nó tính bằng giây và mã thông báo Làm mới để chúng tôi có thể cập nhật mã thông báo Access.

Ứng dụng phải lưu trữ mã thông báo ở nơi an toàn với thời hạn sử dụng lâu dài, vì vậy cho đến khi chúng tôi thu hồi quyền truy cập đã nhận, ứng dụng sẽ không trả lại mã thông báo làm mới. Cuối cùng, tôi đã thêm yêu cầu thu hồi mã thông báo; nếu ứng dụng không được hoàn thành thành công và mã thông báo làm mới không được trả về, nó sẽ bắt đầu lại quy trình (chúng tôi cho rằng việc lưu trữ mã thông báo cục bộ trên thiết bị đầu cuối là không an toàn và chúng tôi không không muốn phức tạp hóa mọi thứ bằng mật mã hoặc mở trình duyệt thường xuyên).

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
}

Như bạn đã nhận thấy, khi thu hồi mã thông báo, Invoke-WebRequest sẽ được sử dụng. Không giống như Invoke-RestMethod, nó không trả về dữ liệu đã nhận ở định dạng có thể sử dụng được và hiển thị trạng thái của yêu cầu.

Tiếp theo, tập lệnh yêu cầu bạn nhập họ và tên của người dùng, tạo thông tin đăng nhập + email.

yêu cầu

Các yêu cầu tiếp theo sẽ là - trước hết, bạn cần kiểm tra xem người dùng có cùng thông tin đăng nhập đã tồn tại hay chưa để đưa ra quyết định tạo một yêu cầu mới hay kích hoạt yêu cầu hiện tại.

Tôi quyết định triển khai tất cả các yêu cầu ở định dạng của một hàm bằng một lựa chọn, sử dụng 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'])
      }
    }
  }
}

Trong mỗi yêu cầu, bạn cần gửi tiêu đề Ủy quyền chứa loại mã thông báo và chính mã thông báo Truy cập. Hiện tại, loại mã thông báo luôn là Bearer. Bởi vì chúng tôi cần kiểm tra xem mã thông báo chưa hết hạn và cập nhật mã thông báo sau một giờ kể từ thời điểm mã thông báo được phát hành, tôi đã chỉ định yêu cầu cho một chức năng khác trả về mã thông báo Access. Đoạn mã tương tự nằm ở đầu tập lệnh khi nhận mã thông báo Truy cập đầu tiên:

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
}

Kiểm tra đăng nhập để tồn tại:

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
}

Yêu cầu email:$query sẽ yêu cầu API tìm kiếm người dùng có chính xác email đó, bao gồm cả bí danh. Bạn cũng có thể sử dụng ký tự đại diện: =, :, :{PREFIX}*.

Để lấy dữ liệu, hãy sử dụng phương thức yêu cầu GET, để chèn dữ liệu (tạo tài khoản hoặc thêm thành viên vào nhóm) - POST, để cập nhật dữ liệu hiện có - PUT, để xóa bản ghi (ví dụ: thành viên khỏi nhóm) - XÓA BỎ.

Tập lệnh cũng sẽ yêu cầu số điện thoại (một chuỗi chưa được xác thực) và đưa vào nhóm phân phối khu vực. Nó quyết định đơn vị tổ chức nào mà người dùng nên có dựa trên OU Active Directory đã chọn và đưa ra mật khẩu:

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"

Và sau đó anh ta bắt đầu thao túng tài khoản:

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

Các chức năng cập nhật và tạo tài khoản có cú pháp tương tự; không yêu cầu tất cả các trường bổ sung; trong phần có số điện thoại, bạn cần chỉ định một mảng có thể chứa tối đa một bản ghi có số và loại của nó.

Để không gặp lỗi khi thêm người dùng vào nhóm, trước tiên chúng tôi có thể kiểm tra xem người dùng đó đã là thành viên của nhóm này hay chưa bằng cách lấy danh sách thành viên nhóm hoặc thành phần từ chính người dùng đó.

Truy vấn tư cách thành viên nhóm của một người dùng cụ thể sẽ không đệ quy và sẽ chỉ hiển thị tư cách thành viên trực tiếp. Việc đưa người dùng vào nhóm chính đã có nhóm con mà người dùng đó là thành viên sẽ thành công.

Kết luận

Tất cả những gì còn lại là gửi cho người dùng mật khẩu của tài khoản mới. Chúng tôi thực hiện việc này thông qua SMS và gửi thông tin chung kèm theo hướng dẫn và đăng nhập vào email cá nhân, cùng với số điện thoại do bộ phận tuyển dụng cung cấp. Ngoài ra, bạn có thể tiết kiệm tiền và gửi mật khẩu của mình đến một cuộc trò chuyện điện tín bí mật, đây cũng có thể được coi là yếu tố thứ hai (MacBook sẽ là một ngoại lệ).

Cảm ơn bạn đã đọc đến cuối. Tôi sẽ rất vui khi thấy những gợi ý để cải thiện phong cách viết bài và chúc bạn ít mắc lỗi hơn khi viết script =)

Danh sách các liên kết có thể hữu ích theo chủ đề hoặc chỉ đơn giản là trả lời các câu hỏi:

Nguồn: www.habr.com

Thêm một lời nhận xét