From e42cede27c80764dea9bfa706c61fed69d74df43 Mon Sep 17 00:00:00 2001 From: rr- Date: Sun, 17 Apr 2016 19:02:39 +0200 Subject: [PATCH] server/users: allow rank+avatar when creating user --- API.md | 48 +++++---- server/szurubooru/api/user_api.py | 16 ++- .../tests/api/test_user_creating.py | 99 +++++++++++++++++-- server/szurubooru/util/users.py | 4 +- 4 files changed, 137 insertions(+), 30 deletions(-) diff --git a/API.md b/API.md index 6407ea12..71fd66b0 100644 --- a/API.md +++ b/API.md @@ -222,10 +222,10 @@ data. ```json5 { - "names": [, , ...], - "category": , - "implications": [, , ...], - "suggestions": [, , ...] + "names": [, , ...], // optional + "category": , // optional + "implications": [, , ...], // optional + "suggestions": [, , ...] // optional } ``` @@ -384,10 +384,16 @@ data. { "name": , "password": , - "email": + "email": , // optional + "rank": , // optional + "avatarStyle": // optional } ``` +- **Files** + + - `avatar` - the content of the new avatar (optional). + - **Output** ```json5 @@ -399,17 +405,23 @@ data. - **Errors** - - such user already exists (names are case insensitive) - - either user name, password or email are invalid + - a user with such name already exists (names are case insensitive) + - either user name, password, email or rank are invalid + - the user is trying to update their or someone else's rank to higher than + their own + - avatar is missing for manual avatar style - privileges are too low - **Description** Creates a new user using specified parameters. Names and passwords must match `user_name_regex` and `password_regex` from server's configuration, - respectively. Email address is optional. If the user happens to be the - first user ever created, they're granted highest available rank, becoming - an administrator. Subsequent users will be given the rank indicated by + respectively. Email address, rank and avatar fields are optional. Avatar + style can be either `gravatar` or `manual`. `manual` avatar style requires + client to pass also `avatar` file - see [file uploads](#file-uploads) for + details. If the rank is empty and the user happens to be the first user + ever created, they're granted highest available rank, becoming an + administrator, whereas subsequent users will be given the rank indicated by `default_rank` in the server's configuration. @@ -422,17 +434,17 @@ data. ```json5 { - "name": , - "password": , - "email": , - "rank": , - "avatarStyle": + "name": , // optional + "password": , // optional + "email": , // optional + "rank": , // optional + "avatarStyle": // optional } ``` - **Files** - - `avatar` - the content of the new avatar. + - `avatar` - the content of the new avatar (optional). - **Output** @@ -446,12 +458,12 @@ data. - **Errors** - the user does not exist - - the user with new name already exists (names are case insensitive) + - a user with new name already exists (names are case insensitive) - either user name, password, email or rank are invalid - the user is trying to update their or someone else's rank to higher than their own - - privileges are too low - avatar is missing for manual avatar style + - privileges are too low - **Description** diff --git a/server/szurubooru/api/user_api.py b/server/szurubooru/api/user_api.py index 24941553..deb7bd9e 100644 --- a/server/szurubooru/api/user_api.py +++ b/server/szurubooru/api/user_api.py @@ -54,9 +54,20 @@ class UserListApi(BaseApi): name = ctx.get_param_as_string('name', required=True) password = ctx.get_param_as_string('password', required=True) - email = ctx.get_param_as_string('email', required=True) + email = ctx.get_param_as_string('email', required=False, default='') user = users.create_user(ctx.session, name, password, email, ctx.user) + + if ctx.has_param('rank'): + users.update_rank( + ctx.session, user, ctx.get_param_as_string('rank'), ctx.user) + + if ctx.has_param('avatarStyle'): + users.update_avatar( + user, + ctx.get_param_as_string('avatarStyle'), + ctx.get_file('avatar')) + ctx.session.add(user) ctx.session.commit() return {'user': _serialize_user(ctx.user, user)} @@ -94,7 +105,8 @@ class UserDetailApi(BaseApi): if ctx.has_param('rank'): auth.verify_privilege(ctx.user, 'users:edit:%s:rank' % infix) - users.update_rank(user, ctx.get_param_as_string('rank'), ctx.user) + users.update_rank( + ctx.session, user, ctx.get_param_as_string('rank'), ctx.user) if ctx.has_param('avatarStyle'): auth.verify_privilege(ctx.user, 'users:edit:%s:avatar' % infix) diff --git a/server/szurubooru/tests/api/test_user_creating.py b/server/szurubooru/tests/api/test_user_creating.py index 4eecd649..85372be9 100644 --- a/server/szurubooru/tests/api/test_user_creating.py +++ b/server/szurubooru/tests/api/test_user_creating.py @@ -1,8 +1,13 @@ import datetime import pytest -from szurubooru import api, db, errors +from szurubooru import api, config, db, errors from szurubooru.util import auth, misc, users +EMPTY_PIXEL = \ + b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00' \ + b'\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00' \ + b'\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b' + def get_user(session, name): return session.query(db.User).filter_by(name=name).first() @@ -14,7 +19,7 @@ def test_ctx( 'user_name_regex': '.{3,}', 'password_regex': '.{3,}', 'default_rank': 'regular_user', - 'thumbnails': {'avatar_width': 200}, + 'thumbnails': {'avatar_width': 200, 'avatar_height': 200}, 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], 'rank_names': {}, 'privileges': {'users:create': 'anonymous'}, @@ -63,7 +68,7 @@ def test_first_user_becomes_admin_others_not(test_ctx): 'email': 'asd@asd.asd', 'password': 'oks', }, - user=test_ctx.user_factory(rank='regular_user'))) + user=test_ctx.user_factory(rank='anonymous'))) result2 = test_ctx.api.post( test_ctx.context_factory( input={ @@ -71,7 +76,7 @@ def test_first_user_becomes_admin_others_not(test_ctx): 'email': 'asd@asd.asd', 'password': 'sok', }, - user=test_ctx.user_factory(rank='regular_user'))) + user=test_ctx.user_factory(rank='anonymous'))) assert result1['user']['rank'] == 'admin' assert result2['user']['rank'] == 'regular_user' first_user = get_user(test_ctx.session, 'chewie1') @@ -79,6 +84,18 @@ def test_first_user_becomes_admin_others_not(test_ctx): assert first_user.rank == 'admin' assert other_user.rank == 'regular_user' +def test_first_user_does_not_become_admin_if_they_dont_wish_so(test_ctx): + result = test_ctx.api.post( + test_ctx.context_factory( + input={ + 'name': 'chewie1', + 'email': 'asd@asd.asd', + 'password': 'oks', + 'rank': 'regular_user', + }, + user=test_ctx.user_factory(rank='anonymous'))) + assert result['user']['rank'] == 'regular_user' + def test_creating_user_that_already_exists(test_ctx): test_ctx.api.post( test_ctx.context_factory( @@ -107,8 +124,8 @@ def test_creating_user_that_already_exists(test_ctx): }, user=test_ctx.user_factory(rank='regular_user'))) -@pytest.mark.parametrize('field', ['name', 'email', 'password']) -def test_missing_field(test_ctx, field): +@pytest.mark.parametrize('field', ['name', 'password']) +def test_missing_mandatory_field(test_ctx, field): input = { 'name': 'chewie', 'email': 'asd@asd.asd', @@ -121,6 +138,24 @@ def test_missing_field(test_ctx, field): input=input, user=test_ctx.user_factory(rank='regular_user'))) +@pytest.mark.parametrize('field', ['rank', 'email', 'avatarStyle']) +def test_missing_optional_field(test_ctx, tmpdir, field): + config.config['data_dir'] = str(tmpdir.mkdir('data')) + config.config['data_url'] = 'http://example.com/data/' + input = { + 'name': 'chewie', + 'email': 'asd@asd.asd', + 'password': 'oks', + 'rank': 'mod', + 'avatarStyle': 'manual', + } + del input[field] + test_ctx.api.post( + test_ctx.context_factory( + input=input, + files={'avatar': EMPTY_PIXEL}, + user=test_ctx.user_factory(rank='mod'))) + @pytest.mark.parametrize('input', [ {'name': '.'}, {'name': 'x' * 51}, @@ -134,7 +169,55 @@ def test_invalid_inputs(test_ctx, input): user = test_ctx.user_factory(name='u1', rank='admin') test_ctx.session.add(user) with pytest.raises(errors.ValidationError): + real_input={ + 'name': 'chewie', + 'email': 'asd@asd.asd', + 'password': 'oks', + } + for key, value in input.items(): + real_input[key] = value test_ctx.api.post( - test_ctx.context_factory(input=input, user=user)) + test_ctx.context_factory(input=real_input, user=user)) -# TODO: support avatar and avatarStyle +def test_mods_trying_to_become_admin(test_ctx): + user1 = test_ctx.user_factory(name='u1', rank='mod') + user2 = test_ctx.user_factory(name='u2', rank='mod') + test_ctx.session.add_all([user1, user2]) + context = test_ctx.context_factory(input={ + 'name': 'chewie', + 'email': 'asd@asd.asd', + 'password': 'oks', + 'rank': 'admin', + }, user=user1) + with pytest.raises(errors.AuthError): + test_ctx.api.post(context) + +def test_admin_creating_mod_account(test_ctx): + user = test_ctx.user_factory(rank='admin') + test_ctx.session.add(user) + context = test_ctx.context_factory(input={ + 'name': 'chewie', + 'email': 'asd@asd.asd', + 'password': 'oks', + 'rank': 'mod', + }, user=user) + result = test_ctx.api.post(context) + assert result['user']['rank'] == 'mod' + +def test_uploading_avatar(test_ctx, tmpdir): + config.config['data_dir'] = str(tmpdir.mkdir('data')) + config.config['data_url'] = 'http://example.com/data/' + response = test_ctx.api.post( + test_ctx.context_factory( + input={ + 'name': 'chewie', + 'email': 'asd@asd.asd', + 'password': 'oks', + 'avatarStyle': 'manual', + }, + files={'avatar': EMPTY_PIXEL}, + user=test_ctx.user_factory(rank='mod'))) + user = get_user(test_ctx.session, 'chewie') + assert user.avatar_style == user.AVATAR_MANUAL + assert response['user']['avatarUrl'] == \ + 'http://example.com/data/avatars/chewie.jpg' diff --git a/server/szurubooru/util/users.py b/server/szurubooru/util/users.py index c451eeeb..6ae457ad 100644 --- a/server/szurubooru/util/users.py +++ b/server/szurubooru/util/users.py @@ -66,14 +66,14 @@ def update_email(user, email): raise InvalidEmailError('E-mail is invalid.') user.email = email -def update_rank(user, rank, authenticated_user): +def update_rank(session, user, rank, authenticated_user): rank = rank.strip() available_ranks = config.config['ranks'] if not rank in available_ranks: raise InvalidRankError( 'Rank %r is invalid. Valid ranks: %r' % (rank, available_ranks)) if available_ranks.index(authenticated_user.rank) \ - < available_ranks.index(rank): + < available_ranks.index(rank) and session.query(db.User).count() > 0: raise errors.AuthError('Trying to set higher rank than your own.') user.rank = rank