На хабре уже были статьи по настройке почтового клиента 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

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

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

  • LDAP - указывается группа из ldap, в которой должен состоять пользователь, LDAP-поля пользователя из которых необходимо брать свойства для заполнения шаблона и соответствующий шаблон. Если логин не определен в локальном списке, будет выполнен LDAP запрос на поиск пользователя и его свойств.

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

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: Сетевой администратор
  • local.list.user1: шаблон-default_users, поля заполнены по умолчанию

  • local.list.user2: шаблон-managers, поля всё те же

  • local.list.user3: шаблон-admins, поля - индивидуальные для данного пользователя

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:
  • ldap.uid - уникальный идентификатор для поиска пользователя в LDAP.

  • ldap.usersSearchBase - начальный каталог, от которого выполнять поиск пользователей в LDAP

  • ldap.groupSearchBase - начальный каталог, от которого выполнять поиск групп в LDAP

  • ldap.filter - дополнительный фильтр при поиске пользователей. Базовый поиск выполняется по фильтру ($UID=$username) или (sAMAccountName=pupkin.v). При указании данного поля, он расширяется через аргумент И - (&($UID=$username)$AddFilter) и получается, например:

(&(sAMAccountName=pupkin.v)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(objectCategory=person)(objectClass=user))
  • ldap.subgroups: [true|false] - выполнять ли поиск в дочерних LDAP группах или нет. Например у вас пользователи находятся в общей группе Domain Peoples, а tacs_default - группа для выбора шаблона. Можно было бы tacs_default скриптом навесить на каждого пользователя:

# Условная структура LDAP

user1.memberOf:
	- Domain Peoples
	- tacs_default
user1.memberOf:
	- Domain Peoples
	- tacs_default
...
  • а можно повесить на Domain Peoples, чтобы через наследование её влияние распространялось ниже:

# Условная структура 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

  • Переменная шаблона {{ .cn }} - соответствует полю cn из LDAP

  • Переменная шаблона {{ .position }} - соответствует полю desccription из LDAP

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}} 
  • Имя шаблона указывается в scheme.yaml, в свойстве template и используется при генерации страницы конфигурации:

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}}
  • Имя шаблона должно быть уникальным по сравнению с остальными, иначе шаблоны могут переписать друг-друга.

  • В шаблоне можно использовать переменные для подстановки значений из scheme.yaml/LDAP:

# Все переменные хранятся в "точке":
{{define "default"}}
{{.var_name}}
{{end}}
  • Переменные объявляются в scheme.yaml, в свойстве fields:

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

Примеры

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

Источники

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