PowerShell и Microsoft Graph API: автоматизация Microsoft 365
Москва, Щёлковское шоссе, д. 92, корп. 7 · Пн–Пт 9:00–19:00 · +7 903 729-62-41

PowerShell и Microsoft Graph API: автоматизация Microsoft 365

PowerShell и Microsoft Graph API: автоматизация Microsoft 365

Что такое Microsoft Graph API

Microsoft Graph — это единая точка входа ко всем сервисам Microsoft 365: Azure AD, Exchange Online, SharePoint, Teams, OneDrive, Intune. На практике это означает, что вместо целого зоопарка модулей PowerShell — MSOnline, AzureAD, ExchangeOnlineManagement и других — достаточно одного универсального SDK. Меньше зависимостей, меньше головной боли.

Чем Microsoft Graph лучше старых модулей — и почему мы давно перешли на него:

  • Единый API — один модуль вместо пяти для всех сервисов M365
  • Активная разработка — MSOnline и AzureAD уже объявлены deprecated, Microsoft открыто говорит: пора уходить
  • Гранулярные разрешения — запрашиваете ровно те права, которые нужны, и ни байтом больше
  • REST API — при желании вызываете напрямую через Invoke-RestMethod, без лишних абстракций
  • Поддержка приложений — автоматизация работает без интерактивного входа, ночью, по расписанию, без человека

Первый шаг — установить модуль Microsoft Graph PowerShell SDK:

Install-Module Microsoft.Graph -Scope CurrentUser

# Или установите только нужные подмодули
Install-Module Microsoft.Graph.Users
Install-Module Microsoft.Graph.Groups
Install-Module Microsoft.Graph.Mail
Install-Module Microsoft.Graph.Teams

Аутентификация в Microsoft Graph

Graph API поддерживает два режима аутентификации. Делегированный — скрипт работает от имени конкретного пользователя. Серверный — от имени приложения, без участия человека. Для автоматизации нужен второй вариант.

Интерактивная аутентификация — подходит для ручных скриптов, когда за клавиатурой сидит живой человек:

Connect-MgGraph -Scopes "User.Read.All","Group.ReadWrite.All","Mail.Send"

# Проверка подключения
Get-MgContext | Format-List Account, TenantId, Scopes

Аутентификация приложения — для автоматизации, которая должна работать без участия пользователя:

  1. Зарегистрируйте приложение в Azure AD через App registrations
  2. Создайте Client Secret или, лучше, загрузите сертификат
  3. Назначьте именно Application permissions — не Delegated
  4. Подтвердите Admin consent — без этого шага ничего не заработает
$TenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$ClientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$ClientSecret = ConvertTo-SecureString "секрет" -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential($ClientId, $ClientSecret)

Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $Credential

Аутентификация по сертификату

Сертификат надёжнее Client Secret. Секрет может утечь в логи — сертификат нет. Мы в большинстве проектов используем именно сертификаты:

# Создание самоподписанного сертификата
$Cert = New-SelfSignedCertificate -Subject "CN=GraphApp" `
    -CertStoreLocation "Cert:\CurrentUser\My" `
    -KeyExportPolicy Exportable `
    -KeySpec Signature `
    -NotAfter (Get-Date).AddYears(2)

# Экспорт публичного ключа для загрузки в Azure AD
Export-Certificate -Cert $Cert -FilePath "C:\GraphApp.cer"

# Подключение по отпечатку сертификата
Connect-MgGraph -TenantId $TenantId `
    -ClientId $ClientId `
    -CertificateThumbprint $Cert.Thumbprint

Файл GraphApp.cer загрузите в Azure AD → App registrations → Certificates & secrets → Upload certificate.

Управление пользователями

Базовые операции с пользователями Azure AD через Graph — то, с чего начинается большинство проектов автоматизации:

# Получить всех пользователей
$Users = Get-MgUser -All -Property DisplayName, Mail, UserPrincipalName, AccountEnabled
$Users | Format-Table DisplayName, Mail, AccountEnabled

# Найти пользователя
$User = Get-MgUser -Filter "userPrincipalName eq 'ivanov@company.ru'"

# Создать нового пользователя
$PasswordProfile = @{
    Password = "TempP@ss2024!"
    ForceChangePasswordNextSignIn = $true
}

New-MgUser -DisplayName "Петров Иван" `
    -UserPrincipalName "petrov@company.ru" `
    -MailNickname "petrov" `
    -PasswordProfile $PasswordProfile `
    -AccountEnabled:$true `
    -UsageLocation "RU" `
    -Department "IT" `
    -JobTitle "Системный администратор"

Массовое обновление пользователей из CSV — удобно, когда нужно разом поправить атрибуты для сотни сотрудников:

Import-Csv "C:\users-update.csv" -Encoding UTF8 | ForEach-Object {
    Update-MgUser -UserId $_.UPN `
        -Department $_.Department `
        -JobTitle $_.JobTitle `
        -OfficeLocation $_.Office
    Write-Host "Обновлён: $($_.UPN)"
}
Управление группами и членством
Создание групп в Microsoft 365 через PowerShell

Управление группами и членством

Группы безопасности и Microsoft 365 — ещё одна частая задача:

# Создание группы безопасности
New-MgGroup -DisplayName "IT-Admins" `
    -MailEnabled:$false `
    -MailNickname "it-admins" `
    -SecurityEnabled:$true `
    -Description "Группа ИТ-администраторов"

# Создание группы Microsoft 365 (с почтой и SharePoint)
New-MgGroup -DisplayName "Проект Альфа" `
    -MailEnabled:$true `
    -MailNickname "project-alpha" `
    -SecurityEnabled:$false `
    -GroupTypes @("Unified") `
    -Description "Рабочая группа проекта"

# Добавление участников
$GroupId = (Get-MgGroup -Filter "displayName eq 'IT-Admins'").Id
$UserId = (Get-MgUser -Filter "userPrincipalName eq 'ivanov@company.ru'").Id

New-MgGroupMember -GroupId $GroupId `
    -DirectoryObjectId $UserId

Аудит членства в группах — клиенты периодически просят выгрузить, кто вообще куда входит:

# Все участники группы с должностями
Get-MgGroupMember -GroupId $GroupId -All | ForEach-Object {
    $User = Get-MgUser -UserId $_.Id -Property DisplayName, JobTitle, Department
    [PSCustomObject]@{
        Name       = $User.DisplayName
        Title      = $User.JobTitle
        Department = $User.Department
    }
} | Format-Table

Работа с почтой Exchange Online

Для базовых операций с почтой Microsoft Graph вполне заменяет ExchangeOnlineManagement. Не для всего, но для большинства типовых задач — точно:

# Отправка письма от имени пользователя (delegated permission)
$Message = @{
    Subject = "Отчёт за неделю"
    Body = @{
        ContentType = "HTML"
        Content = "<h2>Еженедельный отчёт</h2><p>Все системы работают штатно.</p>"
    }
    ToRecipients = @(
        @{ EmailAddress = @{ Address = "director@company.ru" } }
    )
}

Send-MgUserMail -UserId "admin@company.ru" -Message $Message

# Чтение почтового ящика
$Messages = Get-MgUserMessage -UserId "admin@company.ru" -Top 10 `
    -OrderBy "receivedDateTime desc" `
    -Property Subject, From, ReceivedDateTime

$Messages | Format-Table Subject, @{N='From';E={$_.From.EmailAddress.Address}}, ReceivedDateTime

В серверных сценариях с разрешением Mail.Send на уровне приложения используйте параметр -UserId — это позволяет отправлять письма от имени любого ящика в тенанте без интерактивного входа.

Создание правил почтового ящика

Автоматизация создания почтовых правил для новых сотрудников — избавляет от ручной работы при онбординге:

# Создание правила перенаправления
$Rule = @{
    DisplayName = "Пересылка уведомлений"
    Sequence = 1
    IsEnabled = $true
    Conditions = @{
        SubjectContains = @("[ALERT]", "[URGENT]")
    }
    Actions = @{
        ForwardTo = @(
            @{ EmailAddress = @{ Address = "sms-gateway@company.ru" } }
        )
    }
}

New-MgUserMailFolderMessageRule -UserId "admin@company.ru" `
    -MailFolderId "inbox" -BodyParameter $Rule

Управление лицензиями

Назначение и отзыв лицензий M365 через скрипт — то, что раньше делалось руками в портале:

# Получить доступные лицензии (SKU)
Get-MgSubscribedSku | Format-Table SkuPartNumber, `
    @{N='Total';E={$_.PrepaidUnits.Enabled}}, `
    @{N='Used';E={$_.ConsumedUnits}}, `
    @{N='Free';E={$_.PrepaidUnits.Enabled - $_.ConsumedUnits}}

# Назначить лицензию пользователю
$SkuId = (Get-MgSubscribedSku | Where-Object SkuPartNumber -eq "O365_BUSINESS_PREMIUM").SkuId

Set-MgUserLicense -UserId "petrov@company.ru" `
    -AddLicenses @(@{SkuId = $SkuId}) `
    -RemoveLicenses @()

# Массовое назначение из группы AD
$GroupMembers = Get-MgGroupMember -GroupId $GroupId -All
foreach ($Member in $GroupMembers) {
    $CurrentLicenses = (Get-MgUserLicenseDetail -UserId $Member.Id).SkuId
    if ($SkuId -notin $CurrentLicenses) {
        Set-MgUserLicense -UserId $Member.Id `
            -AddLicenses @(@{SkuId = $SkuId}) `
            -RemoveLicenses @()
        Write-Host "Лицензия назначена: $($Member.Id)"
    }
}

Если лицензии нужно назначать группам, удобнее настроить Group-based licensing прямо в портале Azure — там это делается через Azure AD группы и работает автоматически.

Отчёты и аналитика
Анализ активности пользователей в M365

Отчёты и аналитика

Graph API отдаёт подробную статистику по использованию M365 — Teams, почта, SharePoint, OneDrive:

# Отчёт о входах пользователей (требует AuditLog.Read.All)
$SignIns = Get-MgAuditLogSignIn -Top 100 -Filter "status/errorCode ne 0" `
    -OrderBy "createdDateTime desc"

$SignIns | Select-Object UserDisplayName, AppDisplayName, `
    @{N='Error';E={$_.Status.FailureReason}}, `
    CreatedDateTime, IPAddress | Format-Table

# Неактивные пользователи (не входили более 90 дней)
$Threshold = (Get-Date).AddDays(-90).ToString("yyyy-MM-ddTHH:mm:ssZ")
$Inactive = Get-MgUser -All -Property DisplayName, UserPrincipalName, SignInActivity | 
    Where-Object {
        $_.SignInActivity.LastSignInDateTime -lt $Threshold -or 
        $null -eq $_.SignInActivity.LastSignInDateTime
    }

$Inactive | Select-Object DisplayName, UserPrincipalName, `
    @{N='LastSignIn';E={$_.SignInActivity.LastSignInDateTime}} | 
    Export-Csv "C:\inactive-users.csv" -NoTypeInformation -Encoding UTF8

На практике эти отчёты окупаются быстро. Мы регулярно помогаем клиентам выявить пользователей, которые не заходили в M365 по 60–90 дней, отозвать лицензии — и сэкономить 20–30% бюджета на подписке.

Постраничная выдача, фильтры и расширенные запросы

Первая ошибка, на которой спотыкаются почти все, — скрипт видит только часть пользователей. Graph отдаёт данные страницами: для списка пользователей размер страницы по умолчанию — 100 объектов, максимум — 999 (при выборке или фильтре по signInActivity — не больше 500). Параметр -All в командлетах SDK сам проходит по всем страницам, поэтому в выгрузках я пишу его всегда. -Top без -All ограничивает результат, а не ускоряет полную выгрузку.

Вторая ловушка — фильтры. Часть операторов (ne, not, endsWith) и подсчёт через $count работают только в так называемых расширенных запросах. Для них нужен заголовок ConsistencyLevel: eventual, а в PowerShell — параметры -ConsistencyLevel eventual и -CountVariable:

# Все отключённые учётные записи и их количество
Get-MgUser -Filter "accountEnabled ne true" -All `
    -ConsistencyLevel eventual -CountVariable CountVar |
    Select-Object DisplayName, UserPrincipalName
"Отключено: $CountVar"

Без -ConsistencyLevel eventual такой запрос вернёт ошибку о неподдерживаемом запросе, хотя синтаксис фильтра правильный. При этом $skip для пользователей не поддерживается вовсе — листать можно только ссылками на следующую страницу, что -All и делает.

Командлеты для бета-версии API живут в отдельном модуле Microsoft.Graph.Beta и называются с префиксом MgBeta, например Get-MgBetaGroup. В рабочие скрипты бету я не тащу: её контракт Microsoft может поменять без предупреждения.

Как узнать, какие разрешения нужны скрипту

Ошибка Insufficient privileges или HTTP 403 почти всегда означает одно: в токене нет нужного разрешения. Сначала смотрю, с какими правами я подключён — поле Scopes в выводе Get-MgContext. Потом выясняю, какие разрешения нужны командлету. Для этого в SDK есть два штатных инструмента:

# Какой URI вызывает командлет и какие разрешения ему подходят
Find-MgGraphCommand -Command 'Get-MgUser' | Select-Object Method, URI, Permissions

# Поиск разрешений по ключевому слову: делегированные и прикладные
Find-MgGraphPermission mail

Find-MgGraphCommand показывает HTTP-метод, адрес API и список разрешений, с которыми этот вызов работает. Find-MgGraphPermission выводит разрешения отдельно для типов Delegated и Application с пометкой, требуется ли согласие администратора. Документация к Find-MgGraphCommand оговаривает, что показанные разрешения не отражают уровень привилегий, — самое узкое разрешение я выбираю по странице конкретного метода API.

Мой порядок при разработке нового скрипта: пишу его в интерактивной сессии с минимальными scope, по Find-MgGraphCommand собираю список нужных разрешений, и только после этого выдаю ровно эти Application permissions приложению, от имени которого скрипт будет работать по расписанию.

Часто задаваемые вопросы

Модуль AzureAD (и MSOnline) работает поверх устаревшего Azure AD Graph API, который Microsoft закрывает. Microsoft Graph PowerShell SDK — официальная замена. Он покрывает не только Azure AD, но и Exchange, Teams, SharePoint, Intune. Если у вас ещё живут скрипты на AzureAD — самое время мигрировать на Microsoft.Graph, пока это не стало срочной проблемой.

Для делегированного доступа от имени пользователя достаточно разрешения Mail.Send. Для серверного — Mail.Send как Application permission плюс Admin consent. Важный момент: серверное разрешение даёт приложению право отправлять от имени любого пользователя в тенанте. Если такая широта вас пугает — ограничьте через Application Access Policy, оставив доступ только к нужным ящикам.

Microsoft Graph применяет throttling — при слишком частых запросах вернётся HTTP 429 Too Many Requests. PowerShell SDK обрабатывает это автоматически, но знать об этом всё равно нужно. Для массовых операций используйте batch-запросы через Invoke-MgGraphRequest с JSON batch — до 20 запросов за один вызов, это ощутимо ускоряет работу со списками.

Да, Graph API — чистый REST, и никто не запрещает обращаться к нему напрямую через Invoke-RestMethod. Получаете токен через MSAL или client credentials и шлёте HTTP-запросы: Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/users' -Headers @{Authorization = "Bearer $token"}. Для простых сценариев, где тащить весь SDK нет смысла, это вполне рабочий подход.

#PowerShell Graph API#Microsoft Graph#автоматизация Microsoft 365#Microsoft Graph PowerShell#управление Azure AD#PowerShell Teams#автоматизация Exchange Online#Graph API примеры

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Комментарии 0

Оставить комментарий

загрузка...

Источники

Подпишитесь на рассылку ITfresh

Каждую неделю мы выпускаем практические гайды для руководителей IT и системных администраторов. Это не просто теория! Здесь вы найдёте всё: безопасность, 1С, миграции, резервные копии и проверенные лайфхаки из наших реальных проектов.

Письмо придёт в течение минутыНе нашли его во «Входящих» — загляните в папку «Спам» или «Промоакции» и нажмите «Не спам». Так все следующие выпуски будут приходить прямо в основную почту.
Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.