Thunderbird Auto Config Server (TACS)

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

Каждый из авторов решал настройку почты своим путём, используя разные языки и подходы.

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

Обзор

Основой для TACS является файл описания schema.yaml, в котором задаются основные параметры:

# Directory to search for templates
# The search is also performed in subdirectories, but without following symbolic links
# Required file extension: *.tmpl
#templateDir: templates
# Block for local processing of user logins.
# Executes first because it doesn't require external requests.
# If there is a match, subsequent stages are not checked
#local:
  # Default fields and properties
  #default:
    # Name of the GO template (*.tmpl), with insert fields - {{define "templateName"}}
    #template: default
    # A key-value map (hash table) that is used to fill the template.
    # Can be overridden at the user level.
    # If the field is not present at the user level, it will be set to the specified value.
    # The key is a variable declared in the template - {{ .variableName }}
    # Value - what will be substituted for the key
    #fields:
    #  cn: user
    #  mail: mail@example.com
    #  telephoneNumber:
    #  position: no_body
    #  company: "\"Example\""
  # List of users to handle requests.
  # Is a hash table of "username: params",
  # so each iteration of the key overwrites the previous values
  #list:
  #root:
  #  template: root
  #  fields:
  #    company: ""
  #    mail: root@example.com
  #    cn: Administrator
  #    signatureIsHTML: true
  #    signature: 'Sincerely\nroot'

# Block for processing users from LDAP.
# If there is a match, subsequent stages are not checked
#ldap:
  # A unique field that identifies the user, a filter compiled for this:
  #  (uid=username), example - (sAMAccountName=username)
  #uid: sAMAccountName
  # Ldap path where to start the search
  #searchBase: OU=SystemUsers,OU=Corp,DC=corp,DC=domain,DC=com
  # Additional filter for user search. Added to the filter with uid:
  # (&(uid=username)filter)
  # In this case, search only for enabled users:
  #filter: (!(userAccountControl:1.2.840.113556.1.4.803:=2))(objectCategory=person)(objectClass=user)
  # Search in subgroups?
  # If true - adds the option ":1.2.840.113556.1.4.1941:" to the filter
  #subgroups: true
  # Should I generate a response for a user who is not a member of any declared group?
  # In this case, a default section must be declared
  #allowWithoutGroups: true
  # A key-value map (hash table) that is used to fill the template.
  # Can be overridden at the ldap group level.
  # If the field is not present at the user level, it will be set to the specified value.
  # The key is a variable declared in the template - {{ .variableName }}
  # Value - fields taken from the user's LDAP profile.
  # Note.
  # If you prefix an LDAP field name with "raw:", the value will be queried raw and base64 encoded
  #default:
  #  template: default
  #  fields:
  #    cn: cn
  #    mail: mail
  #    telephoneNumber: telephoneNumber
  #    position: position
  #    company: company
  #    photo: raw:jpegPhoto
  #list:
    # The list of groups by which the user's groups are matched.
    # The check is performed one by one, and the first match
    # interrupts further processing.
    #- group: "CN=Domain Users,OU=SystemUsers,OU=Corp,DC=corp,DC=domain,DC=com"
    #  template: default
    #  fields:
    #    cn: cn
    #    mail: mail
    #    telephoneNumber: telephoneNumber
    #    position: position
    #    company: company
    #    photo: raw:jpegPhoto

Для выдачи требуемой конфигурации, составляются списки с привязкой к ним соответствующих шаблонов. Списков два:

Рассмотрим более подробно оба вида списков:

local

local:
  default:
    template: users
    fields:
      cn: user
      mail: mail@example.com
      telephoneNumber:
      position: no_body
      company: "\"Example\""
  list:

local.default - этот раздел используется как значения по умолчанию для всех участников списка, если на самом участнике списка, не переопределено иное.
local.default.template - задаёт имя шаблона, для формирования ответа
local.default.fields - карта соответствия Go-Template переменных и значений, которые должны быть подставлены вместо них. В примере выше видно, что если в шаблоне будут переменные

{{ .cn }}, {{ .mail }}, {{ .position }}

то они будут заменены указанными значениями:

user, mail@example.com, no_body

local.list - карта со списком логинов, на которые будут отдаваться настройки клиента. Здесь можно переопределить значения по умолчанию, заданные в разделе local.default с тем же синтаксисом, например:

local:
  default:
    template: default_users
    fields:
      cn: user
      mail: mail@example.com
      telephoneNumber:
      position: no_body
      company: "\"Example\""
  list:
    user1:
    user2:
      template: managers
    user3:
       template: admins
       fields:
         cn: Вася Пупкин
         position: Сетевой администратор

LDAP

Если логин пользователя не найден в локальном списке, выполняется поиск в LDAP.
Настройка LDAP списка расширяет локальные списки:

ldap:
  uid: sAMAccountName
  usersSearchBase: OU=Users,DC=corp,DC=example,DC=com
  groupsSearchBase: OU=Groups,DC=corp,DC=example,DC=com
  filter: (!(userAccountControl:1.2.840.113556.1.4.803:=2))(objectCategory=person)(objectClass=user)
  subgroups: true
  allowWithoutGroups: true
  default:
  list:
(&(sAMAccountName=pupkin.v)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(objectCategory=person)(objectClass=user))
# Условная структура LDAP

user1.memberOf:
	- Domain Peoples
	- tacs_default
user1.memberOf:
	- Domain Peoples
	- tacs_default
...
# Условная структура LDAP

# Было:
user1.memberOf:
	- Domain Peoples
user2.memberOf:
	- Domain Peoples
# Стало:
Domain Peoples.memberOf:
	- tacs_default

При true, к фильтру добавляется опция :1.2.840.113556.1.4.1941: которая позволяет просмотреть все дочерние подгруппы и найти искомого пользователя, если тот присутствует в дереве дочерних групп.

ldap.allowWithoutGroups: [true|false] - разрешить ли пользователей без группы. Если пользователь не найден в группах LDAP, следует ли применить к нему значения LDAP по умолчанию или выдать 404 ошибку.

ldap.default - так же как и в локальном списке, если не переназначено на группе, применяются данные значения.
ldap.default.template - шаблон по умолчанию для LDAP групп
ldap.default.fields - карта полей вида Go-Template-переменная: ldap-поле-пользователя. Если в локальном списке значение было просто значением, то в LDAP-списке, значения берутся из свойств объекта пользователя. Например:

  default:
    template: ldap-default
    fields:
      cn: cn
      position: description

ldap.default.fields.*: raw:* - если перед именем LDAP-поля указать префикс raw: тогда значение поля будет выгружено в сыром виде и закодировано в base64. Это используется для выгрузки фотографий из поля LDAP jpegPhoto:

  default:
    ...
    fields:
      photo: "raw:jpegPhoto"

Получив фото из LDAP, можно составить html-подпись для пользователя:

<img src="data:image/jpeg;base64,{{ .photo }}"/>

ldap.list - список групп с привязкой к ним шаблонов и необязательной карты свойств

  list:
    - group: "CN=tacs-with-photo,OU=tacs,OU=Groups,DC=corp,DC=example,DC=com"
      template: with-ldap-photo
      fields:
        cn: cn
        mail: mail
        position: position
        company: company
        photo: raw:jpegPhoto
        address: physicalDeliveryOfficeName
        telephoneNumber: telephoneNumber

ldap.list.[*].group - полный DN-путь к группе, например: "CN=tacs-with-photo,OU=tacs,OU=Groups,DC=corp,DC=example,DC=com"
ldap.list.[*].template - необязательное переопределение используемого шаблона для указанной группы
ldap.list.[*].fields - карта необязательных переопределений полей, аналогично разделу ldap.default.fields.

Шаблоны

Каталог для поиска шаблонов указывается в scheme.yaml:
templateDir - TACS проходит по всем подкаталогам, кроме символических ссылок, анализируя и загружая в память *.tmpl файлы, которые являются go-template.

{{define "template_name"}}...{{end}} 
local:
  default:
    template: template_name
...
  list:
    username:
      template: template_name
...
ldap:
  default:
    template: template_name
...
  list:
    - group: ...
      template: template_name
{{define "temp1"}}
...
{{end}}
{{define "temp2"}}
...
{{end}}
{{define "temp3"}}
{{template "temp1"}}
{{template "temp2"}}
{{end}}
# Все переменные хранятся в "точке":
{{define "default"}}
{{.var_name}}
{{end}}
local:
  default:
    template_key: value
...
list:
  username:
    template_key: value
...
ldap:
default:
  fields:
    template_key: ldap_field_with_value
    template_key: raw:ldap_field_with_binary_value
...
list:
  - group: ...
    fields:
      template_key: ldap_field_with_value

Примеры

Примерная конфигурация приведена в репозитории.

Источники

@GrimAnEye
26.05.2024 14:43 UTC
Первоисточник

Комментарии

@Abyss777
21.06.2024 12:54 UTC
0

А что будет если пользователь попадает в несколько групп?

Как бы вы сделали с помощью TACS ситуацию, когда некоторой группе пользователей нужно добавить дополнительный ящик?

@GrimAnEye
21.06.2024 13:18 UTC
0

Если пользователь состоит в нескольких группах ldap, то будет применен первая совпавшая группа из списка конфигурации, о чем сказано в ReadMe, help и коде.

Если пользователю потребуется несколько ящиков(а у нас такая ситуация так же имеется) - создаётся шаблон конфигурации thunderbird, описывающий 2 ящика, вместо одного.

Создаётся группа для двух ящиков и пользователи, которым это надо- перемещаются в неё

21.06.2024 13:33 UTC
0

Спасибо. Но фактически у нас сложнее, есть пользователи кому нужно подключить очень разные наборы ящиков. И шаблонов получится по сути как половина пользователей.
Проще уж индивидуальные шаблоны сделать.

Мелькнула идея, если в go-templates передать поле memberOf из ldap, то наверное на уровне шаблона можно как-то проанализировать с помощью {{ if }}. Не пробовали такое?

22.06.2024 12:05 UTC
+1

Если упрощать, то tacs в основном используется для выгрузки данных откуда-то и подстановки их куда-то (я изучал HELM, когда решил сделать TACS). Изначально предполагалось, что для каждого выгружаемого параметра требуется отдельное LDAP поле (ориентируясь на LDAP и массовость).

Специфичных функций для анализа и обработки в шаблонизаторе нет, но они и не нужны, ведь у нас есть JavaScript движок из самого Thunderbird (а сама конфигурация является js-скриптом).

У нас используется максимум 2 ящика с разных серверов, а значит в шаблоне у нас будет проверка:

{{ if .twoEmail }} /*добавляем второй ящик со специфичными параметрами*/ {{ end }}

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

Вспоминаем, что у нас есть полноценный JS, а потому пишем функции для настройки ящика для каждого типа серверов, которыми будем пользоваться:

{{ define "libs"}}
function server1(email, name, etc){
  pref(anything1,name);
  pref(anything2,email);
  pref(anything3,etc);
}
function server2(email, name, etc){
  pref(anything1,name);
  pref(anything2,email);
  pref(anything3,etc);
}
{{end}}

В LDAP выделяем поле с текстом и в формате JSON прописываем требуемые ящики и сервера:

[{"user1@example.com":"server1"},{"user2@example.ru":"server2"}]

В шаблоне, присваиваем эту строку в переменную (я так уже делал), а дальше просто анализируем с помощью JS и вызываем соответствующие функции:

{{define "base"}}

// [{"email":"user1@example.com","server":"server1"},{"email":"user2@example.ru","server":"server2"}]
var list = {{.mailList}};

list.forEach((elem)=>{

  switch(elem.server){
    case "server1":
      server1(elem.email,name,etc);
      break;
  
    case "server2":
      server2(elem.email,name,etc);
      break;

    default: console.log("anything");
  }
})
{{end}}

Исходя из этого мы получим неограниченное число ящиков для каждого домена с индивидуальными параметрами.

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