Как стать автором
Обновить

Ограничение доступа к репозиториям

Время на прочтение 3 мин
Количество просмотров 4.2K
Чтобы управлять доступом можно использовать различные решения gitosys, gitolite, mercurial-server, но эти решения работают через SSH, что не всегда удобно (должен быть ключ). В добавок не хватает гибкости у подобных решений.

Основные требования:
  • доступ по логину/паролю (HTTPS)
  • контроль прав на чтение/запись
  • публичный/приватный репозиторий
  • управления всем через веб интерфейс
  • все данные (информация о проекте и пользователях) должны храниться в базе (MySQL)


Для решения этой задачи сделал следующую систему…


Собрал nginx с модулем Auth Request
Часть файла конфигурации:
server {
...

    location = /auth {
        include /etc/nginx/uwsgi_params;
        uwsgi_pass unix:/tmp/uwsgi.sock;
    }

    location /hg {
        set $project "";
        if ( $uri ~ "^/hg/([a-zA-Z0-9_\-]+).*$" ) {
            set $project $1;
        }
        if ( $project = "" ) {
            return 403;
        }
        if ( !-d /var/projects/$repo ) {
            return 404;
        }
        auth_request /auth;
        include /etc/nginx/proxy_params;
        proxy_redirect off;
        proxy_pass http://127.0.0.1:8002;
    }
}

127.0.0.1:8002 — в моём случае по этому адресу работает hgserve

Вся магия кроется в uwsgi-скрипте. Его задача проверить данные полученные от nginx. И вернуть код в зависимости от результата проверки (200,401,403).

#!/usr/bin/python

# configure
db_host = 'localhost'
db_name = 'authdb'
db_user = 'test'
db_pass = 'test'

role_id_read = 1 # member
role_id_write = 2 # developer

#

import re
import os
import base64
import MySQLdb

db_conn = MySQLdb.connect(host = db_host,
                          user = db_user,
                          passwd = db_pass,
                          db = db_name)

def get_project_info(project_name):
    global db_conn
    cursor = db_conn.cursor()
    query = 'SELECT id,is_private FROM projects WHERE name=%s'
    cursor.execute(query, (project_name, ))
    row = cursor.fetchone()
    cursor.close()
    return row

def get_role_id(project_id, username, password):
    global db_conn
    cursor = db_conn.cursor()
    query = 'SELECT id FROM users WHERE name=%s AND pass=sha1(%s)'
    cursor.execute(query, (username, password, ))
    row = cursor.fetchone()
    if row == None:
        return -1
    user_id = row[0]
    query = 'SELECT role_id FROM project_user_perm WHERE project_id=%s AND user_id=%s'
    cursor.execute(query, (project_id, user_id, ))
    row = cursor.fetchone()
    cursor.close()
    if row == None:
        return 0
    return row[0]

def can_user_read(project_id, username, password):
    role_id = get_role_id(project_id, username, password)
    if role_id == -1:
        return -1
    if role_id == role_id_read or role_id == role_id_write :
        return 1
    return 0

def can_user_write(project_id, username, password):
    role_id = get_role_id(project_id, username, password)
    if role_id == -1:
        return -1
    if role_id == role_id_write :
        return 1
    return 0

def ok200(callback):
    callback('200 OK', [])
    return []

def err401(callback):
    callback('401 Unauthorized', [('WWW-Authenticate', 'Basic realm="Restrict"')])
    return []

def err403(callback):
    callback('403 Forbidden', [])
    return []

def application(env, resp):
    req_uri = env.get('REQUEST_URI', '')
    m = re.match('^/(\w+)/(\w+).*$', req_uri)

    if m == None:
        return err403(resp)
    project_name = m.group(2)

    project_info = get_project_info(project_name)
    if project_info == None:
        return err403(resp)

    req_method = env.get('REQUEST_METHOD', '')
    if req_method == 'GET' and project_info[1] == 0:
        return ok200(resp)

    req_http_auth = env.get('HTTP_AUTHORIZATION', '')
    if req_http_auth == '':
        return err401(resp)

    m = re.match('^Basic ([a-zA-Z0-9=]+)$', req_http_auth)
    if m == None:
        return err403(resp)
    userpass = base64.b64decode(m.group(1))
    m = re.match('^(\w+):(\S+)$', userpass)
    if m == None:
        return err403(resp)
    username = m.group(1)
    password = m.group(2)

    result = 0
    if req_method == 'GET':
        result = can_user_read(project_info[0], username, password)
    else:
        result = can_user_write(project_info[0], username, password)

    if result == -1:
        return err401(resp)
    elif result == 1:
        return ok200(resp)
    else:
        return err403(resp)


Скрипт запускается с помощью uWSGI:
uwsgi -s /tmp/uwsgi.sock \
    --wsgi-file /var/projects/vcs_auth/vcs_auth_app.py \
    --uid www-data --gid www-data \
    --pidfile /var/run/vcs_auth_app.pid \
    -d /var/log/uwsgi/vcs_auth_app.log


Схема базы данных:
db schema
Теги:
Хабы:
+16
Комментарии 9
Комментарии Комментарии 9

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн