From 219ab7c2c350c4893cea57ba2fa81438680a5cdf Mon Sep 17 00:00:00 2001 From: rr- Date: Sun, 3 Apr 2016 22:03:58 +0200 Subject: [PATCH] server/general: be more pythonic --- .../szurubooru/api/password_reminder_api.py | 38 +++----- server/szurubooru/api/user_api.py | 90 ++++++------------- server/szurubooru/app.py | 63 +++++-------- server/szurubooru/config.py | 12 +-- server/szurubooru/db/__init__.py | 4 + server/szurubooru/{model => db}/base.py | 0 server/szurubooru/{model => db}/user.py | 4 +- server/szurubooru/middleware/authenticator.py | 25 ++---- server/szurubooru/middleware/db_session.py | 6 +- .../szurubooru/middleware/json_translator.py | 2 - server/szurubooru/middleware/require_json.py | 1 - server/szurubooru/migrations/env.py | 6 +- server/szurubooru/model/__init__.py | 6 -- server/szurubooru/search/__init__.py | 4 + .../search/base_search_config.py | 22 ++--- .../{services => }/search/criteria.py | 0 .../{services => }/search/search_executor.py | 10 +-- .../szurubooru/search/user_search_config.py | 42 +++++++++ server/szurubooru/services/__init__.py | 9 -- server/szurubooru/services/auth_service.py | 28 ------ server/szurubooru/services/mailer.py | 19 ---- .../szurubooru/services/password_service.py | 34 ------- server/szurubooru/services/search/__init__.py | 2 - .../services/search/user_search_config.py | 44 --------- server/szurubooru/services/user_service.py | 56 ------------ .../tests/api/test_updating_user.py | 55 ++++++------ server/szurubooru/tests/database_test_case.py | 6 +- .../tests/{services => search}/__init__.py | 0 .../search/test_user_search_config.py | 8 +- .../tests/services/search/__init__.py | 0 .../tests/{test_util.py => util/test_misc.py} | 21 +++-- server/szurubooru/util/__init__.py | 1 + server/szurubooru/util/auth.py | 59 ++++++++++++ server/szurubooru/util/mailer.py | 15 ++++ server/szurubooru/{util.py => util/misc.py} | 4 +- server/szurubooru/util/users.py | 67 ++++++++++++++ 36 files changed, 335 insertions(+), 428 deletions(-) create mode 100644 server/szurubooru/db/__init__.py rename server/szurubooru/{model => db}/base.py (100%) rename server/szurubooru/{model => db}/user.py (87%) delete mode 100644 server/szurubooru/model/__init__.py create mode 100644 server/szurubooru/search/__init__.py rename server/szurubooru/{services => }/search/base_search_config.py (82%) rename server/szurubooru/{services => }/search/criteria.py (100%) rename server/szurubooru/{services => }/search/search_executor.py (98%) create mode 100644 server/szurubooru/search/user_search_config.py delete mode 100644 server/szurubooru/services/__init__.py delete mode 100644 server/szurubooru/services/auth_service.py delete mode 100644 server/szurubooru/services/mailer.py delete mode 100644 server/szurubooru/services/password_service.py delete mode 100644 server/szurubooru/services/search/__init__.py delete mode 100644 server/szurubooru/services/search/user_search_config.py delete mode 100644 server/szurubooru/services/user_service.py rename server/szurubooru/tests/{services => search}/__init__.py (100%) rename server/szurubooru/tests/{services => }/search/test_user_search_config.py (97%) delete mode 100644 server/szurubooru/tests/services/search/__init__.py rename server/szurubooru/tests/{test_util.py => util/test_misc.py} (67%) create mode 100644 server/szurubooru/util/__init__.py create mode 100644 server/szurubooru/util/auth.py create mode 100644 server/szurubooru/util/mailer.py rename server/szurubooru/{util.py => util/misc.py} (93%) create mode 100644 server/szurubooru/util/users.py diff --git a/server/szurubooru/api/password_reminder_api.py b/server/szurubooru/api/password_reminder_api.py index 659355a..da2870e 100644 --- a/server/szurubooru/api/password_reminder_api.py +++ b/server/szurubooru/api/password_reminder_api.py @@ -1,5 +1,5 @@ -import hashlib -from szurubooru import errors +from szurubooru import config, errors +from szurubooru.util import auth, mailer, users from szurubooru.api.base_api import BaseApi MAIL_SUBJECT = 'Password reset for {name}' @@ -9,45 +9,35 @@ MAIL_BODY = \ 'Otherwise, please ignore this email.' class PasswordReminderApi(BaseApi): - def __init__(self, config, mailer, user_service): - super().__init__() - self._config = config - self._mailer = mailer - self._user_service = user_service - def get(self, context, user_name): - user = self._user_service.get_by_name(context.session, user_name) + ''' Send a mail with secure token to the correlated user. ''' + user = users.get_by_name(context.session, user_name) if not user: raise errors.NotFoundError('User %r not found.' % user_name) if not user.email: raise errors.ValidationError( 'User %r hasn\'t supplied email. Cannot reset password.' % user_name) - token = self._generate_authentication_token(user) + token = auth.generate_authentication_token(user) url = '%s/password-reset/%s' % ( - self._config['basic']['base_url'].rstrip('/'), token) - self._mailer.send( - 'noreply@%s' % self._config['basic']['name'], + config.config['basic']['base_url'].rstrip('/'), token) + mailer.send_mail( + 'noreply@%s' % config.config['basic']['name'], user.email, - MAIL_SUBJECT.format(name=self._config['basic']['name']), - MAIL_BODY.format(name=self._config['basic']['name'], url=url)) + MAIL_SUBJECT.format(name=config.config['basic']['name']), + MAIL_BODY.format(name=config.config['basic']['name'], url=url)) return {} def post(self, context, user_name): - user = self._user_service.get_by_name(context.session, user_name) + ''' Verify token from mail, generate a new password and return it. ''' + user = users.get_by_name(context.session, user_name) if not user: raise errors.NotFoundError('User %r not found.' % user_name) - good_token = self._generate_authentication_token(user) + good_token = auth.generate_authentication_token(user) if not 'token' in context.request: raise errors.ValidationError('Missing password reset token.') token = context.request['token'] if token != good_token: raise errors.ValidationError('Invalid password reset token.') - new_password = self._user_service.reset_password(user) + new_password = users.reset_password(user) context.session.commit() return {'password': new_password} - - def _generate_authentication_token(self, user): - digest = hashlib.sha256() - digest.update(self._config['basic']['secret'].encode('utf8')) - digest.update(user.password_salt.encode('utf8')) - return digest.hexdigest() diff --git a/server/szurubooru/api/user_api.py b/server/szurubooru/api/user_api.py index 946c413..df30903 100644 --- a/server/szurubooru/api/user_api.py +++ b/server/szurubooru/api/user_api.py @@ -1,9 +1,7 @@ -import re import sqlalchemy -from szurubooru import errors -from szurubooru import util +from szurubooru import errors, search +from szurubooru.util import auth, users from szurubooru.api.base_api import BaseApi -from szurubooru.services import search def _serialize_user(authenticated_user, user): ret = { @@ -21,29 +19,27 @@ def _serialize_user(authenticated_user, user): class UserListApi(BaseApi): ''' API for lists of users. ''' - def __init__(self, auth_service, user_service): + def __init__(self): super().__init__() - self._auth_service = auth_service - self._user_service = user_service self._search_executor = search.SearchExecutor(search.UserSearchConfig()) def get(self, context): - ''' Retrieves a list of users. ''' - self._auth_service.verify_privilege(context.user, 'users:list') + ''' Retrieve a list of users. ''' + auth.verify_privilege(context.user, 'users:list') query = context.get_param_as_string('query') page = context.get_param_as_int('page', 1) - count, users = self._search_executor.execute(context.session, query, page) + count, user_list = self._search_executor.execute(context.session, query, page) return { 'query': query, 'page': page, 'page_size': self._search_executor.page_size, 'total': count, - 'users': [_serialize_user(context.user, user) for user in users], + 'users': [_serialize_user(context.user, user) for user in user_list], } def post(self, context): - ''' Creates a new user. ''' - self._auth_service.verify_privilege(context.user, 'users:create') + ''' Create a new user. ''' + auth.verify_privilege(context.user, 'users:create') try: name = context.request['name'].strip() @@ -52,9 +48,9 @@ class UserListApi(BaseApi): except KeyError as ex: raise errors.ValidationError('Field %r not found.' % ex.args[0]) - user = self._user_service.create_user( - context.session, name, password, email) + user = users.create_user(name, password, email) try: + context.session.add(user) context.session.commit() except sqlalchemy.exc.IntegrityError: raise errors.IntegrityError('User %r already exists.' % name) @@ -63,26 +59,17 @@ class UserListApi(BaseApi): class UserDetailApi(BaseApi): ''' API for individual users. ''' - def __init__(self, config, auth_service, password_service, user_service): - super().__init__() - self._available_access_ranks = config['service']['user_ranks'] - self._name_regex = config['service']['user_name_regex'] - self._password_regex = config['service']['password_regex'] - self._password_service = password_service - self._auth_service = auth_service - self._user_service = user_service - def get(self, context, user_name): - ''' Retrieves an user. ''' - self._auth_service.verify_privilege(context.user, 'users:view') - user = self._user_service.get_by_name(context.session, user_name) + ''' Retrieve an user. ''' + auth.verify_privilege(context.user, 'users:view') + user = users.get_by_name(context.session, user_name) if not user: raise errors.NotFoundError('User %r not found.' % user_name) return {'user': _serialize_user(context.user, user)} def put(self, context, user_name): - ''' Updates an existing user. ''' - user = self._user_service.get_by_name(context.session, user_name) + ''' Update an existing user. ''' + user = users.get_by_name(context.session, user_name) if not user: raise errors.NotFoundError('User %r not found.' % user_name) @@ -92,53 +79,26 @@ class UserDetailApi(BaseApi): infix = 'any' if 'name' in context.request: - self._auth_service.verify_privilege( - context.user, 'users:edit:%s:name' % infix) - name = context.request['name'].strip() - if not re.match(self._name_regex, name): - raise errors.ValidationError( - 'Name must satisfy regex %r.' % self._name_regex) - user.name = name + auth.verify_privilege(context.user, 'users:edit:%s:name' % infix) + users.update_name(user, context.request['name']) if 'password' in context.request: - password = context.request['password'] - self._auth_service.verify_privilege( - context.user, 'users:edit:%s:pass' % infix) - if not re.match(self._password_regex, password): - raise errors.ValidationError( - 'Password must satisfy regex %r.' % self._password_regex) - user.password_salt = self._password_service.create_password() - user.password_hash = self._password_service.get_password_hash( - user.password_salt, password) + auth.verify_privilege(context.user, 'users:edit:%s:pass' % infix) + users.update_password(user, context.request['password']) if 'email' in context.request: - self._auth_service.verify_privilege( - context.user, 'users:edit:%s:email' % infix) - email = context.request['email'].strip() or None - if not util.is_valid_email(email): - raise errors.ValidationError( - '%r is not a vaild email address.' % email) - user.email = email + auth.verify_privilege(context.user, 'users:edit:%s:email' % infix) + users.update_email(user, context.request['email']) if 'accessRank' in context.request: - self._auth_service.verify_privilege( - context.user, 'users:edit:%s:rank' % infix) - rank = context.request['accessRank'].strip() - if not rank in self._available_access_ranks: - raise errors.ValidationError( - 'Bad access rank. Valid access ranks: %r' \ - % self._available_access_ranks) - if self._available_access_ranks.index(context.user.access_rank) \ - < self._available_access_ranks.index(rank): - raise errors.AuthError( - 'Trying to set higher access rank than one has') - user.access_rank = rank + auth.verify_privilege(context.user, 'users:edit:%s:rank' % infix) + users.update_rank(user, context.request['accessRank'], context.user) # TODO: avatar try: context.session.commit() except sqlalchemy.exc.IntegrityError: - raise errors.IntegrityError('User %r already exists.' % name) + raise errors.IntegrityError('User %r already exists.' % user.name) return {'user': _serialize_user(context.user, user)} diff --git a/server/szurubooru/app.py b/server/szurubooru/app.py index e23cdb5..ede59c5 100644 --- a/server/szurubooru/app.py +++ b/server/szurubooru/app.py @@ -3,16 +3,11 @@ import falcon import sqlalchemy import sqlalchemy.orm -import szurubooru.api -import szurubooru.config -import szurubooru.errors -import szurubooru.middleware -import szurubooru.services -import szurubooru.services.search -import szurubooru.util +from szurubooru import api, config, errors, middleware +from szurubooru.util import misc class _CustomRequest(falcon.Request): - context_type = szurubooru.util.dotdict + context_type = misc.dotdict def get_param_as_string(self, name, required=False, store=None, default=None): params = self._params @@ -45,47 +40,37 @@ def _on_not_found_error(ex, _request, _response, _params): raise falcon.HTTPNotFound(title='Not found', description=str(ex)) def create_app(): - ''' Creates a WSGI compatible App object. ''' - config = szurubooru.config.Config() - + ''' Create a WSGI compatible App object. ''' engine = sqlalchemy.create_engine( '{schema}://{user}:{password}@{host}:{port}/{name}'.format( - schema=config['database']['schema'], - user=config['database']['user'], - password=config['database']['pass'], - host=config['database']['host'], - port=config['database']['port'], - name=config['database']['name'])) + schema=config.config['database']['schema'], + user=config.config['database']['user'], + password=config.config['database']['pass'], + host=config.config['database']['host'], + port=config.config['database']['port'], + name=config.config['database']['name'])) session_maker = sqlalchemy.orm.sessionmaker(bind=engine) scoped_session = sqlalchemy.orm.scoped_session(session_maker) - # TODO: is there a better way? - mailer = szurubooru.services.Mailer(config) - password_service = szurubooru.services.PasswordService(config) - auth_service = szurubooru.services.AuthService(config, password_service) - user_service = szurubooru.services.UserService(config, password_service) - - user_list_api = szurubooru.api.UserListApi(auth_service, user_service) - user_detail_api = szurubooru.api.UserDetailApi( - config, auth_service, password_service, user_service) - password_reminder_api = szurubooru.api.PasswordReminderApi( - config, mailer, user_service) - app = falcon.API( request_type=_CustomRequest, middleware=[ - szurubooru.middleware.ImbueContext(), - szurubooru.middleware.RequireJson(), - szurubooru.middleware.JsonTranslator(), - szurubooru.middleware.DbSession(scoped_session), - szurubooru.middleware.Authenticator(auth_service, user_service), + middleware.ImbueContext(), + middleware.RequireJson(), + middleware.JsonTranslator(), + middleware.DbSession(scoped_session), + middleware.Authenticator(), ]) - app.add_error_handler(szurubooru.errors.AuthError, _on_auth_error) - app.add_error_handler(szurubooru.errors.IntegrityError, _on_integrity_error) - app.add_error_handler(szurubooru.errors.ValidationError, _on_validation_error) - app.add_error_handler(szurubooru.errors.SearchError, _on_search_error) - app.add_error_handler(szurubooru.errors.NotFoundError, _on_not_found_error) + user_list_api = api.UserListApi() + user_detail_api = api.UserDetailApi() + password_reminder_api = api.PasswordReminderApi() + + app.add_error_handler(errors.AuthError, _on_auth_error) + app.add_error_handler(errors.IntegrityError, _on_integrity_error) + app.add_error_handler(errors.ValidationError, _on_validation_error) + app.add_error_handler(errors.SearchError, _on_search_error) + app.add_error_handler(errors.NotFoundError, _on_not_found_error) app.add_route('/users/', user_list_api) app.add_route('/user/{user_name}', user_detail_api) diff --git a/server/szurubooru/config.py b/server/szurubooru/config.py index d5bb065..ccb8b23 100644 --- a/server/szurubooru/config.py +++ b/server/szurubooru/config.py @@ -1,6 +1,6 @@ import os import configobj -import szurubooru.errors +from szurubooru import errors class Config(object): ''' INI config parser and container. ''' @@ -15,20 +15,22 @@ class Config(object): def _validate(self): ''' - Checks whether config.ini doesn't contain errors that might prove + Check whether config.ini doesn't contain errors that might prove lethal at runtime. ''' all_ranks = self['service']['user_ranks'] for privilege, rank in self['privileges'].items(): if rank not in all_ranks: - raise szurubooru.errors.ConfigError( + raise errors.ConfigError( 'Rank %r for privilege %r is missing from user_ranks' % ( rank, privilege)) for rank in ['anonymous', 'admin', 'nobody']: if rank not in all_ranks: - raise szurubooru.errors.ConfigError( + raise errors.ConfigError( 'Fixed rank %r is missing from user_ranks' % rank) if self['service']['default_user_rank'] not in all_ranks: - raise szurubooru.errors.ConfigError( + raise errors.ConfigError( 'Default rank %r is missing from user_ranks' % ( self['service']['default_user_rank'])) + +config = Config() # pylint: disable=invalid-name diff --git a/server/szurubooru/db/__init__.py b/server/szurubooru/db/__init__.py new file mode 100644 index 0000000..634c179 --- /dev/null +++ b/server/szurubooru/db/__init__.py @@ -0,0 +1,4 @@ +''' Database layer. ''' + +from szurubooru.db.base import Base +from szurubooru.db.user import User diff --git a/server/szurubooru/model/base.py b/server/szurubooru/db/base.py similarity index 100% rename from server/szurubooru/model/base.py rename to server/szurubooru/db/base.py diff --git a/server/szurubooru/model/user.py b/server/szurubooru/db/user.py similarity index 87% rename from server/szurubooru/model/user.py rename to server/szurubooru/db/user.py index f5f5ecd..1b0f9be 100644 --- a/server/szurubooru/model/user.py +++ b/server/szurubooru/db/user.py @@ -1,7 +1,5 @@ -# pylint: disable=too-many-instance-attributes,too-few-public-methods - import sqlalchemy as sa -from szurubooru.model.base import Base +from szurubooru.db.base import Base class User(Base): __tablename__ = 'user' diff --git a/server/szurubooru/middleware/authenticator.py b/server/szurubooru/middleware/authenticator.py index 58ed357..0f85027 100644 --- a/server/szurubooru/middleware/authenticator.py +++ b/server/szurubooru/middleware/authenticator.py @@ -1,24 +1,20 @@ import base64 import falcon -from szurubooru import errors -from szurubooru import model +from szurubooru import db, errors +from szurubooru.util import auth, users class Authenticator(object): ''' - Authenticates every request and puts information on active user in the + Authenticates every request and put information on active user in the request context. ''' - def __init__(self, auth_service, user_service): - self._auth_service = auth_service - self._user_service = user_service - def process_request(self, request, _response): - ''' Executed before passing the request to the API. ''' + ''' Bind the user to request. Update last login time if needed. ''' request.context.user = self._get_user(request) if request.get_param_as_bool('bump-login') \ and request.context.user.user_id: - self._user_service.bump_login_time(request.context.user) + users.bump_login_time(request.context.user) request.context.session.commit() def _get_user(self, request): @@ -27,15 +23,12 @@ class Authenticator(object): try: auth_type, user_and_password = request.auth.split(' ', 1) - if auth_type.lower() != 'basic': raise falcon.HTTPBadRequest( 'Invalid authentication type', 'Only basic authorization is supported.') - username, password = base64.decodebytes( user_and_password.encode('ascii')).decode('utf8').split(':') - return self._authenticate( request.context.session, username, password) except ValueError as err: @@ -46,16 +39,16 @@ class Authenticator(object): 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) + ''' Try to authenticate user. Throw AuthError for invalid users. ''' + user = users.get_by_name(session, username) if not user: raise errors.AuthError('No such user.') - if not self._auth_service.is_valid_password(user, password): + if not auth.is_valid_password(user, password): raise errors.AuthError('Invalid password.') return user def _create_anonymous_user(self): - user = model.User() + user = db.User() user.name = None user.access_rank = 'anonymous' user.password = None diff --git a/server/szurubooru/middleware/db_session.py b/server/szurubooru/middleware/db_session.py index 12bbad7..059b073 100644 --- a/server/szurubooru/middleware/db_session.py +++ b/server/szurubooru/middleware/db_session.py @@ -5,12 +5,8 @@ class DbSession(object): 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): - ''' - Executed before passing the response to falcon. - Any commits to database need to happen explicitly in the API layer. - ''' + # any commits need to happen explicitly in the API layer. request.context.session.close() diff --git a/server/szurubooru/middleware/json_translator.py b/server/szurubooru/middleware/json_translator.py index cc42bf4..ae973d3 100644 --- a/server/szurubooru/middleware/json_translator.py +++ b/server/szurubooru/middleware/json_translator.py @@ -16,7 +16,6 @@ class JsonTranslator(object): ''' def process_request(self, request, _response): - ''' Executed before passing the request to the API. ''' if request.content_length in (None, 0): return @@ -36,7 +35,6 @@ class JsonTranslator(object): 'JSON was incorrect or not encoded as UTF-8.') def process_response(self, request, response, _resource): - ''' Executed before passing the response to falcon. ''' if 'result' not in request.context: return response.body = json.dumps( diff --git a/server/szurubooru/middleware/require_json.py b/server/szurubooru/middleware/require_json.py index ef901ca..a965a7b 100644 --- a/server/szurubooru/middleware/require_json.py +++ b/server/szurubooru/middleware/require_json.py @@ -4,7 +4,6 @@ class RequireJson(object): ''' Sanitizes requests so that only JSON is accepted. ''' def process_request(self, request, _response): - ''' Executed before passing the request to the API. ''' if not request.client_accepts_json: raise falcon.HTTPNotAcceptable( 'This API only supports responses encoded as JSON.') diff --git a/server/szurubooru/migrations/env.py b/server/szurubooru/migrations/env.py index c10f0a1..c109d9a 100644 --- a/server/szurubooru/migrations/env.py +++ b/server/szurubooru/migrations/env.py @@ -9,13 +9,13 @@ import logging.config dir_to_self = os.path.dirname(os.path.realpath(__file__)) sys.path.append(os.path.join(dir_to_self, *[os.pardir] * 2)) -import szurubooru.model.base +import szurubooru.db.base import szurubooru.config alembic_config = alembic.context.config logging.config.fileConfig(alembic_config.config_file_name) -szuru_config = szurubooru.config.Config() +szuru_config = szurubooru.config.config alembic_config.set_main_option( 'sqlalchemy.url', '{schema}://{user}:{password}@{host}:{port}/{name}'.format( @@ -26,7 +26,7 @@ alembic_config.set_main_option( port=szuru_config['database']['port'], name=szuru_config['database']['name'])) -target_metadata = szurubooru.model.Base.metadata +target_metadata = szurubooru.db.Base.metadata def run_migrations_offline(): ''' diff --git a/server/szurubooru/model/__init__.py b/server/szurubooru/model/__init__.py deleted file mode 100644 index 8423e0d..0000000 --- a/server/szurubooru/model/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -''' -Database models. -''' - -from szurubooru.model.base import Base -from szurubooru.model.user import User diff --git a/server/szurubooru/search/__init__.py b/server/szurubooru/search/__init__.py new file mode 100644 index 0000000..f614db6 --- /dev/null +++ b/server/szurubooru/search/__init__.py @@ -0,0 +1,4 @@ +''' Search parsers and services. ''' + +from szurubooru.search.search_executor import SearchExecutor +from szurubooru.search.user_search_config import UserSearchConfig diff --git a/server/szurubooru/services/search/base_search_config.py b/server/szurubooru/search/base_search_config.py similarity index 82% rename from server/szurubooru/services/search/base_search_config.py rename to server/szurubooru/search/base_search_config.py index 7aba690..0e6712f 100644 --- a/server/szurubooru/services/search/base_search_config.py +++ b/server/szurubooru/search/base_search_config.py @@ -1,11 +1,11 @@ import sqlalchemy import szurubooru.errors -from szurubooru import util -from szurubooru.services.search import criteria +from szurubooru.util import misc +from szurubooru.search import criteria def _apply_criterion_to_column( column, query, criterion, allow_composite=True, allow_ranged=True): - ''' Decorates SQLAlchemy filter on given column using supplied criterion. ''' + ''' Decorate SQLAlchemy filter on given column using supplied criterion. ''' if isinstance(criterion, criteria.StringSearchCriterion): expr = column == criterion.value if criterion.negated: @@ -32,11 +32,11 @@ def _apply_criterion_to_column( def _apply_date_criterion_to_column(column, query, criterion): ''' - Decorates SQLAlchemy filter on given column using supplied criterion. - Parses the datetime inside the criterion. + Decorate SQLAlchemy filter on given column using supplied criterion. + Parse the datetime inside the criterion. ''' if isinstance(criterion, criteria.StringSearchCriterion): - min_date, max_date = util.parse_time_range(criterion.value) + min_date, max_date = misc.parse_time_range(criterion.value) expr = column.between(min_date, max_date) if criterion.negated: expr = ~expr @@ -44,7 +44,7 @@ def _apply_date_criterion_to_column(column, query, criterion): elif isinstance(criterion, criteria.ArraySearchCriterion): expr = sqlalchemy.sql.false() for value in criterion.values: - min_date, max_date = util.parse_time_range(value) + min_date, max_date = misc.parse_time_range(value) expr = expr | column.between(min_date, max_date) if criterion.negated: expr = ~expr @@ -52,14 +52,14 @@ def _apply_date_criterion_to_column(column, query, criterion): elif isinstance(criterion, criteria.RangedSearchCriterion): assert criterion.min_value or criterion.max_value if criterion.min_value and criterion.max_value: - min_date = util.parse_time_range(criterion.min_value)[0] - max_date = util.parse_time_range(criterion.max_value)[1] + min_date = misc.parse_time_range(criterion.min_value)[0] + max_date = misc.parse_time_range(criterion.max_value)[1] expr = column.between(min_date, max_date) elif criterion.min_value: - min_date = util.parse_time_range(criterion.min_value)[0] + min_date = misc.parse_time_range(criterion.min_value)[0] expr = column >= min_date elif criterion.max_value: - max_date = util.parse_time_range(criterion.max_value)[1] + max_date = misc.parse_time_range(criterion.max_value)[1] expr = column <= max_date if criterion.negated: expr = ~expr diff --git a/server/szurubooru/services/search/criteria.py b/server/szurubooru/search/criteria.py similarity index 100% rename from server/szurubooru/services/search/criteria.py rename to server/szurubooru/search/criteria.py diff --git a/server/szurubooru/services/search/search_executor.py b/server/szurubooru/search/search_executor.py similarity index 98% rename from server/szurubooru/services/search/search_executor.py rename to server/szurubooru/search/search_executor.py index f12b539..52d3c71 100644 --- a/server/szurubooru/services/search/search_executor.py +++ b/server/szurubooru/search/search_executor.py @@ -1,19 +1,17 @@ -''' Exports SearchExecutor. ''' - import re import sqlalchemy from szurubooru import errors -from szurubooru.services.search import criteria +from szurubooru.search import criteria class SearchExecutor(object): - ORDER_DESC = 1 - ORDER_ASC = 2 - ''' Class for search parsing and execution. Handles plaintext parsing and delegates sqlalchemy filter decoration to SearchConfig instances. ''' + ORDER_DESC = 1 + ORDER_ASC = 2 + def __init__(self, search_config): self.page_size = 100 self._search_config = search_config diff --git a/server/szurubooru/search/user_search_config.py b/server/szurubooru/search/user_search_config.py new file mode 100644 index 0000000..c8583d9 --- /dev/null +++ b/server/szurubooru/search/user_search_config.py @@ -0,0 +1,42 @@ +from sqlalchemy.sql.expression import func +from szurubooru import db +from szurubooru.search.base_search_config import BaseSearchConfig + +class UserSearchConfig(BaseSearchConfig): + ''' Executes searches related to the users. ''' + + def create_query(self, session): + return session.query(db.User) + + @property + def anonymous_filter(self): + return self._create_basic_filter(db.User.name, allow_ranged=False) + + @property + def special_filters(self): + return {} + + @property + def named_filters(self): + return { + 'name': self._create_basic_filter(db.User.name, allow_ranged=False), + 'creation_date': self._create_date_filter(db.User.creation_time), + 'creation_time': self._create_date_filter(db.User.creation_time), + 'last_login_date': self._create_date_filter(db.User.last_login_time), + 'last_login_time': self._create_date_filter(db.User.last_login_time), + 'login_date': self._create_date_filter(db.User.last_login_time), + 'login_time': self._create_date_filter(db.User.last_login_time), + } + + @property + def order_columns(self): + return { + 'random': func.random(), + 'name': db.User.name, + 'creation_date': db.User.creation_time, + 'creation_time': db.User.creation_time, + 'last_login_date': db.User.last_login_time, + 'last_login_time': db.User.last_login_time, + 'login_date': db.User.last_login_time, + 'login_time': db.User.last_login_time, + } diff --git a/server/szurubooru/services/__init__.py b/server/szurubooru/services/__init__.py deleted file mode 100644 index 83e57e2..0000000 --- a/server/szurubooru/services/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -''' -Middle layer between REST API and database. -All the business logic goes here. -''' - -from szurubooru.services.mailer import Mailer -from szurubooru.services.auth_service import AuthService -from szurubooru.services.user_service import UserService -from szurubooru.services.password_service import PasswordService diff --git a/server/szurubooru/services/auth_service.py b/server/szurubooru/services/auth_service.py deleted file mode 100644 index 1f4a412..0000000 --- a/server/szurubooru/services/auth_service.py +++ /dev/null @@ -1,28 +0,0 @@ -from szurubooru import errors - -class AuthService(object): - def __init__(self, config, password_service): - self._config = config - self._password_service = password_service - - def is_valid_password(self, user, password): - ''' Returns whether the given password for a given user is valid. ''' - salt, valid_hash = user.password_salt, user.password_hash - possible_hashes = [ - self._password_service.get_password_hash(salt, password), - self._password_service.get_legacy_password_hash(salt, password) - ] - return valid_hash in possible_hashes - - def verify_privilege(self, user, privilege_name): - ''' - Throws an AuthError if the given user doesn't have given privilege. - ''' - all_ranks = self._config['service']['user_ranks'] - - assert privilege_name in self._config['privileges'] - assert user.access_rank in all_ranks - minimal_rank = self._config['privileges'][privilege_name] - good_ranks = all_ranks[all_ranks.index(minimal_rank):] - if user.access_rank not in good_ranks: - raise errors.AuthError('Insufficient privileges to do this.') diff --git a/server/szurubooru/services/mailer.py b/server/szurubooru/services/mailer.py deleted file mode 100644 index 7e5d3b3..0000000 --- a/server/szurubooru/services/mailer.py +++ /dev/null @@ -1,19 +0,0 @@ -import smtplib -import email.mime.text - -class Mailer(object): - def __init__(self, config): - self._config = config - - def send(self, sender, recipient, subject, body): - msg = email.mime.text.MIMEText(body) - msg['Subject'] = subject - msg['From'] = sender - msg['To'] = recipient - - smtp = smtplib.SMTP( - self._config['smtp']['host'], - int(self._config['smtp']['port'])) - smtp.login(self._config['smtp']['user'], self._config['smtp']['pass']) - smtp.send_message(msg) - smtp.quit() diff --git a/server/szurubooru/services/password_service.py b/server/szurubooru/services/password_service.py deleted file mode 100644 index f13da94..0000000 --- a/server/szurubooru/services/password_service.py +++ /dev/null @@ -1,34 +0,0 @@ -import hashlib -import random - -class PasswordService(object): - ''' Stateless utilities for passwords ''' - - def __init__(self, config): - self._config = config - - def get_password_hash(self, salt, password): - ''' Retrieves new-style password hash. ''' - digest = hashlib.sha256() - digest.update(self._config['basic']['secret'].encode('utf8')) - digest.update(salt.encode('utf8')) - digest.update(password.encode('utf8')) - return digest.hexdigest() - - def get_legacy_password_hash(self, salt, password): - ''' Retrieves old-style password hash. ''' - digest = hashlib.sha1() - digest.update(b'1A2/$_4xVa') - digest.update(salt.encode('utf8')) - digest.update(password.encode('utf8')) - return digest.hexdigest() - - def create_password(self): - ''' Creates an easy-to-remember password. ''' - alphabet = { - 'c': list('bcdfghijklmnpqrstvwxyz'), - 'v': list('aeiou'), - 'n': list('0123456789'), - } - pattern = 'cvcvnncvcv' - return ''.join(random.choice(alphabet[l]) for l in list(pattern)) diff --git a/server/szurubooru/services/search/__init__.py b/server/szurubooru/services/search/__init__.py deleted file mode 100644 index c05934d..0000000 --- a/server/szurubooru/services/search/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from szurubooru.services.search.search_executor import SearchExecutor -from szurubooru.services.search.user_search_config import UserSearchConfig diff --git a/server/szurubooru/services/search/user_search_config.py b/server/szurubooru/services/search/user_search_config.py deleted file mode 100644 index bd95ec6..0000000 --- a/server/szurubooru/services/search/user_search_config.py +++ /dev/null @@ -1,44 +0,0 @@ -''' Exports UserSearchConfig. ''' - -from sqlalchemy.sql.expression import func -from szurubooru.model import User -from szurubooru.services.search.base_search_config import BaseSearchConfig - -class UserSearchConfig(BaseSearchConfig): - ''' Executes searches related to the users. ''' - - def create_query(self, session): - return session.query(User) - - @property - def anonymous_filter(self): - return self._create_basic_filter(User.name, allow_ranged=False) - - @property - def special_filters(self): - return {} - - @property - def named_filters(self): - return { - 'name': self._create_basic_filter(User.name, allow_ranged=False), - 'creation_date': self._create_date_filter(User.creation_time), - 'creation_time': self._create_date_filter(User.creation_time), - 'last_login_date': self._create_date_filter(User.last_login_time), - 'last_login_time': self._create_date_filter(User.last_login_time), - 'login_date': self._create_date_filter(User.last_login_time), - 'login_time': self._create_date_filter(User.last_login_time), - } - - @property - def order_columns(self): - return { - 'random': func.random(), - 'name': User.name, - 'creation_date': User.creation_time, - 'creation_time': User.creation_time, - 'last_login_date': User.last_login_time, - 'last_login_time': User.last_login_time, - 'login_date': User.last_login_time, - 'login_time': User.last_login_time, - } diff --git a/server/szurubooru/services/user_service.py b/server/szurubooru/services/user_service.py deleted file mode 100644 index ca4f221..0000000 --- a/server/szurubooru/services/user_service.py +++ /dev/null @@ -1,56 +0,0 @@ -import re -from datetime import datetime -from szurubooru import errors -from szurubooru import model -from szurubooru import util - -class UserService(object): - ''' User management ''' - - def __init__(self, config, password_service): - self._config = config - self._password_service = password_service - self._name_regex = self._config['service']['user_name_regex'] - self._password_regex = self._config['service']['password_regex'] - - def create_user(self, session, name, password, email): - ''' Creates an user with given parameters and returns it. ''' - - if not re.match(self._name_regex, name): - raise errors.ValidationError( - 'Name must satisfy regex %r.' % self._name_regex) - - if not re.match(self._password_regex, password): - raise errors.ValidationError( - 'Password must satisfy regex %r.' % self._password_regex) - - if not util.is_valid_email(email): - raise errors.ValidationError( - '%r is not a vaild email address.' % email) - - user = model.User() - user.name = name - user.password_salt = self._password_service.create_password() - user.password_hash = self._password_service.get_password_hash( - user.password_salt, password) - user.email = email or None - user.access_rank = self._config['service']['default_user_rank'] - user.creation_time = datetime.now() - user.avatar_style = model.User.AVATAR_GRAVATAR - - session.add(user) - return user - - def bump_login_time(self, user): - user.last_login_time = datetime.now() - - def reset_password(self, user): - password = self._password_service.create_password() - user.password_salt = self._password_service.create_password() - user.password_hash = self._password_service.get_password_hash( - user.password_salt, password) - return password - - def get_by_name(self, session, name): - ''' Retrieves an user by its name. ''' - return session.query(model.User).filter_by(name=name).first() diff --git a/server/szurubooru/tests/api/test_updating_user.py b/server/szurubooru/tests/api/test_updating_user.py index 6ffa09a..c1155f2 100644 --- a/server/szurubooru/tests/api/test_updating_user.py +++ b/server/szurubooru/tests/api/test_updating_user.py @@ -1,15 +1,12 @@ from datetime import datetime -import szurubooru.services -from szurubooru.api.user_api import UserDetailApi -from szurubooru.errors import AuthError, ValidationError -from szurubooru.model.user import User +from szurubooru import api, db, errors, config +from szurubooru.util import auth, misc from szurubooru.tests.database_test_case import DatabaseTestCase -from szurubooru.util import dotdict class TestUserDetailApi(DatabaseTestCase): def setUp(self): super().setUp() - config = { + config_mock = { 'basic': { 'secret': '', }, @@ -30,18 +27,18 @@ class TestUserDetailApi(DatabaseTestCase): 'users:edit:any:rank': 'admin', } } - password_service = szurubooru.services.PasswordService(config) - auth_service = szurubooru.services.AuthService(config, password_service) - user_service = szurubooru.services.UserService(config, password_service) - self.auth_service = auth_service - self.api = UserDetailApi( - config, auth_service, password_service, user_service) - self.context = dotdict() + self.old_config = config.config + config.config = config_mock + self.api = api.UserDetailApi() + self.context = misc.dotdict() self.context.session = self.session self.context.request = {} + def tearDown(self): + config.config = self.old_config + def _create_user(self, name, rank='admin'): - user = User() + user = db.User() user.name = name user.password = 'dummy' user.password_salt = 'dummy' @@ -49,7 +46,7 @@ class TestUserDetailApi(DatabaseTestCase): user.email = 'dummy' user.access_rank = rank user.creation_time = datetime.now() - user.avatar_style = User.AVATAR_GRAVATAR + user.avatar_style = db.User.AVATAR_GRAVATAR return user def test_updating_nothing(self): @@ -57,7 +54,7 @@ class TestUserDetailApi(DatabaseTestCase): self.session.add(admin_user) self.context.user = admin_user self.api.put(self.context, 'u1') - admin_user = self.session.query(User).filter_by(name='u1').one() + admin_user = self.session.query(db.User).filter_by(name='u1').one() self.assertEqual(admin_user.name, 'u1') self.assertEqual(admin_user.email, 'dummy') self.assertEqual(admin_user.access_rank, 'admin') @@ -69,16 +66,16 @@ class TestUserDetailApi(DatabaseTestCase): self.context.request = { 'name': 'chewie', 'email': 'asd@asd.asd', - 'password': 'valid', + 'password': 'oks', 'accessRank': 'mod', } self.api.put(self.context, 'u1') - admin_user = self.session.query(User).filter_by(name='chewie').one() + admin_user = self.session.query(db.User).filter_by(name='chewie').one() self.assertEqual(admin_user.name, 'chewie') self.assertEqual(admin_user.email, 'asd@asd.asd') self.assertEqual(admin_user.access_rank, 'mod') - self.assertTrue(self.auth_service.is_valid_password(admin_user, 'valid')) - self.assertFalse(self.auth_service.is_valid_password(admin_user, 'invalid')) + self.assertTrue(auth.is_valid_password(admin_user, 'oks')) + self.assertFalse(auth.is_valid_password(admin_user, 'invalid')) def test_removing_email(self): admin_user = self._create_user('u1', 'admin') @@ -86,7 +83,7 @@ class TestUserDetailApi(DatabaseTestCase): self.context.user = admin_user self.context.request = {'email': ''} self.api.put(self.context, 'u1') - admin_user = self.session.query(User).filter_by(name='u1').one() + admin_user = self.session.query(db.User).filter_by(name='u1').one() self.assertEqual(admin_user.email, None) def test_invalid_inputs(self): @@ -95,16 +92,16 @@ class TestUserDetailApi(DatabaseTestCase): self.context.user = admin_user self.context.request = {'name': '.'} self.assertRaises( - ValidationError, self.api.put, self.context, 'u1') + errors.ValidationError, self.api.put, self.context, 'u1') self.context.request = {'password': '.'} self.assertRaises( - ValidationError, self.api.put, self.context, 'u1') + errors.ValidationError, self.api.put, self.context, 'u1') self.context.request = {'accessRank': '.'} self.assertRaises( - ValidationError, self.api.put, self.context, 'u1') + errors.ValidationError, self.api.put, self.context, 'u1') self.context.request = {'email': '.'} self.assertRaises( - ValidationError, self.api.put, self.context, 'u1') + errors.ValidationError, self.api.put, self.context, 'u1') def test_user_trying_to_update_someone_else(self): user1 = self._create_user('u1', 'regular_user') @@ -118,7 +115,7 @@ class TestUserDetailApi(DatabaseTestCase): {'password': 'whatever'}]: self.context.request = request self.assertRaises( - AuthError, self.api.put, self.context, user2.name) + errors.AuthError, self.api.put, self.context, user2.name) def test_user_trying_to_become_someone_else(self): user1 = self._create_user('u1', 'regular_user') @@ -127,7 +124,7 @@ class TestUserDetailApi(DatabaseTestCase): self.context.user = user1 self.context.request = {'name': 'u2'} self.assertRaises( - ValidationError, self.api.put, self.context, 'u1') + errors.ValidationError, self.api.put, self.context, 'u1') def test_mods_trying_to_become_admin(self): user1 = self._create_user('u1', 'mod') @@ -136,6 +133,6 @@ class TestUserDetailApi(DatabaseTestCase): self.context.user = user1 self.context.request = {'accessRank': 'admin'} self.assertRaises( - AuthError, self.api.put, self.context, user1.name) + errors.AuthError, self.api.put, self.context, user1.name) self.assertRaises( - AuthError, self.api.put, self.context, user2.name) + errors.AuthError, self.api.put, self.context, user2.name) diff --git a/server/szurubooru/tests/database_test_case.py b/server/szurubooru/tests/database_test_case.py index 0d89e74..e6336d9 100644 --- a/server/szurubooru/tests/database_test_case.py +++ b/server/szurubooru/tests/database_test_case.py @@ -1,11 +1,11 @@ import unittest import sqlalchemy -from szurubooru.model import Base +from szurubooru import db class DatabaseTestCase(unittest.TestCase): def setUp(self): engine = sqlalchemy.create_engine('sqlite:///:memory:') session_maker = sqlalchemy.orm.sessionmaker(bind=engine) self.session = sqlalchemy.orm.scoped_session(session_maker) - Base.query = self.session.query_property() - Base.metadata.create_all(bind=engine) + db.Base.query = self.session.query_property() + db.Base.metadata.create_all(bind=engine) diff --git a/server/szurubooru/tests/services/__init__.py b/server/szurubooru/tests/search/__init__.py similarity index 100% rename from server/szurubooru/tests/services/__init__.py rename to server/szurubooru/tests/search/__init__.py diff --git a/server/szurubooru/tests/services/search/test_user_search_config.py b/server/szurubooru/tests/search/test_user_search_config.py similarity index 97% rename from server/szurubooru/tests/services/search/test_user_search_config.py rename to server/szurubooru/tests/search/test_user_search_config.py index 148f79e..f02c693 100644 --- a/server/szurubooru/tests/services/search/test_user_search_config.py +++ b/server/szurubooru/tests/search/test_user_search_config.py @@ -1,7 +1,5 @@ from datetime import datetime -from szurubooru import errors -from szurubooru import model -from szurubooru.services import search +from szurubooru import db, errors, search from szurubooru.tests.database_test_case import DatabaseTestCase class TestUserSearchExecutor(DatabaseTestCase): @@ -11,7 +9,7 @@ class TestUserSearchExecutor(DatabaseTestCase): self.executor = search.SearchExecutor(self.search_config) def _create_user(self, name): - user = model.User() + user = db.User() user.name = name user.password = 'dummy' user.password_salt = 'dummy' @@ -19,7 +17,7 @@ class TestUserSearchExecutor(DatabaseTestCase): user.email = 'dummy' user.access_rank = 'dummy' user.creation_time = datetime.now() - user.avatar_style = model.User.AVATAR_GRAVATAR + user.avatar_style = db.User.AVATAR_GRAVATAR return user def _test(self, query, page, expected_count, expected_user_names): diff --git a/server/szurubooru/tests/services/search/__init__.py b/server/szurubooru/tests/services/search/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/szurubooru/tests/test_util.py b/server/szurubooru/tests/util/test_misc.py similarity index 67% rename from server/szurubooru/tests/test_util.py rename to server/szurubooru/tests/util/test_misc.py index 526b97c..171d483 100644 --- a/server/szurubooru/tests/test_util.py +++ b/server/szurubooru/tests/util/test_misc.py @@ -1,8 +1,7 @@ import unittest from datetime import datetime -import szurubooru.util -from szurubooru.util import parse_time_range -from szurubooru.errors import ValidationError +from szurubooru import errors +from szurubooru.util import misc class FakeDatetime(datetime): @staticmethod @@ -11,33 +10,33 @@ class FakeDatetime(datetime): class TestParseTime(unittest.TestCase): def test_empty(self): - self.assertRaises(ValidationError, parse_time_range, '') + self.assertRaises(errors.ValidationError, misc.parse_time_range, '') def test_today(self): - szurubooru.util.datetime.datetime = FakeDatetime - date_min, date_max = parse_time_range('today') + misc.datetime.datetime = FakeDatetime + date_min, date_max = misc.parse_time_range('today') self.assertEqual(date_min, datetime(1997, 1, 2, 0, 0, 0)) self.assertEqual(date_max, datetime(1997, 1, 2, 23, 59, 59)) def test_yesterday(self): - szurubooru.util.datetime.datetime = FakeDatetime - date_min, date_max = parse_time_range('yesterday') + misc.datetime.datetime = FakeDatetime + date_min, date_max = misc.parse_time_range('yesterday') self.assertEqual(date_min, datetime(1997, 1, 1, 0, 0, 0)) self.assertEqual(date_max, datetime(1997, 1, 1, 23, 59, 59)) def test_year(self): - date_min, date_max = parse_time_range('1999') + date_min, date_max = misc.parse_time_range('1999') self.assertEqual(date_min, datetime(1999, 1, 1, 0, 0, 0)) self.assertEqual(date_max, datetime(1999, 12, 31, 23, 59, 59)) def test_month(self): for text in ['1999-2', '1999-02']: - date_min, date_max = parse_time_range(text) + date_min, date_max = misc.parse_time_range(text) self.assertEqual(date_min, datetime(1999, 2, 1, 0, 0, 0)) self.assertEqual(date_max, datetime(1999, 2, 28, 23, 59, 59)) def test_day(self): for text in ['1999-2-6', '1999-02-6', '1999-2-06', '1999-02-06']: - date_min, date_max = parse_time_range(text) + date_min, date_max = misc.parse_time_range(text) self.assertEqual(date_min, datetime(1999, 2, 6, 0, 0, 0)) self.assertEqual(date_max, datetime(1999, 2, 6, 23, 59, 59)) diff --git a/server/szurubooru/util/__init__.py b/server/szurubooru/util/__init__.py new file mode 100644 index 0000000..e0cd4cb --- /dev/null +++ b/server/szurubooru/util/__init__.py @@ -0,0 +1 @@ +''' Cool functions. ''' diff --git a/server/szurubooru/util/auth.py b/server/szurubooru/util/auth.py new file mode 100644 index 0000000..741e58b --- /dev/null +++ b/server/szurubooru/util/auth.py @@ -0,0 +1,59 @@ +import hashlib +import random +from szurubooru import config +from szurubooru import errors + +def get_password_hash(salt, password): + ''' Retrieve new-style password hash. ''' + digest = hashlib.sha256() + digest.update(config.config['basic']['secret'].encode('utf8')) + digest.update(salt.encode('utf8')) + digest.update(password.encode('utf8')) + return digest.hexdigest() + +def get_legacy_password_hash(salt, password): + ''' Retrieve old-style password hash. ''' + digest = hashlib.sha1() + digest.update(b'1A2/$_4xVa') + digest.update(salt.encode('utf8')) + digest.update(password.encode('utf8')) + return digest.hexdigest() + +def create_password(): + ''' Create an easy-to-remember password. ''' + alphabet = { + 'c': list('bcdfghijklmnpqrstvwxyz'), + 'v': list('aeiou'), + 'n': list('0123456789'), + } + pattern = 'cvcvnncvcv' + return ''.join(random.choice(alphabet[l]) for l in list(pattern)) + +def is_valid_password(user, password): + ''' Return whether the given password for a given user is valid. ''' + salt, valid_hash = user.password_salt, user.password_hash + possible_hashes = [ + get_password_hash(salt, password), + get_legacy_password_hash(salt, password) + ] + return valid_hash in possible_hashes + +def verify_privilege(user, privilege_name): + ''' + Throw an AuthError if the given user doesn't have given privilege. + ''' + all_ranks = config.config['service']['user_ranks'] + + assert privilege_name in config.config['privileges'] + assert user.access_rank in all_ranks + minimal_rank = config.config['privileges'][privilege_name] + good_ranks = all_ranks[all_ranks.index(minimal_rank):] + if user.access_rank not in good_ranks: + raise errors.AuthError('Insufficient privileges to do this.') + +def generate_authentication_token(user): + ''' Generate nonguessable challenge (e.g. links in password reminder). ''' + digest = hashlib.sha256() + digest.update(config.config['basic']['secret'].encode('utf8')) + digest.update(user.password_salt.encode('utf8')) + return digest.hexdigest() diff --git a/server/szurubooru/util/mailer.py b/server/szurubooru/util/mailer.py new file mode 100644 index 0000000..0f9828c --- /dev/null +++ b/server/szurubooru/util/mailer.py @@ -0,0 +1,15 @@ +import smtplib +import email.mime.text +from szurubooru import config + +def send_mail(sender, recipient, subject, body): + msg = email.mime.text.MIMEText(body) + msg['Subject'] = subject + msg['From'] = sender + msg['To'] = recipient + + smtp = smtplib.SMTP( + config.config['smtp']['host'], int(config.config['smtp']['port'])) + smtp.login(config.config['smtp']['user'], config.config['smtp']['pass']) + smtp.send_message(msg) + smtp.quit() diff --git a/server/szurubooru/util.py b/server/szurubooru/util/misc.py similarity index 93% rename from server/szurubooru/util.py rename to server/szurubooru/util/misc.py index 9407d37..4095519 100644 --- a/server/szurubooru/util.py +++ b/server/szurubooru/util/misc.py @@ -3,7 +3,7 @@ import re from szurubooru.errors import ValidationError def is_valid_email(email): - ''' Validates given email address. ''' + ''' Return whether given email address is valid or empty. ''' return not email or re.match(r'^[^@]*@[^@]*\.[^@]*$', email) class dotdict(dict): # pylint: disable=invalid-name @@ -14,7 +14,7 @@ class dotdict(dict): # pylint: disable=invalid-name __delattr__ = dict.__delitem__ def parse_time_range(value, timezone=datetime.timezone(datetime.timedelta())): - ''' Returns tuple containing min/max time for given text representation. ''' + ''' Return tuple containing min/max time for given text representation. ''' one_day = datetime.timedelta(days=1) one_second = datetime.timedelta(seconds=1) diff --git a/server/szurubooru/util/users.py b/server/szurubooru/util/users.py new file mode 100644 index 0000000..a51a374 --- /dev/null +++ b/server/szurubooru/util/users.py @@ -0,0 +1,67 @@ +import re +from datetime import datetime +from szurubooru import config, db, errors +from szurubooru.util import auth, misc + +def create_user(name, password, email): + ''' Create an user with given parameters and returns it. ''' + user = db.User() + update_name(user, name) + update_password(user, password) + update_email(user, email) + user.access_rank = config.config['service']['default_user_rank'] + user.creation_time = datetime.now() + user.avatar_style = db.User.AVATAR_GRAVATAR + return user + +def update_name(user, name): + ''' Validate and update user's name. ''' + name = name.strip() + name_regex = config.config['service']['user_name_regex'] + if not re.match(name_regex, name): + raise errors.ValidationError( + 'Name must satisfy regex %r.' % name_regex) + user.name = name + +def update_password(user, password): + ''' Validate and update user's password. ''' + password_regex = config.config['service']['password_regex'] + if not re.match(password_regex, password): + raise errors.ValidationError( + 'Password must satisfy regex %r.' % password_regex) + user.password_salt = auth.create_password() + user.password_hash = auth.get_password_hash(user.password_salt, password) + +def update_email(user, email): + ''' Validate and update user's email. ''' + email = email.strip() or None + if not misc.is_valid_email(email): + raise errors.ValidationError( + '%r is not a vaild email address.' % email) + user.email = email + +def update_rank(user, rank, authenticated_user): + rank = rank.strip() + available_access_ranks = config.config['service']['user_ranks'] + if not rank in available_access_ranks: + raise errors.ValidationError( + 'Bad access rank. Valid access ranks: %r' % available_access_ranks) + if available_access_ranks.index(authenticated_user.access_rank) \ + < available_access_ranks.index(rank): + raise errors.AuthError('Trying to set higher access rank than one has') + user.access_rank = rank + +def bump_login_time(user): + ''' Update user's login time to current date. ''' + user.last_login_time = datetime.now() + +def reset_password(user): + ''' Reset password for an user. ''' + password = auth.create_password() + user.password_salt = auth.create_password() + user.password_hash = auth.get_password_hash(user.password_salt, password) + return password + +def get_by_name(session, name): + ''' Retrieve an user by its name. ''' + return session.query(db.User).filter_by(name=name).first()