Coverage for users/api/serializers_/tokens.py: 75%
59 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django.contrib.auth import authenticate
2from rest_framework import serializers
3from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
5from netbox.api.fields import IPNetworkSerializer
6from netbox.api.serializers import ValidatedModelSerializer
7from users.models import Token
8from users.utils import user_may_grant_token
10from .users import *
12__all__ = (
13 'TokenProvisionSerializer',
14 'TokenSerializer',
15)
18class TokenSerializer(ValidatedModelSerializer):
19 token = serializers.CharField(
20 read_only=True,
21 )
22 user = UserSerializer(
23 nested=True
24 )
25 allowed_ips = serializers.ListField(
26 child=IPNetworkSerializer(),
27 required=False,
28 allow_empty=True,
29 default=[]
30 )
32 class Meta:
33 model = Token
34 fields = (
35 'id', 'url', 'display_url', 'display', 'version', 'key', 'user', 'description', 'created', 'expires',
36 'last_used', 'enabled', 'write_enabled', 'pepper_id', 'allowed_ips', 'token',
37 )
38 read_only_fields = ('key',)
39 brief_fields = ('id', 'url', 'display', 'version', 'key', 'enabled', 'write_enabled', 'description')
41 def get_fields(self):
42 fields = super().get_fields()
44 # Make user field read-only if updating an existing Token.
45 if self.instance is not None:
46 fields['user'].read_only = True
48 return fields
50 def validate(self, data):
52 # If the Token is being created on behalf of another user, enforce the grant_token permission.
53 # Use user_may_grant_token() rather than has_perm(obj=None): the latter short-circuits to True
54 # when the permission is present in cache without evaluating ObjectPermission constraints.
55 request = self.context.get('request')
56 token_user = data.get('user')
57 if token_user and token_user != request.user and not user_may_grant_token(request.user, token_user): 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true
58 raise PermissionDenied("This user does not have permission to create tokens for other users.")
60 return super().validate(data)
62 def create(self, validated_data):
63 instance = super().create(validated_data)
64 # The plaintext token is only available in memory after save(); v2 tokens persist only an
65 # HMAC digest, so it can't be recovered later. Stash it on the request so to_representation()
66 # can return it even after the viewset re-fetches the instance from the database.
67 if request := self.context.get('request'):
68 if not hasattr(request, '_token_plaintexts'):
69 request._token_plaintexts = {}
70 request._token_plaintexts[instance.pk] = instance.token
71 return instance
73 def to_representation(self, instance):
74 data = super().to_representation(instance)
75 if not data.get('token') and (request := self.context.get('request')): 75 ↛ 78line 75 didn't jump to line 78 because the condition on line 75 was always true
76 if plaintext := getattr(request, '_token_plaintexts', {}).get(instance.pk): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 data['token'] = plaintext
78 return data
81class TokenProvisionSerializer(TokenSerializer):
82 user = UserSerializer(
83 nested=True,
84 read_only=True
85 )
86 username = serializers.CharField(
87 write_only=True
88 )
89 password = serializers.CharField(
90 write_only=True
91 )
92 last_used = serializers.DateTimeField(
93 read_only=True
94 )
95 key = serializers.CharField(
96 read_only=True
97 )
99 class Meta:
100 model = Token
101 fields = (
102 'id', 'url', 'display_url', 'display', 'version', 'user', 'key', 'created', 'expires', 'last_used', 'key',
103 'enabled', 'write_enabled', 'description', 'allowed_ips', 'username', 'password', 'token',
104 )
106 def validate(self, data):
107 # Validate the username and password
108 username = data.pop('username')
109 password = data.pop('password')
110 user = authenticate(request=self.context.get('request'), username=username, password=password)
111 if user is None: 111 ↛ 115line 111 didn't jump to line 115 because the condition on line 111 was always true
112 raise AuthenticationFailed("Invalid username/password")
114 # Inject the user into the validated data
115 data['user'] = user
117 return data