تحية!
توضح هذه المقالة كيفية تفاعل PowerShell مع Google API لمعالجة مستخدمي G Suite.
في المنظمة ، نستخدم العديد من الخدمات الداخلية والسحابية. بالنسبة للجزء الأكبر ، يأتي التفويض فيها إلى Google أو Active Directory ، حيث لا يمكننا الاحتفاظ بنسخة متماثلة ، على التوالي ، عندما يتم إصدار موظف جديد ، فإنك تحتاج إلى إنشاء / تمكين حساب في هذين النظامين. لأتمتة العملية ، قررنا كتابة برنامج نصي يجمع المعلومات ويرسلها إلى كلتا الخدمتين.
ترخيص
عند تكوين المتطلبات ، قررنا استخدام أشخاص حقيقيين كمسؤولين للحصول على إذن ، وهذا يبسط تحليل الإجراءات في حالة حدوث تغييرات هائلة عن غير قصد أو مقصودة.
تستخدم واجهات برمجة تطبيقات Google بروتوكول OAuth 2.0 للمصادقة والترخيص. يمكن استخدام حالات الاستخدام ووصف أكثر تفصيلًا هنا:
استخدام OAuth 2.0 للوصول إلى واجهات برمجة تطبيقات Google .
اخترت البرنامج النصي المستخدم للترخيص في تطبيقات سطح المكتب. هناك أيضًا خيار لاستخدام حساب خدمة لا يتطلب حركات غير ضرورية من المستخدم.
الصورة أدناه هي وصف تخطيطي للسيناريو المحدد من صفحة Google.

- أولاً ، نرسل المستخدم إلى صفحة المصادقة في حساب Google ، مع الإشارة إلى معلمات GET:
- معرف التطبيق
- المناطق التي يحتاج التطبيق إلى الوصول إليها
- العنوان الذي سيتم إعادة توجيه المستخدم إليه بعد الانتهاء من الإجراء
- الطريقة التي سنقوم بتحديث الرمز
- رمز التحقق
- شكل انتقال رمز التحقق
- بعد اكتمال التفويض ، سيتم إعادة توجيه المستخدم إلى الصفحة المحددة في الطلب الأول ، مع إرسال رمز خطأ أو ترخيص بواسطة معلمات GET
- سيحتاج التطبيق (البرنامج النصي) إلى الحصول على هذه المعلمات ، وفي حالة تلقي الرمز ، قم بتنفيذ الطلب التالي للرموز المميزة
- إذا كان الطلب صحيحًا ، فتُرجع واجهة برمجة تطبيقات Google:
- رمز الوصول الذي يمكننا من خلاله تقديم طلبات
- صلاحية هذا الرمز
- قم بتحديث الرمز المميز اللازم لتحديث الرمز المميز للوصول.
تحتاج أولاً إلى الانتقال إلى وحدة التحكم في واجهة برمجة تطبيقات Google:
بيانات الاعتماد - وحدة التحكم في واجهة برمجة تطبيقات 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 على الجهاز المحلي للحصول على استجابة بعد التخويل ، والذي سيعود كإعادة توجيه.
يتم تنفيذ المهام الإدارية على خادم خاص ، لا يمكننا استبعاد احتمال قيام العديد من المسؤولين بتشغيل البرنامج النصي في نفس الوقت ، لذلك سيختار عشوائيًا منفذًا للمستخدم الحالي ، لكنني حددت منافذ محددة مسبقًا ، لأن يجب أيضًا إضافتها على أنها موثوق بها في وحدة تحكم API.
access_type = بلا اتصال يعني أن التطبيق يمكنه تحديث الرمز المميز منتهي الصلاحية بشكل مستقل دون تدخل المستخدم مع المستعرض ،
response_type = الكود يحدد التنسيق لكيفية إرجاع الكود (يشير إلى طريقة التفويض القديمة عندما يقوم المستخدم بنسخ الكود من المستعرض إلى البرنامج النصي) ،
يشير
النطاق إلى
نطاق ونوع الوصول. يجب أن تكون مفصولة بمسافات أو٪ 20 (وفقًا لترميز URL). يمكن الاطلاع على قائمة بمناطق الوصول بأنواعها:
OAuth 2.0 Scopes for Google APIs .
بعد تلقي رمز التفويض ، سيقوم التطبيق بإرجاع رسالة إغلاق إلى المستعرض ، والتوقف عن الاستماع إلى المنفذ وإرسال طلب 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 }
طلب البريد الإلكتروني: سيطلب الاستعلام $ من واجهة برمجة التطبيقات (API) البحث عن مستخدم له عنوان البريد الإلكتروني هذا بالضبط ، بما في ذلك الأسماء المستعارة. يمكنك أيضًا استخدام أحرف البدل:
= ،: ،: {PREFIX} * .
للحصول على البيانات ، يتم استخدام طريقة طلب GET ، لإدراج البيانات (إنشاء حساب أو إضافة عضو إلى مجموعة) - POST ، لتحديث البيانات الموجودة - PUT ، لحذف إدخال (على سبيل المثال ، مشارك من مجموعة) - DELETE.
سيطلب البرنامج النصي أيضًا رقم هاتف (سلسلة غير صالحة) وإدراجه في مجموعة توزيع إقليمية. يقرر الوحدة التنظيمية التي يجب أن يكون المستخدم على أساس 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 = 'moscowoffice@rocketguys.com' email = $email } (GoogleQuery 'AddMember' $query) | fl }
وظائف تحديث وإنشاء حساب لها نفس الصيغة ، وليس كل الحقول الإضافية مطلوبة ، في القسم الذي يحتوي على أرقام هواتف تحتاج إلى تحديد صفيف يمكن أن يحتوي من سجل واحد برقم ونوعه.
من أجل عدم حدوث خطأ عند إضافة مستخدم إلى مجموعة ، يمكننا أولاً التحقق مما إذا كان موجودًا بالفعل في هذه المجموعة من خلال تلقي قائمة بأعضاء المجموعة أو تكوينها من المستخدم نفسه.
لن يكون طلب تكوين مجموعات مستخدم معين متكررًا وسيُظهر فقط العضوية المباشرة. إن إدراج المستخدم في المجموعة الأم ، والتي كانت المجموعة الفرعية التي يكون المستخدم عضوًا فيها ، قد نجحت بالفعل.
استنتاج
يبقى لإرسال المستخدم كلمة المرور للحساب الجديد. نقوم بذلك من خلال الرسائل النصية القصيرة ، ونرسل معلومات عامة تحتوي على إرشادات وتسجيل الدخول إلى البريد الشخصي ، والذي تم توفيره مع رقم الهاتف بواسطة قسم اختيار الموظفين. وكبديل لذلك ، يمكنك توفير المال وإرسال كلمة مرور إلى دردشة برقية سرية ، والتي يمكن اعتبارها أيضًا العامل الثاني (ستكون macbooks استثناءً).
شكرا لك على القراءة حتى النهاية. سأكون سعيدًا لرؤية اقتراحات لتحسين أسلوب كتابة المقالات وأتمنى لك أن تصاب بأخطاء أقل عند كتابة البرامج النصية =)
قائمة الروابط التي قد تكون مفيدة من الناحية الموضوعية أو مجرد الإجابة على أسئلتك: