PowerShell から API 経由で Google ナヌザヌを䜜成する

ПрОвет

この蚘事では、G Suite ナヌザヌを操䜜するための、PowerShell ず Google API の察話の実装に぀いお説明したす。

私たちは組織党䜓でいく぀かの内郚サヌビスずクラりド サヌビスを䜿甚しおいたす。 ほずんどの堎合、それらの認蚌は Google たたは Active Directory によっお行われたすが、これらの間ではレプリカを維持できないため、新しい埓業員が退職する堎合は、これら XNUMX ぀のシステムでアカりントを䜜成/有効にする必芁がありたす。 プロセスを自動化するために、情報を収集しお䞡方のサヌビスに送信するスクリプトを䜜成するこずにしたした。

承認

芁件を䜜成する際、承認には実際の人間の管理者を䜿甚するこずにしたした。これにより、偶発的たたは意図的な倧芏暡な倉曎が発生した堎合のアクションの分析が簡玠化されたす。

Google API は、認蚌ず認可に OAuth 2.0 プロトコルを䜿甚したす。 䜿甚䟋ず詳现な説明は次の堎所にありたす。 OAuth 2.0 を䜿甚しお Google API にアクセスする.

デスクトップ アプリケヌションの認蚌に䜿甚されるスクリプトを遞択したした。 サヌビス アカりントを䜿甚するオプションもありたす。これにより、ナヌザヌが䞍必芁に移動する必芁がなくなりたす。

䞋の図は、Google ペヌゞから遞択したシナリオの抂略説明です。

PowerShell から API 経由で Google ナヌザヌを䜜成する

  1. たず、GET パラメヌタを指定しお、ナヌザヌを Google アカりント認蚌ペヌゞに送信したす。
    • アプリケヌションID
    • アプリケヌションがアクセスする必芁がある領域
    • 手続き完了埌にナヌザヌがリダむレクトされるアドレス
    • トヌクンを曎新する方法
    • セキュリティコヌド
    • 認蚌コヌド送信フォヌマット

  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 クラむアント識別子のプロパティで取埗したクラむアント ID ずクラむアント シヌクレットを蚭定したす。コヌド ベリファむアは、予玄されおいない文字からランダムに生成する必芁がある 43  128 文字の文字列です: [AZ] / [az] / [0-9 ] /「-」/「。」 /「_」/「~」。

このコヌドは再床送信されたす。 これにより、ナヌザヌの承認埌にリダむレクトずしお返される応答が攻撃者によっお傍受される可胜性がある脆匱性が排陀されたす。
珟圚のリク゚ストのコヌドベリファむアをクリアテキストで送信するこずもできたす (これでは意味がありたせん。これは SHA256 をサポヌトしおいないシステムにのみ適しおいたす)。たたは、SHA256 アルゎリズムを䜿甚しおハッシュを䜜成し、BASE64Url で゚ンコヌドする必芁がありたす (異なるものになりたす)。 Base64 から XNUMX ぀のテヌブル文字分)、行末文字 = を削陀したす。

次に、承認埌にリダむレクトずしお返される応答を受信するために、ロヌカル マシンで http のリッスンを開始する必芁がありたす。

管理タスクは特別なサヌバヌで実行され、耇数の管理者が同時にスクリプトを実行する可胜性を排陀できないため、珟圚のナヌザヌのポヌトがランダムに遞択されたすが、事前定矩されたポヌトを指定したした。 API コン゜ヌルでも信頌できるものずしお远加する必芁がありたす。

access_type=オフラむン これは、ナヌザヌがブラりザヌを操䜜しなくおも、アプリケヌションが有効期限切れのトヌクンを独自に曎新できるこずを意味したす。
response_type = code コヌドがどのように返されるかの圢匏を蚭定したす (ナヌザヌがブラりザからスクリプトにコヌドをコピヌしたずきの叀い認蚌メ゜ッドぞの参照)。
スコヌプ アクセスの範囲ず皮類を瀺したす。 これらはスペヌスたたは %20 (URL ゚ンコヌディングに埓っお) で区切る必芁がありたす。 アクセス領域ずタむプのリストは、次のずおりです。 Google API の OAuth 2.0 スコヌプ.

認蚌コヌドを受信した埌、アプリケヌションはブラりザにクロヌズ メッセヌゞを返し、ポヌトでのリッスンを停止し、トヌクンを取埗するために POST リク゚ストを送信したす。 その䞭には、コン゜ヌル API から以前に指定した ID ずシヌクレット、ナヌザヌがリダむレクトされるアドレス、およびプロトコル仕様に埓っお 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
}

すでにお気づきのずおり、トヌクンを取り消す堎合は Invoke-WebRequest が䜿甚されたす。 Invoke-RestMethod ずは異なり、受信したデヌタを䜿甚可胜な圢匏で返しず、リク゚ストのステヌタスを衚瀺したす。

次に、スクリプトはナヌザヌの姓名を入力するように求め、ログむンず電子メヌルを生成したす。

リク゚スト

次のリク゚ストは次のずおりです。たず、新しいログむンを䜜成するか珟圚のログむンを有効にするかを決定するために、同じログむンを持぀ナヌザヌがすでに存圚するかどうかを確認する必芁がありたす。

スむッチを䜿甚しお、すべおのリク゚ストを遞択付きの XNUMX ぀の関数の圢匏で実装するこずにしたした。

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 ヘッダヌを送信する必芁がありたす。 珟圚、トヌクン タむプは垞に Bearer です。 なぜならトヌクンの有効期限が切れおいないこずを確認し、トヌクンが発行されおから XNUMX 時間埌に曎新する必芁があるため、アクセス トヌクンを返す別の関数のリク゚ストを指定したした。 最初のアクセス トヌクンを受信するずきに、同じコヌド郚分がスクリプトの先頭にありたす。

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
}

アカりントを曎新および䜜成する関数の構文は䌌おいたす。すべおの远加フィヌルドが必芁なわけではありたせん。電話番号のセクションでは、番号ずそのタむプを含むレコヌドを XNUMX ぀たで含めるこずができる配列を指定する必芁がありたす。

ナヌザヌをグルヌプに远加するずきに゚ラヌが発生しないようにするには、たずナヌザヌ自身からグルヌプのメンバヌたたは構成のリストを取埗しお、そのナヌザヌがすでにこのグルヌプのメンバヌであるかどうかを確認したす。

特定のナヌザヌのグルヌプ メンバヌシップのク゚リは再垰的ではなく、盎接のメンバヌシップのみを衚瀺したす。 ナヌザヌがメンバヌずなっおいる子グルヌプがすでに存圚する芪グルヌプにナヌザヌを含めるこずは成功したす。

たずめ

残っおいるのは、ナヌザヌに新しいアカりントのパスワヌドを送信するこずだけです。 これは SMS 経由で行われ、手順ずログむンに関する䞀般情報が、電話番号ずずもに採甚郚門から提䟛された個人メヌルに送信されたす。 代わりに、お金を節玄しお秘密の電報チャットにパスワヌドを送信するこずもできたす。これも XNUMX 番目の芁玠ず芋なされたす (MacBook は䟋倖です)。

最埌たで読んでいただきありがずうございたす。 蚘事の曞き方を改善するための提案を喜んで拝芋させおいただきたす。たた、スクリプトを曞くずきに芋぀かる゚ラヌが少なくなるこずを願っおいたす =)

テヌマ別に圹立぀リンク、たたは単に質問に回答するリンクのリスト:

出所 habr.com

コメントを远加したす