diff --git a/arkindex/users/migrations/0017_right_unique_constraints.py b/arkindex/users/migrations/0017_right_unique_constraints.py
new file mode 100644
index 0000000000000000000000000000000000000000..57b3276b592e77722e798c4cbe462c5740425d1e
--- /dev/null
+++ b/arkindex/users/migrations/0017_right_unique_constraints.py
@@ -0,0 +1,68 @@
+# Generated by Django 4.0.1 on 2022-04-08 13:50
+
+from django.db import migrations, models
+
+FORWARD_SQL = [
+    """
+    WITH duplicate_rights AS (
+        SELECT id, ROW_NUMBER() OVER (
+            PARTITION BY group_id, content_type_id, content_id
+            ORDER BY level DESC
+        ) AS number
+        FROM users_right
+        WHERE user_id IS NULL
+    )
+    DELETE FROM users_right
+    USING duplicate_rights
+    WHERE duplicate_rights.id = users_right.id
+    AND duplicate_rights.number > 1
+    """,
+    """
+    WITH duplicate_rights AS (
+        SELECT id, ROW_NUMBER() OVER (
+            PARTITION BY user_id, content_type_id, content_id
+            ORDER BY level DESC
+        ) AS number
+        FROM users_right
+        WHERE group_id IS NULL
+    )
+    DELETE FROM users_right
+    USING duplicate_rights
+    WHERE duplicate_rights.id = users_right.id
+    AND duplicate_rights.number > 1
+    """
+]
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0016_alter_user_email_user_email_unique'),
+    ]
+
+    operations = [
+        migrations.RemoveConstraint(
+            model_name='right',
+            name='right_unique_target',
+        ),
+        migrations.RunSQL(
+            sql=FORWARD_SQL,
+            reverse_sql=migrations.RunSQL.noop,
+        ),
+        migrations.AddConstraint(
+            model_name='right',
+            constraint=models.UniqueConstraint(
+                condition=models.Q(user__isnull=False),
+                fields=('user', 'content_id', 'content_type'),
+                name='right_user_unique_target',
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name='right',
+            constraint=models.UniqueConstraint(
+                condition=models.Q(group__isnull=False),
+                fields=('group', 'content_id', 'content_type'),
+                name='right_group_unique_target',
+            ),
+        ),
+    ]
diff --git a/arkindex/users/models.py b/arkindex/users/models.py
index 704a972de6ff3b68147aeef01127e3a35bb1859a..5fcd2f1beda12eaf35e03149b385d01546059119 100644
--- a/arkindex/users/models.py
+++ b/arkindex/users/models.py
@@ -44,15 +44,27 @@ class Right(models.Model):
 
     class Meta:
         constraints = [
-            models.UniqueConstraint(fields=['user', 'group', 'content_id', 'content_type'], name='right_unique_target'),
             # User XOR Group is the owner of this access right
             models.CheckConstraint(
                 name='user_xor_group',
                 check=(
                     models.Q(group_id__isnull=False, user_id__isnull=True)
                     | models.Q(group_id__isnull=True, user_id__isnull=False)
-                )
-            )
+                ),
+            ),
+            # Ensure that the rights owner only has one Right instance for the same target.
+            # Since either user or group can be NULL, and a NULL in the fields of a unique constraint causes the
+            # constraint to still allow duplicates, this constraint is split in two partial constraints.
+            models.UniqueConstraint(
+                fields=['user', 'content_id', 'content_type'],
+                name='right_user_unique_target',
+                condition=models.Q(user__isnull=False),
+            ),
+            models.UniqueConstraint(
+                fields=['group', 'content_id', 'content_type'],
+                name='right_group_unique_target',
+                condition=models.Q(group__isnull=False),
+            ),
         ]
 
 
diff --git a/arkindex/users/tests/test_generic_memberships.py b/arkindex/users/tests/test_generic_memberships.py
index 0b02b581185612629fd37517184463d981215c04..510e8ddaf9b8f99dcea9a8c6e5fee727f4693dc0 100644
--- a/arkindex/users/tests/test_generic_memberships.py
+++ b/arkindex/users/tests/test_generic_memberships.py
@@ -1,5 +1,6 @@
 import uuid
 
+from django.db import IntegrityError
 from django.urls import reverse
 from rest_framework import status
 
@@ -1063,3 +1064,15 @@ class TestMembership(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
         with self.assertRaises(Right.DoesNotExist):
             new_member.refresh_from_db()
+
+    def test_right_unique_user(self):
+        repo = Repository.objects.get(hook_token='worker-hook-token')
+        repo.memberships.create(user=self.user, level=Role.Admin.value)
+        with self.assertRaises(IntegrityError):
+            repo.memberships.create(user=self.user, level=Role.Admin.value)
+
+    def test_right_unique_group(self):
+        repo = Repository.objects.get(hook_token='worker-hook-token')
+        repo.memberships.create(group=self.group, level=Role.Admin.value)
+        with self.assertRaises(IntegrityError):
+            repo.memberships.create(group=self.group, level=Role.Admin.value)