|
@@ -310,6 +310,109 @@ class TokenTestCase(
|
|
|
response = self.client.post(url, data, format='json', **self.header)
|
|
response = self.client.post(url, data, format='json', **self.header)
|
|
|
self.assertEqual(response.status_code, 201)
|
|
self.assertEqual(response.status_code, 201)
|
|
|
|
|
|
|
|
|
|
+ def test_grant_token_constrained_permission_is_enforced(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Regression: SR-001 / VM-322 — constrained grant_token ObjectPermissions must not be
|
|
|
|
|
+ bypassed. has_perm('users.grant_token', obj=None) short-circuits to True without
|
|
|
|
|
+ evaluating constraints; the fix uses user_may_grant_token() which applies them.
|
|
|
|
|
+ """
|
|
|
|
|
+ # Clear the unconstrained grant_token added by setUp.
|
|
|
|
|
+ ObjectPermission.objects.filter(users=self.user, actions__contains=['grant']).delete()
|
|
|
|
|
+ self.add_permissions('users.add_token')
|
|
|
|
|
+
|
|
|
|
|
+ superuser = User.objects.create_user(username='sec_superuser', is_superuser=True)
|
|
|
|
|
+ regular = User.objects.create_user(username='sec_regular')
|
|
|
|
|
+
|
|
|
|
|
+ # Add a *constrained* grant_token permission: only tokens for non-superusers.
|
|
|
|
|
+ token_ct = ObjectType.objects.get_by_natural_key('users', 'token')
|
|
|
|
|
+ perm = ObjectPermission(
|
|
|
|
|
+ name='constrained_grant_token',
|
|
|
|
|
+ constraints={'user__is_superuser': False},
|
|
|
|
|
+ actions=['grant'],
|
|
|
|
|
+ )
|
|
|
|
|
+ perm.save()
|
|
|
|
|
+ perm.users.add(self.user)
|
|
|
|
|
+ perm.object_types.add(token_ct)
|
|
|
|
|
+
|
|
|
|
|
+ url = reverse('users-api:token-list')
|
|
|
|
|
+
|
|
|
|
|
+ # Attempt to create a token for the superuser — must be denied (constraint violation).
|
|
|
|
|
+ response = self.client.post(url, {'user': superuser.pk}, format='json', **self.header)
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ response.status_code, 403,
|
|
|
|
|
+ "Constrained grant_token must deny token creation for users that violate the constraint",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Attempt to create a token for the regular user — must succeed.
|
|
|
|
|
+ response = self.client.post(url, {'user': regular.pk}, format='json', **self.header)
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ response.status_code, 201,
|
|
|
|
|
+ "Constrained grant_token must allow token creation for users that satisfy the constraint",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def test_grant_token_superuser_always_allowed(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Superusers must be able to create tokens for any user without an explicit
|
|
|
|
|
+ grant_token ObjectPermission. Regression guard: _user_may_grant_token() must
|
|
|
|
|
+ mirror ObjectPermissionMixin.has_perm's superuser bypass.
|
|
|
|
|
+ """
|
|
|
|
|
+ ObjectPermission.objects.filter(users=self.user, actions__contains=['grant']).delete()
|
|
|
|
|
+ self.add_permissions('users.add_token')
|
|
|
|
|
+ self.user.is_superuser = True
|
|
|
|
|
+ self.user.save()
|
|
|
|
|
+ try:
|
|
|
|
|
+ other = User.objects.create_user(username='superuser_grant_target')
|
|
|
|
|
+ url = reverse('users-api:token-list')
|
|
|
|
|
+ response = self.client.post(url, {'user': other.pk}, format='json', **self.header)
|
|
|
|
|
+ self.assertEqual(response.status_code, 201, "Superuser must be able to grant tokens for any user")
|
|
|
|
|
+ finally:
|
|
|
|
|
+ self.user.is_superuser = False
|
|
|
|
|
+ self.user.save()
|
|
|
|
|
+
|
|
|
|
|
+ def test_grant_token_self_only_constraint(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ A {"user": "$user"} constraint means "only grant tokens for yourself".
|
|
|
|
|
+ Since creating a token for oneself bypasses the grant_token check entirely,
|
|
|
|
|
+ this constraint effectively blocks all cross-user grants for the holder.
|
|
|
|
|
+ Exercises the $user placeholder substitution path.
|
|
|
|
|
+ """
|
|
|
|
|
+ ObjectPermission.objects.filter(users=self.user, actions__contains=['grant']).delete()
|
|
|
|
|
+ self.add_permissions('users.add_token')
|
|
|
|
|
+ token_ct = ObjectType.objects.get_by_natural_key('users', 'token')
|
|
|
|
|
+ perm = ObjectPermission(name='self_only_grant', constraints={'user': '$user'}, actions=['grant'])
|
|
|
|
|
+ perm.save()
|
|
|
|
|
+ perm.users.add(self.user)
|
|
|
|
|
+ perm.object_types.add(token_ct)
|
|
|
|
|
+
|
|
|
|
|
+ other = User.objects.create_user(username='self_only_target')
|
|
|
|
|
+ url = reverse('users-api:token-list')
|
|
|
|
|
+ response = self.client.post(url, {'user': other.pk}, format='json', **self.header)
|
|
|
|
|
+ self.assertEqual(response.status_code, 403, "Self-only constraint must deny cross-user token grants")
|
|
|
|
|
+
|
|
|
|
|
+ def test_grant_token_non_user_field_constraint_fails_closed(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ A constraint referencing a non-user Token field (e.g. {"write_enabled": True})
|
|
|
|
|
+ cannot be evaluated for an unsaved token; _user_may_grant_token() must fail
|
|
|
|
|
+ closed and deny rather than bypass the constraint.
|
|
|
|
|
+ """
|
|
|
|
|
+ ObjectPermission.objects.filter(users=self.user, actions__contains=['grant']).delete()
|
|
|
|
|
+ self.add_permissions('users.add_token')
|
|
|
|
|
+ token_ct = ObjectType.objects.get_by_natural_key('users', 'token')
|
|
|
|
|
+ perm = ObjectPermission(
|
|
|
|
|
+ name='non_user_field_grant', constraints={'write_enabled': True}, actions=['grant']
|
|
|
|
|
+ )
|
|
|
|
|
+ perm.save()
|
|
|
|
|
+ perm.users.add(self.user)
|
|
|
|
|
+ perm.object_types.add(token_ct)
|
|
|
|
|
+
|
|
|
|
|
+ other = User.objects.create_user(username='non_user_field_target')
|
|
|
|
|
+ url = reverse('users-api:token-list')
|
|
|
|
|
+ response = self.client.post(url, {'user': other.pk}, format='json', **self.header)
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ response.status_code, 403,
|
|
|
|
|
+ "Non-user Token field constraints must fail closed for new (unsaved) tokens",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
def test_create_token_returns_plaintext(self):
|
|
def test_create_token_returns_plaintext(self):
|
|
|
"""
|
|
"""
|
|
|
Creating a Token via the REST API must return the usable plaintext value in the response.
|
|
Creating a Token via the REST API must return the usable plaintext value in the response.
|