ایجاد کاربران گوگل از PowerShell از طریق API

سلام!

این مقاله اجرای تعامل PowerShell با Google API برای دستکاری کاربران G Suite را شرح می‌دهد.

ما از چندین سرویس داخلی و ابری در سراسر سازمان استفاده می کنیم. در بیشتر موارد، مجوز در آنها به گوگل یا اکتیو دایرکتوری می رسد، که بین آنها ما نمی توانیم یک نسخه را حفظ کنیم؛ بر این اساس، وقتی یک کارمند جدید ترک می کند، باید یک حساب کاربری در این دو سیستم ایجاد یا فعال کنید. برای خودکارسازی فرآیند، تصمیم گرفتیم اسکریپتی بنویسیم که اطلاعات را جمع آوری کرده و به هر دو سرویس ارسال می کند.

اجازه

هنگام تنظیم الزامات، تصمیم گرفتیم از مدیران انسانی واقعی برای مجوز استفاده کنیم؛ این تجزیه و تحلیل اقدامات را در صورت تغییرات عظیم تصادفی یا عمدی ساده می کند.

API های Google از پروتکل OAuth 2.0 برای احراز هویت و مجوز استفاده می کنند. موارد استفاده و توضیحات دقیق تر را می توانید در اینجا بیابید: استفاده از OAuth 2.0 برای دسترسی به Google API.

من اسکریپتی را انتخاب کردم که برای مجوز در برنامه های دسکتاپ استفاده می شود. همچنین گزینه ای برای استفاده از حساب کاربری وجود دارد که نیازی به جابجایی های غیر ضروری از جانب کاربر ندارد.

تصویر زیر شرح شماتیک سناریوی انتخاب شده از صفحه گوگل است.

ایجاد کاربران گوگل از PowerShell از طریق API

  1. ابتدا کاربر را با مشخص کردن پارامترهای GET به صفحه احراز هویت حساب Google می فرستیم:
    • شناسه برنامه
    • مناطقی که برنامه نیاز به دسترسی به آنها دارد
    • آدرسی که کاربر پس از تکمیل مراحل به آن هدایت می شود
    • روشی که ما توکن را به روز خواهیم کرد
    • کد امنیتی
    • فرمت انتقال کد تایید

  2. پس از تکمیل مجوز، کاربر با یک خطا یا کد مجوز که توسط پارامترهای GET ارسال شده است، به صفحه مشخص شده در اولین درخواست هدایت می شود.
  3. برنامه (اسکریپت) باید این پارامترها را دریافت کند و در صورت دریافت کد، درخواست زیر را برای به دست آوردن توکن ها ارائه دهد.
  4. اگر درخواست درست باشد، Google API برمی‌گرداند:
    • نشانه دسترسی که با آن می توانیم درخواست کنیم
    • مدت اعتبار این توکن
    • برای بازخوانی نشانه دسترسی، نشانه Refresh مورد نیاز است.

ابتدا باید به کنسول Google API بروید: اعتبار - کنسول API Google، اپلیکیشن مورد نظر را انتخاب کرده و در قسمت Credentials یک شناسه مشتری OAuth ایجاد کنید. در آنجا (یا بعداً، در ویژگی های شناسه ایجاد شده) باید آدرس هایی را که تغییر مسیر به آنها مجاز است مشخص کنید. در مورد ما، اینها چندین ورودی لوکال هاست با پورت های مختلف خواهند بود (به زیر مراجعه کنید).

برای راحت‌تر کردن خواندن الگوریتم اسکریپت، می‌توانید اولین گام‌ها را در یک تابع جداگانه نمایش دهید که Access را برمی‌گرداند و نشانه‌های برنامه را تازه‌سازی می‌کند:

$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

ما Client ID و Client Secret به‌دست‌آمده در ویژگی‌های شناسه مشتری OAuth را تنظیم می‌کنیم، و تأییدکننده کد رشته‌ای از 43 تا 128 کاراکتر است که باید به‌طور تصادفی از نویسه‌های رزرو نشده تولید شود: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".

سپس این کد دوباره ارسال می شود. این آسیب‌پذیری را که در آن یک مهاجم می‌تواند پاسخی را که به عنوان تغییر مسیر پس از مجوز کاربر بازگردانده می‌شود، رهگیری کند، از بین می‌برد.
شما می توانید یک تأیید کننده کد را در درخواست فعلی در متن واضح ارسال کنید (که آن را بی معنی می کند - این فقط برای سیستم هایی مناسب است که SHA256 را پشتیبانی نمی کنند)، یا با ایجاد یک هش با استفاده از الگوریتم SHA256، که باید در BASE64Url کدگذاری شود (متفاوت از Base64 توسط دو کاراکتر جدول) و حذف انتهای خط کاراکتر: =.

در مرحله بعد، باید شروع به گوش دادن به http در ماشین محلی کنیم تا پس از مجوز، پاسخی دریافت کنیم که به صورت تغییر مسیر برگردانده می شود.

کارهای اداری روی یک سرور خاص انجام می شود، ما نمی توانیم این احتمال را رد کنیم که چندین مدیر همزمان اسکریپت را اجرا کنند، بنابراین به طور تصادفی یک پورت برای کاربر فعلی انتخاب می کند، اما من پورت های از پیش تعریف شده را مشخص کردم زیرا آنها همچنین باید به عنوان مورد اعتماد در کنسول API اضافه شوند.

access_type=آفلاین به این معنی که برنامه می تواند یک توکن منقضی شده را به تنهایی بدون تعامل کاربر با مرورگر به روز کند،
answer_type=کد فرمت نحوه بازگرداندن کد را تنظیم می کند (اشاره به روش مجوز قدیمی، زمانی که کاربر کد را از مرورگر در اسکریپت کپی می کند)،
حوزه دامنه و نوع دسترسی را نشان می دهد. آنها باید با فاصله یا %20 (بر اساس کدگذاری URL) از هم جدا شوند. لیستی از مناطق دسترسی با انواع را می توان در اینجا مشاهده کرد: OAuth 2.0 Scopes for Google APIs.

پس از دریافت کد مجوز، برنامه یک پیام نزدیک به مرورگر برمی‌گرداند، گوش دادن به پورت را متوقف می‌کند و یک درخواست POST برای دریافت توکن ارسال می‌کند. ما در آن شناسه و راز مشخص شده قبلی از API کنسول را نشان می دهیم، آدرسی که کاربر به آن هدایت می شود و مطابق با مشخصات پروتکل grant_type می شود.

در پاسخ، یک توکن Access، مدت اعتبار آن در چند ثانیه و یک توکن Refresh دریافت می کنیم که با آن می توانیم توکن Access را به روز کنیم.

برنامه باید توکن‌ها را در مکانی امن با ماندگاری طولانی ذخیره کند، بنابراین تا زمانی که دسترسی دریافت‌شده را لغو نکنیم، برنامه توکن تازه‌سازی را برنمی‌گرداند. در پایان، من یک درخواست برای لغو توکن اضافه کردم؛ اگر برنامه با موفقیت تکمیل نشد و توکن رفرش بازگردانده نشد، روند دوباره شروع می‌شود (ما ذخیره توکن‌ها را به صورت محلی در ترمینال ناامن می‌دانیم و انجام نمی‌دهیم. نمی خواهم کارها را با رمزنگاری پیچیده کنم یا مرورگر را مرتباً باز کنم).

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
}

همانطور که قبلاً متوجه شدید، هنگام لغو یک توکن، از Invoke-WebRequest استفاده می شود. برخلاف Invoke-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'])
      }
    }
  }
}

در هر درخواست، شما باید یک هدر Authorization حاوی نوع توکن و خود رمز دسترسی ارسال کنید. در حال حاضر، نوع توکن همیشه حامل است. زیرا باید بررسی کنیم که توکن منقضی نشده باشد و بعد از یک ساعت از لحظه صدور آن را به روز کنیم، من یک درخواست برای تابع دیگری که یک توکن Access را برمی گرداند، مشخص کردم. همان قطعه کد در ابتدای اسکریپت هنگام دریافت اولین نشانه دسترسی است:

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 می خواهد که دقیقاً به دنبال کاربری با همان ایمیل، از جمله نام مستعار، بگردد. همچنین می توانید از wildcard استفاده کنید: =، :، :{PREFIX}*.

برای به دست آوردن داده ها، از روش درخواست GET، برای درج داده ها (ایجاد یک حساب کاربری یا اضافه کردن یک عضو به یک گروه) - POST، برای به روز رسانی داده های موجود - PUT، برای حذف یک رکورد (به عنوان مثال، عضوی از یک گروه) - استفاده کنید. حذف.

اسکریپت همچنین یک شماره تلفن (رشته نامعتبر) و گنجاندن در یک گروه توزیع منطقه ای را درخواست می کند. بر اساس 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
}

توابع به روز رسانی و ایجاد حساب دارای نحو مشابهی هستند؛ همه فیلدهای اضافی مورد نیاز نیستند؛ در بخش شماره تلفن، باید آرایه ای را مشخص کنید که می تواند حداکثر یک رکورد با شماره و نوع آن داشته باشد.

برای اینکه هنگام اضافه کردن کاربر به یک گروه خطایی دریافت نکنیم، ابتدا می‌توانیم با دریافت لیستی از اعضای گروه یا ترکیب از خود کاربر، بررسی کنیم که آیا او قبلاً عضو این گروه است یا خیر.

پرس و جو عضویت گروهی یک کاربر خاص بازگشتی نخواهد بود و فقط عضویت مستقیم را نشان می دهد. قرار دادن کاربری در گروه والدین که قبلاً دارای یک گروه فرزند است که کاربر عضو آن است، موفق خواهد بود.

نتیجه

تنها چیزی که باقی می ماند ارسال رمز عبور حساب جدید برای کاربر است. ما این کار را از طریق پیامک انجام می دهیم و اطلاعات کلی را به همراه دستورالعمل و ورود به ایمیل شخصی ارسال می کنیم که به همراه شماره تلفن توسط بخش کارگزینی ارائه شده است. به عنوان یک جایگزین، می توانید در هزینه خود صرفه جویی کنید و رمز عبور خود را به یک چت تلگرام مخفی ارسال کنید، که می تواند عامل دوم نیز در نظر گرفته شود (مک بوک ها استثنا خواهند بود).

ممنون که تا آخر خواندید. خوشحال می شوم پیشنهاداتی را برای بهبود سبک مقاله نویسی ببینم و آرزو می کنم هنگام نوشتن اسکریپت ها خطاهای کمتری داشته باشید =)

لیست پیوندهایی که ممکن است از نظر موضوعی مفید باشند یا به سادگی به سوالات پاسخ دهند:

منبع: www.habr.com

اضافه کردن نظر