diff --git a/hiboo/models.py b/hiboo/models.py index 10e90f84281a90eac858af7c5b8495afcbe209b7..eb40676c0574af2fb55828a60a1c935a2501dddd 100644 --- a/hiboo/models.py +++ b/hiboo/models.py @@ -158,7 +158,7 @@ class Service(db.Model): description = db.Column(db.String()) policy = db.Column(db.String(255)) max_profiles = db.Column(db.Integer(), nullable=False, default=1) - profile_regex = db.Column(db.String(255)) + profile_format = db.Column(db.String(255)) same_username = db.Column(db.Boolean(), nullable=False, default=False) config = db.Column(mutable.MutableDict.as_mutable(JSONEncoded)) diff --git a/hiboo/profile/__init__.py b/hiboo/profile/__init__.py index de935134e9b3105b89e0d41f4e46d09828c387b3..61168d9068dbb469caac289c6005858acd2ffd22 100644 --- a/hiboo/profile/__init__.py +++ b/hiboo/profile/__init__.py @@ -5,6 +5,12 @@ blueprint = flask.Blueprint("profile", __name__, template_folder="templates") import flask_login from hiboo import models, utils +from hiboo.profile import format + + +formats = format.ProfileFormat.registry + + from hiboo.profile import login, admin, forms, cli diff --git a/hiboo/profile/format.py b/hiboo/profile/format.py new file mode 100644 index 0000000000000000000000000000000000000000..c53903169e4528630eb48061e5868014af93d090 --- /dev/null +++ b/hiboo/profile/format.py @@ -0,0 +1,79 @@ +from wtforms import validators +from flask_babel import lazy_gettext as _ + +import string + + +class ProfileFormat(object): + + registry = {} + regex = ".*" + allowed = string.printable + transform = str + message = "" + min_length = 3 + max_length = 30 + + @classmethod + def register(cls, *format_ids): + """ Class decorator + """ + def register_function(format): + for format_id in format_ids: + cls.registry[format_id] = format + return format + return register_function + + @classmethod + def validators(cls): + """ Return a username validator for wtforms + """ + return [ + validators.DataRequired(), + validators.Length( + min=cls.min_length, max=cls.max_length, + message=_("must be at least {} and at most {} characters long".format(cls.min_length, cls.max_length)) + ), + validators.Regexp( + "^{}$".format(cls.regex), + message=_("must comprise only of ") + cls.message + ) + ] + + @classmethod + def coalesce(cls, username): + """ Transform a username into its valid form + """ + return filter(cls.allowed.__contains__, cls.transform(username)) + + @classmethod + def alternatives(cls, username): + """ Generate alternate usernames for a given username + """ + index = 1 + while True: + yield username + "_" + str(index) + + +register = ProfileFormat.register + + +@register("lowercase", "", None) +class LowercaseAlphanumPunct(ProfileFormat): + """ Lowercase username, including digits and very basic punctuation + """ + + regex = "[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?" + allowed = string.digits + string.ascii_lowercase + ".-_" + transform = str.lower + message = _("lowercase letters, digits, dots, dashes, and underscores") + + +@register("alnum") +class AlphanumPunct(ProfileFormat): + """ Alphanum username, including some very basic punctuation + """ + + regex = "[a-zA-Z0-9_]+([a-zA-Z0-9_\.-]+[a-zA-Z0-9_]+)?" + allowed = string.digits + string.ascii_letters + ".-_" + message = _("letters, digits, dots, dashes and underscores") diff --git a/hiboo/profile/login.py b/hiboo/profile/login.py index 0859fe942babdb9da2fa8b7c75578d3e511caaab..077417319b7f961a4c5025b5a9f3fd0ac1e2f5f9 100644 --- a/hiboo/profile/login.py +++ b/hiboo/profile/login.py @@ -1,4 +1,4 @@ -from hiboo.profile import blueprint, forms +from hiboo.profile import blueprint, forms, formats from hiboo import models, utils, security from hiboo import user as hiboo_user from passlib import context, hash @@ -50,10 +50,7 @@ def create(service_uuid, create_for=False): status = models.Profile.REQUEST # Actually display the form form = forms.ProfileForm() - if service.profile_regex: - form.username.validators.append( - validators.Regexp("^{}$".format(service.profile_regex)), - ) + form.username.validators = formats[service.profile_format].validators() if service.same_username: form.username.data = user.username form.username.render_kw = {"readonly": True} diff --git a/hiboo/service/forms.py b/hiboo/service/forms.py index ee17f4a076eacc46e7da8ef458bda7aae91c77e4..ed087baac46740ef054fe46a22a6d945e6a4943c 100644 --- a/hiboo/service/forms.py +++ b/hiboo/service/forms.py @@ -1,7 +1,7 @@ from wtforms import validators, fields, widgets from flask_babel import lazy_gettext as _ -from hiboo import models, application +from hiboo import models, application, profile import flask_wtf @@ -14,6 +14,11 @@ class ServiceForm(flask_wtf.FlaskForm): choices=list(models.Service.POLICIES.items())) max_profiles = fields.IntegerField(_('Maximum profile count'), [validators.NumberRange(1, 1000)]) - profile_regex = fields.StringField(_('Profile username regex')) + profile_format = fields.SelectField(_('Profile username format'), + choices=( + [("", _("Default ({})".format(profile.formats[None].message)))] + + [(name, format.message.capitalize()) for name, format in profile.formats.items() if name] + ) + ) same_username = fields.BooleanField(_('Disable per-profile username')) submit = fields.SubmitField(_('Submit')) diff --git a/migrations/versions/059b2c50d7e1_regex_to_profile_format.py b/migrations/versions/059b2c50d7e1_regex_to_profile_format.py new file mode 100644 index 0000000000000000000000000000000000000000..57c5f26724cb0a1db834c7cafacddaa445929a8c --- /dev/null +++ b/migrations/versions/059b2c50d7e1_regex_to_profile_format.py @@ -0,0 +1,28 @@ +""" Replace profile regex by format + +Revision ID: 059b2c50d7e1 +Revises: c5109b93fc0f +Create Date: 2020-05-11 17:22:47.462998 +""" + +from alembic import op +import sqlalchemy as sa +import hiboo + + +revision = '059b2c50d7e1' +down_revision = 'c5109b93fc0f' +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table('service') as batch_op: + batch_op.add_column(sa.Column('profile_format', sa.String(length=255), nullable=True)) + batch_op.drop_column('profile_regex') + + +def downgrade(): + with op.batch_alter_table('service') as batch_op: + batch_op.add_column(sa.Column('profile_regex', sa.VARCHAR(length=255), nullable=True)) + batch_op.drop_column('profile_format')