Skip to content
Open
4 changes: 2 additions & 2 deletions api/mulysaoauthvalidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ class MulysaOAuth2Validator(OAuth2Validator):

def get_additional_claims(self, request):
"""
give email, firstname and lastname in oid claims data
give sub, email, firstname and lastname in oid claims data
Comment thread
brndd marked this conversation as resolved.
"""
return {
"sub": request.user.email,
"sub": request.user.oidc_sub,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we add the uuid for everybody (a real uuid, not "some users have uuid some email") and a "useLegacySSOIdentifier" bit.

In the migration generate uuid for everybody and mark the old users with the "use legacy" bit.

Then have a property decorator "ssoidentifier" that returns either the uuid or the email depending on the users legacy bit.

This would be cleaner from db perspective and might allow for easier migration in the long run.

"email": request.user.email,
"firstName": request.user.first_name,
"lastName": request.user.last_name,
Expand Down
2 changes: 1 addition & 1 deletion api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_oauthvalidator(self):
req = HttpRequest()
req.user = self.ok_user
expected = {
"sub": self.ok_user.email,
"sub": self.ok_user.oidc_sub,
"email": self.ok_user.email,
"firstName": self.ok_user.first_name,
"lastName": self.ok_user.last_name,
Expand Down
131 changes: 102 additions & 29 deletions users/locale/fi/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
Expand All @@ -7,9 +6,9 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-23 13:25+0000\n"
"PO-Revision-Date: 2025-02-08 15:14+0200\n"
"Last-Translator: Sami Olmari <sami@olmari.fi>\n"
"POT-Creation-Date: 2025-11-17 05:41+0000\n"
"PO-Revision-Date: 2025-11-17 05:47+0200\n"
"Last-Translator: Pekka Oinas <peoinas@protonmail.com>\n"
"Language-Team: \n"
"Language: fi\n"
"MIME-Version: 1.0\n"
Expand All @@ -18,7 +17,7 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Language: fi\n"
"X-Source-Language: en\n"
"X-Generator: Poedit 3.5\n"
"X-Generator: Lokalize 25.08.0\n"

#: users/custom_user_manager.py:34
msgid "User must have an email address"
Expand Down Expand Up @@ -68,7 +67,7 @@ msgstr "20-60-vuotias"
msgid "Over 63 years"
msgstr "Yli 63-vuotias"

#: users/filters.py:90 users/filters.py:98 users/models/custom_user.py:130
#: users/filters.py:90 users/filters.py:98 users/models/custom_user.py:140
msgid "Marked for deletion"
msgstr "Merkattu poistettavaksi"

Expand Down Expand Up @@ -174,7 +173,7 @@ msgstr "Laskun luontipäivä"
msgid "Automatically set to now when invoice is created"
msgstr "Asetetaan automaattisesti kun lasku luodaan"

#: users/models/custom_invoice.py:49 users/models/custom_user.py:121
#: users/models/custom_invoice.py:49 users/models/custom_user.py:131
#: users/models/membership_application.py:30 users/models/nfc_card.py:22
msgid "Last modified datetime"
msgstr "Viimeksi muokattu"
Expand All @@ -192,96 +191,96 @@ msgstr ""
"Vapaamuotoinen lasku joka maksaa %(days)s päivää palvelua %(servicename)s "
"käyttäjälle %(username)s - %(amount)s€, viitenumero: %(reference)s"

#: users/models/custom_user.py:36
#: users/models/custom_user.py:41
msgid "Email address"
msgstr "Sähköpostiosoite"

#: users/models/custom_user.py:38
#: users/models/custom_user.py:43
msgid ""
"Your email address will be used for important notifications about your "
"membership"
msgstr ""
"Sähköpostia käytetään labin toimintaan ja jäsenyyteen liittyvään viestintään"

#: users/models/custom_user.py:45
#: users/models/custom_user.py:55
msgid "First name"
msgstr "Etunimi"

#: users/models/custom_user.py:48
#: users/models/custom_user.py:58
msgid "Last name"
msgstr "Sukunimi"

#: users/models/custom_user.py:53
#: users/models/custom_user.py:63
msgid "Municipality / City"
msgstr "Kotikunta"

#: users/models/custom_user.py:60
#: users/models/custom_user.py:70
msgid "Nick"
msgstr "Nimimerkki"

#: users/models/custom_user.py:61
#: users/models/custom_user.py:71
msgid "Nickname you are known with on Internet"
msgstr "Nimimerkki jolla sinut tunnetaan internetissä"

#: users/models/custom_user.py:69
#: users/models/custom_user.py:79
msgid "Matrix ID"
msgstr "Matrix ID"

#: users/models/custom_user.py:70
#: users/models/custom_user.py:80
msgid "Matrix ID (@user:example.org)"
msgstr "Matrix ID (@käyttäjä:esimerkki.org)"

#: users/models/custom_user.py:77
#: users/models/custom_user.py:87
msgid "Birthday"
msgstr "Syntymäpäivä"

#: users/models/custom_user.py:84
#: users/models/custom_user.py:94
msgid "Mobile phone number"
msgstr "Matkapuhelinnumero"

#: users/models/custom_user.py:86
#: users/models/custom_user.py:96
msgid ""
"This number will also be the one that gets access to the hacklab premises. "
"International format (+35840123567)."
msgstr ""
"Tätä numeroa käytetään myös esim. hacklabin oven avaamiseen. Kansainvälinen "
"muoto (+35840123567)."

#: users/models/custom_user.py:90
#: users/models/custom_user.py:100
msgid "This phone number is already registered to a member"
msgstr "Tämä puhelinnumero on jo käytössä jäsenellä"

#: users/models/custom_user.py:99
#: users/models/custom_user.py:109
msgid "Bank account"
msgstr "Pankkitili"

#: users/models/custom_user.py:100
#: users/models/custom_user.py:110
msgid "Bank account for paying invoices (IBAN format: FI123567890)"
msgstr "Pankkitili mahdolliseen laskujen maksamiseen (IBAN-muoto: FI123567890)"

#: users/models/custom_user.py:106
#: users/models/custom_user.py:116
msgid "Language"
msgstr "Kieli"

#: users/models/custom_user.py:107
#: users/models/custom_user.py:117
msgid "Language preferred by user"
msgstr "Käyttämäsi kieli"

#: users/models/custom_user.py:115
#: users/models/custom_user.py:125
msgid "User creation date"
msgstr "Käyttäjän luontipäivä"

#: users/models/custom_user.py:116
#: users/models/custom_user.py:126
#, fuzzy
#| msgid "Automatically set to now when user is create"
msgid "Automatically set to now when user is created"
msgstr "Asetetaan automaattisesti kun käyttäjä luodaan"

#: users/models/custom_user.py:122
#: users/models/custom_user.py:132
msgid "Last time this user was modified"
msgstr "Päiväys, kun käyttäjän tietoja on viimeksi muokattu"

#: users/models/custom_user.py:132
#: users/models/custom_user.py:142
msgid ""
"Filled if the user has marked themself as wanting to end their membership"
msgstr "Täytetty, jos käyttäjä on ilmoittanut haluavansa lopettaa jäsenyyden"
Expand Down Expand Up @@ -544,6 +543,25 @@ msgstr ""
msgid "Your application has been rejected."
msgstr "Hakemuksesi on hylätty."

#: users/templates/mail/confirm_email_change.txt:4
#, python-format
msgid ""
"\n"
"Hello %(first_name)s,\n"
"\n"
"To finish changing your email on %(sitename)s, please click the following "
"link:\n"
"\n"
"%(confirm_url)s\n"
msgstr ""
"\n"
"Hei %(first_name)s,\n"
"\n"
"Viimeistelläksi sähköpostiosoitteesi vaihdon sivulla %(sitename)s, klikkaa "
"alla olevaa linkkiä:\n"
"\n"
"%(confirm_url)s\n"

#: users/templates/mail/door_access_denied.txt:4
#, python-format
msgid ""
Expand Down Expand Up @@ -578,10 +596,65 @@ msgstr "Palveluitesi tila:"
msgid "No services"
msgstr "Ei palveluja"

#: users/templates/mail/email_changed.txt:4
#, python-format
msgid ""
"\n"
"Hello %(first_name)s!\n"
"\n"
"The email address for your account on %(sitename)s was just changed. The new "
"email is %(new_email)s.\n"
"\n"
"If this was done by you, please ignore this email.\n"
"\n"
"If this was not done by you, please contact an administrator immediately.\n"
msgstr ""
"\n"
"Hei %(first_name)s!\n"
"\n"
"Sähköpostiosoitteesi sivustolla %(sitename)s on vaihdettu. Uusi osoite on "
"%(new_email)s.\n"
"\n"
"Jos vaihdoit sähköpostiosoitteesi itse, ei sinun tarvitse reagoida tähän "
"viestiin.\n"
"\n"
"Jos et vaihtanut sähköpostiosoitettasi itse, ota välittömästi yhteyttä "
"ylläpitäjään.\n"

#: users/templates/mail/new_application.txt:4
msgid "New membership application"
msgstr "Uusi jäsenhakemus vastaanotettu"

#: users/templates/mail/password_changed.txt:4
#, python-format
msgid ""
"\n"
"Hello %(first_name)s!\n"
"\n"
"The password for your account on %(sitename)s was just changed.\n"
"\n"
"If this was done by you, please ignore this email.\n"
"\n"
"If this was not done by you, please reset your password and contact an "
"administrator immediately.\n"
"\n"
"You may reset your password by navigating to %(siteurl)s, clicking on \"Log "
"in\", and then clicking on \"Forgot password\".\n"
msgstr ""
"\n"
"Hei %(first_name)s!\n"
"\n"
"Salasanasi sivustolla %(sitename)s on vaihdettu.\n"
"\n"
"Jos vaihdoit salasanan, ei sinun tarvitse reagoida tähän viestiin.\n"
"\n"
"Jos et vaihtanut salasanaasi itse, resetoi salasanasi ja ota yhteys "
"ylläpitäjään.\n"
"\n"
"Voit resetoida salasanasi menemällä osoitteeseen %(siteurl)s, klikkaamalla "
"linkkiä \"Kirjaudu sisään\", ja sitten klikkaamalla \"Unohtuiko salasana?"
"\".\n"

#: users/templates/mail/service_subscription_about_to_expire.txt:4
#, python-format
msgid ""
Expand Down Expand Up @@ -664,7 +737,7 @@ msgid ""
"%(value)s is not a valid Matrix id. It must be in format @user:example.org"
msgstr ""
"%(value)s ei ole kelvollinen Matrix id. Sen tulee olla muodossa @käyttäjä:"
"esimerkki.org"
"@esimerkki.org"

#: users/validators.py:23
#, python-format
Expand Down
24 changes: 24 additions & 0 deletions users/migrations/0031_customuser_add_oidc_sub_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.9 on 2025-10-01 02:44

import users.models.custom_user
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0030_alter_banktransaction_unique_together_and_more"),
]

operations = [
migrations.AddField(
model_name="customuser",
name="oidc_sub",
field=models.CharField(
default=users.models.custom_user.get_uuid_str,
editable=False,
max_length=255,
null=True,
),
),
]
27 changes: 27 additions & 0 deletions users/migrations/0032_customuser_populate_oidc_sub_values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.1.9 on 2025-10-01 02:45

from django.db import migrations

# Note there is a race condition if you allow objects to be created while this migration is running.
# Objects created after the AddField and before RunPython will have their original uuid’s overwritten.
Comment on lines +5 to +6
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The migration comment mentions a race condition but doesn't provide a solution. Consider adding a database transaction or advisory lock during migration to prevent this race condition, or document that migrations should be run during a maintenance window when object creation is disabled.

Suggested change
# Note there is a race condition if you allow objects to be created while this migration is running.
# Objects created after the AddField and before RunPython will have their original uuid’s overwritten.
# IMPORTANT: There is a race condition if you allow objects to be created while this migration is running.
# To prevent data corruption, you MUST run this migration during a maintenance window when object creation is disabled.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe? This comment is a result of copy pasting directly from Django docs. I was under the impression that migrations are run at server startup before any object creation is possible.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually migrations are ran before the service is started.

But if you have hundreds of workers running with "internet scale" traffic then the correct way is to first deploy a version that can handle both situations. Then the migration that can run in the background without downtime. Then a new version that has just the new logic. And all of this would happen in a way that first just some traffic, say 10% is sent to the new workers and after verifying it works then the rest of the traffic in few batches.

No we are not nowhere near that kind of requirements here :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually migrations are ran before the service is started.

But if you have hundreds of workers running with "internet scale" traffic then the correct way is to first deploy a version that can handle both situations. Then the migration that can run in the background without downtime. Then a new version that has just the new logic. And all of this would happen in a way that first just some traffic, say 10% is sent to the new workers and after verifying it works then the rest of the traffic in few batches.

No we are not nowhere near that kind of requirements here :)

def populate_oidc_sub(apps, schema_editor):
CustomUser = apps.get_model("users", "CustomUser")
for user in CustomUser.objects.all():
user.oidc_sub = user.email
user.save(update_fields=["oidc_sub"])

def reverse_populate_oidc_sub(apps, schema_editor):
# On reverse, we can't meaningfully restore the state since new users
# would have UUIDs. Just leave oidc_sub as-is since the field will be
# removed by the previous migration's reverse.
pass

class Migration(migrations.Migration):

dependencies = [
("users", "0031_customuser_add_oidc_sub_field"),
]

operations = [
migrations.RunPython(populate_oidc_sub, reverse_populate_oidc_sub)
]
Comment thread
brndd marked this conversation as resolved.
24 changes: 24 additions & 0 deletions users/migrations/0033_customuser_remove_oidc_sub_null.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.1.9 on 2025-10-01 02:45

import users.models.custom_user
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0032_customuser_populate_oidc_sub_values"),
]

operations = [
migrations.AlterField(
model_name="customuser",
name="oidc_sub",
field=models.CharField(
default=users.models.custom_user.get_uuid_str,
editable=False,
max_length=255,
unique=True,
),
),
]
10 changes: 10 additions & 0 deletions users/models/custom_user.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import logging
import uuid

from django.contrib.auth.models import AbstractUser
from django.db import models
Expand All @@ -13,6 +14,10 @@
logger = logging.getLogger(__name__)


def get_uuid_str():
return str(uuid.uuid4())


class CustomUser(AbstractUser):
class Meta:
ordering = (
Expand Down Expand Up @@ -40,6 +45,11 @@ class Meta:
max_length=255,
)

# unique subject identifier for SSO apps. UUID for new accounts, but email for legacy accounts for legacy reasons
oidc_sub = models.CharField(
max_length=255, unique=True, editable=False, default=get_uuid_str
)

# django does not make these mandatory by default, lets make them mandatory
first_name = models.CharField(
max_length=30, blank=False, null=False, verbose_name=_("First name")
Expand Down
Loading