إنشاء مستخدمي Google من PowerShell عبر API

مرحبا!

ستصف هذه المقالة كيفية تفاعل PowerShell مع Google API للتعامل مع مستخدمي G Suite.

في المنظمة ، نستخدم العديد من الخدمات الداخلية والسحابة. بالنسبة للجزء الأكبر ، يعود التفويض فيها إلى Google أو Active Directory ، حيث لا يمكننا الاحتفاظ بنسخة متماثلة بينهما ، على التوالي ، عندما يغادر موظف جديد ، تحتاج إلى إنشاء / تمكين حساب في هذين النظامين. لأتمتة العملية ، قررنا كتابة نص يقوم بجمع المعلومات وإرسالها إلى كلتا الخدمتين.

ترخيص

عند تجميع المتطلبات ، قررنا استخدام مسئولين بشريين حقيقيين للحصول على إذن ، وهذا يبسط تحليل الإجراءات في حالة حدوث تغييرات ضخمة عرضية أو مقصودة.

تستخدم Google APIs بروتوكول OAuth 2.0 للمصادقة والتفويض. يمكن العثور على سيناريوهات الاستخدام ووصف أكثر تفصيلاً هنا: استخدام OAuth 2.0 للوصول إلى Google APIs.

اخترت البرنامج النصي المستخدم للترخيص في تطبيقات سطح المكتب. هناك أيضًا خيار لاستخدام حساب خدمة لا يتطلب تحركات غير ضرورية من المستخدم.

الصورة أدناه وصف تخطيطي للسيناريو المحدد من صفحة Google.

إنشاء مستخدمي Google من PowerShell عبر API

  1. أولاً ، نرسل المستخدم إلى صفحة مصادقة حساب Google ، مع تحديد معلمات GET:
    • رقم الاستمارة
    • المناطق التي يحتاج التطبيق الوصول إليها
    • العنوان الذي سيتم إعادة توجيه المستخدم إليه بعد الانتهاء من الإجراء
    • الطريقة التي سنقوم بها بتحديث الرمز المميز
    • رمز الحماية
    • تنسيق إرسال رمز التحقق

  2. بعد اكتمال التفويض ، سيتم إعادة توجيه المستخدم إلى الصفحة المحددة في الطلب الأول ، مع وجود خطأ أو رمز تفويض تم تمريره بواسطة معلمات GET
  3. سيحتاج التطبيق (البرنامج النصي) إلى الحصول على هذه المعلمات ، وإذا تم استلام الرمز ، فقم بتنفيذ الطلب التالي للحصول على الرموز المميزة
  4. عند الطلب بشكل صحيح ، تعود Google API:
    • رمز الوصول الذي يمكننا من خلاله تقديم الطلبات
    • تاريخ انتهاء صلاحية هذا الرمز المميز
    • مطلوب تحديث رمز لتحديث رمز الوصول.

تحتاج أولاً إلى الانتقال إلى وحدة تحكم Google API: بيانات الاعتماد - وحدة تحكم واجهة برمجة تطبيقات Google، وحدد التطبيق المطلوب ، وفي قسم بيانات الاعتماد ، أنشئ معرف عميل 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

قمنا بتعيين معرف العميل وسر العميل الذي تم الحصول عليه في خصائص معرف عميل OAuth ، ومدقق الكود عبارة عن سلسلة من 43 إلى 128 حرفًا يجب إنشاؤها عشوائيًا من الأحرف غير المحجوزة: [AZ] / [az] / [0-9] / "-" / "." / "_" / "~".

سيتم بعد ذلك إعادة إرسال هذا الرمز. إنه يزيل الثغرة الأمنية التي يمكن للمهاجم من خلالها اعتراض الاستجابة التي يتم إرجاعها من خلال إعادة التوجيه بعد أن يتم تفويض المستخدم.
يمكنك إرسال مدقق الكود في الطلب الحالي بنص واضح (مما يجعله بلا معنى - وهذا مناسب فقط للأنظمة التي لا تدعم SHA256) ، أو عن طريق إنشاء تجزئة باستخدام خوارزمية SHA256 ، والتي يجب ترميزها في BASE64Url (مختلفة من Base64 بواسطة حرفين من جدول) وإزالة نهايات سطر الأحرف: =.

بعد ذلك ، نحتاج إلى بدء الاستماع إلى http على الجهاز المحلي لتلقي رد بعد التفويض ، والذي سيتم إرجاعه عن طريق إعادة التوجيه.

يتم تنفيذ المهام الإدارية على خادم مخصص ، ولا يمكننا استبعاد احتمال قيام العديد من المسؤولين بتشغيل البرنامج النصي في نفس الوقت ، لذلك سيختار منفذًا عشوائيًا للمستخدم الحالي ، لكنني حددت المنافذ المحددة مسبقًا ، لأن. يجب أيضًا إضافتها على أنها موثوقة في وحدة تحكم واجهة برمجة التطبيقات.

access_type = غير متصل يعني أن التطبيق يمكنه تجديد رمز مميز منتهي الصلاحية من تلقاء نفسه دون تفاعل المستخدم مع المتصفح ،
response_type = كود يضبط تنسيق كيفية إرجاع الكود (إشارة إلى طريقة الترخيص القديمة ، عندما قام المستخدم بنسخ الكود من المتصفح إلى البرنامج النصي) ،
نطاق يحدد النطاقات ونوع الوصول. يجب أن تكون مفصولة بمسافات أو٪ 20 (وفقًا لتشفير URL). يمكن رؤية قائمة مناطق الوصول مع الأنواع هنا: نطاقات OAuth 2.0 لواجهات برمجة تطبيقات Google.

بعد استلام رمز التفويض ، سيعيد التطبيق رسالة إغلاق إلى المتصفح ، ويتوقف عن الاستماع على المنفذ ويرسل طلب POST لاستلام الرمز المميز. نحدد فيه المعرف والسري المحددين مسبقًا من واجهة برمجة تطبيقات وحدة التحكم ، والعنوان الذي سيتم إعادة توجيه المستخدم إليه ونوع المنحة وفقًا لمواصفات البروتوكول.

رداً على ذلك ، سوف نتلقى رمز الوصول ، وفترة صلاحيته بالثواني ، ورمز التحديث ، والذي يمكننا من خلاله تحديث رمز الوصول.

يجب أن يخزن التطبيق الرموز المميزة في مكان آمن مع فترة صلاحية طويلة ، لذلك حتى نقوم بإلغاء الوصول المستلم ، لن يتم إرجاع رمز التحديث إلى التطبيق. في النهاية ، أضفت طلبًا لإلغاء الرمز المميز ، إذا لم يكتمل التطبيق بنجاح ولم يعود رمز التحديث ، فسيبدأ الإجراء مرة أخرى (اعتبرنا أنه من غير الآمن تخزين الرموز محليًا على الجهاز ، ونحن لا نفعل ذلك لا أريد تعقيدها بالتشفير أو فتح المتصفح كثيرًا).

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

في كل طلب ، تحتاج إلى إرسال رأس ترخيص يحتوي على نوع الرمز المميز ورمز الوصول نفسه. حاليًا ، نوع الرمز المميز هو حامل دائمًا. لأن نحتاج إلى التحقق من عدم انتهاء صلاحية الرمز المميز وتحديثه بعد ساعة من لحظة الإصدار ، لقد حددت طلبًا لوظيفة أخرى تقوم بإرجاع رمز الوصول. يوجد نفس الجزء من التعليمات البرمجية في بداية البرنامج النصي عند تلقي أول رمز وصول:

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
}

البريد الإلكتروني: طلب استعلام $ سيطلب من واجهة برمجة التطبيقات البحث عن مستخدم بهذا البريد الإلكتروني بالضبط ، بما في ذلك الأسماء المستعارة. يمكنك أيضًا استخدام حرف البدل: =،:،: {PREFIX} *.

للحصول على البيانات ، يتم استخدام طريقة طلب GET ، لإدخال البيانات (إنشاء حساب أو إضافة عضو إلى مجموعة) - POST ، لتحديث البيانات الموجودة - PUT ، لحذف سجل (على سبيل المثال ، عضو من مجموعة) - يمسح.

سيطلب البرنامج النصي أيضًا رقم هاتف (سلسلة لم يتم التحقق من صحتها) وإدراجها في مجموعة توزيع إقليمية. يقرر الوحدة التنظيمية التي يجب أن يمتلكها المستخدم استنادًا إلى الوحدة التنظيمية المحددة لـ Active Directory وسيأتي بكلمة مرور:

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
}

تحتوي وظيفتا التحديث وإنشاء الحساب على نفس البنية ، وليست جميع الحقول الإضافية مطلوبة ، في القسم الذي يحتوي على أرقام الهاتف ، تحتاج إلى تحديد مصفوفة يمكن أن تحتوي على سجل واحد على الأقل برقم ونوعه.

من أجل عدم حدوث خطأ عند إضافة مستخدم إلى مجموعة ، يمكننا أولاً التحقق مما إذا كان موجودًا بالفعل في هذه المجموعة من خلال الحصول على قائمة بأعضاء المجموعة أو التكوين من المستخدم نفسه.

لن يكون الاستعلام عن عضوية المجموعة لمستخدم معين متكررًا وسيظهر فقط العضوية الفورية. سينجح تضمين مستخدم في مجموعة رئيسية لديها بالفعل مجموعة فرعية يكون المستخدم عضوًا فيها.

اختتام

يبقى إرسال كلمة مرور لحساب جديد للمستخدم. نقوم بذلك عبر الرسائل النصية القصيرة ، ونرسل معلومات عامة مع التعليمات وتسجيل الدخول إلى البريد الشخصي ، والذي تم توفيره مع رقم الهاتف من قبل قسم التوظيف. بدلاً من ذلك ، يمكنك توفير المال وإرسال كلمة المرور إلى الدردشة البرقية السرية ، والتي يمكن اعتبارها أيضًا العامل الثاني (ستكون أجهزة Macbook استثناءً).

شكرا لك على القراءة حتى النهاية. سأكون سعيدًا برؤية اقتراحات لتحسين أسلوب كتابة المقالات وأتمنى أن تقلل من الأخطاء عند كتابة النصوص =)

قائمة الروابط التي قد تكون مفيدة من حيث الموضوع أو الإجابة على الأسئلة فقط:

المصدر: www.habr.com

إضافة تعليق