# HG changeset patch # User Marcin Kuzminski # Date 2017-06-21 13:36:24 # Node ID d176880cc29a36145a65bfff84d0984f39210beb # Parent 87ca65d79bcb031720102b5e3675444e628fc6b4 user-api: use simple schema validator to be consistent how we validate between API and web views. diff --git a/rhodecode/api/tests/test_create_user.py b/rhodecode/api/tests/test_create_user.py --- a/rhodecode/api/tests/test_create_user.py +++ b/rhodecode/api/tests/test_create_user.py @@ -59,6 +59,21 @@ class TestCreateUser(object): expected = "email `%s` already exist" % (TEST_USER_REGULAR_EMAIL,) assert_error(id_, expected, given=response.body) + def test_api_create_user_with_wrong_username(self): + bad_username = '<> HELLO WORLD <>' + id_, params = build_data( + self.apikey, 'create_user', + username=bad_username, + email='new@email.com', + password='trololo') + response = api_call(self.app, params) + + expected = {'username': + "Username may only contain alphanumeric characters " + "underscores, periods or dashes and must begin with " + "alphanumeric character or underscore"} + assert_error(id_, expected, given=response.body) + def test_api_create_user(self): username = 'test_new_api_user' email = username + "@foo.com" @@ -175,7 +190,6 @@ class TestCreateUser(object): fixture.destroy_repo_group(username) fixture.destroy_user(usr.user_id) - @mock.patch.object(UserModel, 'create_or_update', crash) def test_api_create_user_when_exception_happened(self): diff --git a/rhodecode/api/views/user_api.py b/rhodecode/api/views/user_api.py --- a/rhodecode/api/views/user_api.py +++ b/rhodecode/api/views/user_api.py @@ -20,7 +20,8 @@ import logging -from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden +from rhodecode.api import ( + jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) from rhodecode.api.utils import ( Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update) from rhodecode.lib import audit_logger @@ -29,6 +30,8 @@ from rhodecode.lib.exceptions import Def from rhodecode.lib.utils2 import safe_int, str2bool from rhodecode.model.db import Session, User, Repository from rhodecode.model.user import UserModel +from rhodecode.model import validation_schema +from rhodecode.model.validation_schema.schemas import user_schema log = logging.getLogger(__name__) @@ -238,17 +241,45 @@ def create_user(request, apiuser, userna if isinstance(create_repo_group, basestring): create_repo_group = str2bool(create_repo_group) + username = Optional.extract(username) + password = Optional.extract(password) + email = Optional.extract(email) + first_name = Optional.extract(firstname) + last_name = Optional.extract(lastname) + active = Optional.extract(active) + admin = Optional.extract(admin) + extern_type = Optional.extract(extern_type) + extern_name = Optional.extract(extern_name) + + schema = user_schema.UserSchema().bind( + # user caller + user=apiuser) + try: + schema_data = schema.deserialize(dict( + username=username, + email=email, + password=password, + first_name=first_name, + last_name=last_name, + active=active, + admin=admin, + extern_type=extern_type, + extern_name=extern_name, + )) + except validation_schema.Invalid as err: + raise JSONRPCValidationError(colander_exc=err) + try: user = UserModel().create_or_update( - username=Optional.extract(username), - password=Optional.extract(password), - email=Optional.extract(email), - firstname=Optional.extract(firstname), - lastname=Optional.extract(lastname), - active=Optional.extract(active), - admin=Optional.extract(admin), - extern_type=Optional.extract(extern_type), - extern_name=Optional.extract(extern_name), + username=schema_data['username'], + password=schema_data['password'], + email=schema_data['email'], + firstname=schema_data['first_name'], + lastname=schema_data['last_name'], + active=schema_data['active'], + admin=schema_data['admin'], + extern_type=schema_data['extern_type'], + extern_name=schema_data['extern_name'], force_password_change=Optional.extract(force_password_change), create_repo_group=create_repo_group ) diff --git a/rhodecode/api/views/user_group_api.py b/rhodecode/api/views/user_group_api.py --- a/rhodecode/api/views/user_group_api.py +++ b/rhodecode/api/views/user_group_api.py @@ -20,8 +20,8 @@ import logging -from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden, \ - JSONRPCValidationError +from rhodecode.api import ( + jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) from rhodecode.api.utils import ( Optional, OAttr, store_update, has_superadmin_permission, get_origin, get_user_or_error, get_user_group_or_error, get_perm_or_error) diff --git a/rhodecode/model/validation_schema/schemas/user_schema.py b/rhodecode/model/validation_schema/schemas/user_schema.py --- a/rhodecode/model/validation_schema/schemas/user_schema.py +++ b/rhodecode/model/validation_schema/schemas/user_schema.py @@ -18,10 +18,12 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import re import colander from rhodecode import forms from rhodecode.model.db import User +from rhodecode.model.validation_schema import types, validators from rhodecode.translation import _ from rhodecode.lib.auth import check_password @@ -52,10 +54,72 @@ class ChangePasswordSchema(colander.Sche widget=forms.widget.CheckedPasswordWidget(redisplay=True), validator=colander.Length(min=6)) - def validator(self, form, values): if values['current_password'] == values['new_password']: exc = colander.Invalid(form) exc['new_password'] = _('New password must be different ' 'to old password') raise exc + + +@colander.deferred +def deferred_username_validator(node, kw): + + def name_validator(node, value): + msg = _( + u'Username may only contain alphanumeric characters ' + u'underscores, periods or dashes and must begin with ' + u'alphanumeric character or underscore') + + if not re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value): + raise colander.Invalid(node, msg) + + return name_validator + + +@colander.deferred +def deferred_email_validator(node, kw): + # NOTE(marcink): we might provide uniqueness validation later here... + return colander.Email() + + +class UserSchema(colander.Schema): + username = colander.SchemaNode( + colander.String(), + validator=deferred_username_validator) + + email = colander.SchemaNode( + colander.String(), + validator=deferred_email_validator) + + password = colander.SchemaNode( + colander.String(), missing='') + + first_name = colander.SchemaNode( + colander.String(), missing='') + + last_name = colander.SchemaNode( + colander.String(), missing='') + + active = colander.SchemaNode( + types.StringBooleanType(), + missing=False) + + admin = colander.SchemaNode( + types.StringBooleanType(), + missing=False) + + extern_name = colander.SchemaNode( + colander.String(), missing='') + + extern_type = colander.SchemaNode( + colander.String(), missing='') + + def deserialize(self, cstruct): + """ + Custom deserialize that allows to chain validation, and verify + permissions, and as last step uniqueness + """ + + appstruct = super(UserSchema, self).deserialize(cstruct) + return appstruct