server/users: allow rank+avatar when creating user
This commit is contained in:
parent
08271caf32
commit
e42cede27c
48
API.md
48
API.md
|
@ -222,10 +222,10 @@ data.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
"names": [<name1>, <name2>, ...],
|
"names": [<name1>, <name2>, ...], // optional
|
||||||
"category": <category>,
|
"category": <category>, // optional
|
||||||
"implications": [<name1>, <name2>, ...],
|
"implications": [<name1>, <name2>, ...], // optional
|
||||||
"suggestions": [<name1>, <name2>, ...]
|
"suggestions": [<name1>, <name2>, ...] // optional
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -384,10 +384,16 @@ data.
|
||||||
{
|
{
|
||||||
"name": <user-name>,
|
"name": <user-name>,
|
||||||
"password": <user-password>,
|
"password": <user-password>,
|
||||||
"email": <email>
|
"email": <email>, // optional
|
||||||
|
"rank": <rank>, // optional
|
||||||
|
"avatarStyle": <avatar-style> // optional
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **Files**
|
||||||
|
|
||||||
|
- `avatar` - the content of the new avatar (optional).
|
||||||
|
|
||||||
- **Output**
|
- **Output**
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
|
@ -399,17 +405,23 @@ data.
|
||||||
|
|
||||||
- **Errors**
|
- **Errors**
|
||||||
|
|
||||||
- such user already exists (names are case insensitive)
|
- a user with such name already exists (names are case insensitive)
|
||||||
- either user name, password or email are invalid
|
- 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
|
- privileges are too low
|
||||||
|
|
||||||
- **Description**
|
- **Description**
|
||||||
|
|
||||||
Creates a new user using specified parameters. Names and passwords must
|
Creates a new user using specified parameters. Names and passwords must
|
||||||
match `user_name_regex` and `password_regex` from server's configuration,
|
match `user_name_regex` and `password_regex` from server's configuration,
|
||||||
respectively. Email address is optional. If the user happens to be the
|
respectively. Email address, rank and avatar fields are optional. Avatar
|
||||||
first user ever created, they're granted highest available rank, becoming
|
style can be either `gravatar` or `manual`. `manual` avatar style requires
|
||||||
an administrator. Subsequent users will be given the rank indicated by
|
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.
|
`default_rank` in the server's configuration.
|
||||||
|
|
||||||
|
|
||||||
|
@ -422,17 +434,17 @@ data.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
"name": <user-name>,
|
"name": <user-name>, // optional
|
||||||
"password": <user-password>,
|
"password": <user-password>, // optional
|
||||||
"email": <email>,
|
"email": <email>, // optional
|
||||||
"rank": <rank>,
|
"rank": <rank>, // optional
|
||||||
"avatarStyle": <avatar-style>
|
"avatarStyle": <avatar-style> // optional
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Files**
|
- **Files**
|
||||||
|
|
||||||
- `avatar` - the content of the new avatar.
|
- `avatar` - the content of the new avatar (optional).
|
||||||
|
|
||||||
- **Output**
|
- **Output**
|
||||||
|
|
||||||
|
@ -446,12 +458,12 @@ data.
|
||||||
- **Errors**
|
- **Errors**
|
||||||
|
|
||||||
- the user does not exist
|
- 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
|
- either user name, password, email or rank are invalid
|
||||||
- the user is trying to update their or someone else's rank to higher than
|
- the user is trying to update their or someone else's rank to higher than
|
||||||
their own
|
their own
|
||||||
- privileges are too low
|
|
||||||
- avatar is missing for manual avatar style
|
- avatar is missing for manual avatar style
|
||||||
|
- privileges are too low
|
||||||
|
|
||||||
- **Description**
|
- **Description**
|
||||||
|
|
||||||
|
|
|
@ -54,9 +54,20 @@ class UserListApi(BaseApi):
|
||||||
|
|
||||||
name = ctx.get_param_as_string('name', required=True)
|
name = ctx.get_param_as_string('name', required=True)
|
||||||
password = ctx.get_param_as_string('password', 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)
|
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.add(user)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
return {'user': _serialize_user(ctx.user, user)}
|
return {'user': _serialize_user(ctx.user, user)}
|
||||||
|
@ -94,7 +105,8 @@ class UserDetailApi(BaseApi):
|
||||||
|
|
||||||
if ctx.has_param('rank'):
|
if ctx.has_param('rank'):
|
||||||
auth.verify_privilege(ctx.user, 'users:edit:%s:rank' % infix)
|
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'):
|
if ctx.has_param('avatarStyle'):
|
||||||
auth.verify_privilege(ctx.user, 'users:edit:%s:avatar' % infix)
|
auth.verify_privilege(ctx.user, 'users:edit:%s:avatar' % infix)
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import datetime
|
import datetime
|
||||||
import pytest
|
import pytest
|
||||||
from szurubooru import api, db, errors
|
from szurubooru import api, config, db, errors
|
||||||
from szurubooru.util import auth, misc, users
|
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):
|
def get_user(session, name):
|
||||||
return session.query(db.User).filter_by(name=name).first()
|
return session.query(db.User).filter_by(name=name).first()
|
||||||
|
|
||||||
|
@ -14,7 +19,7 @@ def test_ctx(
|
||||||
'user_name_regex': '.{3,}',
|
'user_name_regex': '.{3,}',
|
||||||
'password_regex': '.{3,}',
|
'password_regex': '.{3,}',
|
||||||
'default_rank': 'regular_user',
|
'default_rank': 'regular_user',
|
||||||
'thumbnails': {'avatar_width': 200},
|
'thumbnails': {'avatar_width': 200, 'avatar_height': 200},
|
||||||
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
|
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
|
||||||
'rank_names': {},
|
'rank_names': {},
|
||||||
'privileges': {'users:create': 'anonymous'},
|
'privileges': {'users:create': 'anonymous'},
|
||||||
|
@ -63,7 +68,7 @@ def test_first_user_becomes_admin_others_not(test_ctx):
|
||||||
'email': 'asd@asd.asd',
|
'email': 'asd@asd.asd',
|
||||||
'password': 'oks',
|
'password': 'oks',
|
||||||
},
|
},
|
||||||
user=test_ctx.user_factory(rank='regular_user')))
|
user=test_ctx.user_factory(rank='anonymous')))
|
||||||
result2 = test_ctx.api.post(
|
result2 = test_ctx.api.post(
|
||||||
test_ctx.context_factory(
|
test_ctx.context_factory(
|
||||||
input={
|
input={
|
||||||
|
@ -71,7 +76,7 @@ def test_first_user_becomes_admin_others_not(test_ctx):
|
||||||
'email': 'asd@asd.asd',
|
'email': 'asd@asd.asd',
|
||||||
'password': 'sok',
|
'password': 'sok',
|
||||||
},
|
},
|
||||||
user=test_ctx.user_factory(rank='regular_user')))
|
user=test_ctx.user_factory(rank='anonymous')))
|
||||||
assert result1['user']['rank'] == 'admin'
|
assert result1['user']['rank'] == 'admin'
|
||||||
assert result2['user']['rank'] == 'regular_user'
|
assert result2['user']['rank'] == 'regular_user'
|
||||||
first_user = get_user(test_ctx.session, 'chewie1')
|
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 first_user.rank == 'admin'
|
||||||
assert other_user.rank == 'regular_user'
|
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):
|
def test_creating_user_that_already_exists(test_ctx):
|
||||||
test_ctx.api.post(
|
test_ctx.api.post(
|
||||||
test_ctx.context_factory(
|
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')))
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
|
||||||
@pytest.mark.parametrize('field', ['name', 'email', 'password'])
|
@pytest.mark.parametrize('field', ['name', 'password'])
|
||||||
def test_missing_field(test_ctx, field):
|
def test_missing_mandatory_field(test_ctx, field):
|
||||||
input = {
|
input = {
|
||||||
'name': 'chewie',
|
'name': 'chewie',
|
||||||
'email': 'asd@asd.asd',
|
'email': 'asd@asd.asd',
|
||||||
|
@ -121,6 +138,24 @@ def test_missing_field(test_ctx, field):
|
||||||
input=input,
|
input=input,
|
||||||
user=test_ctx.user_factory(rank='regular_user')))
|
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', [
|
@pytest.mark.parametrize('input', [
|
||||||
{'name': '.'},
|
{'name': '.'},
|
||||||
{'name': 'x' * 51},
|
{'name': 'x' * 51},
|
||||||
|
@ -134,7 +169,55 @@ def test_invalid_inputs(test_ctx, input):
|
||||||
user = test_ctx.user_factory(name='u1', rank='admin')
|
user = test_ctx.user_factory(name='u1', rank='admin')
|
||||||
test_ctx.session.add(user)
|
test_ctx.session.add(user)
|
||||||
with pytest.raises(errors.ValidationError):
|
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.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'
|
||||||
|
|
|
@ -66,14 +66,14 @@ def update_email(user, email):
|
||||||
raise InvalidEmailError('E-mail is invalid.')
|
raise InvalidEmailError('E-mail is invalid.')
|
||||||
user.email = email
|
user.email = email
|
||||||
|
|
||||||
def update_rank(user, rank, authenticated_user):
|
def update_rank(session, user, rank, authenticated_user):
|
||||||
rank = rank.strip()
|
rank = rank.strip()
|
||||||
available_ranks = config.config['ranks']
|
available_ranks = config.config['ranks']
|
||||||
if not rank in available_ranks:
|
if not rank in available_ranks:
|
||||||
raise InvalidRankError(
|
raise InvalidRankError(
|
||||||
'Rank %r is invalid. Valid ranks: %r' % (rank, available_ranks))
|
'Rank %r is invalid. Valid ranks: %r' % (rank, available_ranks))
|
||||||
if available_ranks.index(authenticated_user.rank) \
|
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.')
|
raise errors.AuthError('Trying to set higher rank than your own.')
|
||||||
user.rank = rank
|
user.rank = rank
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue