Домой Edit me on GitHub

2017-09-19

Безопасность

Аутентификация vs Авторизация

В пирамиде система безопасности поделена на 2 части. Первая это аутентификация, которая производит идентификацию пользователя, его проверку (например что он есть в БД и он не заблокирован) и определяет какими правами он наделен. Второе это авторизация, система которая проверяет имеет ли этот пользователь доступ к запрошенному ресурсу.

Кто ты?

Примечание

Фреймворк Repoze.bfg имеет расширение repoze.who, которое отвечает за идентификацию и аутентификацию пользователя.

Who? т.е. Кто? ты.

Для авторизации используется расширение repoze.what, которое проверяет какие ресурсы тебе доступны.

What? т.е. Что? доступно тебе.

Несмотря на то, что фреймворк Pyramid это по сути переименованный repoze.bfg, в нем есть собственный механизм авторизации и аутентификации из коробки.

Определение текущего пользователя при поступлении HTTP запроса, это задача аутентификации (authentication policy). Производится она в 3 этапа:

  1. Идентифицируем пользователя путем проверки токенов/заголовков/итд в HTTP запросе. (см. pyramid.request.Request.unauthenticated_userid)

    Например: ищем auth_token в куках запроса, проверяем что токен правильно подписан, и возвращаем id пользователя.

  2. Подтверждаем статус идентифицированного пользователя. (authenticated_userid)

    Например: проверяем что id этого пользователя все еще в базе данных и пользователь еще активен. Пользователя могли удалить из БД, но при этом в куках браузера хранится валидный токен auth_token.

  3. Ищем группы (principal) которые принадлежат пользователю и добавляем их в список. (effective_principals)

    Например: берем из БД группы пользователя и добавляем в список. Для текущего идентифицированного пользователя это может быть: “vasya”, “user_admin”, “editor”.

Что тебе дозволенно?

Каждый ресурс пирамиды может быть защищен правами доступа (permission). Задача авторизации определение того, какие пользователи имеют доступ к ресурсам.

После аутентификации создается список групп пользователя (principal). Политика авторизации (authorization policy) запрещает или разрешает доступ к ресурсу на основании этого списка групп, сверяя его с правами ресурса.

Добавление авторизации в проект

В пирамиде по умолчанию авторизация отключена. Все представления (views) полностью доступны анонимным пользователям. Для того что бы их защитить нужно добавить в настройки политику безопасности.

Для включения политики авторизации используется метод конфигуратора pyramid.config.Configurator.set_authorization_policy(). Для аутентификации pyramid.config.Configurator.set_authentication_policy() соответственно. Так-как авторизация не может существовать без аутентификации, необходимо указывать обе политики в проекте.

from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy

authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
authz_policy = ACLAuthorizationPolicy()

config = Configurator()
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)

Здесь pyramid.authentication.AuthTktAuthenticationPolicy это механизм аутентификации пользователя, который ищет его из “auth ticket” cookie. pyramid.authorization.ACLAuthorizationPolicy механизм авторизации по аксесс листам (ACL).

Права доступа для View

Императивно:

config.add_view('mypackage.views.blog_entry_add_view',
                name='add_entry.html',
                permission='add')

Декларативно:

from pyramid.view import view_config
from resources import Blog

@view_config(name='add_entry.html', permission='add')
def blog_entry_add_view(request):
    """ Add blog entry code goes here """
    # ...

Права доступа по умолчанию

Если ресурсу не присвоены права доступа, то используются права по умолчанию. В пирамиде права по умолчанию (pyramid.security.NO_PERMISSION_REQUIRED) подразумевают что ресурсы доступны всем, даже анонимным пользователям.

Это поведение возможно изменить при помощи метода pyramid.config.Configurator.set_default_permission().

config.set_default_permission('my_default_permission')

Аксесс листы (ACL)

Access Control List или ACL — список контроля доступа, который определяет, кто или что может получать доступ к конкретному объекту, и какие именно операции разрешено или запрещено этому субъекту проводить над объектом.

В пирамиде аксесс лист это список содержащий записи, определяющие права индивидуального пользователя или группы на ресурсы проекта. Элементы ACL также еще называют Access Control Entry или ACE.

Например:

from pyramid.security import Allow, Deny
from pyramid.security import Everyone

__acl__ = [
    (Deny, 'vasya', 'move'),
    (Deny, 'group:blacklist', ('add', 'delete', 'edit')),

    (Allow, Everyone, 'view'),
    (Allow, 'group:editors', ('add', 'edit')),
    (Allow, 'group:editors', 'move'),
    (Allow, 'group:deleter', 'delete'),
]

__acl__ из примера выше, это список контроля доступа (ACL).

(Allow, Everyone, 'delete') это ACE, т.е. запись в ACL.

  1. Первый элемент в списке ACE это действие, т.е. “что делать?” разрешить или запретить. Действия представляются константами pyramid.security.Allow и pyramid.security.Deny.
  2. Второй элемент списка это группы к которым принадлежит пользователь (principal).
  3. Последний элемент это права или список прав.

Также существую специальные группы (principal):

Если мы захотим запретить все, кроме тех ACE которые в списке, мы можем написать это так:

from pyramid.security import Allow
from pyramid.security import ALL_PERMISSIONS

__acl__ = [(Allow, 'fred', 'view'),
           (Deny, Everyone, ALL_PERMISSIONS)]

или воспользоваться встроенным в пирамиду ACE:

from pyramid.security import Allow
from pyramid.security import DENY_ALL

__acl__ = [(Allow, 'fred', 'view'),
           DENY_ALL]

ACL для ресурса

ACL для роутов

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy

from pyramid.security import Allow
from pyramid.security import Everyone


class HelloFactory(object):
    def __init__(self, request):
        self.__acl__ = [
            (Allow, Everyone, 'view'),
            (Allow, 'group:editors', 'add'),
            (Allow, 'group:editors', 'edit'),
        ]


def hello_world(request):
    return Response('Hello %(name)s!' % request.matchdict)

if __name__ == '__main__':
    authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
    authz_policy = ACLAuthorizationPolicy()

    config = Configurator()
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(authz_policy)

    config.add_route('hello', '/hello/{name}',
                     factory=HelloFactory)
    config.add_view(hello_world,
                    route_name='hello',
                    permission='view')

    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()

Глобальный ACL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy

from pyramid.security import Allow
from pyramid.security import Everyone


class HelloFactory(object):
    def __init__(self, request):
        self.__acl__ = [
            (Allow, Everyone, 'view'),
            (Allow, 'group:editors', 'add'),
            (Allow, 'group:editors', 'edit'),
        ]


def hello_world(request):
    return Response('Hello %(name)s!' % request.matchdict)

if __name__ == '__main__':
    authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
    authz_policy = ACLAuthorizationPolicy()

    config = Configurator(root_factory=HelloFactory)
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(authz_policy)

    config.add_route('hello', '/hello/{name}')
    config.add_view(hello_world,
                    route_name='hello',
                    permission='view')

    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()

Логин & Логаут

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from wsgiref.simple_server import make_server

from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPFound
from pyramid.response import Response
from pyramid.security import Allow, forget, remember


class HelloFactory(object):
    def __init__(self, request):
        self.__acl__ = [
            (Allow, 'vasya', 'view'),
            (Allow, 'group:editors', 'add'),
            (Allow, 'group:editors', 'edit'),
        ]


def hello_world(request):
    return Response('Hello %(name)s!' % request.matchdict)


def login(request):
    headers = remember(request, 'vasya')
    return HTTPFound(location=request.route_url('hello', name='vasya'),
                     headers=headers)


def logout(request):
    headers = forget(request)
    return HTTPFound(location=request.route_url('hello', name='log out!!!'),
                     headers=headers)


if __name__ == '__main__':
    authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
    authz_policy = ACLAuthorizationPolicy()

    config = Configurator(root_factory=HelloFactory)
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(authz_policy)

    config.add_route('hello', '/hello/{name}')
    config.add_view(hello_world,
                    route_name='hello',
                    permission='view')

    # login form
    config.add_route('login', '/login')
    config.add_route('logout', '/logout')
    config.add_view(login, route_name='login')
    config.add_view(logout, route_name='logout')

    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()

Basic Auth

from __future__ import absolute_import

from waitress import serve
from pyramid.config import Configurator
from pyramid.response import Response
from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
from pyramid.security import Authenticated, Allow, Everyone
from pyramid.authorization import ACLAuthorizationPolicy


class Root(object):
    __acl__ = [
        (Allow, Authenticated, 'view'),
    ]

    def __init__(self, request):
        self.request = request


def checkauth(username, password):
    return username == 'pyramid' and password == 'aliens'


class BasicAuthenticationPolicy(object):
    def authenticated_userid(self, request):
        authorization = AUTHORIZATION(request.environ)
        if not authorization:
            return None
        (authmeth, auth) = authorization.split(' ', 1)
        if 'basic' != authmeth.lower():
            return None
        auth = auth.strip().decode('base64')
        username, password = auth.split(':', 1)
        if not checkauth(username, password):
            return None
        return username

    def effective_principals(self, request):
        ep = [Everyone]
        username = self.authenticated_userid(request)
        if username is not None:
            ep.append(Authenticated)
            ep.append(username)
            ep.append('g:admin')
        return ep

    def unauthenticated_userid(self, request):
        authorization = AUTHORIZATION(request.environ)
        if not authorization:
            return None
        (authmeth, auth) = authorization.split(' ', 1)
        if 'basic' != authmeth.lower():
            return None
        auth = auth.strip().decode('base64')
        username, password = auth.split(':', 1)
        return username

    def remember(self, request, principal, **kw):
        return []

    def forget(self, request):
        return []


def forbidden_view(request):
    head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % 'fnord')
    return Response('Not Authorized', status='401 Not Authorized', headers=head)


def hello_world(request):
    return Response('Hello {!r}!'.format(request.effective_principals))

if __name__ == '__main__':
    config = Configurator(root_factory=Root)
    config.add_route('hello', '/hello')
    config.add_view(hello_world, route_name='hello', permission='view')
    config.set_authentication_policy(BasicAuthenticationPolicy())
    config.set_authorization_policy(ACLAuthorizationPolicy())
    config.add_forbidden_view(forbidden_view)
    app = config.make_wsgi_app()
    serve(app, host='0.0.0.0', port=8080)
Previous: Админка Next: Блог