back/auth: refactor authentication

- Removed transaction manager
- Each request gets its own session via DbSession middleware
- Moved authentication details to Authenticator middleware
- Fixed cyclic dependency between AuthService and UserService
This commit is contained in:
rr- 2016-03-28 22:31:08 +02:00
parent 8cf9b1dae4
commit 2e4e77791d
9 changed files with 73 additions and 98 deletions

View File

@ -2,6 +2,7 @@
import re import re
import falcon import falcon
from szurubooru.services.errors import IntegrityError
def _serialize_user(user): def _serialize_user(user):
return { return {
@ -24,7 +25,7 @@ class UserListApi(object):
def on_get(self, request, response): def on_get(self, request, response):
''' Retrieves a list of users. ''' ''' Retrieves a list of users. '''
self._auth_service.verify_privilege(request.context['user'], 'users:list') self._auth_service.verify_privilege(request.context['user'], 'users:list')
request.context['reuslt'] = {'message': 'Searching for users'} request.context['result'] = {'message': 'Searching for users'}
def on_post(self, request, response): def on_post(self, request, response):
''' Creates a new user. ''' ''' Creates a new user. '''
@ -33,7 +34,7 @@ class UserListApi(object):
password_regex = self._config['service']['password_regex'] password_regex = self._config['service']['password_regex']
try: try:
name = request.context['doc']['user'] name = request.context['doc']['name']
password = request.context['doc']['password'] password = request.context['doc']['password']
email = request.context['doc']['email'].strip() email = request.context['doc']['email'].strip()
if not email: if not email:
@ -52,7 +53,12 @@ class UserListApi(object):
'Malformed data', 'Malformed data',
'Password must validate %r expression' % password_regex) 'Password must validate %r expression' % password_regex)
user = self._user_service.create_user(name, password, email) session = request.context['session']
try:
user = self._user_service.create_user(session, name, password, email)
session.commit()
except:
raise IntegrityError('User %r already exists.' % name)
request.context['result'] = {'user': _serialize_user(user)} request.context['result'] = {'user': _serialize_user(user)}
class UserDetailApi(object): class UserDetailApi(object):
@ -65,7 +71,8 @@ class UserDetailApi(object):
def on_get(self, request, response, user_name): def on_get(self, request, response, user_name):
''' Retrieves an user. ''' ''' Retrieves an user. '''
self._auth_service.verify_privilege(request.context['user'], 'users:view') self._auth_service.verify_privilege(request.context['user'], 'users:view')
user = self._user_service.get_by_name(user_name) session = request.context['session']
user = self._user_service.get_by_name(session, user_name)
request.context['result'] = _serialize_user(user) request.context['result'] = _serialize_user(user)
def on_put(self, request, response, user_name): def on_put(self, request, response, user_name):

View File

@ -6,7 +6,6 @@ import sqlalchemy
import sqlalchemy.orm import sqlalchemy.orm
import szurubooru.api import szurubooru.api
import szurubooru.config import szurubooru.config
import szurubooru.db
import szurubooru.middleware import szurubooru.middleware
import szurubooru.services import szurubooru.services
@ -30,15 +29,13 @@ def create_app():
host=config['database']['host'], host=config['database']['host'],
port=config['database']['port'], port=config['database']['port'],
name=config['database']['name'])) name=config['database']['name']))
session_factory = sqlalchemy.orm.sessionmaker(bind=engine) session_maker = sqlalchemy.orm.sessionmaker(bind=engine)
transaction_manager = szurubooru.db.TransactionManager(session_factory) scoped_session = sqlalchemy.orm.scoped_session(session_maker)
# TODO: is there a better way? # TODO: is there a better way?
password_service = szurubooru.services.PasswordService(config) password_service = szurubooru.services.PasswordService(config)
user_service = szurubooru.services.UserService( auth_service = szurubooru.services.AuthService(config, password_service)
config, transaction_manager, password_service) user_service = szurubooru.services.UserService(config, password_service)
auth_service = szurubooru.services.AuthService(
config, user_service, password_service)
user_list = szurubooru.api.UserListApi(config, auth_service, user_service) user_list = szurubooru.api.UserListApi(config, auth_service, user_service)
user = szurubooru.api.UserDetailApi(config, auth_service, user_service) user = szurubooru.api.UserDetailApi(config, auth_service, user_service)
@ -46,7 +43,8 @@ def create_app():
app = falcon.API(middleware=[ app = falcon.API(middleware=[
szurubooru.middleware.RequireJson(), szurubooru.middleware.RequireJson(),
szurubooru.middleware.JsonTranslator(), szurubooru.middleware.JsonTranslator(),
szurubooru.middleware.Authenticator(auth_service), szurubooru.middleware.DbSession(session_maker),
szurubooru.middleware.Authenticator(auth_service, user_service),
]) ])
app.add_error_handler(szurubooru.services.AuthError, _on_auth_error) app.add_error_handler(szurubooru.services.AuthError, _on_auth_error)

View File

@ -1,36 +0,0 @@
''' Exports TransactionManager. '''
from contextlib import contextmanager
class TransactionManager(object):
''' Helper class for managing database transactions. '''
def __init__(self, session_factory):
self._session_factory = session_factory
@contextmanager
def transaction(self):
'''
Provides a transactional scope around a series of DB operations that
might change the database.
'''
return self._open_transaction(lambda session: session.commit)
@contextmanager
def read_only_transaction(self):
'''
Provides a transactional scope around a series of read-only DB
operations.
'''
return self._open_transaction(lambda session: session.rollback)
def _open_transaction(self, session_finalizer):
session = self._session_factory()
try:
yield session
session_finalizer(session)
except:
session.rollback()
raise
finally:
session.close()

View File

@ -3,3 +3,4 @@
from szurubooru.middleware.authenticator import Authenticator from szurubooru.middleware.authenticator import Authenticator
from szurubooru.middleware.json_translator import JsonTranslator from szurubooru.middleware.json_translator import JsonTranslator
from szurubooru.middleware.require_json import RequireJson from szurubooru.middleware.require_json import RequireJson
from szurubooru.middleware.db_session import DbSession

View File

@ -2,6 +2,8 @@
import base64 import base64
import falcon import falcon
from szurubooru.model.user import User
from szurubooru.services.errors import AuthError
class Authenticator(object): class Authenticator(object):
''' '''
@ -9,8 +11,9 @@ class Authenticator(object):
request context. request context.
''' '''
def __init__(self, auth_service): def __init__(self, auth_service, user_service):
self._auth_service = auth_service self._auth_service = auth_service
self._user_service = user_service
def process_request(self, request, response): def process_request(self, request, response):
''' Executed before passing the request to the API. ''' ''' Executed before passing the request to the API. '''
@ -18,7 +21,7 @@ class Authenticator(object):
def _get_user(self, request): def _get_user(self, request):
if not request.auth: if not request.auth:
return self._auth_service.authenticate(None, None) return self._create_anonymous_user()
try: try:
auth_type, user_and_password = request.auth.split(' ', 1) auth_type, user_and_password = request.auth.split(' ', 1)
@ -31,10 +34,27 @@ class Authenticator(object):
username, password = base64.decodebytes( username, password = base64.decodebytes(
user_and_password.encode('ascii')).decode('utf8').split(':') user_and_password.encode('ascii')).decode('utf8').split(':')
return self._auth_service.authenticate(username, password) session = request.context['session']
return self._authenticate(session, username, password)
except ValueError as err: except ValueError as err:
msg = 'Basic authentication header value not properly formed. ' \ msg = 'Basic authentication header value not properly formed. ' \
+ 'Supplied header {0}. Got error: {1}' + 'Supplied header {0}. Got error: {1}'
raise falcon.HTTPBadRequest( raise falcon.HTTPBadRequest(
'Malformed authentication request', 'Malformed authentication request',
msg.format(request.auth, str(err))) msg.format(request.auth, str(err)))
def _authenticate(self, session, username, password):
''' Tries to authenticate user. Throws AuthError for invalid users. '''
user = self._user_service.get_by_name(session, username)
if not user:
raise AuthError('No such user.')
if not self._auth_service.is_valid_password(user, password):
raise AuthError('Invalid password.')
return user
def _create_anonymous_user(self):
user = User()
user.name = None
user.access_rank = 'anonymous'
user.password = None
return user

View File

@ -0,0 +1,14 @@
''' Exports DbSession. '''
class DbSession(object):
''' Attaches database session to the context of every request. '''
def __init__(self, session_factory):
self._session_factory = session_factory
def process_request(self, request, response):
''' Executed before passing the request to the API. '''
request.context['session'] = self._session_factory()
def process_response(self, request, response, resource):
request.context['session'].close()

View File

@ -1,8 +1,8 @@
''' Exports JsonTranslator. ''' ''' Exports JsonTranslator. '''
import json import json
import falcon
from datetime import datetime from datetime import datetime
import falcon
def json_serial(obj): def json_serial(obj):
''' JSON serializer for objects not serializable by default JSON code ''' ''' JSON serializer for objects not serializable by default JSON code '''

View File

@ -1,27 +1,14 @@
''' Exports AuthService. ''' ''' Exports AuthService. '''
from szurubooru.model.user import User
from szurubooru.services.errors import AuthError from szurubooru.services.errors import AuthError
class AuthService(object): class AuthService(object):
''' Services related to user authentication ''' ''' Services related to user authentication '''
def __init__(self, config, user_service, password_service): def __init__(self, config, password_service):
self._config = config self._config = config
self._user_service = user_service
self._password_service = password_service self._password_service = password_service
def authenticate(self, username, password):
''' Tries to authenticate user. Throws AuthError for invalid users. '''
if not username:
return self._create_anonymous_user()
user = self._user_service.get_by_name(username)
if not user:
raise AuthError('No such user.')
if not self.is_valid_password(user, password):
raise AuthError('Invalid password.')
return user
def is_valid_password(self, user, password): def is_valid_password(self, user, password):
''' Returns whether the given password for a given user is valid. ''' ''' Returns whether the given password for a given user is valid. '''
salt, valid_hash = user.password_salt, user.password_hash salt, valid_hash = user.password_salt, user.password_hash
@ -45,10 +32,3 @@ class AuthService(object):
good_ranks = all_ranks[all_ranks.index(minimal_rank):] good_ranks = all_ranks[all_ranks.index(minimal_rank):]
if user.access_rank not in good_ranks: if user.access_rank not in good_ranks:
raise AuthError('Insufficient privileges to do this.') raise AuthError('Insufficient privileges to do this.')
def _create_anonymous_user(self):
user = User()
user.name = None
user.access_rank = 'anonymous'
user.password = None
return user

View File

@ -2,19 +2,16 @@
from datetime import datetime from datetime import datetime
from szurubooru.model.user import User from szurubooru.model.user import User
from szurubooru.services.errors import IntegrityError
class UserService(object): class UserService(object):
''' User management ''' ''' User management '''
def __init__(self, config, transaction_manager, password_service): def __init__(self, config, password_service):
self._config = config self._config = config
self._transaction_manager = transaction_manager
self._password_service = password_service self._password_service = password_service
def create_user(self, name, password, email): def create_user(self, session, name, password, email):
''' Creates an user with given parameters and returns it. ''' ''' Creates an user with given parameters and returns it. '''
with self._transaction_manager.transaction() as session:
user = User() user = User()
user.name = name user.name = name
user.password = password user.password = password
@ -26,15 +23,9 @@ class UserService(object):
user.creation_time = datetime.now() user.creation_time = datetime.now()
user.avatar_style = User.AVATAR_GRAVATAR user.avatar_style = User.AVATAR_GRAVATAR
try:
session.add(user) session.add(user)
session.commit()
except:
raise IntegrityError('User %r already exists.' % name)
return user return user
def get_by_name(self, name): def get_by_name(self, session, name):
''' Retrieves an user by its name. ''' ''' Retrieves an user by its name. '''
with self._transaction_manager.read_only_transaction() as session:
return session.query(User).filter_by(name=name).first() return session.query(User).filter_by(name=name).first()