Данная публикация начинает серию в которой описан один (мой) подход к архитектуре web-приложения некоторого типа и установки его на сервере.
Что будем понимать под web-приложением
Web-приложения решают очень широкий спектр задач. В этой и последующих частях статьи описана архитектура и способ реализации web-приложения, решающий задачи одного, но очень распространенного типа. Далее под web-приложением будем понимать приложение, которое:
- предоставляет пользователям данные по результатам запросов. Изменение данных пользователями невозможны.
- Запросы пользователей и получение данных по результатам запросов реализуется по протоколу http(s)
Из перечисленных условий вытекают следующие условия и ограничения архитектуры приложения:
объем данных, предоставляемых пользователю, большой. Иначе приложение будет малополезным - если данных мало, пользователь вряд ли по запросам сможет получить из них полезную информацию. Большой объем данных предполагает их хранение в некоторой базе. В качестве базы для хранения данных для приложения выбран postgresql.
Данные могут иметь полезную информацию различной природы. Пользователю может быть интересна информация одного типа и совершенно не интересовать другие типы информации. Поэтому информацию web-приложения целесообразно разделить по категориям, будем называть их проектами. Т.е., web-приложение включает в себя один или несколько проектов (категорий информации различной природы)
- Информация (полезные знания), содержащаяся в данных, имеет различную ценность. Поэтому целесообразно организовать доступ к данным с несколькими уровнями привелегий. Из данного условия следуют такие требования к архитектуре приложения как:
- Запросы на получение данных поступают как от авторизованных пользователей (имеющих различные уровни привелений), так и от анонимных
- Каждый пользователь (в том числе и анонимный), выполняющий запрос на получение данных, принадлежит только одной группе в рамках одного проекта
- Для каждого проекта (категории информации) определены свои группы привелегий пользователей на получение данных. Для каждого проекта существует группа приведелий анонимных пользователей.
- Зарегистрированный пользователь может иметь доступ к нескольким проектам (категориям информации). Причем привелегии этого пользователя в различных проектах могут отличаться
- Протокол http(s) выполнения запросов и получения данных предполагает доступ к приложению любого, имеющего доступ в Интернет. Поэтому запросы пользователей на получение данных должны выполняться web-приложением с учетом требований безопасности и проверки привелегий получения данных. Поэтому на приложения накладываются следующие ограничения:
- Для каждого проекта (категории информации) задан собственный фиксированный набор запросов
- Для каждого запроса определены группы привелегий на его выполнение
Архитектура web-приложения
Исходя из перечисленных условий и требований к нашему приложению, примем следующую архитектуру.
- Данные хранятся в базе данных postgresql
- Также на базу данных возложим следуюшие задачи приложения:
- идентификация пользователя
- проверка (определение) привелегий пользователя на выполнение запроса получения данных
- выполнение запросов
- логгирование запросов пользователя к проектам
Для выполнения этих задач в базе данных будет хранится схема конфигурации приложения (по умолчанию config) содержащая необходимые таблицы, функции, триггеры. И схема логов (logs), хранящая историю запросов пользователей к проектам приложения. Данные проектов (категории информации) хранятся раздельно в таблицах соотвествуюших схем.
В итоге имеем базу данных - наше приложение, в которой присутствует:
- схема конфигурации (config)
- схема логов запросов
- одна или более схем проектов с данными различных категория
Логическая схема таблиц (пункты 1,2) представлена на рисунке

- projects таблица, хранящая имена проектов.
- user_groups таблица содержит названия групп привелегий пользователей для всех проектов. Для каждого создаваемого проекта создается группа 'anonymous', описывающая привелегии для неавторизованных пользователей. Группа создается автоматически для каждого проекта, за это отвечает соотвествующий триггер
- users таблица хранит конфигурацию пользователей.
- request_sql_list - таблица хранит список доступных запросов к БД для каждого из созданных проектов.
- log_requests - таблица логов в схеме logs, хранящая все запросы пользователей к данным проектов
Другие задачи, решаемые приложением (предварительная обработка запросов пользователей,...) описаны в последующих статьях. Здесь мы подробнее остановимся на реализации задач, решаемых на стороне базы данных.
Реализация логики приложения на стороне БД
Для создания приложения в базе данных и настройки его конфигурации написан python-скрипт. Его описание приведено в следующей публикации. Здесь мы остановимя на наиболее важных логических аспектах задач приложения, рещаемых на стороне базы данных.
Идентификация пользователя
- Согласно принятой архитектуре, при добавлении нового пользователя мы привязываем его к одной группе привелегий. Но прямой ссылки на проект в связях таблиц не предусмотрено. Это необходимо для того, чтобы была возможность подключить одного пользователя (имя+пароль) к нескольким проектам. Но в данном случае на этапе добавления нового пользователя может возникнуть ситуация, когда пользователь с одинаковым именем может быть подключен к одному проекту с различными превилегиями. Чтобы исключить такую возможность создаются триггерная процедура check_user_in_projects и триггер check_user_in_projects_trigger
- В схему конфигурации добавлена триггерная процедура addAnonymous() и соотвествующий триггер, которые для каждого создаваемого проекта автоматически добавляет в него группу привелегий 'anonymous' (в таблицу privilege_groups).
Привелегии на выполнение запроса получения данных
- Привелегии на получение данных по запросу определяются следующими типами ограничений а) количество запросов в единицу времени, б) интервал времени, через который могут выполняться запросы, в) объем данных, возвращаемых по результатам запросов.
2. Привелегии задаются для каждого запроса (таблица request_sql_list). Логика такая: а) пользователь (в том числе и анонимный) отправляет запрос с уникальным идентификатором (логин+пароль) и ссылкой на проект, которому адресов запрос, б) приложение (на стороне базы данных) определяет группу привелегий, которую имеет пользователь в указываемом проекте в) определяются и проверяются привелегии на выполнение данного запроса г) запрос выполняется с учетом привелегий
Таким образом, для каждого запроса привелегии его выполнения удобно записать в виде json-объекта следующенй структуры (с учетом определенных выше типов ограничений)
{
'group1':{'count':vc1, 'timeout':vt1, 'record':vr1},
'group2':{'count':vc2, 'timeout':vt2, 'record':vr2},
...
}
где ключи - имена групп, 'count' - допустимое количество запросов (в час), 'timeout' - минимальный интервал времени (сек), через который пользователь может повторить данный запрос (?? или любой другой запрос к проектцу), 'record' - максимальное число записей результатов запроса, которые может получить пользователь за одно обращение по данному запросу.
В таком формате привелегии указываются (вручную) для каждого запроса (таблица request_sql_list, поле privilege). При этом сущесвует возможность неправильно указать имя группы или указать несуществующую в проекте, к которому относится запрос, группу привелегий. Чтобы исключить такие ситуации, создана триггерная процедура check_groups и триггер check_groups_trigger, возвращающая ошибку если хотя бы одна из групп привелегий для запроса была указана неверно.
Отсутствие какой-либо группы (ключа) означает, что все пользолватели данной группы не имеют права выполнения данного запроса.
Нулевое значение любого из параметров привелегии означает, что у данной группы отсутствуют ограничение по данному типу привелегий.
Выполнение запросов
Одна из идей безопасности приложения состоит в том, что пользователь не может выполнить произволный запрос к данным приложения. Для каждого проекта (категории информации) существует фиксированный (изменяемый) набор допустимых запросов. Точнее шаблонов запросов с точностью до параметров (о шаблонах читайте тут ). Каждый запрос имеет псевдоним.
Исходя из сказанного, чтобы получить нужные данные, пользователь для получения данных должен передать приложению следующую информацию - 'project' - название проекта из которого необходимо извлечь данные - 'alias' - псевдоним запроса - 'params' - словарь значений параметров запроса. - 'user' - имя пользователя - 'password': пароль (хеш пароля)
Приложение, получив эту информацию выполняет ряд проверок с точки зрения безопасности и корректности запрашиваемых данных. На уровне postgresql выполняются следующие проверки
- проверка авторизации пользователя. Если пользователь не авторизован, ему автоматически присваивается группа привелегий 'anonymous'
- проверка существования запрашиваемых данных в данном проекте. В случае неудачи возвращается соотвествующее информационное сообщение.
- проверка привелегий пользователя на получение данных в проекте. В случае неудачи пользователю возвращается информационное сообщение.
Для выполнения этих проверок в конфигурации приложения создается материализованное представление rsl_p_u_pg которое содержит всю необходимую для проверок информацию и обновляется каждый раз когда изменяются данные в любой (??? проверить) из таблиц конфигурации - пользователей, пользовательских запросов, проектов или прав привелегий. Представление содержит следующие поля:
- id и porject_id (дублирующее поле) - идентификатор проекта
- project_name - имя проекта
- pg_id - идентификатор группы превилегий
- group_name - имя группы привелегий
- u_id - идентификатор пользователя
- user_name - имя пользователя
- password_hash - хеш пароля пользователя
- rsl_id - идентификатор запроса
- alias - псевдоним запроса
- sql_query - строка запроса
- privilege - привелегии групп на выполнение запроса
Созданное материализованное представление сохраняет результат в виде физической таблицы. Эта таблица статична в том смысле, что изменения в одной из исходных таблиц, например, добавление нового пользователя, не отразятся в нашем представлении автоматически. Чтобы увидеть изменения в представлении его нужно обновить. Для этого в конфигурации создается триггер update_view для каждой исходной таблицы, который будет выполнять обновления после каждой из операций добавления, удаления или изменения записи.
И, наконец для выполнения перечисленных выше проверок пользовательского запроса создана функция getQuery
select config.getQuery('user', 'password', 'project_name', 'alias_query')
В случае успешного выполнения, функция вернет шаблон запроса. Как писать шаблоны запроса читайте в этой публикации
Если пользователь не идентифицирован, или не имеет привелегий на выполнение запроса выполняется, функция проверяет привелегии группы 'anonymous' на выполнение этого запроса. Если группа имеет привелегии - возвращается шаблон запроса. Если нет - возвращается пустая строка. Если запрос для проекта не определен, функция вернет пустое значение
Логирование запросов
Логирование реализовано для анализа пользовательских запросов на предмет оптимизации и безопасности. В конфигурации приложения логируются следующие данные
- параметры пользователя на выполнение запроса (имя, пароль, проект, псевдоним)
- временная метка, пользоватеского запроса
Чтобы сохранять в таблице все обращения пользователей, в функцию getQuery добавлена строка
INSERT INTO config2.log_requests(request_time, user_name, request_alias, project, passwd)
VALUES (now(), usr, al, project, passwd);
Таблица log_requests может быстро увеличиваться в размере, поэтому в конфигурации предусматрено патриционирование (???!!! не реализовано). Логи запросов пользователей группируются в таблицах по месяцам. За этим следить функция add_log и триггер add_log_trigger
Таблицы логов хранятся в отдельной схеме logs. Это делается из соображений безопасности. Соднанный пользователь postgresql от имени которого выполняться запросы должен иметь право создавать дочерние таблицы логов и добавлять в них записи. Создание таблиц (и других объектов) можно ограничить только на уровне схемы. Но разрешать создавать пользователю объекты в схеме config, где расположены таблицы пользователей и паролей, таблицы доступа к кпроектам и т.д. не безопасно. Поэтому логи и были вынесены в отдельную схему
Чтобы выполнять быстрый поиск по логам в функции предусмотрено создание индексов. !!!?? тема индексов пока на пактике не реализована
Вот и все, что хотелось рассказать об архитектуре и логике работы приоложения на стороне postgresql. В слежующей публикации рассмотрим как быстто развернуть и насроить такое приложение.