Repository: netbox-community/netbox
Pull request: Fixes: #16973 - Resolve $user token to User.id for use in permissions based on custom fields
### Fixes: #16973 - Resolve $user token to User.id for use in permissions based on custom fields

Adds special handling for `CONSTRAINT_TOKEN_USER` ("$user" token) when used in custom fields in object-based permissions.


Problem statement:
Object of type User is not JSON serializable
### Deployment Type

Self-hosted

### NetBox Version

v3.7.6

### Python Version

3.12

### Steps to Reproduce

I created a custom_field for device roles content type named "created_by" .
 It accepts Users > User as object type. 
After I have created a permission with {"custom_field_data__created_by": "$user"} as constraint, I get 'Object of type User is not JSON serializable' when I try to either change or delete a device role created by myself. Putting my user id instead of "$user" works as expected.


### Expected Behavior

Constraint with {"custom_field_data__created_by": "$user"} should recognize the User token

### Observed Behavior

The user token "$user" is not recognized and I get 'Object of type User is not JSON serializable'

Hints from issue discussion:
User token as defined in NetBox documentation returns an error : https://netboxlabs.com/docs/netbox/en/stable/administration/permissions/
Our constraint scheme is based on Sites and Tenancy but it is incomplete as some Object Types are independent. User Token would help fill the gap but I confirm the behaviour described in the documentation is broken.

Commit message: Cleaner type check

Patch to review:
diff --git a/netbox/utilities/permissions.py b/netbox/utilities/permissions.py
index 893cc619e0d..321d065cf92 100644
--- a/netbox/utilities/permissions.py
+++ b/netbox/utilities/permissions.py
@@ -1,7 +1,10 @@
 from django.conf import settings
+from django.apps import apps
 from django.db.models import Q
 from django.utils.translation import gettext_lazy as _
 
+from users.constants import CONSTRAINT_TOKEN_USER
+
 __all__ = (
     'get_permission_for_model',
     'permission_is_exempt',
@@ -93,6 +96,9 @@ def qs_filter_from_constraints(constraints, tokens=None):
     def _replace_tokens(value, tokens):
         if type(value) is list:
             return list(map(lambda v: tokens.get(v, v), value))
+        User = apps.get_model('users.User')
+        if value == CONSTRAINT_TOKEN_USER and isinstance(tokens.get(CONSTRAINT_TOKEN_USER), User):
+            return tokens[CONSTRAINT_TOKEN_USER].id
         return tokens.get(value, value)
 
     params = Q()


Code review comments:
diff_hunk: @@ -93,6 +96,9 @@ def qs_filter_from_constraints(constraints, tokens=None):
     def _replace_tokens(value, tokens):
         if type(value) is list:
             return list(map(lambda v: tokens.get(v, v), value))
+        User = apps.get_model('users.User')
+        if value == CONSTRAINT_TOKEN_USER and isinstance(tokens.get(CONSTRAINT_TOKEN_USER), User):
+            return tokens[CONSTRAINT_TOKEN_USER].id
path: netbox/utilities/permissions.py
text: @user1:
We need to catch the presence of the token inside a list of values as well (above). It might be simpler to just swap out the token value with an integer before we start iterating through the constraints.

@author:
Agreed, this is much nicer.