การสร้างผู้ใช้ Google จาก PowerShell ผ่าน API

Hi!

บทความนี้จะอธิบายการใช้งานการโต้ตอบของ PowerShell กับ Google API เพื่อจัดการผู้ใช้ G Suite

เราใช้บริการภายในและคลาวด์หลายแห่งทั่วทั้งองค์กร โดยส่วนใหญ่ การอนุญาตจะตกอยู่ที่ Google หรือ Active Directory ซึ่งเราไม่สามารถรักษาแบบจำลองไว้ได้ ดังนั้น เมื่อพนักงานใหม่ลาออก คุณจะต้องสร้าง/เปิดใช้งานบัญชีในทั้งสองระบบนี้ เพื่อให้กระบวนการเป็นอัตโนมัติ เราตัดสินใจเขียนสคริปต์ที่รวบรวมข้อมูลและส่งไปยังบริการทั้งสอง

การอนุญาต

เมื่อร่างข้อกำหนด เราตัดสินใจใช้ผู้ดูแลระบบที่เป็นมนุษย์จริงในการอนุญาต ซึ่งช่วยลดความยุ่งยากในการวิเคราะห์การดำเนินการในกรณีที่มีการเปลี่ยนแปลงครั้งใหญ่โดยไม่ได้ตั้งใจหรือโดยเจตนา

Google API ใช้โปรโตคอล OAuth 2.0 สำหรับการตรวจสอบสิทธิ์และการอนุญาต กรณีการใช้งานและคำอธิบายโดยละเอียดเพิ่มเติมสามารถพบได้ที่นี่: การใช้ OAuth 2.0 เพื่อเข้าถึง Google API.

ฉันเลือกสคริปต์ที่ใช้สำหรับการอนุญาตในแอปพลิเคชันเดสก์ท็อป นอกจากนี้ยังมีตัวเลือกในการใช้บัญชีบริการซึ่งผู้ใช้ไม่ต้องการการเคลื่อนไหวที่ไม่จำเป็น

รูปภาพด้านล่างเป็นคำอธิบายแผนผังของสถานการณ์ที่เลือกจากเพจ Google

การสร้างผู้ใช้ Google จาก PowerShell ผ่าน API

  1. ขั้นแรก เราจะส่งผู้ใช้ไปที่หน้าการตรวจสอบบัญชี Google โดยระบุพารามิเตอร์ GET:
    • รหัสแอปพลิเคชัน
    • พื้นที่ที่แอปพลิเคชันต้องการเข้าถึง
    • ที่อยู่ที่ผู้ใช้จะถูกเปลี่ยนเส้นทางหลังจากเสร็จสิ้นขั้นตอน
    • วิธีที่เราจะอัปเดตโทเค็น
    • รหัสรักษาความปลอดภัย
    • รูปแบบการส่งรหัสยืนยัน

  2. หลังจากการอนุญาตเสร็จสิ้น ผู้ใช้จะถูกเปลี่ยนเส้นทางไปยังหน้าที่ระบุในคำขอแรก โดยมีข้อผิดพลาดหรือรหัสการอนุญาตที่ส่งผ่านพารามิเตอร์ GET
  3. แอปพลิเคชัน (สคริปต์) จะต้องได้รับพารามิเตอร์เหล่านี้ และหากได้รับรหัส ให้ส่งคำขอต่อไปนี้เพื่อรับโทเค็น
  4. หากคำขอถูกต้อง Google API จะส่งคืน:
    • โทเค็นการเข้าถึงที่เราใช้ในการร้องขอได้
    • ระยะเวลาที่ถูกต้องของโทเค็นนี้
    • ต้องมีโทเค็นการรีเฟรชเพื่อรีเฟรชโทเค็นการเข้าถึง

ก่อนอื่นคุณต้องไปที่คอนโซล Google API: ข้อมูลรับรอง - คอนโซล Google APIเลือกแอปพลิเคชันที่ต้องการและในส่วนข้อมูลรับรองให้สร้างตัวระบุ OAuth ของไคลเอ็นต์ ที่นั่น (หรือใหม่กว่าในคุณสมบัติของตัวระบุที่สร้างขึ้น) ​​คุณต้องระบุที่อยู่ที่อนุญาตให้มีการเปลี่ยนเส้นทาง ในกรณีของเรา สิ่งเหล่านี้จะเป็นรายการ localhost หลายรายการที่มีพอร์ตต่างกัน (ดูด้านล่าง)

เพื่อให้สะดวกยิ่งขึ้นในการอ่านอัลกอริธึมสคริปต์ คุณสามารถแสดงขั้นตอนแรกในฟังก์ชันแยกต่างหากที่จะส่งคืนโทเค็นการเข้าถึงและรีเฟรชสำหรับแอปพลิเคชัน:

$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 และผู้ตรวจสอบโค้ดคือสตริงที่มีความยาว 43 ถึง 128 อักขระที่ต้องสร้างขึ้นแบบสุ่มจากอักขระที่ไม่ได้สงวนไว้: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~"

รหัสนี้จะถูกส่งอีกครั้ง ช่วยขจัดช่องโหว่ที่ผู้โจมตีสามารถสกัดกั้นการตอบสนองที่ส่งคืนเป็นการเปลี่ยนเส้นทางหลังจากผู้ใช้ให้สิทธิ์แล้ว
คุณสามารถส่งตัวตรวจสอบรหัสในคำขอปัจจุบันเป็นข้อความที่ชัดเจน (ซึ่งทำให้ไม่มีความหมาย - เหมาะสำหรับระบบที่ไม่รองรับ SHA256 เท่านั้น) หรือโดยการสร้างแฮชโดยใช้อัลกอริทึม SHA256 ซึ่งจะต้องเข้ารหัสใน BASE64Url (ต่างกัน จาก Base64 ด้วยอักขระตารางสองตัว) และลบการสิ้นสุดบรรทัดอักขระ: =

ต่อไป เราต้องเริ่มฟัง http บนเครื่องท้องถิ่นเพื่อรับการตอบกลับหลังจากการอนุญาต ซึ่งจะถูกส่งกลับเป็นการเปลี่ยนเส้นทาง

งานด้านการดูแลระบบจะดำเนินการบนเซิร์ฟเวอร์พิเศษ เราไม่สามารถตัดความเป็นไปได้ที่ผู้ดูแลระบบหลายคนจะเรียกใช้สคริปต์ในเวลาเดียวกัน ดังนั้นมันจะสุ่มเลือกพอร์ตสำหรับผู้ใช้ปัจจุบัน แต่ฉันระบุพอร์ตที่กำหนดไว้ล่วงหน้าเพราะ จะต้องเพิ่มว่าเชื่อถือได้ในคอนโซล API ด้วย

access_type=ออฟไลน์ หมายความว่าแอปพลิเคชันสามารถอัปเดตโทเค็นที่หมดอายุได้ด้วยตัวเองโดยที่ผู้ใช้ไม่ต้องโต้ตอบกับเบราว์เซอร์
ตอบ_ประเภท=รหัส กำหนดรูปแบบวิธีการส่งคืนรหัส (อ้างอิงถึงวิธีการอนุญาตแบบเก่าเมื่อผู้ใช้คัดลอกรหัสจากเบราว์เซอร์ลงในสคริปต์)
ขอบเขต ระบุขอบเขตและประเภทของการเข้าถึง ต้องคั่นด้วยช่องว่างหรือ %20 (ตามการเข้ารหัส URL) รายชื่อพื้นที่การเข้าถึงพร้อมประเภทต่างๆ สามารถดูได้ที่นี่: ขอบเขต OAuth 2.0 สำหรับ Google API.

หลังจากได้รับรหัสอนุญาตแล้ว แอปพลิเคชันจะส่งข้อความปิดไปยังเบราว์เซอร์ หยุดฟังบนพอร์ต และส่งคำขอ POST เพื่อรับโทเค็น เราระบุรหัสและความลับที่ระบุก่อนหน้านี้จากคอนโซล API ที่อยู่ที่ผู้ใช้จะถูกเปลี่ยนเส้นทางและ grant_type ตามข้อกำหนดของโปรโตคอล

เพื่อเป็นการตอบสนอง เราจะได้รับโทเค็นการเข้าถึง ระยะเวลาที่ถูกต้องเป็นวินาที และโทเค็นการรีเฟรช ซึ่งเราสามารถอัปเดตโทเค็นการเข้าถึงได้

แอปพลิเคชันจะต้องจัดเก็บโทเค็นไว้ในที่ปลอดภัยและมีอายุการเก็บรักษาที่ยาวนาน ดังนั้นจนกว่าเราจะเพิกถอนการเข้าถึงที่ได้รับ แอปพลิเคชันจะไม่ส่งคืนโทเค็นการรีเฟรช ในตอนท้าย ฉันได้เพิ่มคำขอเพื่อเพิกถอนโทเค็น หากแอปพลิเคชันไม่เสร็จสมบูรณ์และโทเค็นการรีเฟรชไม่ได้รับการส่งคืน แอปพลิเคชันจะเริ่มขั้นตอนอีกครั้ง (เราถือว่าไม่ปลอดภัยที่จะจัดเก็บโทเค็นในเครื่องบนเทอร์มินัล และเราไม่ ไม่ต้องการทำให้สิ่งต่าง ๆ ซับซ้อนด้วยการเข้ารหัสหรือเปิดเบราว์เซอร์บ่อยๆ)

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
}

ดังที่คุณสังเกตเห็นแล้ว เมื่อเพิกถอนโทเค็น จะใช้ Inrigg-WebRequest ต่างจาก Invivo-RestMethod ตรงที่จะไม่ส่งคืนข้อมูลที่ได้รับในรูปแบบที่ใช้งานได้และแสดงสถานะของคำขอ

ถัดไป สคริปต์จะขอให้คุณป้อนชื่อและนามสกุลของผู้ใช้เพื่อสร้างข้อมูลเข้าสู่ระบบ + อีเมล

การร้องขอ

คำขอถัดไปจะเป็น - ก่อนอื่นคุณต้องตรวจสอบว่ามีผู้ใช้ที่มีการเข้าสู่ระบบเดียวกันอยู่แล้วหรือไม่ เพื่อรับการตัดสินใจในการสร้างใหม่หรือเปิดใช้งานผู้ใช้ปัจจุบัน

ฉันตัดสินใจที่จะดำเนินการคำขอทั้งหมดในรูปแบบของฟังก์ชันเดียวโดยมีตัวเลือกโดยใช้สวิตช์:

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

ในคำขอแต่ละรายการ คุณต้องส่งส่วนหัวการอนุญาตที่มีประเภทโทเค็นและโทเค็นการเข้าถึงนั้นเอง ปัจจุบันประเภทโทเค็นจะเป็นผู้ถือเสมอ เพราะ เราจำเป็นต้องตรวจสอบว่าโทเค็นยังไม่หมดอายุและอัปเดตหลังจากผ่านไปหนึ่งชั่วโมงนับจากเวลาที่ออก ฉันระบุคำขอสำหรับฟังก์ชันอื่นที่ส่งคืนโทเค็นการเข้าถึง รหัสชิ้นเดียวกันอยู่ที่จุดเริ่มต้นของสคริปต์เมื่อได้รับโทเค็นการเข้าถึงแรก:

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
}

ตรวจสอบการเข้าสู่ระบบว่ามีอยู่:

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 จะขอให้ API ค้นหาผู้ใช้ที่มีอีเมลนั้นทุกประการ รวมถึงนามแฝงด้วย คุณยังสามารถใช้ไวด์การ์ดได้: =, :, :{คำนำหน้า}*.

หากต้องการรับข้อมูล ให้ใช้วิธีการร้องขอ GET เพื่อแทรกข้อมูล (การสร้างบัญชีหรือการเพิ่มสมาชิกในกลุ่ม) - POST เพื่ออัปเดตข้อมูลที่มีอยู่ - PUT เพื่อลบบันทึก (เช่น สมาชิกจากกลุ่ม) - ลบ.

สคริปต์จะขอหมายเลขโทรศัพท์ (สตริงที่ไม่ถูกต้อง) และรวมไว้ในกลุ่มการแจกจ่ายระดับภูมิภาค จะตัดสินใจว่าผู้ใช้ควรมีหน่วยขององค์กรใดโดยอิงตาม Active Directory OU ที่เลือก และตั้งรหัสผ่านขึ้นมา:

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"

จากนั้นเขาก็เริ่มจัดการบัญชี:

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

ฟังก์ชั่นสำหรับการอัปเดตและสร้างบัญชีมีไวยากรณ์ที่คล้ายกัน ไม่จำเป็นต้องกรอกฟิลด์เพิ่มเติมทั้งหมด ในส่วนที่มีหมายเลขโทรศัพท์คุณจะต้องระบุอาร์เรย์ที่สามารถมีได้สูงสุดหนึ่งรายการพร้อมหมายเลขและประเภทของมัน

เพื่อไม่ให้เกิดข้อผิดพลาดเมื่อเพิ่มผู้ใช้ในกลุ่ม อันดับแรกเราสามารถตรวจสอบได้ว่าเขาเป็นสมาชิกของกลุ่มนี้อยู่แล้วหรือไม่ โดยรับรายชื่อสมาชิกกลุ่มหรือองค์ประกอบจากผู้ใช้เอง

การสืบค้นความเป็นสมาชิกกลุ่มของผู้ใช้รายใดรายหนึ่งจะไม่เกิดซ้ำและจะแสดงเฉพาะการเป็นสมาชิกโดยตรงเท่านั้น รวมผู้ใช้ในกลุ่มผู้ปกครองที่มีกลุ่มลูกที่ผู้ใช้เป็นสมาชิกอยู่แล้วจะสำเร็จ

ข้อสรุป

สิ่งที่เหลืออยู่คือส่งรหัสผ่านให้กับผู้ใช้สำหรับบัญชีใหม่ เราทำเช่นนี้ผ่านทาง SMS และส่งข้อมูลทั่วไปพร้อมคำแนะนำและเข้าสู่ระบบไปยังอีเมลส่วนตัว ซึ่งแผนกจัดหางานให้ไว้พร้อมกับหมายเลขโทรศัพท์ อีกทางเลือกหนึ่ง คุณสามารถประหยัดเงินและส่งรหัสผ่านของคุณไปที่การแชททางโทรเลขลับ ซึ่งถือได้ว่าเป็นปัจจัยที่สองด้วย (MacBooks จะเป็นข้อยกเว้น)

ขอบคุณที่อ่านจนจบ ยินดีรับฟังข้อเสนอแนะในการปรับปรุงรูปแบบการเขียนบทความ และขอให้พบข้อผิดพลาดในการเขียนสคริปต์น้อยลงครับ =)

รายการลิงก์ที่อาจเป็นประโยชน์ตามหัวข้อหรือเพียงตอบคำถาม:

ที่มา: will.com

เพิ่มความคิดเห็น