(oidc) add django-oauth-toolkit w/ configuration

This allows to use `people` as an identity provider using
OIDC and local users.
This commit is partial, because it does not manage a way to
create "local" users and the login page is the admin one, which
can't be used for non staff users or login with email.
This commit is contained in:
Quentin BEY
2025-01-14 11:43:42 +01:00
committed by BEY Quentin
parent 8faa049046
commit db6cdadd72
30 changed files with 1505 additions and 38 deletions

View File

@@ -1,6 +1,7 @@
"""Admin classes and registrations for People's mailbox manager app."""
from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin
from django.utils.html import format_html_join, mark_safe
from django.utils.translation import gettext_lazy as _
@@ -163,7 +164,7 @@ class MailDomainAdmin(admin.ModelAdmin):
@admin.register(models.Mailbox)
class MailboxAdmin(admin.ModelAdmin):
class MailboxAdmin(UserAdmin):
"""Admin for mailbox model."""
list_display = ("__str__", "domain", "status", "updated_at")
@@ -171,6 +172,29 @@ class MailboxAdmin(admin.ModelAdmin):
search_fields = ("local_part", "domain__name")
readonly_fields = ["updated_at", "local_part", "domain"]
fieldsets = None
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"first_name",
"last_name",
"local_part",
"domain",
"secondary_email",
"status",
"usable_password",
"password1",
"password2",
),
},
),
)
ordering = ("local_part", "domain")
filter_horizontal = ()
@admin.register(models.MailDomainAccess)
class MailDomainAccessAdmin(admin.ModelAdmin):

View File

@@ -2,6 +2,8 @@
from logging import getLogger
from django.contrib.auth.hashers import make_password
from requests.exceptions import HTTPError
from rest_framework import exceptions, serializers
@@ -33,8 +35,16 @@ class MailboxSerializer(serializers.ModelSerializer):
def create(self, validated_data):
"""
Override create function to fire a request on mailbox creation.
By default, we generate an unusable password for the mailbox, meaning that the mailbox
will not be able to be used as a login credential until the password is set.
"""
mailbox = super().create(validated_data)
mailbox = super().create(
validated_data
| {
"password": make_password(None), # generate an unusable password
}
)
if mailbox.domain.status == enums.MailDomainStatusChoices.ENABLED:
client = DimailAPIClient()
# send new mailbox request to dimail

View File

@@ -2,6 +2,7 @@
Mailbox manager application factories
"""
from django.contrib.auth.hashers import make_password
from django.utils.text import slugify
import factory.fuzzy
@@ -76,6 +77,7 @@ class MailboxFactory(factory.django.DjangoModelFactory):
)
domain = factory.SubFactory(MailDomainEnabledFactory)
secondary_email = factory.Faker("email")
password = make_password("password")
class MailboxEnabledFactory(MailboxFactory):

View File

@@ -1,20 +0,0 @@
# Generated by Django 5.1.5 on 2025-02-10 11:10
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0010_team_depth_team_numchild_team_path_and_more'),
('mailbox_manager', '0017_alter_maildomain_status'),
]
operations = [
migrations.AddField(
model_name='maildomain',
name='organization',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='mail_domains', to='core.organization'),
),
]

View File

@@ -19,7 +19,7 @@ def fill_dn_email(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('mailbox_manager', '0020_maildomain_organization'),
('mailbox_manager', '0022_maildomain_organization'),
]
operations = [

View File

@@ -3,6 +3,7 @@ Declare and configure the models for the People additional application : mailbox
"""
from django.conf import settings
from django.contrib.auth.base_user import AbstractBaseUser
from django.core import exceptions, validators
from django.db import models
from django.utils.text import slugify
@@ -94,6 +95,14 @@ class MailDomain(BaseModel):
"manage_accesses": is_owner_or_admin,
}
def is_identity_provider_ready(self) -> bool:
"""
Check if the identity provider is ready to manage the domain.
"""
return (
bool(self.organization) and self.status == MailDomainStatusChoices.ENABLED
)
class MailDomainAccess(BaseModel):
"""Allow to manage users' accesses to mail domains."""
@@ -188,7 +197,7 @@ class MailDomainAccess(BaseModel):
}
class Mailbox(BaseModel):
class Mailbox(AbstractBaseUser, BaseModel):
"""Mailboxes for users from mail domain."""
first_name = models.CharField(max_length=200, blank=False)
@@ -216,6 +225,13 @@ class Mailbox(BaseModel):
default=MailboxStatusChoices.PENDING,
)
# Store the denormalized email address to allow Django admin to work (USERNAME_FIELD)
# This field *must* not be used for authentication (or anything sensitive),
# use the `local_part` and `domain__name` fields
dn_email = models.EmailField(_("email"), blank=True, unique=True, editable=False)
USERNAME_FIELD = "dn_email"
class Meta:
db_table = "people_mail_box"
verbose_name = _("Mailbox")
@@ -241,9 +257,19 @@ class Mailbox(BaseModel):
Override save function to not allow to create or update mailbox of a disabled domain.
"""
self.full_clean()
self.dn_email = self.get_email()
if self.domain.status == MailDomainStatusChoices.DISABLED:
raise exceptions.ValidationError(
_("You can't create or update a mailbox for a disabled domain.")
)
return super().save(*args, **kwargs)
@property
def is_active(self):
"""Return True if the mailbox is enabled."""
return self.status == MailboxStatusChoices.ENABLED
def get_email(self):
"""Return the email address of the mailbox."""
return f"{self.local_part}@{self.domain.name}"

View File

@@ -8,6 +8,7 @@ from email.headerregistry import Address
from logging import getLogger
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.contrib.sites.models import Site
from django.core import exceptions, mail
from django.template.loader import render_to_string
@@ -341,6 +342,7 @@ class DimailAPIClient:
# secondary email is mandatory. Unfortunately, dimail doesn't
# store any. We temporarily give current email as secondary email.
status=enums.MailboxStatusChoices.ENABLED,
password=make_password(None), # unusable password
)
imported_mailboxes.append(str(mailbox))
else: