feat: Allow underscores and hyphens in account name (#1047)

This commit is contained in:
jbe-dw 2022-06-18 15:14:37 +02:00 committed by GitHub
commit 2c0225e961
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 76 additions and 45 deletions

View File

@ -136,6 +136,13 @@ class AccountNotExists(StructuredException):
self.message = message
self.name = name
class InvalidAccountNameException(StructuredException):
status_code = 400
def __init__(self, name=None, message="The account name is invalid"):
StructuredException.__init__(self)
self.message = message
self.name = name
class UserCreateFail(StructuredException):
status_code = 500
@ -145,7 +152,6 @@ class UserCreateFail(StructuredException):
self.message = message
self.name = name
class UserCreateDuplicate(StructuredException):
status_code = 409

View File

@ -3,6 +3,7 @@ from flask import current_app
from urllib.parse import urljoin
from ..lib import utils
from ..lib.errors import InvalidAccountNameException
from .base import db
from .setting import Setting
from .user import User
@ -22,7 +23,7 @@ class Account(db.Model):
back_populates="accounts")
def __init__(self, name=None, description=None, contact=None, mail=None):
self.name = name
self.name = Account.sanitize_name(name) if name is not None else name
self.description = description
self.contact = contact
self.mail = mail
@ -33,9 +34,30 @@ class Account(db.Model):
self.PDNS_VERSION = Setting().get('pdns_version')
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
if self.name is not None:
self.name = ''.join(c for c in self.name.lower()
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
@staticmethod
def sanitize_name(name):
"""
Formats the provided name to fit into the constraint
"""
if not isinstance(name, str):
raise InvalidAccountNameException("Account name must be a string")
allowed_characters = "abcdefghijklmnopqrstuvwxyz0123456789"
if Setting().get('account_name_extra_chars'):
allowed_characters += "_-."
sanitized_name = ''.join(c for c in name.lower() if c in allowed_characters)
if len(sanitized_name) > Account.name.type.length:
current_app.logger.error("Account name {0} too long. Truncated to: {1}".format(
sanitized_name, sanitized_name[:Account.name.type.length]))
if not sanitized_name:
raise InvalidAccountNameException("Empty string is not a valid account name")
return sanitized_name[:Account.name.type.length]
def __repr__(self):
return '<Account {0}r>'.format(self.name)
@ -68,11 +90,9 @@ class Account(db.Model):
"""
Create a new account
"""
# Sanity check - account name
if self.name == "":
return {'status': False, 'msg': 'No account name specified'}
self.name = Account.sanitize_name(self.name)
# check that account name is not already used
# Check that account name is not already used
account = Account.query.filter(Account.name == self.name).first()
if account:
return {'status': False, 'msg': 'Account already exists'}

View File

@ -192,7 +192,8 @@ class Setting(db.Model):
'custom_css': '',
'otp_force': False,
'max_history_records': 1000,
'deny_domain_override': False
'deny_domain_override': False,
'account_name_extra_chars': False
}
def __init__(self, id=None, name=None, value=None):

View File

@ -1268,7 +1268,8 @@ def setting_basic():
'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name',
'session_timeout', 'warn_session_timeout', 'ttl_options',
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email',
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force', 'deny_domain_override', 'enforce_api_ttl'
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force',
'deny_domain_override', 'enforce_api_ttl', 'account_name_extra_chars'
]
return render_template('admin_setting_basic.html', settings=settings)

View File

@ -23,7 +23,7 @@ from ..lib.errors import (
AccountCreateFail, AccountUpdateFail, AccountDeleteFail,
AccountCreateDuplicate, AccountNotExists,
UserCreateFail, UserCreateDuplicate, UserUpdateFail, UserDeleteFail,
UserUpdateFailEmail
UserUpdateFailEmail, InvalidAccountNameException
)
from ..decorators import (
api_basic_auth, api_can_create_domain, is_json, apikey_auth,
@ -870,12 +870,15 @@ def api_create_account():
contact = data['contact'] if 'contact' in data else None
mail = data['mail'] if 'mail' in data else None
if not name:
current_app.logger.debug("Account name missing")
abort(400)
current_app.logger.debug("Account creation failed: name missing")
raise InvalidAccountNameException(message="Account name missing")
sanitized_name = Account.sanitize_name(name)
account_exists = Account.query.filter(Account.name == sanitized_name).all()
account_exists = [] or Account.query.filter(Account.name == name).all()
if len(account_exists) > 0:
msg = "Account {} already exists".format(name)
msg = ("Requested Account {} would be translated to {}"
" which already exists").format(name, sanitized_name)
current_app.logger.debug(msg)
raise AccountCreateDuplicate(message=msg)
@ -913,8 +916,9 @@ def api_update_account(account_id):
if not account:
abort(404)
if name and name != account.name:
abort(400)
if name and Account.sanitize_name(name) != account.name:
msg = "Account name is immutable"
raise AccountUpdateFail(message=msg)
if current_user.role.name not in ['Administrator', 'Operator']:
msg = "User role update accounts"

View File

@ -323,8 +323,8 @@ def login():
# Regexp didn't match, continue to next iteration
continue
account = Account()
account_id = account.get_id_by_name(account_name=group_name)
sanitized_group_name = Account.sanitize_name(group_name)
account_id = account.get_id_by_name(account_name=sanitized_group_name)
if account_id:
account = Account.query.get(account_id)
@ -345,10 +345,12 @@ def login():
current_app.logger.info('User {} added to Account {}'.format(
user.username, account.name))
else:
account.name = group_name
account.description = group_description
account.contact = ''
account.mail = ''
account = Account(
name=sanitized_group_name,
description=group_description,
contact='',
mail=''
)
account.create_account()
history = History(msg='Create account {0}'.format(
account.name),
@ -1092,14 +1094,10 @@ def create_group_to_account_mapping():
def handle_account(account_name, account_description=""):
clean_name = ''.join(c for c in account_name.lower()
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
if len(clean_name) > Account.name.type.length:
current_app.logger.error(
"Account name {0} too long. Truncated.".format(clean_name))
clean_name = Account.sanitize_name(account_name)
account = Account.query.filter_by(name=clean_name).first()
if not account:
account = Account(name=clean_name.lower(),
account = Account(name=clean_name,
description=account_description,
contact='',
mail='')

View File

@ -49,7 +49,7 @@
<span class="fa fa-cog form-control-feedback"></span>
{% if invalid_accountname %}
<span class="help-block">Cannot be blank and must only contain alphanumeric
characters.</span>
characters{% if SETTING.get('account_name_extra_chars') %}, dots, hyphens or underscores{% endif %}.</span>
{% elif duplicate_accountname %}
<span class="help-block">Account name already in use.</span>
{% endif %}
@ -112,8 +112,9 @@
</p>
<p>Fill in all the fields to the in the form to the left.</p>
<p>
<strong>Name</strong> is an account identifier. It will be stored as all lowercase letters (no
spaces, special characters etc).<br />
<strong>Name</strong> is an account identifier. It will be lowercased and can contain alphanumeric
characters{% if SETTING.get('account_name_extra_chars') %}, dots, hyphens and underscores (no space or other special character is allowed)
{% else %} (no extra character is allowed){% endif %}.<br />
<strong>Description</strong> is a user friendly name for this account.<br />
<strong>Contact person</strong> is the name of a contact person at the account.<br />
<strong>Mail Address</strong> is an e-mail address for the contact person.