5
5
import json
6
6
7
7
import ddt
8
- from django .contrib .auth .models import Permission
8
+ from django .contrib .auth .models import Group , Permission
9
9
from django .core .urlresolvers import reverse
10
10
from rest_framework .test import APITestCase , APIRequestFactory
11
11
from testfixtures import LogCapture
12
12
13
13
from credentials .apps .api .serializers import UserCredentialSerializer
14
14
from credentials .apps .api .tests import factories
15
+ from credentials .apps .core .constants import Role
15
16
from credentials .apps .credentials .models import UserCredential
16
17
17
18
@@ -40,6 +41,11 @@ def setUp(self):
40
41
self .username = "test_user"
41
42
self .request = APIRequestFactory ().get ('/' )
42
43
44
+ def _add_permission (self , perm ):
45
+ """ DRY helper to add usercredential model permissions to self.user """
46
+ # pylint: disable=no-member
47
+ self .user .user_permissions .add (Permission .objects .get (codename = '{}_usercredential' .format (perm )))
48
+
43
49
def _attempt_update_user_credential (self , data ):
44
50
""" Helper method that attempts to patch an existing credential object.
45
51
@@ -50,13 +56,13 @@ def _attempt_update_user_credential(self, data):
50
56
Response: HTTP response from the API.
51
57
"""
52
58
# pylint: disable=no-member
53
- self .user . user_permissions . add ( Permission . objects . get ( codename = "change_usercredential" ) )
59
+ self ._add_permission ( 'change' )
54
60
path = reverse ("api:v1:usercredential-detail" , args = [self .user_credential .id ])
55
61
return self .client .patch (path = path , data = json .dumps (data ), content_type = JSON_CONTENT_TYPE )
56
62
57
63
def test_get (self ):
58
64
""" Verify a single user credential is returned. """
59
-
65
+ self . _add_permission ( 'view' )
60
66
path = reverse ("api:v1:usercredential-detail" , args = [self .user_credential .id ])
61
67
response = self .client .get (path )
62
68
self .assertEqual (response .status_code , 200 )
@@ -115,7 +121,7 @@ def _attempt_create_user_credentials(self, data):
115
121
Response: HTTP response from the API.
116
122
"""
117
123
# pylint: disable=no-member
118
- self .user . user_permissions . add ( Permission . objects . get ( codename = "add_usercredential" ) )
124
+ self ._add_permission ( 'add' )
119
125
path = self .list_path
120
126
return self .client .post (path = path , data = json .dumps (data ), content_type = JSON_CONTENT_TYPE )
121
127
@@ -268,6 +274,7 @@ def test_create_with_empty_attributes(self):
268
274
269
275
def test_list_with_username_filter (self ):
270
276
""" Verify the list endpoint supports filter data by username."""
277
+ self ._add_permission ('view' )
271
278
factories .UserCredentialFactory (username = "dummy-user" )
272
279
response = self .client .get (self .list_path , data = {'username' : self .user_credential .username })
273
280
self .assertEqual (response .status_code , 200 )
@@ -281,6 +288,7 @@ def test_list_with_username_filter(self):
281
288
282
289
def test_list_with_status_filter (self ):
283
290
""" Verify the list endpoint supports filtering by status."""
291
+ self ._add_permission ('view' )
284
292
factories .UserCredentialFactory .create_batch (2 , status = "revoked" , username = self .user_credential .username )
285
293
response = self .client .get (self .list_path , data = {'status' : self .user_credential .status })
286
294
self .assertEqual (response .status_code , 400 )
@@ -441,6 +449,122 @@ def test_users_lists_access_by_authenticated_users(self):
441
449
self .assertEqual (response .status_code , 401 )
442
450
443
451
452
+ @ddt .ddt
453
+ class UserCredentialViewSetPermissionsTests (APITestCase ):
454
+ """
455
+ Thoroughly exercise the custom view- and object-level permissions for this viewset.
456
+ """
457
+
458
+ def make_user (self , group = None , perm = None , ** kwargs ):
459
+ """ DRY helper to create users with specific groups and/or permissions. """
460
+ # pylint: disable=no-member
461
+ user = factories .UserFactory (** kwargs )
462
+ if group :
463
+ user .groups .add (Group .objects .get (name = group ))
464
+ if perm :
465
+ user .user_permissions .add (Permission .objects .get (codename = '{}_usercredential' .format (perm )))
466
+ return user
467
+
468
+ @ddt .data (
469
+ ({'group' : Role .ADMINS }, 200 ),
470
+ ({'perm' : 'view' }, 200 ),
471
+ ({'perm' : 'add' }, 404 ),
472
+ ({'perm' : 'change' }, 404 ),
473
+ ({'username' : 'test-user' }, 200 ),
474
+ ({'username' : 'TeSt-uSeR' }, 200 ),
475
+ ({'username' : 'other' }, 404 ),
476
+ )
477
+ @ddt .unpack
478
+ def test_list (self , user_kwargs , expected_status ):
479
+ """
480
+ The list method (GET) requires either 'view' permission, or for the
481
+ 'username' query parameter to match that of the requesting user.
482
+ """
483
+ list_path = reverse ("api:v1:usercredential-list" )
484
+
485
+ self .client .force_authenticate (self .make_user (** user_kwargs )) # pylint: disable=no-member
486
+ response = self .client .get (list_path , {'username' : 'test-user' })
487
+ self .assertEqual (response .status_code , expected_status )
488
+
489
+ @ddt .data (
490
+ ({'group' : Role .ADMINS }, 201 ),
491
+ ({'perm' : 'add' }, 201 ),
492
+ ({'perm' : 'view' }, 403 ),
493
+ ({'perm' : 'change' }, 403 ),
494
+ ({}, 403 ),
495
+ ({'username' : 'test-user' }, 403 ),
496
+ )
497
+ @ddt .unpack
498
+ def test_create (self , user_kwargs , expected_status ):
499
+ """
500
+ The creation (POST) method requires the 'add' permission.
501
+ """
502
+ list_path = reverse ('api:v1:usercredential-list' )
503
+ program_certificate = factories .ProgramCertificateFactory ()
504
+ post_data = {
505
+ 'username' : 'test-user' ,
506
+ 'credential' : {
507
+ 'program_id' : program_certificate .program_id
508
+ },
509
+ 'attributes' : [],
510
+ }
511
+
512
+ self .client .force_authenticate (self .make_user (** user_kwargs )) # pylint: disable=no-member
513
+ response = self .client .post (list_path , data = json .dumps (post_data ), content_type = JSON_CONTENT_TYPE )
514
+ self .assertEqual (response .status_code , expected_status )
515
+
516
+ @ddt .data (
517
+ ({'group' : Role .ADMINS }, 200 ),
518
+ ({'perm' : 'view' }, 200 ),
519
+ ({'perm' : 'add' }, 404 ),
520
+ ({'perm' : 'change' }, 404 ),
521
+ ({'username' : 'test-user' }, 200 ),
522
+ ({'username' : 'TeSt-uSeR' }, 200 ),
523
+ ({'username' : 'other-user' }, 404 ),
524
+ )
525
+ @ddt .unpack
526
+ def test_retrieve (self , user_kwargs , expected_status ):
527
+ """
528
+ The retrieve (GET) method requires the 'view' permission, or for the
529
+ requested object to be associated with the username of the requesting
530
+ user.
531
+ """
532
+ program_cert = factories .ProgramCertificateFactory ()
533
+ user_credential = factories .UserCredentialFactory .create (credential = program_cert , username = 'test-user' )
534
+ detail_path = reverse ("api:v1:usercredential-detail" , args = [user_credential .id ])
535
+
536
+ self .client .force_authenticate (self .make_user (** user_kwargs )) # pylint: disable=no-member
537
+ response = self .client .get (detail_path )
538
+ self .assertEqual (response .status_code , expected_status )
539
+
540
+ @ddt .data (
541
+ ({'group' : Role .ADMINS }, 200 ),
542
+ ({'perm' : 'view' }, 403 ),
543
+ ({'perm' : 'add' }, 403 ),
544
+ ({'perm' : 'change' }, 200 ),
545
+ ({'username' : 'test-user' }, 403 ),
546
+ ({}, 403 ),
547
+ )
548
+ @ddt .unpack
549
+ def test_partial_update (self , user_kwargs , expected_status ):
550
+ """
551
+ The partial update (PATCH) method requires the 'change' permission.
552
+ """
553
+ program_cert = factories .ProgramCertificateFactory ()
554
+ user_credential = factories .UserCredentialFactory .create (credential = program_cert , username = 'test-user' )
555
+ detail_path = reverse ("api:v1:usercredential-detail" , args = [user_credential .id ])
556
+ post_data = {
557
+ 'username' : 'test-user' ,
558
+ 'credential' : {
559
+ 'program_id' : program_cert .program_id
560
+ },
561
+ 'attributes' : [{'name' : 'dummy-attr-name' , 'value' : 'dummy-attr-value' }],
562
+ }
563
+ self .client .force_authenticate (self .make_user (** user_kwargs )) # pylint: disable=no-member
564
+ response = self .client .patch (path = detail_path , data = json .dumps (post_data ), content_type = JSON_CONTENT_TYPE )
565
+ self .assertEqual (response .status_code , expected_status )
566
+
567
+
444
568
class CredentialViewSetTests (APITestCase ):
445
569
""" Base Class for ProgramCredentialViewSetTests and CourseCredentialViewSetTests. """
446
570
@@ -450,10 +574,21 @@ class CredentialViewSetTests(APITestCase):
450
574
def setUp (self ):
451
575
super (CredentialViewSetTests , self ).setUp ()
452
576
577
+ # pylint: disable=no-member
453
578
self .user = factories .UserFactory ()
579
+ self .user .groups .add (Group .objects .get (name = Role .ADMINS ))
454
580
self .client .force_authenticate (self .user ) # pylint: disable=no-member
455
581
self .request = APIRequestFactory ().get ('/' )
456
582
583
+ def assert_permission_required (self , data ):
584
+ """
585
+ Ensure access to these APIs is restricted to those with explicit model
586
+ permissions.
587
+ """
588
+ self .client .force_authenticate (user = factories .UserFactory ()) # pylint: disable=no-member
589
+ response = self .client .get (self .list_path , data )
590
+ self .assertEqual (response .status_code , 403 )
591
+
457
592
def assert_list_without_id_filter (self , path , expected ):
458
593
"""Helper method used for making request and assertions. """
459
594
response = self .client .get (path )
@@ -508,6 +643,10 @@ def test_list_with_status_filter(self):
508
643
factories .UserCredentialFactory .create_batch (2 , status = "revoked" , username = self .user_credential .username )
509
644
self .assert_list_with_status_filter (data = {'program_id' : self .program_id , 'status' : UserCredential .AWARDED }, )
510
645
646
+ def test_permission_required (self ):
647
+ """ Verify that requests require explicit model permissions. """
648
+ self .assert_permission_required ({'program_id' : self .program_id , 'status' : UserCredential .AWARDED })
649
+
511
650
512
651
class CourseCredentialViewSetTests (CredentialViewSetTests ):
513
652
""" Tests for CourseCredentialViewSetTests. """
@@ -555,3 +694,7 @@ def test_list_with_certificate_type(self):
555
694
json .loads (response .content ),
556
695
{'count' : 1 , 'next' : None , 'previous' : None , 'results' : [expected ]}
557
696
)
697
+
698
+ def test_permission_required (self ):
699
+ """ Verify that requests require explicit model permissions. """
700
+ self .assert_permission_required ({'course_id' : self .course_id , 'status' : UserCredential .AWARDED })
0 commit comments