Как создать бота Telegram для разблокировки AD аккаунтов


16 февраля 2021


Создания бота Telegram на Powershell для разблокировки пользователей AD

В этой статье будет рассмотрен способ работы с Telegram через Powershell. Основная задача, которая будет решаться в статье, получение списка заблокированных пользователей и их разблокировка. Обычно, когда такие ситуации случаются, я редко нахожусь за компьютером и мне приходится делать подключение через телефон и RDP - это не удобно. С помощью описанного методы разблокировать пользователя можно будет в 2 клика.

Логика, описанная в статье, применима и для других похожих сервисов. Например я планировал написать тесты для Exchange если пользователь сообщает, что у него проблемы с ящиком.

 

Для чего нужен API

У многих приложение (веб и локальных) есть понятие API, которое описывает процесс взаимодействия с программой и упрощает его. Например вам нужно узнать количество новых сообщений на своей почте и для этого вы открываете веб страницу. Веб страница весит 7Мб, а такую процедуру вы проделываете 100 раз в минуту. Этот способ плох и для вас и для сервера. Будет лучше, если вам будет отправлено только количество новых сообщений. Одна из таких проблем и решается с помощью API. У нас будет возвращена информация типа:

{response: {messages: 100, new: 10}}

Формат выше называется JSON и это самый популярный способ общения с веб-API. Он похож на хэш-таблицы Powershell и для работы с ним есть отдельный командлет.

Умение разбираться и взаимодействовать с разными API - это весомый навык разработчиков и ,особенно, для системных администраторов.

 

Регистрация бота Telegram

Для ботов в Telegram есть свой публичный API и что бы им воспользоваться - вы должны пройти регистрацию. Это делается в несколько шагов.

Регистрация вашего персонального бота выполняется через специальный канал BotFather. Найти его вы можете через поиск (у него будет галочка):

Регистрация бота в Telegram через BotFather

В этом боте мы должны выполнить несколько команд. Выведем окно взаимодействия с ботом:

/start

Диалоговое окно BotFather в Telegram

Команда '/start' поможет в случаях, если вы забудете названия ботов или захотите изменить какие-то данные.

Что бы создать бота пишем следующую команду:

/newbot

В Telegram нет ограничений на создания ботов и, если, вы совершите ошибку вы всегда сможете удалить бота и создать нового.

После этого нам будет предложено ввести название для бота. Это название, которое легче всего будет понять вам. Я назову его:

  • 'fixmypc_ad_test'

После названия для бота нужно будет придумать название для канала, который он сможет обслуживать. Главным правилом названия - оно должно заканчиваться на 'bot'. Учтите, что вашего бота можно будет найти через поиск и лучше не использовать идентифицирующие вас имена. Я буду использовать:

  • 'fixmypc_ad_test_bot'

Регистрация бота в Telegram для Powershell

В выделенном фрагменте, под номером 4, ссылка на ваш канал. Под номером 5 - ваш персональный токен. Токен - это как пароль, если кто-то получит к нему доступ сможет управлять ботом. Этот токен нужно будет отправлять при каждом запросе Telegram.

Нажмите на ссылку во фрагменте 4 и нажмите кнопку /start что бы у вас было хотя бы 1 сообщение.

 

Взаимодействие с API Telegram

Большая часть взаимодействия с API ботов (есть и другие) сводится к обращению по 1 url. Этот url выглядит следующим образом и состоит из частей:

https://api.telegram.org/bot<token>/<METHOD_NAME>

Где:

  • <token> - это поле 'пароля' от вашего бота. Мы его получили раннее;
  • <METHOD_NAME> - это команда, которую мы отправляем к Telegram и получаем какой-то ответ.

Названия методов и способ обращения к ним заранее определен и описан в документации. Документация по работе с ботами описана по ссылке.

В рамках всей статьи у меня определены 2 переменных, которые составят часть Url для обращения к Telegram. В примерах вы можете не увидеть эти переменны, но они могут использоваться:

$token = "12345:sadasdasdasd"
$url = "https://api.telegram.org/bot$token/"

Invoke-RestMethod

Как уже писалось выше - Telegram использует формат JSON. Такой формат не поддерживается в базовой реализации Powershell, но он содержит команду Invoke-RestMethod для преобразования JSON в hash-table. 

В следующем примере мы соединим наш Url и токеном с методом getMe, который вернет базовую информацию о боте:

$result = Invoke-RestMethod -Uri $($url+'getMe')
$result

Выполнение запроса в Telegram через Powershell Invoke-RestMethod

Скриншот выше демонстрирует, что мы получили вложенные хэш таблицы типа @{key1=@{key2=@{key='val';}}}. Мы можем 'распаковать' эти массивы:

$result.result
$result.result.username

Получение информации о боте в Telegram через Powershell Invoke-RestMethod

Получение сообщений

После регистрации бота вы должны были нажать на кнопку /start, т.е. у вас должно быть хотя бы одно сообщение. Что бы получить его воспользуемся методом getUpdates:

$result = Invoke-RestMethod -Uri $($url+'getUpdates')
$result

Выведем информацию, которая находится внутри массивов:

$result.result
# выводим информацию о каждом сообщении
$result.result.message
# выводим текст из каждого сообщения
$result.result.message.text

Получение информации о сообщении в Telegram через Powershell Invoke-RestMethod

Обратите внимание, что у каждого пользователя бота свой собственный чат/канал с ним. Вы, как пользователь, не можете видеть сообщения другого пользователя, а он ваши. Бот же видит оба ваших чата и идентифицирует вас по идентификатору 'chat.id'. В дальнейшем мы будем использовать этот идентификатор что бы понять от кого мы получили сообщение и кому будем отправлять ответ:

$result.result.message.chat.id

Получение идентификатора сообщения Telegram через Powershell Invoke-RestMethod

У меня 2 'chat_id' так как я отправлял 2 сообщения. Для ответа нужно только 1 - и так его можно получить:

$result.result.message.chat.id[-1]

Отправка сообщений

Методы для получения данных от API могут нуждаться в дополнительных, обязательных, параметрах. Эти параметры описаны в документации. Для отправки сообщений используется метод 'sendMessage' и в документации для него описаны следующие обязательные параметры (в колонке Required):

Описание API ботов Telegram

В предыдущем примере мы уже получили chat.id и должны его передать для отправки сообщения. Текст сообщения вы можете изменить на свое усмотрение. Все параметры и их значения помещаются в хэш таблицу, а затем в -Body команды Invoke-RestMethod :

$form = @{
   chat_id = $result.result.message.chat.id[-1];
   text = 'Пишу бота для AD';
}
$result = Invoke-RestMethod -Uri $($url+'sendMessage') -Body $form
$result

Отправка сообщения в Telegram через Powershell Invoke-RestMethod

 

Получаем заблокированных пользователей AD

Свойство, которое отображает заблокированных пользователей AD, называется LockedOut. С помощью следующей команды мы получим всех таких пользователей:

Get-ADUser -Filter * -Properties LockedOut | where LockedOut -eq True

Получение заблокированного пользователя AD в Powershell

Что бы такой пользователь смог зайти в систему он должен быть разблокирован и ему должен быть установлен новый пароль. В этой статье будет рассмотрен одинаковый пароль во всех случаях - 'Zx12345':

$SID = (Get-ADUser -Filter * -Properties LockedOut | where LockedOut -eq True).SID
$new_password = 'Zx12345'
Unlock-AdAccount -Identity $SID
Set-ADAccountPassword -Identity $SID -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $new_password -Force)

Снятие блокировки пользователя AD и установка пароля с Powershell

Одинаковый пароль нужен т.к. его легче всего будет запомнить пользователям. Если для вас это не подходит то, опираясь на предыдущую статью по отправке эл.писем, вы можете сделать функционал по генерированию новых паролей и отправкой их начальникам.

 

Связываем Telegram и AD

Все сообщения, которые будут посланы пользователями боту, можно разделить на обычный текст и команды. Команды - это то что мы должны обработать. Удобнее всего сделать команды через Telegram кнопки или ссылки. Их удобство в том, что мы можем не печатать текст, а просто нажать на эту команду. В этой статье будут рассмотрены только ссылки. Они начинаются со знака '/' и не могут содержать символы пробелов:

Команды Telegram

Таких типов команд должно быть 2 - на получение списка пользователей и разблокировку. Как видно на примере выше - мы не можем использовать в качестве команды имена пользователей т.к. они содержат пробелы. Мы можем использовать UPN, но по нему не всегда понятно что это за пользователь (например aa@domain.local). Кроме этого команды, которые устанавливают пароль и снимают блок (Unlock-AdAccount и Set-ADAccountPassword), работают только с SID/GUID(самые удобные способы). Эти данные тоже не очень хорошо выводить т.к. они являются идентификаторами безопасности. Я предлагаю решить эту проблему создав следующий вид команд и сопутствующего текста:

# Ожидаем получить список заблокированных пользователей
/get_user

# После выполнения предыдущей команды
# будет выводиться текст с информацией и командами
Unlock Anna Petrovna /0
Unlock Елена Патрушева /1
Unlock Игорь Пелевен /2

Команды Telegram

Каждый идентификатор типа /0, /1, /2 будет соответствовать индексу значения в массиве (массив содержащий пользователей). Создадим функции, которые позволят сделать это.

Формирование списка пользователей

Нам нужно создать функцию, которая получила бы заблокированных пользователей и создала бы из них массив вида:

$users = @(
   @{name='Anna Petrovna'; SID='1234'},
   @{name='Елена Патрушева'; SID='4567'},
)

В примере выше, Anna Petrovna, будет соответствовать индексу 0, а Патрушева - 1. Нужно так же учитывать, что заблокированных пользователей может не быть. Создадим функцию получения пользователей:

function Get-LockedUsers(){
    [CmdletBinding()]
    # Пустой массив. Сюда будут помещены пользователи
    $locked_users = New-Object System.Collections.Generic.List[System.Object]
    # Получаем заблокированных пользователей
    $users = Get-ADUser -Filter * -Properties LockedOut | where LockedOut -eq True
    # Если заблокированных пользователей нет, то
    # прекращаем работу функции и возвращаем 0
    if ($users.Length -eq 0){
        return 0
    }
    # Добавляем в новый список пользователей только имя и SID
    foreach ($user in $users){
        $name = $user.Name
        $sid = $user.SID
        $locked_users.Add(@(@{name=$name; SID=$sid}))
    }
    # Возвращаем готовый список
    return $locked_users
}

Получение списка заблокированных пользователей AD для Telegram

С помощью следующего скрипта мы получим заблокированных пользователей использую созданный командлет и создадим текст для отправки в Telegram (который мы спланировали раннее):

$users = Get-LockedUsers
# текст для отправки
$full_text = ''
foreach ($user in $users){
    $name = $user.Name
    # Получаем индекс элемента в массиве
    $index = $users.indexOf($user)
    # формируем сообщение для конкретного пользователя с новой строкой
    # и добавляем все к полному тексту
    $full_text += "Unlock user $name /$index`n"
}

Создание текста с заблокированными пользователей AD для Telegram

Отправка списка пользователей

Создадим функцию с помощью которой мы будем отправлять сообщения. Как уже и описывалось раньше для отправки сообщений нужно указать текст и chat_id. Эти данные мы тоже будем передавать через функцию. В примере ниже не видно переменных $token и $url, подразумевается что вы уже объявляли их раннее:

$result = Invoke-RestMethod -Uri $($url+'getUpdates')
$ChatID = $result.result.message.chat.id[-1]

function Send-TelegramMessage($Message, $ChatId){
    [CmdletBinding()]
    # Создаем объект для отправки в Telegram
    $form = @{
       chat_id = $ChatId;
       text = $Message;
    }
    $result = Invoke-RestMethod -Uri $($url+'sendMessage') -Body $form
    return $result    
}

# Блок формирующий текст для отправки в Telegram
$users = Get-LockedUsers
$full_text = ''
foreach ($user in $users){
    $name = $user.Name
    $index = $users.indexOf($user)
    $full_text += "Unlock user $name /$index`n"
}
# Передаем в функцию идентификатор чата и текст
Send-TelegramMessage -Message $full_text -ChatId $ChatID

Отправка текста с заблокированными пользователей AD для Telegram

В выделенном фрагменте видно, что у нас получилось реализовать вывод сообщений в планируемом формате.

Реализация команд

Теперь нам нужно создать команды, которые описывались ранее:

  • Команду на получение списка заблокированных пользователей - /get_user;
  • Команду на разблокировку пользователей - они у нас уже выводятся в виде /1, /2 и т.д.

Что бы суметь получить команды мы должны создать вечный цикл. Этот вечный цикл, раз в 2 секунды, будет выполнять запрос к API telegram и получать последнее сообщение. Выполнять запросы чаще чем раз в 2 секунды не рекомендуется, т.к. у Telegram может быть на это ограничение. Получить последнее сообщение из чата можно так:

$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
$result.result.message.message_id

Получение последнего сообщения Telegram через Invoke-RestMethod Powershell

У пользователей есть идентификаторы чатов (chat_id), а у чатов есть идентификаторы сообщений (message_id). В примере выше мы вернули последнее сообщение из всех чатов (с помощью offset='-1'). Последнее сообщение не значит "новое" и нам нужно научится их различать. Один из способов сделать это, при запуске программы, запомнить последнее сообщение написанное в чате. Каждые 2 секунды мы будем повторять запрос и сравнивать идентификаторы сообщений (message_id). Если они отличаются, то значит пришло новое сообщение и мы заменяем старый message_id на новый и выполняем еще какую-то логику:

$token = "12345:sadasdasdasd"
$url = "https://api.telegram.org/bot$token/"
# Получаем последнее сообщение
$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
# Присваиваем идентификатор последнего сообщения переменной
$MessageId = $result.result.message.message_id
# Идентификатор чата для отправки конкретному пользователю
$ChatId = $result.result.message.chat.id

function Get-TelegramMessage(){
    [CmdletBinding()]
    # Делаем запрос на новые сообщения
    $result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
    # Сравниваем идентификаторы сообщений
    # идентификатор нового должен быть больше старого
    if ($result.result.message.message_id -gt $MessageId){
        # возвращаем сообщение, если оно новое
        return $result.result.message
    }
}

# вечный цикл с таймаутов в 2 секунды
while ($True){
   # каждые 2 секунды ищем новое сообщение
   $response = Get-TelegramMessage
   if ($response){
      # если мы получили новое сообщение
      # то его идентификатор становится последним
      $MessageId = $response.message_id
      # выводим текст последнего сообщения
      $response.text
      # Еще какая-то логика (получение пользователей или разблокировка)
   }
   sleep 2
}

Получение сообщений Telegram через Invoke-RestMethod Powershell

Теперь нам нужно понять начинается ли сообщение на '/' (т.е. является командой). Если это команда - мы определяем какая именно (разблокировать или получить). Ниже уже готовый скрипт, в котором объедены предыдущие части. Добавлены моменты, которые могли бы вызвать недопонимание:

  1. Если нет заблокированных пользователей (массив пустой) - будет отправлено сообщение "Нет заблокированных пользователей";
  2. Если пользователь успешно разблокирован (не было критичных ошибок) - "Пользователь $($user.Name) разблокирован".
$token = "12345"
$url = "https://api.telegram.org/bot$token/"
$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
$MessageId = $result.result.message.message_id
$ChatId = $result.result.message.chat.id
$new_password = 'Zx12345'
# список последних полученных пользователей
# он будет заполнен при первом запросе /get_user
$users = @()

function Get-TelegramMessage(){
    [CmdletBinding()]
    $result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
    if ($result.result.message.message_id -gt $MessageId){
        return $result.result.message
    }
}

function Get-LockedUsers(){
    [CmdletBinding()]
    $locked_users = New-Object System.Collections.Generic.List[System.Object]
    $users = Get-ADUser -Filter * -Properties LockedOut | where LockedOut -eq True
    if ($users.Length -eq 0){
        return 0
    }
    foreach ($user in $users){
        $name = $user.Name
        $sid = $user.SID
        $locked_users.Add(@(@{name=$name; SID=$sid}))
    }
    return $locked_users
}


function Send-TelegramMessage($Message, $ChatId){
    [CmdletBinding()]
    $form = @{
       chat_id = $ChatId;
       text = $Message;
    }
    $result = Invoke-RestMethod -Uri $($url+'sendMessage') -Body $form
    return $result    
}

# вечный цикл с таймаутов в 2 секунды
while ($True){
   $response = Get-TelegramMessage
   if ($response){
      $MessageId = $response.message_id
      
      # Начинаем определять какой тип сообщений
      # Это может быть обычный текст или команда
      # которая начинается на /
      if ($response.text -match '/'){
          # убираем знак / из команды и пытаемся понять
          # нужно ли вернуть список пользователей
          # или нужно разблокировать кого-то
          $text = $response.text -replace '/',''
          if ("get_user" -eq $text){
            $users = Get-LockedUsers
            # если у нас нет заблокированных пользователей
            # то $users будет равен 0
            if ($users -ne 0){
                # Формируем текст для отправки в Telegram
                $full_text = ''
                foreach ($user in $users){
                    $name = $user.Name
                    $index = $users.indexOf($user)
                    $full_text += "Unlock user $name /$index`n"
                }
                Send-TelegramMessage -Message $full_text -ChatId $ChatID
            }
            else {
                Send-TelegramMessage -Message 'Нет заблокированных пользователей' -ChatId $ChatID
            }
          }
          # если это не команда получение пользователей
          # но строка может быть преобразована в число
          elseif ($text -match "^\d+$"){
             # получаем нужного пользователя по индексу
             $user = $users[$text]
             # если значение user существует - значит индекс верный
             # если этого индекса нет - какая-то ошибка или нас
             # пытаются обмануть
             if ($user){
                 # Разблокируем пользователя
                 $SID = $user.SID
                 Unlock-AdAccount -Identity $SID
                 Set-ADAccountPassword -Identity $SID -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $new_password -Force)
                 # отправляем сообщение о разблокировке
                 Send-TelegramMessage -Message "Пользователь $($user.Name) разблокирован" -ChatId $ChatID
             }
          }
      }
   }
   sleep 2
}

Бот по разблокировки пользователей Telegram через Powershell

 

Безопасность и возможные проблемы

Работая с ботом вы должны понимать, что он публичный и его может найти и использовать каждый. Есть несколько ограничения доступа. Самый популярный способ - задать явно chat_id и исполнять команды только от него. Так как это персональный идентификатор, который присваивается Telegram каждому пользователю, его будет тяжело подделать. Вообще, вместе с сообщением, возвращается несколько идентификаторов, каждый из которых вы можете использовать:

$result = Invoke-RestMethod -Uri $($url+'getUpdates')
$result.result.message

Идентификаторы пользователя Telegram с Powershell Invoke-RestMethod

Чтобы работать с определенным ChatID - нужно будет исправить  скрипт в двух местах: в переменной $MessageID и в командлете Get-TelegramMessage:

# Задаем определенный ChatID
$ChatId = '1234'

function Get-TelegramMessage(){
    [CmdletBinding()]
    $result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
    # дополнительно сравниваем $ChatID
    if (($result.result.message.message_id -gt $MessageId) -and ($result.result.message.chat.id -eq $ChatId)){
        return $result.result.message
    }
}

Учтите, что скрипт читает сообщение раз в 2 секунды и если вы отправите в этот промежуток 2 сообщения - будет прочитано только последнее.

Второй способ - это создание приватного канала, в который будет добавлен этот бот. Это другой API и он не рассматривается в этой статье.

Скрипт изначально не делался для работы более чем с 1 пользователем. Это связано с переменной chatid, которая идентифицирует только одного пользователя. Если вам нужно реализовать приватный доступ более чем 1 пользователю вы должны реализовать следующее:

  1. Объявить массив с ChatID пользователей, которые могут работать через бот;
  2. При проверке последнего сообщения, в Get-TelegramMessage, добавить проверку на ChatID;
  3. Если проверка успешная, командлет должен возвращать не только текст сообщения, но и ChatID. Этот ChatID и станет основным для отправки ответа.

У меня нет возможности проверить работу для 2 пользователей, но исправления в этих местах должны сработать:

# Задаем chatid для несколько пользователей
# которые могут выполнять команды
$AllowedChatId = @('12345', '45678')

# Эта строка уже не нужна
# $ChatId = '1234'

function Get-TelegramMessage(){
    [CmdletBinding()]
    $result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
    # Проверяем что текущий $ChatID в списке разрешенных
    if (($result.result.message.message_id -gt $MessageId) -and ($result.result.message.chat.id -in $AllowedChatId)){
        return $result.result.message, $result.result.message.chat.id
    }
}

# остальной код

while ($True){
    # Так было раньше
    # $response = Get-TelegramMessage
    $answer = Get-TelegramMessage
    $response = $answer[0]
    $ChatId = $answer[1]

# остальной код

При этом проблема с 2-я сообщениями за 2 секунды останется.

Этот скрипт может дорабатываться и его последнюю версию вы можете посмотреть на моем github.

 

...

Теги: #powershell #ad


Каналы
Telegram FixMyPc Telegram Лента FixMyPC RSS Rss
Популярные тэги
О блоге
Этот блог представляет собой конспекты выученного материала, преобретенного опыта и лучшие практики в системном администрировании и программировании.