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

1from django.contrib.auth import authenticate 

2from rest_framework import serializers 

3from rest_framework.exceptions import AuthenticationFailed, PermissionDenied 

4 

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 

9 

10from .users import * 

11 

12__all__ = ( 

13 'TokenProvisionSerializer', 

14 'TokenSerializer', 

15) 

16 

17 

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 ) 

31 

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') 

40 

41 def get_fields(self): 

42 fields = super().get_fields() 

43 

44 # Make user field read-only if updating an existing Token. 

45 if self.instance is not None: 

46 fields['user'].read_only = True 

47 

48 return fields 

49 

50 def validate(self, data): 

51 

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.") 

59 

60 return super().validate(data) 

61 

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 

72 

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 

79 

80 

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 ) 

98 

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 ) 

105 

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") 

113 

114 # Inject the user into the validated data 

115 data['user'] = user 

116 

117 return data