Настройка мандатного управления доступом(МРД) с Kerberos аутентификацией в ЕПП, Apache2 WSGI, Postgres#
Исходные данные#
Имеется сервер контроллера домена FreeIPA:
имя домена astra.aaa
администратор домена admin@astra.aaa
пользователь домена user-01@astra.aaa
имя сервера dc-01.astra.aaa
сервер имеет постоянный IP-адрес, например, 192.168.1.20
Веб-сервер располагается отдельно:
имя сервера websrv-01.astra.aaa
сервер должен быть введен в домен
на сервере установлен и настроен web-сервер Apache2
сервер имеет постоянный IP-адрес, например, 192.168.1.21
Сервер базы данных располагается отдельно:
имя сервера dbsrv-01.astra.aaa
сервер должен быть введен в домен
на сервере должна быть установлена и настроенная СУБД Postgresql
сервер имеет постоянный IP-адрес, например, 192.168.1.23
Пользовательский компьютер располагается на отдельном компьютере:
имя компьютера pc-01.astra.aaa
компьютер должен быть введен в домен
компьютер имеет постоянный IP-адрес, например, 192.168.1.22
Основная концепция, реализация веб-приложения на языке программирования Python с МРД#
Контроллер домена Freeipa, веб сервер Apache2 и СУБД Postgres предварительно настроены для использования МРД и kerberos аутентификации. Пользователь домена при входе в сеанс на своём компьютере выбирает уровень и категорию из доступных для него, далее при отправке запроса из браузера веб сервер Apache2 получает классификационную метку пользователя и его билет kerberos. Веб сервер Apache2 производит аутентификацию пользователя и если она прошла успешно, то обработчик запроса переключается в контекст пользователя, включая классификационную метку МРД его сеанса. Далее запускается WSGI приложение и создается делегируемый kerberos кэш. Если аутентификации прошла неуспешно, выдается ошибка. Далее веб сервер Apache2 передаёт запрос веб приложению Flask. Веб приложение Flask добавляет в окружение переменную KRB5CCNAME. Далее коннектор Psycopg2, в режиме GSS, производит запрос к БД Postgresql с передачей контекста пользователя, включая классификационную метку МРД. СУБД Postgres так же производит kerberos аутентификацию, авторизацию по правилам МРД и возвращает результаты запроса.
Настройка контроллера домена#
Для настройки контроллера домена необходимо перейти по ссылке и выполнить действия по инструкции:
Настройка компьютера пользователя#
Для настройки компьютера пользователя необходимо перейти по ссылке и выполнить действия по инструкции:
Настройка сервера базы данных#
Для настройки сервера базы данных необходимо перейти по ссылке и выполнить действия по инструкции:
Установка и настройка веб-сервера Apache2#
Для установки и настройки веб-сервера Apache2 необходимо перейти по ссылке и выполнить действия по инструкции:
Установка веб-приложения#
Для установки и настройки веб-приложения необходимо:
Пункт 1#
установить следующие пакеты пакет:
sudo apt install python3-flask python3-psycopg2
Пункт 2#
создать по пути /var/www папку flask_app:
sudo mkdir /var/www/flask_app
Пункт 3#
развернуть приложение Flask, со структурой:
flask_app/
├── wsgi.py # Точка входа в WSGI приложение
├── app/ # Приложение
├── __init.py__ # Основной файл приложения
├── templates/ # HTML-шаблоны
│ └── index.html
└── static/ # Статические файлы (CSS, JS, изображения)
├── css/
├── js/
└── images/
Пункт 4#
файл wsgi.py должен иметь следующее содержимое:
import os
import sys
# Добавляем директорию текущего файла в Python path
# Это необходимо для корректного импорта модуля app
sys.path.append(os.path.dirname(__file__))
# Импортируем функцию создания Flask-приложения
from app import get_app
# Создаем экземпляр Flask-приложения
flask_app = get_app()
def application(environ, start_response):
"""
WSGI-интерфейс для интеграции с веб-сервером
Args:
environ (dict): Переменные окружения и параметры запроса
start_response (callable): Функция для отправки HTTP-заголовков
Returns:
Ответ Flask-приложения
Особенности:
- Устанавливает переменную KRB5CCNAME для Kerberos-аутентификации
- Передает управление основному Flask-приложению
"""
# Настраиваем Kerberos credentials из окружения WSGI
os.environ['KRB5CCNAME'] = environ['KRB5CCNAME']
# Делегируем обработку запроса Flask-приложению
return flask_app(environ, start_response)
Пункт 5#
файл __init.py__ должен иметь следующее содержимое:
from flask import Flask, render_template, request, redirect, url_for, flash, get_flashed_messages
import psycopg2
from psycopg2 import sql
import os
from contextlib import contextmanager
def get_app():
app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY', 'your-secret-key-here')
# Конфигурация подключения к БД
DB_CONFIG = {
'host': 'dbsrv-01.astra.aaa',
'port': '5432',
'dbname': 'demoprimer'
}
@contextmanager
def db_connection():
"""Контекстный менеджер для работы с БД"""
conn = None
try:
if 'KRB5CCNAME' in os.environ:
os.environ["KRB5CCNAME"] = os.environ.get('KRB5CCNAME', '')
conn = psycopg2.connect(**DB_CONFIG)
yield conn
except Exception as e:
flash(f"Ошибка подключения к базе данных: {str(e)}", "error")
raise
finally:
if conn:
conn.close()
@contextmanager
def db_cursor():
"""Контекстный менеджер для работы с курсором"""
with db_connection() as conn:
cursor = conn.cursor()
try:
yield cursor
conn.commit()
except Exception:
conn.rollback()
raise
def get_current_user():
"""Получение текущего пользователя"""
return request.remote_user or 'anonymous'
def format_record(record):
"""Форматирование записи из БД"""
return {
'id': record[1],
'maclabel': record[0],
'insert_user': record[2],
'classificator': record[4],
'insert_ts': record[3].strftime('%d-%m-%Y %H:%M:%S')
}
@app.route('/', methods=['GET', 'POST'])
def index():
try:
# Обработка GET параметров
if request.method == 'GET':
return handle_get_requests()
# Обработка POST запросов
return handle_post_requests()
except Exception as e:
flash(f"Ошибка: {str(e)}", "error")
return render_empty_page()
def handle_get_requests():
"""Обработка GET запросов"""
delete_id = request.args.get('delete')
if delete_id:
return delete_record(delete_id)
edit_id = request.args.get('edit')
records = fetch_all_records()
edit_record = fetch_edit_record(edit_id) if edit_id else None
return render_template('index.html',
records=records,
edit_record=edit_record,
messages=get_flashed_messages(with_categories=True))
def handle_post_requests():
"""Обработка POST запросов"""
current_user = get_current_user()
if 'create' in request.form:
return create_record(current_user, request.form['classificator'])
if 'update' in request.form:
return update_record(
request.form['id'],
request.form['classificator']
)
return redirect(url_for('index'))
def delete_record(record_id):
"""Удаление записи"""
with db_cursor() as cursor:
cursor.execute(
sql.SQL("DELETE FROM s1.t1 WHERE id = %s RETURNING id, classificator"),
(record_id,)
)
if cursor.fetchone():
flash("Запись успешно удалена", "success")
else:
flash("Запись не удалена", "warning")
return redirect(url_for('index'))
def create_record(user, classificator):
"""Создание новой записи"""
with db_cursor() as cursor:
cursor.execute(
sql.SQL("""
INSERT INTO s1.t1 (insert_user, insert_date, classificator)
VALUES (%s, CURRENT_TIMESTAMP, %s)
"""),
(user, classificator)
)
flash("Запись успешно создана", "success")
return redirect(url_for('index'))
def update_record(record_id, classificator):
"""Обновление записи"""
with db_cursor() as cursor:
cursor.execute(
sql.SQL("""
UPDATE s1.t1
SET classificator = %s
WHERE id = %s
RETURNING id, classificator
"""),
(classificator, record_id)
)
if cursor.fetchone():
flash("Запись успешно обновлена", "success")
else:
flash("Запись не обновлена", "warning")
return redirect(url_for('index'))
def fetch_all_records():
"""Получение всех записей"""
with db_cursor() as cursor:
cursor.execute("""
SELECT maclabel, *, extract(epoch from insert_date) as insert_ts
FROM s1.t1
ORDER BY insert_date DESC
""")
return [format_record(record) for record in cursor.fetchall()]
def fetch_edit_record(record_id):
"""Получение записи для редактирования"""
with db_cursor() as cursor:
cursor.execute(
sql.SQL("SELECT * FROM s1.t1 WHERE id = %s"),
(record_id,)
)
record = cursor.fetchone()
if record:
return {
'id': record[0],
'insert_user': record[1],
'insert_date': record[2].strftime('%d-%m-%Y %H:%M:%S'),
'classificator': record[3],
}
return None
def render_empty_page():
"""Рендеринг пустой страницы (при ошибках)"""
return render_template('index.html',
records=[],
edit_record=None,
messages=get_flashed_messages(with_categories=True))
return app
Пункт 6#
файл index.html должен иметь следующее содержимое:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Демонстрационный пример</title>
<style>
:root {
--primary-color: #0079C1;
--primary-hover: #00588D;
--secondary-color: #6c757d;
--secondary-hover: #5a6268;
--danger-color: #dc3545;
--danger-hover: #c82333;
--success-bg: #d4edda;
--success-text: #155724;
--success-border: #c3e6cb;
--error-bg: #f8d7da;
--error-text: #721c24;
--error-border: #f5c6cb;
--warning-bg: #ffe8cc;
--warning-text: #cc5500;
--warning-border: #ffd699;
}
body {
font-family: Arial, sans-serif;
margin: 20px;
line-height: 1.6;
}
h1, h2 {
margin: 0 0 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
th, td {
padding: 12px 15px;
text-align: left;
border: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
font-weight: 600;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #f1f1f1;
}
form {
margin-bottom: 20px;
padding: 20px;
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
input {
margin-bottom: 15px;
width: 100%;
max-width: 500px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 5px rgba(0,121,193,0.3);
}
.btn {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
text-decoration: none;
display: inline-block;
margin-right: 8px;
transition: background-color 0.3s ease;
border: none;
color: white;
}
.btn-primary {
background: var(--primary-color);
}
.btn-primary:hover {
background: var(--primary-hover);
}
.btn-secondary {
background: var(--secondary-color);
}
.btn-secondary:hover {
background: var(--secondary-hover);
}
.btn-danger {
background: var(--danger-color);
}
.btn-danger:hover {
background: var(--danger-hover);
}
.actions {
white-space: nowrap;
}
.alert {
padding: 12px 35px 12px 15px;
margin-bottom: 15px;
border-radius: 4px;
position: relative;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.alert-success {
background-color: var(--success-bg);
color: var(--success-text);
border: 1px solid var(--success-border);
}
.alert-error {
background-color: var(--error-bg);
color: var(--error-text);
border: 1px solid var(--error-border);
}
.alert-warning {
background-color: var(--warning-bg);
color: var(--warning-text);
border: 1px solid var(--warning-border);
}
.close-btn {
position: absolute;
right: 10px;
top: 10px;
cursor: pointer;
font-weight: bold;
font-size: 1.2em;
line-height: 1;
color: inherit;
background: none;
border: none;
padding: 0;
}
.close-btn:hover {
color: #000;
}
.form-footer {
margin-top: 15px;
}
@media (max-width: 768px) {
th, td {
padding: 8px 10px;
}
.btn {
padding: 8px 12px;
margin-right: 5px;
font-size: 13px;
}
}
</style>
</head>
<body>
<h1>Демонстрационный пример</h1>
{% for category, message in messages %}
<div class="alert alert-{{ category }}" role="alert">
{{ message }}
<button class="close-btn" aria-label="Закрыть">×</button>
</div>
{% endfor %}
<form method="POST">
<h2>{{ 'Редактировать запись' if edit_record else 'Добавить новую запись' }}</h2>
{% if edit_record %}
<input type="hidden" name="id" value="{{ edit_record.id }}">
{% endif %}
<label for="classificator">Наименование:</label>
<input type="text" id="classificator" name="classificator" required
placeholder="Введите наименование"
value="{{ edit_record.classificator if edit_record else '' }}">
<div class="form-footer">
<button type="submit" class="btn btn-primary" name="{{ 'update' if edit_record else 'create' }}">
{{ 'Обновить' if edit_record else 'Создать' }}
</button>
{% if edit_record %}
<a href="{{ url_for('index') }}" class="btn btn-secondary">Отмена</a>
{% endif %}
</div>
</form>
<table>
<thead>
<tr>
<th>ID</th>
<th>MAC</th>
<th>Наименование</th>
<th>Создано</th>
<th>Кем создано</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr>
<td>{{ record.id }}</td>
<td>{{ record.maclabel }}</td>
<td>{{ record.classificator }}</td>
<td>{{ record.insert_ts }}</td>
<td>{{ record.insert_user }}</td>
<td class="actions">
<a href="{{ url_for('index', edit=record.id) }}" class="btn btn-primary">Редактировать</a>
<a href="{{ url_for('index', delete=record.id) }}"
class="btn btn-danger"
onclick="return confirm('Вы уверены, что хотите удалить запись?')">Удалить</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
// Закрытие алертов при клике
document.querySelectorAll('.close-btn').forEach(btn => {
btn.addEventListener('click', function() {
this.closest('.alert').style.display = 'none';
});
});
// Автоматическое скрытие сообщений через 30 секунд
setTimeout(() => {
document.querySelectorAll('.alert').forEach(alert => {
alert.style.display = 'none';
});
}, 30000);
</script>
</body>
</html>
Сценарий проверки работы МРД в веб-приложении#
Важно
Документация дорабатывается по мере развития продуктов Группы Астра и по пожеланиям пользователей.
Ваши пожелания и замечания направляйте на почту docs@astralinux.ru