diff --git a/CHANGES.rst b/CHANGES.rst
index 77cb187f..8604a2ff 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog `_,
and this project adheres to `Semantic Versioning `_.
+Added
+*****
+
+- User can chose their favourite display name. :pr:`77`
+
[0.0.14] - 2022-11-29
=====================
diff --git a/canaille/conf/config.sample.toml b/canaille/conf/config.sample.toml
index 4146fedd..c97c5e23 100644
--- a/canaille/conf/config.sample.toml
+++ b/canaille/conf/config.sample.toml
@@ -131,7 +131,17 @@ GROUP_BASE = "ou=groups,dc=mydomain,dc=tld"
[ACL.DEFAULT]
PERMISSIONS = ["edit_self", "use_oidc"]
READ = ["uid", "groups"]
-WRITE = ["givenName", "sn", "userPassword", "telephoneNumber", "jpegPhoto", "mail", "labeledURI", "preferredLanguage"]
+WRITE = [
+ "givenName",
+ "sn",
+ "displayName",
+ "userPassword",
+ "telephoneNumber",
+ "jpegPhoto",
+ "mail",
+ "labeledURI",
+ "preferredLanguage",
+]
[ACL.ADMIN]
FILTER = "memberof=cn=moderators,ou=groups,dc=mydomain,dc=tld"
@@ -172,7 +182,7 @@ PHONE_NUMBER = "{{ user.telephoneNumber[0] }}"
EMAIL = "{{ user.mail[0] }}"
GIVEN_NAME = "{{ user.givenName[0] }}"
FAMILY_NAME = "{{ user.sn[0] }}"
-PREFERRED_USERNAME = "{{ user.displayName[0] }}"
+PREFERRED_USERNAME = "{{ user.displayName }}"
LOCALE = "{{ user.preferredLanguage[0] }}"
ADDRESS = "{{ user.postalAddress[0] }}"
PICTURE = "{% if user.jpegPhoto %}{{ url_for('account.photo', uid=user.uid[0], field='jpegPhoto', _external=True) }}{% endif %}"
diff --git a/canaille/forms.py b/canaille/forms.py
index 2f692e04..6d639e03 100644
--- a/canaille/forms.py
+++ b/canaille/forms.py
@@ -123,6 +123,15 @@ PROFILE_FORM_FIELDS = dict(
"autocorrect": "off",
},
),
+ displayName=wtforms.StringField(
+ _("Display Name"),
+ validators=[wtforms.validators.Optional()],
+ render_kw={
+ "placeholder": _("Johnny"),
+ "spellcheck": "false",
+ "autocorrect": "off",
+ },
+ ),
mail=wtforms.EmailField(
_("Email address"),
validators=[wtforms.validators.DataRequired(), wtforms.validators.Email()],
diff --git a/canaille/templates/profile.html b/canaille/templates/profile.html
index 108e5437..1fc33e72 100644
--- a/canaille/templates/profile.html
+++ b/canaille/templates/profile.html
@@ -118,9 +118,12 @@
{% if "sn" in form %}
{% block sn_field scoped %}{{ render_field(form.sn) }}{% endblock %}
{% endif %}
-
+ {% if "displayName" in form %}
+ {% block display_name_field scoped %}{{ render_field(form.displayName) }}{% endblock %}
+ {% endif %}
+
{% if "jpegPhoto" in form %}{% endif %}
{% if "mail" in form %}
diff --git a/demo/conf-docker/canaille.toml b/demo/conf-docker/canaille.toml
index 2f6f7074..a359c62f 100644
--- a/demo/conf-docker/canaille.toml
+++ b/demo/conf-docker/canaille.toml
@@ -132,7 +132,17 @@ GROUP_BASE = "ou=groups,dc=mydomain,dc=tld"
[ACL.DEFAULT]
PERMISSIONS = ["use_oidc"]
READ = ["uid", "groups"]
-WRITE = ["jpegPhoto", "givenName", "sn", "userPassword", "telephoneNumber", "mail", "labeledURI", "preferredLanguage"]
+WRITE = [
+ "jpegPhoto",
+ "givenName",
+ "sn",
+ "displayName",
+ "userPassword",
+ "telephoneNumber",
+ "mail",
+ "labeledURI",
+ "preferredLanguage",
+]
[ACL.ADMIN]
FILTER = "memberof=cn=admins,ou=groups,dc=mydomain,dc=tld"
@@ -178,7 +188,7 @@ PHONE_NUMBER = "{{ user.telephoneNumber[0] }}"
EMAIL = "{{ user.mail[0] }}"
GIVEN_NAME = "{{ user.givenName[0] }}"
FAMILY_NAME = "{{ user.sn[0] }}"
-PREFERRED_USERNAME = "{{ user.displayName[0] }}"
+PREFERRED_USERNAME = "{{ user.displayName }}"
LOCALE = "{{ user.preferredLanguage[0] }}"
ADDRESS = "{{ user.postalAddress[0] }}"
PICTURE = "{% if user.jpegPhoto %}{{ url_for('account.photo', uid=user.uid[0], field='jpegPhoto', _external=True) }}{% endif %}"
diff --git a/demo/conf/canaille.toml b/demo/conf/canaille.toml
index 2c6f420e..0d18f12a 100644
--- a/demo/conf/canaille.toml
+++ b/demo/conf/canaille.toml
@@ -132,7 +132,18 @@ GROUP_BASE = "ou=groups,dc=mydomain,dc=tld"
[ACL.DEFAULT]
PERMISSIONS = ["edit_self", "use_oidc"]
READ = ["uid", "groups"]
-WRITE = ["jpegPhoto", "givenName", "sn", "userPassword", "telephoneNumber", "mail", "labeledURI", "postalAddress", "preferredLanguage"]
+WRITE = [
+ "jpegPhoto",
+ "givenName",
+ "sn",
+ "displayName",
+ "userPassword",
+ "telephoneNumber",
+ "mail",
+ "labeledURI",
+ "postalAddress",
+ "preferredLanguage",
+]
[ACL.ADMIN]
FILTER = "memberof=cn=admins,ou=groups,dc=mydomain,dc=tld"
@@ -178,7 +189,7 @@ PHONE_NUMBER = "{{ user.telephoneNumber[0] }}"
EMAIL = "{{ user.mail[0] }}"
GIVEN_NAME = "{{ user.givenName[0] }}"
FAMILY_NAME = "{{ user.sn[0] }}"
-PREFERRED_USERNAME = "{{ user.displayName[0] }}"
+PREFERRED_USERNAME = "{{ user.displayName }}"
LOCALE = "{{ user.preferredLanguage[0] }}"
ADDRESS = "{{ user.postalAddress[0] }}"
PICTURE = "{% if user.jpegPhoto %}{{ url_for('account.photo', uid=user.uid[0], field='jpegPhoto', _external=True) }}{% endif %}"
diff --git a/tests/conftest.py b/tests/conftest.py
index 5aa115d7..1b101802 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -167,6 +167,7 @@ def configuration(slapd_server, smtpd, keypair_path):
"givenName",
"jpegPhoto",
"sn",
+ "displayName",
"userPassword",
"telephoneNumber",
"postalAddress",
@@ -206,7 +207,7 @@ def configuration(slapd_server, smtpd, keypair_path):
"EMAIL": "{{ user.mail[0] }}",
"GIVEN_NAME": "{{ user.givenName[0] }}",
"FAMILY_NAME": "{{ user.sn[0] }}",
- "PREFERRED_USERNAME": "{{ user.displayName[0] }}",
+ "PREFERRED_USERNAME": "{{ user.displayName }}",
"LOCALE": "{{ user.preferredLanguage[0] }}",
"PICTURE": "{% if user.jpegPhoto %}{{ url_for('account.photo', uid=user.uid[0], field='jpegPhoto', _external=True) }}{% endif %}",
},
@@ -250,6 +251,7 @@ def user(app, slapd_connection):
uid="user",
mail="john@doe.com",
userPassword="{SSHA}fw9DYeF/gHTHuVMepsQzVYAkffGcU8Fz",
+ displayName="Johnny",
)
u.save()
yield u
diff --git a/tests/oidc/test_authorization_code_flow.py b/tests/oidc/test_authorization_code_flow.py
index b29492c3..489cc6a6 100644
--- a/tests/oidc/test_authorization_code_flow.py
+++ b/tests/oidc/test_authorization_code_flow.py
@@ -93,6 +93,7 @@ def test_authorization_code_flow(
assert {
"name": "John (johnny) Doe",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"email": "john@doe.com",
"sub": "user",
"groups": [],
@@ -161,6 +162,7 @@ def test_authorization_code_flow_preconsented(
assert {
"name": "John (johnny) Doe",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"sub": "user",
} == res.json
@@ -231,6 +233,7 @@ def test_logout_login(testclient, logged_user, client):
assert {
"name": "John (johnny) Doe",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"sub": "user",
} == res.json
@@ -312,6 +315,7 @@ def test_refresh_token(testclient, user, client):
assert {
"name": "John (johnny) Doe",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"sub": "user",
} == res.json
@@ -378,6 +382,7 @@ def test_code_challenge(testclient, logged_user, client):
assert {
"name": "John (johnny) Doe",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"sub": "user",
} == res.json
@@ -705,6 +710,7 @@ def test_authorization_code_request_scope_too_large(
assert {
"name": "John (johnny) Doe",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"sub": "user",
} == res.json
diff --git a/tests/oidc/test_hybrid_flow.py b/tests/oidc/test_hybrid_flow.py
index d73b10f5..aed9d834 100644
--- a/tests/oidc/test_hybrid_flow.py
+++ b/tests/oidc/test_hybrid_flow.py
@@ -49,6 +49,7 @@ def test_oauth_hybrid(testclient, slapd_connection, user, client):
assert {
"name": "John (johnny) Doe",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"sub": "user",
} == res.json
@@ -94,5 +95,6 @@ def test_oidc_hybrid(
assert {
"name": "John (johnny) Doe",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"sub": "user",
} == res.json
diff --git a/tests/oidc/test_implicit_flow.py b/tests/oidc/test_implicit_flow.py
index 0f747e22..3cb4099e 100644
--- a/tests/oidc/test_implicit_flow.py
+++ b/tests/oidc/test_implicit_flow.py
@@ -46,6 +46,7 @@ def test_oauth_implicit(testclient, user, client):
"name": "John (johnny) Doe",
"sub": "user",
"family_name": "Doe",
+ "preferred_username": "Johnny",
} == res.json
client.grant_types = ["code"]
@@ -102,6 +103,7 @@ def test_oidc_implicit(testclient, keypair, user, client, other_client):
"name": "John (johnny) Doe",
"sub": "user",
"family_name": "Doe",
+ "preferred_username": "Johnny",
} == res.json
client.grant_types = ["code"]
@@ -161,6 +163,7 @@ def test_oidc_implicit_with_group(
"name": "John (johnny) Doe",
"sub": "user",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"groups": ["foo"],
} == res.json
diff --git a/tests/oidc/test_oauth2utils.py b/tests/oidc/test_oauth2utils.py
index 4bfb4ce7..3c84e475 100644
--- a/tests/oidc/test_oauth2utils.py
+++ b/tests/oidc/test_oauth2utils.py
@@ -30,7 +30,7 @@ DEFAULT_JWT_MAPPING_CONFIG = {
"EMAIL": "{{ user.mail[0] }}",
"GIVEN_NAME": "{{ user.givenName[0] }}",
"FAMILY_NAME": "{{ user.sn[0] }}",
- "PREFERRED_USERNAME": "{{ user.displayName[0] }}",
+ "PREFERRED_USERNAME": "{{ user.displayName }}",
"LOCALE": "{{ user.preferredLanguage[0] }}",
}
@@ -49,6 +49,7 @@ def test_generate_user_standard_claims_with_default_config(
"email": "john@doe.com",
"sub": "user",
"locale": "fr",
+ "preferred_username": "Johnny",
}
diff --git a/tests/oidc/test_password_flow.py b/tests/oidc/test_password_flow.py
index 8a79330d..e62b943a 100644
--- a/tests/oidc/test_password_flow.py
+++ b/tests/oidc/test_password_flow.py
@@ -32,6 +32,7 @@ def test_password_flow_basic(testclient, user, client):
"name": "John (johnny) Doe",
"sub": "user",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"groups": [],
} == res.json
@@ -69,5 +70,6 @@ def test_password_flow_post(testclient, user, client):
"name": "John (johnny) Doe",
"sub": "user",
"family_name": "Doe",
+ "preferred_username": "Johnny",
"groups": [],
} == res.json
diff --git a/tests/test_profile.py b/tests/test_profile.py
index 47466887..27bd8557 100644
--- a/tests/test_profile.py
+++ b/tests/test_profile.py
@@ -42,6 +42,7 @@ def test_edition(
res.form["uid"] = "toto"
res.form["givenName"] = "given_name"
res.form["sn"] = "family_name"
+ res.form["displayName"] = "display_name"
res.form["mail"] = "email@mydomain.tld"
res.form["telephoneNumber"] = "555-666-777"
res.form["postalAddress"] = "postal_address"
@@ -61,6 +62,7 @@ def test_edition(
assert ["user"] == logged_user.uid
assert ["given_name"] == logged_user.givenName
assert ["family_name"] == logged_user.sn
+ assert "display_name" == logged_user.displayName
assert ["email@mydomain.tld"] == logged_user.mail
assert ["555-666-777"] == logged_user.telephoneNumber
assert ["postal_address"] == logged_user.postalAddress