Squid, Python и external_acl_type

В популярном прокси-сервере Squid реализована замечательная возможность проверять доступ пользователя к Интернет-ресурсам, используя сторонние программы. Отвечает за сие действо директива external_acl_type. С помощью неё становится возможным прикрутить к сквиду свои скрипты, написанные, например, на питоне. Обзор такого скрипта и самого механизма дальше.

Исходные данные:
  • Есть сквид, с настроенной аутентификацией пользователей, логины и пароли хранятся в БД MySQL.
  • Есть желание хранить разрешения для каждого пользователя в той же БД, не засоряя squid.conf
  • Есть возможность решить поставленную задачу используя Python
  • Всё, что явно не запрещено, разрешено
  • В дальнейшем есть планы реализовать веб-интерфейс для редактирования ACL

Используемая база данных выглядит следующим образом:

CREATE TABLE users (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    login VARCHAR(100),
    password VARCHAR(100),
) TYPE=innodb;

CREATE TABLE sites (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    url VARCHAR(100)
) TYPE=innodb;

CREATE TABLE acl (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    site_id INT NOT NULL,
    permit BOOLEAN,
    FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE,
    FOREIGN KEY (site_id) REFERENCES sites(id) ON UPDATE CASCADE ON DELETE CASCADE
) TYPE=innodb;

В ней есть две таблицы-справочника и одна таблица, в которую и записываются необходимые разрешения доступа. Для сохранения целостности данных в таблице acl объявлены внешние ключи user_id и site_id, а так же изменено поведение по-умолчанию MySQL при работе с такими ключами. Без указания «ON UPDATE CASCADE ON DELETE CASCADE» становится невозможным удаление или изменения записи из таблиц users или sites без предварительной правки таблицы acl.

Создаваемая программа должна читать со стандартного ввода (stdin) данные, передаваемые ей сквидом, и печатать на стандартный вывод (stdout) одну из двух строк: OK или ERR. Получив OK, сквид пропустит пользователя на сайт, получив ERR, запретит доступ. Обе строки можно дополнить необязательными параметрами, например: ERR message=«Access denied» (описание параметров, не входит в рассматриваемую тему, за подробностями, добро пожаловать в squid.conf). Параметры, передающиеся программе описываются директивой сквида external_acl_type. Вот наиболее интересные для нас:
  • %LOGIN — имя пользователя
  • %DST — адрес запрашиваемого сайта
Именно с ними предстоит работать нашей программе, поэтому правильная директива в конфиге сквида выглядит следующим образом:

external_acl_type python_test %LOGIN %DST /usr/local/bin/python_test.py

Вот код самой программы:

#!/usr/bin/python2.5 -u
# -*- coding: utf-8 -*-
import sys
import MySQLdb

def log(s):
    sys.stderr.write(s)

def main(arg):
    db=MySQLdb.connect(user='squid', passwd='squid', db='squid', host='10.0.10.100')
    c=db.cursor()

    while 1:
        l = sys.stdin.readline().split()

        try:
            login = l[0]
            url = l[1]
        except:
            continue

        c.execute('SELECT a.permit FROM users u, sites s, acl a WHERE u.login=%s AND s.url=%s AND a.user_id = u.id AND a.site_id = s.id', [login, url])
        r = c.fetchone()
        if r is None:
            permit = 1 # по умолчанию доступ разрешён
        else:
            permit = r[0]

        log('%s -> %s: %s\n' % (login, url, permit))
        if permit:
            sys.stdout.write('OK\n')
        else:
            sys.stdout.write('ERR\n')

    c.close()

if __name__ == '__main__':
    sys.exit(main(arg=sys.argv[1:]))

Небольшие пояснения по коду:
  • Строка #!/usr/bin/python2.5 -u отключает буферизацию вывода в stdout, без этого необходимо после каждого вывода писать дополнительно sys.stdout.flush() иначе работать ничего не будет.
  • Переодически на stdin валятся пустые строки, поэтому без обработки ввода программа будет постоянно вылетать, сквид постоянно делать рестарт и как результат не работать.
  • Всё, записанное в stderr будет отображено в cache.log сквида, очень удобно во время отладки.

Теперь осталось сделать последнее, а именно прописать созданную acl в конфиг сквида:

acl PythonTest          external        python_test
http_access             allow           PythonTest

После рестарта сквида в списке запущенных процессов появится наша программа:

25950 ?        Ss     0:00 /usr/bin/python2.5 -u /usr/local/bin/python_test.py
25951 ?        Ss     0:00 /usr/bin/python2.5 -u /usr/local/bin/python_test.py
25952 ?        Ss     0:00 /usr/bin/python2.5 -u /usr/local/bin/python_test.py
25953 ?        Ss     0:00 /usr/bin/python2.5 -u /usr/local/bin/python_test.py
25954 ?        Ss     0:00 /usr/bin/python2.5 -u /usr/local/bin/python_test.py

Теперь заносим в БД правило, запрещающее пользователю testuser заходить на сайт ya.ru:

INSERT INTO users VALUES (1, 'testuser', 'testpassword', 1);
INSERT INTO sites VALUES (2, 'ya.ru');
INSERT INTO acl VALUES (1, 1, 2, 0);

И проверяем работу программы, читая cache.log:

root@proxy:~# tail /var/log/squid/cache.log
testuser -> ya.ru: 0
testuser -> habrahabr.ru: 1
testuser -> s009.radikal.ru: 1
testuser -> foss-fest.com: 1

Как видно, всё работает.


1 комментарий

avatar
  • ksado
  • 0
Отличная статья, как раз ищу решение данного вопроса. Воспользуюсь идеей построения базы данных и запроса к ней.
Вот только хотелось бы «обработчик» на С или С++. Как никак должен быстрее работать чем питон.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.