1
1
from rest_framework import serializers as ser
2
2
from rest_framework import exceptions
3
3
4
- from framework .auth .oauth_scopes import public_scopes
5
4
from osf .exceptions import ValidationError
6
- from osf .models import ApiOAuth2PersonalToken
5
+ from osf .models import ApiOAuth2PersonalToken , ApiOAuth2Scope
7
6
8
7
from api .base .exceptions import format_validation_error
9
- from api .base .serializers import JSONAPISerializer , LinksField , IDField , TypeField
8
+ from api .base .serializers import JSONAPISerializer , LinksField , IDField , TypeField , RelationshipField , StrictVersion
9
+ from api .scopes .serializers import SCOPES_RELATIONSHIP_VERSION
10
+
11
+
12
+ class TokenScopesRelationshipField (RelationshipField ):
13
+
14
+ def to_internal_value (self , data ):
15
+ return {'scopes' : data }
10
16
11
17
12
18
class ApiOAuth2PersonalTokenSerializer (JSONAPISerializer ):
13
19
"""Serialize data about a registered personal access token"""
14
20
21
+ def __init__ (self , * args , ** kwargs ):
22
+ super (ApiOAuth2PersonalTokenSerializer , self ).__init__ (* args , ** kwargs )
23
+
24
+ request = kwargs ['context' ]['request' ]
25
+
26
+ # Dynamically adding scopes field here, depending on the version
27
+ if expect_scopes_as_relationships (request ):
28
+ field = TokenScopesRelationshipField (
29
+ related_view = 'tokens:token-scopes-list' ,
30
+ related_view_kwargs = {'_id' : '<_id>' },
31
+ always_embed = True ,
32
+ read_only = False ,
33
+ )
34
+ self .fields ['scopes' ] = field
35
+ self .fields ['owner' ] = RelationshipField (
36
+ related_view = 'users:user-detail' ,
37
+ related_view_kwargs = {'user_id' : '<owner._id>' },
38
+ )
39
+ # Making scopes embeddable
40
+ self .context ['embed' ]['scopes' ] = self .context ['view' ]._get_embed_partial ('scopes' , field )
41
+ else :
42
+ self .fields ['scopes' ] = ser .SerializerMethodField ()
43
+ self .fields ['owner' ] = ser .SerializerMethodField ()
44
+
15
45
id = IDField (source = '_id' , read_only = True , help_text = 'The object ID for this token (automatically generated)' )
16
46
type = TypeField ()
17
47
@@ -20,17 +50,6 @@ class ApiOAuth2PersonalTokenSerializer(JSONAPISerializer):
20
50
required = True ,
21
51
)
22
52
23
- owner = ser .CharField (
24
- help_text = 'The user who owns this token' ,
25
- read_only = True , # Don't let user register a token in someone else's name
26
- source = 'owner._id' ,
27
- )
28
-
29
- scopes = ser .CharField (
30
- help_text = 'Governs permissions associated with this token' ,
31
- required = True ,
32
- )
33
-
34
53
token_id = ser .CharField (read_only = True , allow_blank = True )
35
54
36
55
class Meta :
@@ -40,6 +59,12 @@ class Meta:
40
59
'html' : 'absolute_url' ,
41
60
})
42
61
62
+ def get_owner (self , obj ):
63
+ return obj .owner ._id
64
+
65
+ def get_scopes (self , obj ):
66
+ return ' ' .join ([scope .name for scope in obj .scopes .all ()])
67
+
43
68
def absolute_url (self , obj ):
44
69
return obj .absolute_url
45
70
@@ -58,17 +83,21 @@ def to_representation(self, obj, envelope='data'):
58
83
return data
59
84
60
85
def create (self , validated_data ):
61
- validate_requested_scopes (validated_data )
86
+ scopes = validate_requested_scopes (validated_data .pop ('scopes' , None ))
87
+ if not scopes :
88
+ raise exceptions .ValidationError ('Cannot create a token without scopes.' )
62
89
instance = ApiOAuth2PersonalToken (** validated_data )
63
90
try :
64
91
instance .save ()
65
92
except ValidationError as e :
66
93
detail = format_validation_error (e )
67
94
raise exceptions .ValidationError (detail = detail )
95
+ for scope in scopes :
96
+ instance .scopes .add (scope )
68
97
return instance
69
98
70
99
def update (self , instance , validated_data ):
71
- validate_requested_scopes (validated_data )
100
+ scopes = validate_requested_scopes (validated_data . pop ( 'scopes' , None ) )
72
101
assert isinstance (instance , ApiOAuth2PersonalToken ), 'instance must be an ApiOAuth2PersonalToken'
73
102
74
103
instance .deactivate (save = False ) # This will cause CAS to revoke the existing token but still allow it to be used in the future, new scopes will be updated properly at that time.
@@ -79,15 +108,66 @@ def update(self, instance, validated_data):
79
108
continue
80
109
else :
81
110
setattr (instance , attr , value )
111
+ if scopes :
112
+ update_scopes (instance , scopes )
82
113
try :
83
114
instance .save ()
84
115
except ValidationError as e :
85
116
detail = format_validation_error (e )
86
117
raise exceptions .ValidationError (detail = detail )
87
118
return instance
88
119
89
- def validate_requested_scopes (validated_data ):
90
- scopes_set = set (validated_data .get ('scopes' , '' ).split (' ' ))
91
- for scope in scopes_set :
92
- if scope not in public_scopes or not public_scopes [scope ].is_public :
93
- raise exceptions .ValidationError ('User requested invalid scope' )
120
+
121
+ class ApiOAuth2PersonalTokenWritableSerializer (ApiOAuth2PersonalTokenSerializer ):
122
+ def __init__ (self , * args , ** kwargs ):
123
+ super (ApiOAuth2PersonalTokenWritableSerializer , self ).__init__ (* args , ** kwargs )
124
+ request = kwargs ['context' ]['request' ]
125
+
126
+ # Dynamically overriding scopes field for early versions to make scopes writable via an attribute
127
+ if not expect_scopes_as_relationships (request ):
128
+ self .fields ['scopes' ] = ser .CharField (write_only = True , required = False )
129
+
130
+ def to_representation (self , obj , envelope = 'data' ):
131
+ """
132
+ Overriding to_representation allows using different serializers for the request and response.
133
+
134
+ This will allow scopes to be a serializer method field if an early version, or a relationship field for a later version
135
+ """
136
+ context = self .context
137
+ return ApiOAuth2PersonalTokenSerializer (instance = obj , context = context ).data
138
+
139
+
140
+ def expect_scopes_as_relationships (request ):
141
+ """Whether serializer should expect scopes to be a relationship instead of an attribute
142
+
143
+ Scopes were previously an attribute on the serializer to mirror that they were a CharField on the model.
144
+ Now that scopes are an m2m field with tokens, later versions of the serializer represent scopes as relationships.
145
+ """
146
+ return StrictVersion (getattr (request , 'version' , '2.0' )) >= StrictVersion (SCOPES_RELATIONSHIP_VERSION )
147
+
148
+ def update_scopes (token , scopes ):
149
+ to_remove = token .scopes .difference (scopes )
150
+ to_add = scopes .difference (token .scopes .all ())
151
+ for scope in to_remove :
152
+ token .scopes .remove (scope )
153
+ for scope in to_add :
154
+ token .scopes .add (scope )
155
+ return
156
+
157
+ def validate_requested_scopes (data ):
158
+ if not data :
159
+ return []
160
+
161
+ if type (data ) != list :
162
+ data = data .split (' ' )
163
+ scopes = ApiOAuth2Scope .objects .filter (name__in = data )
164
+
165
+ if len (scopes ) != len (data ):
166
+ raise exceptions .NotFound ('Scope names must be one of: {}.' .format (
167
+ ', ' .join (ApiOAuth2Scope .objects .values_list ('name' , flat = True )),
168
+ ))
169
+
170
+ if scopes .filter (is_public = False ):
171
+ raise exceptions .ValidationError ('User requested invalid scope.' )
172
+
173
+ return scopes
0 commit comments