በኤፒአይ በኩል ከPowerShell Google ተጠቃሚዎችን መፍጠር

ሠላም!

ይህ መጣጥፍ የG Suite ተጠቃሚዎችን ለመቆጣጠር የPowerShell መስተጋብር ከGoogle API ጋር መተግበሩን ይገልጻል።

በድርጅቱ ውስጥ በርካታ የውስጥ እና የደመና አገልግሎቶችን እንጠቀማለን። በአብዛኛው፣ በእነሱ ውስጥ ያለው ፍቃድ ወደ Google ወይም Active Directory ይወርዳል፣ በመካከላቸው ቅጂ መያዝ አንችልም፤ በዚህ መሰረት፣ አዲስ ሰራተኛ ሲለቅ፣ በእነዚህ ሁለት ስርዓቶች ውስጥ መለያ መፍጠር/ማንቃት አለብዎት። ሂደቱን በራስ-ሰር ለማድረግ, መረጃን የሚሰበስብ እና ወደ ሁለቱም አገልግሎቶች የሚልክ ስክሪፕት ለመጻፍ ወሰንን.

ፈቃድ

መስፈርቶቹን በምንዘጋጅበት ጊዜ እውነተኛ የሰው አስተዳዳሪዎችን ለፈቃድ ለመጠቀም ወስነናል፣ ይህ በአጋጣሚ ወይም ሆን ተብሎ ከፍተኛ ለውጦች ሲከሰት የእርምጃዎችን ትንተና ቀላል ያደርገዋል።

Google APIs የOAuth 2.0 ፕሮቶኮልን ለማረጋገጫ እና ፍቃድ ይጠቀማሉ። የአጠቃቀም ጉዳዮችን እና የበለጠ ዝርዝር መግለጫዎችን እዚህ ማግኘት ይቻላል፡ ጉግል ኤፒአይዎችን ለመድረስ OAuth 2.0ን በመጠቀም.

በዴስክቶፕ አፕሊኬሽኖች ውስጥ ለፈቃድ ጥቅም ላይ የሚውለውን ስክሪፕት መርጫለሁ። እንዲሁም ከተጠቃሚው አላስፈላጊ እንቅስቃሴዎችን የማይፈልግ የአገልግሎት መለያ የመጠቀም አማራጭ አለ.

ከታች ያለው ስዕል ከ Google ገጽ ላይ የተመረጠውን ሁኔታ የሚያሳይ ንድፍ መግለጫ ነው.

በኤፒአይ በኩል ከPowerShell Google ተጠቃሚዎችን መፍጠር

  1. በመጀመሪያ፣ የGET መለኪያዎችን በመግለጽ ተጠቃሚውን ወደ Google መለያ ማረጋገጫ ገጽ እንልካለን።
    • የመተግበሪያ መታወቂያ
    • አፕሊኬሽኑ መዳረሻ የሚያስፈልጋቸው አካባቢዎች
    • አሰራሩን ከጨረሰ በኋላ ተጠቃሚው የሚዞርበት አድራሻ
    • ማስመሰያውን የምናዘምንበት መንገድ
    • የሚስጥር መለያ ቁጥር
    • የማረጋገጫ ኮድ ማስተላለፊያ ቅርጸት

  2. ፈቃዱ ከተጠናቀቀ በኋላ ተጠቃሚው በGET ግቤቶች የተላለፈ ስህተት ወይም የፈቀዳ ኮድ ወደ መጀመሪያው ጥያቄ ወደተገለጸው ገጽ ይመራል።
  3. አፕሊኬሽኑ (ስክሪፕት) እነዚህን መመዘኛዎች መቀበል እና ኮዱን ከተቀበለ ቶከን ለማግኘት የሚከተለውን ጥያቄ ማቅረብ ይኖርበታል
  4. ጥያቄው ትክክል ከሆነ ጎግል ኤፒአይ ይመልሳል፡-
    • ጥያቄዎችን የምንጠይቅበት የመዳረሻ ማስመሰያ
    • የዚህ ማስመሰያ ትክክለኛ ጊዜ
    • የመዳረሻ ማስመሰያውን ለማደስ የማደስ ማስመሰያ ያስፈልጋል።

መጀመሪያ ወደ Google API console መሄድ ያስፈልግዎታል፡- ምስክርነቶች - Google API Consoleየተፈለገውን መተግበሪያ ይምረጡ እና በምስክርነት ክፍል ውስጥ ደንበኛ OAuth መለያ ይፍጠሩ። እዚያ (ወይም በኋላ, በተፈጠረው መለያ ባህሪያት ውስጥ) አቅጣጫ መቀየር የሚፈቀድባቸውን አድራሻዎች መግለጽ ያስፈልግዎታል. በእኛ ሁኔታ፣ እነዚህ የተለያዩ ወደቦች ያላቸው በርካታ የአካባቢ አስተናጋጅ ግቤቶች ይሆናሉ (ከዚህ በታች ይመልከቱ)።

የስክሪፕት አልጎሪዝምን ለማንበብ የበለጠ ምቹ ለማድረግ የመተግበሪያውን የመዳረሻ እና የማደስ ምልክቶችን በሚመልስ በተለየ ተግባር ውስጥ የመጀመሪያ እርምጃዎችን ማሳየት ይችላሉ-

$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=ከመስመር ውጭ ማለት አፕሊኬሽኑ ያለተጠቃሚው ከአሳሹ ጋር ያለ መስተጋብር ጊዜ ያለፈበትን ማስመሰያ በራሱ ማዘመን ይችላል፣
ምላሽ_አይነት = ኮድ ኮዱ እንዴት እንደሚመለስ ቅርጸቱን ያዘጋጃል (የቀድሞው የፈቀዳ ዘዴ ማጣቀሻ ፣ ተጠቃሚው ኮዱን ከአሳሹ ወደ ስክሪፕቱ ሲገለብጥ) ፣
ወሰን የመዳረሻውን ስፋት እና አይነት ያመለክታል. በቦታ ወይም በ%20 (በዩአርኤል ኢንኮዲንግ መሠረት) መለየት አለባቸው። ዓይነቶች ያሏቸው የመዳረሻ ቦታዎች ዝርዝር እዚህ ሊታይ ይችላል- የOAuth 2.0 ወሰን ለGoogle APIs.

የፍቃድ ኮዱን ከተቀበለ በኋላ አፕሊኬሽኑ የቅርብ መልእክት ወደ አሳሹ ይመልሳል ፣ ወደቡ ላይ ማዳመጥ ያቆማል እና ቶከን ለማግኘት የPOST ጥያቄ ይልካል። በውስጡ ከዚህ ቀደም የተገለጸውን መታወቂያ እና ሚስጥር ከኮንሶል ኤፒአይ፣ ተጠቃሚው የሚዞርበትን አድራሻ እና በፕሮቶኮል ስፔሲፊኬሽኑ መሰረት give_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-Webጥያቄ ጥቅም ላይ ይውላል። እንደ 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 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
}

መለያን የማዘመን እና የመፍጠር ተግባራት ተመሳሳይ አገባብ አላቸው ሁሉም ተጨማሪ መስኮች አያስፈልጉም ፣ስልክ ቁጥሮች ባለው ክፍል ውስጥ ከቁጥሩ እና ከአይነቱ ጋር እስከ አንድ ግቤት ያለው ድርድር መግለጽ ያስፈልግዎታል።

ተጠቃሚን ወደ ቡድን ስንጨምር ስህተት እንዳይደርስብን በመጀመሪያ ከራሱ ተጠቃሚው የቡድን አባላትን ዝርዝር ወይም ቅንብርን በማግኝት የዚህ ቡድን አባል መሆን አለመሆኑን ማረጋገጥ እንችላለን።

የአንድ የተወሰነ ተጠቃሚ የቡድን አባልነት ጥያቄ ተደጋጋሚ አይሆንም እና ቀጥተኛ አባልነትን ብቻ ያሳያል። አስቀድሞ የልጅ ቡድን ያለው ተጠቃሚው አባል የሆነበት በወላጅ ቡድን ውስጥ ያለ ተጠቃሚን ማካተት ይሳካል።

መደምደሚያ

የሚቀረው ለአዲሱ መለያ የይለፍ ቃል ለተጠቃሚው መላክ ብቻ ነው። ይህንን በኤስኤምኤስ እንሰራለን እና አጠቃላይ መረጃን ከመመሪያዎች ጋር እንልካለን እና ወደ ግላዊ ኢሜል ይግቡ ፣ ከስልክ ቁጥር ጋር ፣ በቅጥር ክፍል የቀረበ። እንደ አማራጭ ገንዘብ መቆጠብ እና የይለፍ ቃልዎን ወደ ሚስጥራዊ የቴሌግራም ውይይት መላክ ይችላሉ ፣ ይህ ደግሞ እንደ ሁለተኛው ምክንያት ተደርጎ ሊወሰድ ይችላል (MacBooks የተለየ ይሆናል)።

እስከ መጨረሻው ስላነበባችሁ እናመሰግናለን። የአጻጻፍ ስልቶችን ለማሻሻል ምክሮችን በማየቴ ደስተኛ ነኝ እና ስክሪፕቶችን በሚጽፉበት ጊዜ ያነሱ ስህተቶችን እንዲይዙ እመኛለሁ =)

በገጽታ ጠቃሚ ሊሆኑ የሚችሉ ወይም በቀላሉ ጥያቄዎችን የሚመልሱ አገናኞች ዝርዝር፡-

ምንጭ: hab.com

አስተያየት ያክሉ