✨(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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
32
src/backend/core/migrations/0014_accountservice.py
Normal file
32
src/backend/core/migrations/0014_accountservice.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
46
src/backend/core/tests/test_models_account_services.py
Normal file
46
src/backend/core/tests/test_models_account_services.py
Normal 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="")
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user