Δημιουργία χρηστών Google από το PowerShell μέσω API

Γεια σας!

Αυτό το άρθρο θα περιγράψει την υλοποίηση της αλληλεπίδρασης του PowerShell με το Google API για τον χειρισμό των χρηστών του G Suite.

Χρησιμοποιούμε πολλές εσωτερικές υπηρεσίες και υπηρεσίες cloud σε ολόκληρο τον οργανισμό. Ως επί το πλείστον, η εξουσιοδότηση σε αυτά προέρχεται από την Google ή την υπηρεσία καταλόγου Active Directory, μεταξύ των οποίων δεν μπορούμε να διατηρήσουμε αντίγραφο. Συνεπώς, όταν αποχωρεί ένας νέος υπάλληλος, πρέπει να δημιουργήσετε/ενεργοποιήσετε έναν λογαριασμό σε αυτά τα δύο συστήματα. Για να αυτοματοποιήσουμε τη διαδικασία, αποφασίσαμε να γράψουμε ένα σενάριο που συλλέγει πληροφορίες και τις στέλνει και στις δύο υπηρεσίες.

Είσοδος

Κατά την κατάρτιση των απαιτήσεων, αποφασίσαμε να χρησιμοποιήσουμε πραγματικούς ανθρώπινους διαχειριστές για εξουσιοδότηση· αυτό απλοποιεί την ανάλυση των ενεργειών σε περίπτωση τυχαίων ή σκόπιμων τεράστιων αλλαγών.

Τα API της Google χρησιμοποιούν το πρωτόκολλο OAuth 2.0 για έλεγχο ταυτότητας και εξουσιοδότηση. Περιπτώσεις χρήσης και πιο λεπτομερείς περιγραφές μπορείτε να βρείτε εδώ: Χρήση του OAuth 2.0 για πρόσβαση στα API της Google.

Επέλεξα το σενάριο που χρησιμοποιείται για εξουσιοδότηση σε εφαρμογές επιφάνειας εργασίας. Υπάρχει επίσης η επιλογή χρήσης λογαριασμού υπηρεσίας, ο οποίος δεν απαιτεί άσκοπες μετακινήσεις από τον χρήστη.

Η παρακάτω εικόνα είναι μια σχηματική περιγραφή του επιλεγμένου σεναρίου από τη σελίδα Google.

Δημιουργία χρηστών Google από το PowerShell μέσω API

  1. Αρχικά, στέλνουμε τον χρήστη στη σελίδα ελέγχου ταυτότητας Λογαριασμού Google, προσδιορίζοντας τις παραμέτρους GET:
    • αναγνωριστικό εφαρμογής
    • περιοχές στις οποίες χρειάζεται πρόσβαση η εφαρμογή
    • τη διεύθυνση στην οποία θα ανακατευθυνθεί ο χρήστης μετά την ολοκλήρωση της διαδικασίας
    • τον τρόπο με τον οποίο θα ενημερώσουμε το διακριτικό
    • Κωδικός ασφαλείας
    • Μορφή μετάδοσης κωδικού επαλήθευσης

  2. Μετά την ολοκλήρωση της εξουσιοδότησης, ο χρήστης θα ανακατευθυνθεί στη σελίδα που καθορίστηκε στο πρώτο αίτημα, με ένα σφάλμα ή κωδικό εξουσιοδότησης που θα περάσει από τις παραμέτρους GET
  3. Η εφαρμογή (script) θα χρειαστεί να λάβει αυτές τις παραμέτρους και, εάν λάβει τον κωδικό, να υποβάλει το ακόλουθο αίτημα για να αποκτήσει διακριτικά
  4. Εάν το αίτημα είναι σωστό, το Google API επιστρέφει:
    • Token πρόσβασης με το οποίο μπορούμε να κάνουμε αιτήματα
    • Η περίοδος ισχύος αυτού του διακριτικού
    • Απαιτείται διακριτικό ανανέωσης για την ανανέωση του διακριτικού Access.

Πρώτα πρέπει να μεταβείτε στην κονσόλα Google API: Διαπιστευτήρια - Google API Console, επιλέξτε την επιθυμητή εφαρμογή και στην ενότητα Διαπιστευτήρια δημιουργήστε ένα αναγνωριστικό πελάτη OAuth. Εκεί (ή αργότερα, στις ιδιότητες του δημιουργημένου αναγνωριστικού) πρέπει να καθορίσετε τις διευθύνσεις στις οποίες επιτρέπεται η ανακατεύθυνση. Στην περίπτωσή μας, αυτές θα είναι πολλές καταχωρήσεις localhost με διαφορετικές θύρες (δείτε παρακάτω).

Για να διευκολύνετε την ανάγνωση του αλγόριθμου σεναρίου, μπορείτε να εμφανίσετε τα πρώτα βήματα σε μια ξεχωριστή συνάρτηση που θα επιστρέψει την 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.

πρόσβαση_τύπος=εκτός σύνδεσης σημαίνει ότι η εφαρμογή μπορεί να ενημερώσει μόνη της ένα διακριτικό που έχει λήξει χωρίς αλληλεπίδραση χρήστη με το πρόγραμμα περιήγησης,
answer_type=κωδικός ορίζει τη μορφή του τρόπου επιστροφής του κώδικα (αναφορά στην παλιά μέθοδο εξουσιοδότησης, όταν ο χρήστης αντέγραψε τον κώδικα από το πρόγραμμα περιήγησης στο σενάριο),
έκταση υποδεικνύει το εύρος και τον τύπο πρόσβασης. Πρέπει να διαχωρίζονται με κενά ή %20 (σύμφωνα με την Κωδικοποίηση URL). Μια λίστα περιοχών πρόσβασης με τύπους μπορείτε να δείτε εδώ: OAuth 2.0 Scopes for Google API.

Αφού λάβει τον κωδικό εξουσιοδότησης, η εφαρμογή θα επιστρέψει ένα μήνυμα κλεισίματος στο πρόγραμμα περιήγησης, θα σταματήσει να ακούει στη θύρα και θα στείλει ένα αίτημα POST για να λάβει το διακριτικό. Υποδεικνύουμε σε αυτό το αναγνωριστικό και το μυστικό που καθορίστηκε προηγουμένως από το API της κονσόλας, τη διεύθυνση στην οποία θα ανακατευθυνθεί ο χρήστης και τον τύπο_χορήγησης σύμφωνα με τις προδιαγραφές του πρωτοκόλλου.

Σε απάντηση, θα λάβουμε ένα διακριτικό Access, την περίοδο ισχύος του σε δευτερόλεπτα και ένα διακριτικό Ανανέωσης, με το οποίο μπορούμε να ενημερώσουμε το διακριτικό 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, δεν επιστρέφει τα ληφθέντα δεδομένα σε χρησιμοποιήσιμη μορφή και δείχνει την κατάσταση του αιτήματος.

Στη συνέχεια, το σενάριο σάς ζητά να εισαγάγετε το όνομα και το επώνυμο του χρήστη, δημιουργώντας μια σύνδεση + email.

αιτήσεις

Τα επόμενα αιτήματα θα είναι - πρώτα απ 'όλα, πρέπει να ελέγξετε αν υπάρχει ήδη χρήστης με την ίδια σύνδεση, προκειμένου να λάβετε μια απόφαση για τη δημιουργία ενός νέου ή την ενεργοποίηση του τρέχοντος.

Αποφάσισα να υλοποιήσω όλα τα αιτήματα στη μορφή μιας συνάρτησης με μια επιλογή, χρησιμοποιώντας το διακόπτη:

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

Σε κάθε αίτημα, πρέπει να στείλετε μια κεφαλίδα εξουσιοδότησης που περιέχει τον τύπο διακριτικού και το ίδιο το διακριτικό πρόσβασης. Επί του παρόντος, ο τύπος διακριτικού είναι πάντα Bearer. Επειδή Πρέπει να ελέγξουμε ότι το διακριτικό δεν έχει λήξει και να το ενημερώσουμε μετά από μια ώρα από τη στιγμή που εκδόθηκε, καθόρισα ένα αίτημα για μια άλλη λειτουργία που επιστρέφει ένα διακριτικό Access. Το ίδιο κομμάτι κώδικα βρίσκεται στην αρχή του σεναρίου κατά τη λήψη του πρώτου διακριτικού 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 να αναζητήσει έναν χρήστη με ακριβώς αυτό το email, συμπεριλαμβανομένων των ψευδωνύμων. Μπορείτε επίσης να χρησιμοποιήσετε μπαλαντέρ: =, :, :{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
}

Οι λειτουργίες ενημέρωσης και δημιουργίας λογαριασμού έχουν παρόμοια σύνταξη· δεν απαιτούνται όλα τα πρόσθετα πεδία· στην ενότητα με αριθμούς τηλεφώνου, πρέπει να καθορίσετε έναν πίνακα που μπορεί να περιέχει έως και μία εγγραφή με τον αριθμό και τον τύπο του.

Για να μην λάβουμε σφάλμα κατά την προσθήκη ενός χρήστη σε μια ομάδα, μπορούμε πρώτα να ελέγξουμε αν είναι ήδη μέλος αυτής της ομάδας, λαμβάνοντας μια λίστα με μέλη ομάδας ή σύνθεση από τον ίδιο τον χρήστη.

Το ερώτημα για τη συμμετοχή στην ομάδα ενός συγκεκριμένου χρήστη δεν θα είναι επαναλαμβανόμενο και θα δείχνει μόνο την άμεση συμμετοχή. Η συμπερίληψη ενός χρήστη σε μια γονική ομάδα που έχει ήδη μια θυγατρική ομάδα στην οποία είναι μέλος ο χρήστης θα είναι επιτυχής.

Συμπέρασμα

Το μόνο που μένει είναι να στείλετε στον χρήστη τον κωδικό πρόσβασης για τον νέο λογαριασμό. Αυτό το κάνουμε μέσω SMS και στέλνουμε γενικές πληροφορίες με οδηγίες και σύνδεση σε ένα προσωπικό email, το οποίο, μαζί με έναν αριθμό τηλεφώνου, δόθηκε από το τμήμα προσλήψεων. Εναλλακτικά, μπορείτε να εξοικονομήσετε χρήματα και να στείλετε τον κωδικό πρόσβασής σας σε μια μυστική συνομιλία με τηλεγράφημα, η οποία μπορεί επίσης να θεωρηθεί ο δεύτερος παράγοντας (τα MacBooks θα αποτελούν εξαίρεση).

Σας ευχαριστώ που διαβάσατε μέχρι το τέλος. Θα χαρώ να δω προτάσεις για τη βελτίωση του στυλ γραφής άρθρων και σας εύχομαι να συλλάβετε λιγότερα λάθη όταν γράφετε σενάρια =)

Λίστα συνδέσμων που μπορεί να είναι θεματικά χρήσιμοι ή να απαντούν απλώς σε ερωτήσεις:

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο