(core) create AccountService model

Create new model to allow access of some API
endpoints with API Key authentification.
Scopes will allow to define permission access on those
endpoints.
This commit is contained in:
Sabrina Demagny
2025-03-31 14:14:28 +02:00
parent b4de7fda92
commit f60bfc2676
7 changed files with 144 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to
### Added
- ✨(core) create AccountService model #771
- 🧱(helm) disable createsuperuser job by setting #863
- 🔒️(passwords) add validators for production #850
- ✨(domains) allow to re-run check on domain if status is failed

View File

@@ -1,5 +1,6 @@
"""Admin classes and registrations for People's core app."""
from django.conf import settings
from django.contrib import admin, messages
from django.contrib.auth import admin as auth_admin
from django.utils.translation import gettext_lazy as _
@@ -284,3 +285,20 @@ class ServiceProviderAdmin(admin.ModelAdmin):
)
search_fields = ("name", "audience_id")
readonly_fields = ("created_at", "updated_at")
@admin.register(models.AccountService)
class AccountServiceAdmin(admin.ModelAdmin):
"""Admin interface for account services."""
list_display = ("name", "created_at", "updated_at")
readonly_fields = ("api_key", "created_at", "updated_at")
def get_form(self, request, obj=None, change=False, **kwargs):
"""Add help text to the scopes field to provide list of available scopes."""
form = super().get_form(request, obj, change, **kwargs)
form.base_fields[
"scopes"
].help_text = f"Scopes define what the service can access. \
Available scopes: {', '.join(settings.ACCOUNT_SERVICE_SCOPES)}"
return form

View File

@@ -277,3 +277,13 @@ class ServiceProviderFactory(factory.django.DjangoModelFactory):
if not create or not extracted:
return
self.organizations.set(extracted)
class AccountServiceFactory(factory.django.DjangoModelFactory):
"""A factory to create account services for testing purposes."""
class Meta:
model = models.AccountService
name = factory.Sequence(lambda n: f"Account Service {n!s}")
api_key = factory.Faker("uuid4")

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.1.8 on 2025-04-03 20:37
import core.models
import django.contrib.postgres.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_organization_is_active'),
]
operations = [
migrations.CreateModel(
name='AccountService',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')),
('name', models.CharField(max_length=255, verbose_name='name')),
('api_key', models.CharField(default='Y9-IThtCrKASOvRAKO2MUBd_XvZrnSTGNMMlv-Z1_o8', max_length=255, verbose_name='api key')),
('scopes', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255, validators=[core.models.validate_account_service_scope]), help_text='Allowed scopes for this service', size=None, verbose_name='allowed scopes')),
],
options={
'verbose_name': 'Account service',
'verbose_name_plural': 'Account services',
'db_table': 'people_account_service',
},
),
]

View File

@@ -5,6 +5,7 @@ Declare and configure the models for the People core application
import json
import os
import secrets
import smtplib
import uuid
from contextlib import suppress
@@ -1091,3 +1092,33 @@ class Invitation(BaseInvitation):
"patch": False,
"put": False,
}
def validate_account_service_scope(scope):
"""Validate the scope of the account service."""
if scope not in settings.ACCOUNT_SERVICE_SCOPES:
raise ValidationError(f"Invalid scope: {scope}")
class AccountService(BaseModel):
"""Account service model."""
name = models.CharField(_("name"), max_length=255)
api_key = models.CharField(
_("api key"),
max_length=255,
default=secrets.token_urlsafe(32),
)
scopes = ArrayField(
models.CharField(max_length=255, validators=[validate_account_service_scope]),
verbose_name=_("allowed scopes"),
help_text=_("Allowed scopes for this service"),
)
class Meta:
db_table = "people_account_service"
verbose_name = _("Account service")
verbose_name_plural = _("Account services")
def __str__(self):
return self.name

View File

@@ -0,0 +1,46 @@
"""
Unit tests for the Team model
"""
from django.core.exceptions import ValidationError
from django.test import override_settings
import pytest
from core import factories, models
pytestmark = pytest.mark.django_db
@override_settings(ACCOUNT_SERVICE_SCOPES=["la-suite-list-organizations-siret"])
def test_models_account_services_validate_scope():
"""Test that account service scopes are validated."""
# Valid scope from settings.ACCOUNT_SERVICE_SCOPES
account_service = factories.AccountServiceFactory(
scopes=["la-suite-list-organizations-siret"]
)
assert account_service.scopes == ["la-suite-list-organizations-siret"]
# Invalid scope
with pytest.raises(ValidationError, match="Invalid scope: invalid-scope"):
factories.AccountServiceFactory(scopes=["invalid-scope"])
# Multiple scopes with one invalid
with pytest.raises(ValidationError, match="Invalid scope: invalid-scope"):
factories.AccountServiceFactory(
scopes=["la-suite-list-organizations-siret", "invalid-scope"]
)
@override_settings(ACCOUNT_SERVICE_SCOPES=["la-suite-list-organizations-siret"])
def test_models_account_services_name_null():
"""The "name" field should not be null."""
with pytest.raises(ValidationError, match="This field cannot be null."):
models.AccountService.objects.create(name=None)
@override_settings(ACCOUNT_SERVICE_SCOPES=["la-suite-list-organizations-siret"])
def test_models_account_services_name_empty():
"""The "name" field should not be empty."""
with pytest.raises(ValidationError, match="This field cannot be blank."):
models.AccountService.objects.create(name="")

View File

@@ -526,6 +526,12 @@ class Base(Configuration):
environ_prefix=None,
)
ACCOUNT_SERVICE_SCOPES = values.ListValue(
default=[],
environ_name="ACCOUNT_SERVICE_SCOPES",
environ_prefix=None,
)
# MAILBOX-PROVISIONING API
WEBMAIL_URL = values.Value(
default=None,