From c63d53f0ed85d3cdb511e5d808509df62f5169a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Thu, 26 Nov 2020 15:29:14 +0100 Subject: [PATCH] Profile editable fields are configurable --- canaille/account.py | 54 +++++----- canaille/conf/config.sample.toml | 13 +++ canaille/forms.py | 60 ++++++----- canaille/templates/profile.html | 31 ++++-- .../translations/fr/LC_MESSAGES/messages.mo | Bin 12272 -> 12357 bytes .../translations/fr/LC_MESSAGES/messages.po | 94 ++++++++++-------- canaille/translations/messages.pot | 92 +++++++++-------- demo/conf/canaille.toml | 13 +++ demo/ldif/bootstrap.ldif | 3 + tests/conftest.py | 9 ++ tests/test_profile.py | 29 +++--- 11 files changed, 245 insertions(+), 153 deletions(-) diff --git a/canaille/account.py b/canaille/account.py index 6d7ba4cd..388ef1da 100644 --- a/canaille/account.py +++ b/canaille/account.py @@ -15,9 +15,9 @@ from flask_babel import gettext as _ from .forms import ( LoginForm, - ProfileForm, PasswordResetForm, ForgottenPasswordForm, + profile_form, ) from .apputils import base64logo, send_email from .flaskutils import current_user, user_needed, moderator_needed @@ -151,10 +151,11 @@ def users(user): @bp.route("/profile", methods=("GET", "POST")) @moderator_needed() def profile_creation(user): - claims = current_app.config["JWT"]["MAPPING"] - form = ProfileForm(request.form or None) + form = profile_form(current_app.config["LDAP"]["FIELDS"]) + form.process(request.form or None) try: - del form.sub.render_kw["readonly"] + if "uid" in form: + del form["uid"].render_kw["readonly"] except KeyError: pass @@ -165,14 +166,14 @@ def profile_creation(user): else: user = User(objectClass=current_app.config["LDAP"]["USER_CLASS"]) for attribute in form: - model_attribute_name = claims.get(attribute.name.upper()) - if ( - not model_attribute_name - or model_attribute_name not in user.must + user.may - ): - continue + if attribute.name in user.may + user.must: + if user.attr_type_by_name()[attribute.name].single_value: + user[attribute.name] = attribute.data + else: + user[attribute.name] = [attribute.data] - user[model_attribute_name] = [attribute.data] + if not form["password1"].data or user.set_password(form["password1"].data): + flash(_("Profile updated successfuly."), "success") user.cn = [f"{user.givenName[0]} {user.sn[0]}"] user.save() @@ -204,18 +205,20 @@ def profile_edition(user, username): def profile_edit(user, username): menuitem = "profile" if username == user.uid[0] else "users" - claims = current_app.config["JWT"]["MAPPING"] + fields = current_app.config["LDAP"]["FIELDS"] if username != user.uid[0]: user = User.get(username) or abort(404) data = { - k.lower(): getattr(user, v)[0] - if getattr(user, v) and isinstance(getattr(user, v), list) - else getattr(user, v) or "" - for k, v in claims.items() + k: getattr(user, k)[0] + if getattr(user, k) and isinstance(getattr(user, k), list) + else getattr(user, k) or "" + for k in fields + if hasattr(user, k) } - form = ProfileForm(request.form or None, data=data) - form.sub.render_kw["readonly"] = "true" + form = profile_form(fields) + form.process(request.form or None, data=data) + form["uid"].render_kw["readonly"] = "true" if request.form: if not form.validate(): @@ -223,16 +226,13 @@ def profile_edit(user, username): else: for attribute in form: - model_attribute_name = claims.get(attribute.name.upper()) - if ( - not model_attribute_name - or model_attribute_name not in user.must + user.may - ): - continue + if attribute.name in user.may + user.must: + if user.attr_type_by_name()[attribute.name].single_value: + user[attribute.name] = attribute.data + else: + user[attribute.name] = [attribute.data] - user[model_attribute_name] = [attribute.data] - - if not form.password1.data or user.set_password(form.password1.data): + if not form["password1"].data or user.set_password(form["password1"].data): flash(_("Profile updated successfuly."), "success") user.save() diff --git a/canaille/conf/config.sample.toml b/canaille/conf/config.sample.toml index 1f855325..119f20c0 100644 --- a/canaille/conf/config.sample.toml +++ b/canaille/conf/config.sample.toml @@ -54,6 +54,19 @@ ADMIN_FILTER = "memberof=cn=admins,ou=groups,dc=mydomain,dc=tld" # USER_ADMIN_FILTER = "uid=moderator" USER_ADMIN_FILTER = "memberof=cn=moderators,ou=groups,dc=mydomain,dc=tld" +# The list of ldap fields you want to be editable by the +# users. +FIELDS = [ + "uid", + "mail", + "givenName", + "sn", + "userPassword", + "telephoneNumber", + "employeeNumber", +# "photo", +] + # The jwt configuration. You can generate a RSA keypair with: # ssh-keygen -t rsa -b 4096 -m PEM -f private.pem # openssl rsa -in private.pem -pubout -outform PEM -out public.pem diff --git a/canaille/forms.py b/canaille/forms.py index dbd025c0..93aa80d3 100644 --- a/canaille/forms.py +++ b/canaille/forms.py @@ -1,4 +1,5 @@ import wtforms +import wtforms.form from flask_babel import lazy_gettext as _ from flask_wtf import FlaskForm @@ -46,35 +47,30 @@ class PasswordResetForm(FlaskForm): ) -class ProfileForm(FlaskForm): - sub = wtforms.StringField( +PROFILE_FORM_FIELDS = dict( + uid=wtforms.StringField( _("Username"), render_kw={"placeholder": _("jdoe")}, validators=[wtforms.validators.DataRequired()], - ) - # name = wtforms.StringField(_("Name")) - given_name = wtforms.StringField( + ), + cn=wtforms.StringField(_("Name")), + givenName=wtforms.StringField( _("Given name"), render_kw={ "placeholder": _("John"), "spellcheck": "false", "autocorrect": "off", }, - ) - family_name = wtforms.StringField( + ), + sn=wtforms.StringField( _("Family Name"), render_kw={ "placeholder": _("Doe"), "spellcheck": "false", "autocorrect": "off", }, - ) - # preferred_username = wtforms.StringField(_("Preferred username")) - # gender = wtforms.StringField(_("Gender")) - # birthdate = wtforms.DateField(_("Birth date")) - # zoneinfo = wtforms.StringField(_("Zoneinfo")) - # locale = wtforms.StringField(_("Language")) - email = wtforms.EmailField( + ), + mail=wtforms.EmailField( _("Email address"), validators=[wtforms.validators.DataRequired(), wtforms.validators.Email()], render_kw={ @@ -82,22 +78,38 @@ class ProfileForm(FlaskForm): "spellcheck": "false", "autocorrect": "off", }, - ) - # address = wtforms.StringField(_("Address")) - phone_number = wtforms.TelField( + ), + telephoneNumber=wtforms.TelField( _("Phone number"), render_kw={"placeholder": _("555-000-555")} - ) - # picture = wtforms.StringField(_("Photo")) - # website = wtforms.URLField(_("Website")) - password1 = wtforms.PasswordField( + ), + photo=wtforms.StringField(_("Photo")), + password1=wtforms.PasswordField( _("Password"), validators=[wtforms.validators.Optional(), wtforms.validators.Length(min=8)], - ) - password2 = wtforms.PasswordField( + ), + password2=wtforms.PasswordField( _("Password confirmation"), validators=[ wtforms.validators.EqualTo( "password1", message=_("Password and confirmation do not match.") ) ], - ) + ), + employeeNumber=wtforms.StringField( + _("Number"), + render_kw={ + "placeholder": _("1234"), + }, + ), +) + + +def profile_form(field_names): + if "userPassword" in field_names: + field_names += ["password1", "password2"] + fields = { + name: PROFILE_FORM_FIELDS.get(name) + for name in field_names + if PROFILE_FORM_FIELDS.get(name) + } + return wtforms.form.BaseForm(fields) diff --git a/canaille/templates/profile.html b/canaille/templates/profile.html index 025e1191..a8259f94 100644 --- a/canaille/templates/profile.html +++ b/canaille/templates/profile.html @@ -56,21 +56,36 @@ class="ui form" > - {{ sui.render_field(form.csrf_token) }} + {#{ sui.render_field(form.csrf_token) }#}

{% trans %}Personal information{% endtrans %}

- {{ sui.render_field(form.given_name) }} - {{ sui.render_field(form.family_name) }} + {% if "givenName" in form %} + {{ sui.render_field(form.givenName) }} + {% endif %} + {% if "sn" in form %} + {{ sui.render_field(form.sn) }} + {% endif %}
- {{ sui.render_field(form.email) }} - {{ sui.render_field(form.phone_number) }} + {% if "mail" in form %} + {{ sui.render_field(form.mail) }} + {% endif %} + {% if "telephoneNumber" in form %} + {{ sui.render_field(form.telephoneNumber) }} + {% endif %} + {% if "employeeNumber" in form %} + {{ sui.render_field(form.employeeNumber) }} + {% endif %}

{% trans %}Account information{% endtrans %}

- {{ sui.render_field(form.sub) }} + {% if "uid" in form %} + {{ sui.render_field(form.uid) }} + {% endif %}
- {{ sui.render_field(form.password1) }} - {{ sui.render_field(form.password2) }} + {% if "password1" in form %} + {{ sui.render_field(form.password1) }} + {{ sui.render_field(form.password2) }} + {% endif %}