2024-01-03 10:09:31 +01:00
|
|
|
"""Client serializers for the People core app."""
|
2024-03-07 10:57:05 +01:00
|
|
|
|
2024-01-03 10:09:31 +01:00
|
|
|
from rest_framework import exceptions, serializers
|
|
|
|
|
from timezone_field.rest_framework import TimeZoneSerializerField
|
|
|
|
|
|
|
|
|
|
from core import models
|
2024-11-04 11:32:41 +01:00
|
|
|
from core.models import ServiceProvider
|
2024-01-03 10:09:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ContactSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize contacts."""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.Contact
|
|
|
|
|
fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"base",
|
|
|
|
|
"data",
|
|
|
|
|
"full_name",
|
2024-11-28 10:55:44 +01:00
|
|
|
"notes",
|
2024-01-03 10:09:31 +01:00
|
|
|
"owner",
|
|
|
|
|
"short_name",
|
|
|
|
|
]
|
|
|
|
|
read_only_fields = ["id", "owner"]
|
2024-11-28 10:55:44 +01:00
|
|
|
extra_kwargs = {
|
|
|
|
|
"base": {"required": False},
|
|
|
|
|
}
|
2024-01-03 10:09:31 +01:00
|
|
|
|
|
|
|
|
def update(self, instance, validated_data):
|
|
|
|
|
"""Make "base" field readonly but only for update/patch."""
|
|
|
|
|
validated_data.pop("base", None)
|
|
|
|
|
return super().update(instance, validated_data)
|
|
|
|
|
|
|
|
|
|
|
2024-02-26 18:03:31 +01:00
|
|
|
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""
|
|
|
|
|
A ModelSerializer that takes an additional `fields` argument that
|
|
|
|
|
controls which fields should be displayed.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
2024-10-25 14:59:02 +02:00
|
|
|
"""Pass arguments to superclass except 'fields', then drop fields not listed therein."""
|
|
|
|
|
|
2024-02-26 18:03:31 +01:00
|
|
|
# Don't pass the 'fields' arg up to the superclass
|
|
|
|
|
fields = kwargs.pop("fields", None)
|
|
|
|
|
|
|
|
|
|
# Instantiate the superclass normally
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
if fields is not None:
|
|
|
|
|
# Drop any fields that are not specified in the `fields` argument.
|
|
|
|
|
allowed = set(fields)
|
|
|
|
|
existing = set(self.fields)
|
|
|
|
|
for field_name in existing - allowed:
|
|
|
|
|
self.fields.pop(field_name)
|
|
|
|
|
|
|
|
|
|
|
2024-11-21 22:26:04 +01:00
|
|
|
class OrganizationSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize organizations."""
|
|
|
|
|
|
|
|
|
|
abilities = serializers.SerializerMethodField()
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.Organization
|
|
|
|
|
fields = ["id", "name", "registration_id_list", "domain_list", "abilities"]
|
|
|
|
|
read_only_fields = ["id", "registration_id_list", "domain_list"]
|
|
|
|
|
|
|
|
|
|
def get_abilities(self, organization) -> dict:
|
|
|
|
|
"""Return abilities of the logged-in user on the instance."""
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
if request:
|
|
|
|
|
return organization.get_abilities(request.user)
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
2024-11-22 14:18:50 +01:00
|
|
|
class UserOrganizationSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize organizations for users."""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.Organization
|
|
|
|
|
fields = ["id", "name"]
|
|
|
|
|
read_only_fields = ["id", "name"]
|
|
|
|
|
|
|
|
|
|
|
2024-02-26 18:03:31 +01:00
|
|
|
class UserSerializer(DynamicFieldsModelSerializer):
|
2024-01-03 10:09:31 +01:00
|
|
|
"""Serialize users."""
|
|
|
|
|
|
|
|
|
|
timezone = TimeZoneSerializerField(use_pytz=False, required=True)
|
2024-03-25 23:56:32 +01:00
|
|
|
email = serializers.ReadOnlyField()
|
|
|
|
|
name = serializers.ReadOnlyField()
|
2024-11-22 14:18:50 +01:00
|
|
|
organization = UserOrganizationSerializer(read_only=True)
|
2024-01-03 10:09:31 +01:00
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.User
|
|
|
|
|
fields = [
|
|
|
|
|
"id",
|
✨(api) search users by email (#16)
* ✨(api) search users by email
The front end should be able to search users by email.
To that goal, we added a list method to the users viewset
thus creating the /users/ endpoint.
Results are filtered based on similarity with the query,
based on what preexisted for the /contacts/ endpoint.
* ✅(api) test list users by email
Test search when complete, partial query,
accentuated and capital.
Also, lower similarity threshold for user search by email
as it was too high for some tests to pass.
* 💡(api) improve documentation and test comments
Improve user viewset documentation
and comments describing tests sections
Co-authored-by: aleb_the_flash <45729124+lebaudantoine@users.noreply.github.com>
Co-authored-by: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr>
* 🛂(api) set isAuthenticated as base requirements
Instead of checking permissions or adding decorators
to every viewset, isAuthenticated is set as base requirement.
* 🛂(api) define throttle limits in settings
Use of Djando Rest Framework's throttle options, now set globally
to avoid duplicate code.
* 🩹(api) add email to user serializer
email field added to serializer. Tests modified accordingly.
I added the email field as "read only" to pass tests, but we need to discuss
that point in review.
* 🧱(api) move search logic to queryset
User viewset "list" method was overridden to allow search by email.
This removed the pagination. Instead of manually re-adding pagination at
the end of this method, I moved the search/filter logic to get_queryset,
to leave DRF handle pagination.
* ✅(api) test throttle protection
Test that throttle protection succesfully blocks too many requests.
* 📝(tests) improve tests comment
Fix typos on comments and clarify which setting are tested on test_throttle test
(setting import required disabling pylint false positive error)
Co-authored-by: aleb_the_flash <45729124+lebaudantoine@users.noreply.github.com>
---------
Co-authored-by: aleb_the_flash <45729124+lebaudantoine@users.noreply.github.com>
Co-authored-by: Anthony LC <anthony.le-courric@mail.numerique.gouv.fr>
2024-01-29 10:14:17 +01:00
|
|
|
"email",
|
2024-01-03 10:09:31 +01:00
|
|
|
"language",
|
2024-03-25 23:56:32 +01:00
|
|
|
"name",
|
2024-11-22 14:18:50 +01:00
|
|
|
"organization",
|
2024-01-03 10:09:31 +01:00
|
|
|
"timezone",
|
|
|
|
|
"is_device",
|
|
|
|
|
"is_staff",
|
|
|
|
|
]
|
2024-02-26 18:03:31 +01:00
|
|
|
read_only_fields = ["id", "name", "email", "is_device", "is_staff"]
|
|
|
|
|
|
2024-01-03 10:09:31 +01:00
|
|
|
|
2024-11-06 17:22:01 +01:00
|
|
|
class UserMeSerializer(UserSerializer):
|
|
|
|
|
"""
|
|
|
|
|
Serialize the current user.
|
|
|
|
|
|
|
|
|
|
Same as the `UserSerializer` but with abilities.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
abilities = serializers.SerializerMethodField()
|
2024-11-22 14:18:50 +01:00
|
|
|
organization = UserOrganizationSerializer(read_only=True)
|
2024-11-06 17:22:01 +01:00
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.User
|
|
|
|
|
fields = [
|
|
|
|
|
"email",
|
|
|
|
|
"id",
|
|
|
|
|
"is_device",
|
|
|
|
|
"is_staff",
|
|
|
|
|
"language",
|
|
|
|
|
"name",
|
2024-11-22 14:18:50 +01:00
|
|
|
"organization",
|
2024-11-06 17:22:01 +01:00
|
|
|
"timezone",
|
|
|
|
|
# added fields
|
|
|
|
|
"abilities",
|
|
|
|
|
]
|
|
|
|
|
read_only_fields = ["id", "name", "email", "is_device", "is_staff"]
|
|
|
|
|
|
|
|
|
|
def get_abilities(self, user: models.User) -> dict:
|
|
|
|
|
"""Return abilities of the logged-in user on the instance."""
|
|
|
|
|
if user != self.context["request"].user: # Should not happen
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
"UserMeSerializer.get_abilities: user is not the same as the request user",
|
|
|
|
|
)
|
|
|
|
|
return user.get_abilities()
|
|
|
|
|
|
|
|
|
|
|
2024-01-03 10:09:31 +01:00
|
|
|
class TeamAccessSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize team accesses."""
|
|
|
|
|
|
|
|
|
|
abilities = serializers.SerializerMethodField(read_only=True)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.TeamAccess
|
|
|
|
|
fields = ["id", "user", "role", "abilities"]
|
|
|
|
|
read_only_fields = ["id", "abilities"]
|
|
|
|
|
|
|
|
|
|
def update(self, instance, validated_data):
|
|
|
|
|
"""Make "user" field is readonly but only on update."""
|
|
|
|
|
validated_data.pop("user", None)
|
|
|
|
|
return super().update(instance, validated_data)
|
|
|
|
|
|
|
|
|
|
def get_abilities(self, access) -> dict:
|
|
|
|
|
"""Return abilities of the logged-in user on the instance."""
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
if request:
|
|
|
|
|
return access.get_abilities(request.user)
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
def validate(self, attrs):
|
|
|
|
|
"""
|
|
|
|
|
Check access rights specific to writing (create/update)
|
|
|
|
|
"""
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
user = getattr(request, "user", None)
|
|
|
|
|
role = attrs.get("role")
|
|
|
|
|
|
|
|
|
|
# Update
|
|
|
|
|
if self.instance:
|
|
|
|
|
can_set_role_to = self.instance.get_abilities(user)["set_role_to"]
|
|
|
|
|
|
|
|
|
|
if role and role not in can_set_role_to:
|
|
|
|
|
message = (
|
|
|
|
|
f"You are only allowed to set role to {', '.join(can_set_role_to)}"
|
|
|
|
|
if can_set_role_to
|
|
|
|
|
else "You are not allowed to set this role for this team."
|
|
|
|
|
)
|
|
|
|
|
raise exceptions.PermissionDenied(message)
|
|
|
|
|
|
|
|
|
|
# Create
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
team_id = self.context["team_id"]
|
|
|
|
|
except KeyError as exc:
|
|
|
|
|
raise exceptions.ValidationError(
|
|
|
|
|
"You must set a team ID in kwargs to create a new team access."
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
if not models.TeamAccess.objects.filter(
|
|
|
|
|
team=team_id,
|
|
|
|
|
user=user,
|
|
|
|
|
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
|
|
|
|
|
).exists():
|
|
|
|
|
raise exceptions.PermissionDenied(
|
|
|
|
|
"You are not allowed to manage accesses for this team."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
role == models.RoleChoices.OWNER
|
|
|
|
|
and not models.TeamAccess.objects.filter(
|
|
|
|
|
team=team_id,
|
|
|
|
|
user=user,
|
|
|
|
|
role=models.RoleChoices.OWNER,
|
|
|
|
|
).exists()
|
|
|
|
|
):
|
|
|
|
|
raise exceptions.PermissionDenied(
|
|
|
|
|
"Only owners of a team can assign other users as owners."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
attrs["team_id"] = self.context["team_id"]
|
|
|
|
|
return attrs
|
|
|
|
|
|
|
|
|
|
|
2024-02-26 18:03:31 +01:00
|
|
|
class TeamAccessReadOnlySerializer(TeamAccessSerializer):
|
|
|
|
|
"""Serialize team accesses for list and retrieve actions."""
|
|
|
|
|
|
|
|
|
|
user = UserSerializer(read_only=True, fields=["id", "name", "email"])
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.TeamAccess
|
|
|
|
|
fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"user",
|
|
|
|
|
"role",
|
|
|
|
|
"abilities",
|
|
|
|
|
]
|
|
|
|
|
read_only_fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"user",
|
|
|
|
|
"role",
|
|
|
|
|
"abilities",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2024-01-03 10:09:31 +01:00
|
|
|
class TeamSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize teams."""
|
|
|
|
|
|
|
|
|
|
abilities = serializers.SerializerMethodField(read_only=True)
|
2024-11-04 11:32:41 +01:00
|
|
|
service_providers = serializers.PrimaryKeyRelatedField(
|
|
|
|
|
queryset=ServiceProvider.objects.all(), many=True, required=False
|
|
|
|
|
)
|
2024-01-03 10:09:31 +01:00
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.Team
|
2024-02-12 11:46:15 +01:00
|
|
|
fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"name",
|
|
|
|
|
"accesses",
|
|
|
|
|
"abilities",
|
|
|
|
|
"created_at",
|
|
|
|
|
"updated_at",
|
2024-11-04 11:32:41 +01:00
|
|
|
"service_providers",
|
2024-02-12 11:46:15 +01:00
|
|
|
]
|
|
|
|
|
read_only_fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"accesses",
|
|
|
|
|
"abilities",
|
|
|
|
|
"created_at",
|
|
|
|
|
"updated_at",
|
|
|
|
|
]
|
2024-01-03 10:09:31 +01:00
|
|
|
|
2024-10-17 15:30:00 +02:00
|
|
|
def create(self, validated_data):
|
|
|
|
|
"""Create a new team with organization enforcement."""
|
2024-11-04 11:32:41 +01:00
|
|
|
# When called as a resource server, we enforce the team service provider
|
|
|
|
|
if sp_audience := self.context.get("from_service_provider_audience", None):
|
|
|
|
|
service_provider, _created = models.ServiceProvider.objects.get_or_create(
|
|
|
|
|
audience_id=sp_audience
|
|
|
|
|
)
|
|
|
|
|
validated_data["service_providers"] = [service_provider]
|
|
|
|
|
|
2024-10-17 15:30:00 +02:00
|
|
|
# Note: this is not the purpose of this API to check the user has an organization
|
|
|
|
|
return super().create(
|
|
|
|
|
validated_data=validated_data
|
|
|
|
|
| {"organization_id": self.context["request"].user.organization_id}
|
|
|
|
|
)
|
|
|
|
|
|
2024-01-03 10:09:31 +01:00
|
|
|
def get_abilities(self, team) -> dict:
|
|
|
|
|
"""Return abilities of the logged-in user on the instance."""
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
if request:
|
|
|
|
|
return team.get_abilities(request.user)
|
|
|
|
|
return {}
|
2024-02-01 18:15:25 +01:00
|
|
|
|
2024-02-07 18:03:12 +01:00
|
|
|
|
|
|
|
|
class InvitationSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize invitations."""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.Invitation
|
2024-02-12 19:07:11 +01:00
|
|
|
fields = ["id", "created_at", "email", "team", "role", "issuer", "is_expired"]
|
|
|
|
|
read_only_fields = ["id", "created_at", "team", "issuer", "is_expired"]
|
|
|
|
|
|
|
|
|
|
def validate(self, attrs):
|
|
|
|
|
"""Validate and restrict invitation to new user based on email."""
|
|
|
|
|
|
|
|
|
|
request = self.context.get("request")
|
|
|
|
|
user = getattr(request, "user", None)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
team_id = self.context["team_id"]
|
|
|
|
|
except KeyError as exc:
|
|
|
|
|
raise exceptions.ValidationError(
|
|
|
|
|
"You must set a team ID in kwargs to create a new team invitation."
|
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
if not models.TeamAccess.objects.filter(
|
|
|
|
|
team=team_id,
|
|
|
|
|
user=user,
|
|
|
|
|
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
|
|
|
|
|
).exists():
|
|
|
|
|
raise exceptions.PermissionDenied(
|
|
|
|
|
"You are not allowed to manage invitation for this team."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
attrs["team_id"] = team_id
|
|
|
|
|
attrs["issuer"] = user
|
|
|
|
|
return attrs
|
2024-11-15 15:25:45 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ServiceProviderSerializer(serializers.ModelSerializer):
|
|
|
|
|
"""Serialize service providers."""
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = models.ServiceProvider
|
|
|
|
|
fields = ["id", "audience_id", "name"]
|
|
|
|
|
read_only_fields = ["id", "audience_id"]
|