Project

General

Profile

Revision cbf34809

IDcbf3480950e31ff598ba7ae8eb4447006399688e
Parent 7c52294b
Child 89ea21c4

Added by John Spray about 10 years ago

calamari_rest: User account PATCH/PUT (password change)

Users can change their own password but not those of
other users. Refactored the user view code to
avoid a separate view for /user/me, so that all
ops can refer to a PK or 'me'.

Fixes: #7097

View differences:

rest-api/calamari_rest/serializers/v1.py
43 43
    """
44 44
    class Meta:
45 45
        model = User
46
        fields = ('id', 'username', 'password')
46
        fields = ('id', 'username', 'password', 'email')
47 47

  
48 48
    def to_native(self, obj):
49 49
        # Before conversion, remove the password field. This prevents the hash
rest-api/calamari_rest/urls/v1.py
4 4

  
5 5
router = routers.DefaultRouter(trailing_slash=False)
6 6

  
7
# In v1, the /user list URL existed but only told you usernames
8
# In v2 it should include displaynames and email addresses too (TODO #7097)
7
# In v1, the user/ URL existed but did almost nothing, it only supported GET and told
8
# you your current username.
9
# In v2, the view gets filled out to return the displayname and email address to, and support
10
# PUT/PATCH operations for users to change their passwords
9 11
router.register(r'user', calamari_rest.views.v1.UserViewSet)
10 12

  
11 13
# The cluster view exists in both v1 and v2
......
17 19
urlpatterns = patterns(
18 20
    '',
19 21

  
20
    # In v1, the user/me URL existed but did almost nothing, it only supported GET and told
21
    # you your current username.
22
    # In v2, the view gets filled out to return the displayname and email address to, and support
23
    # PUT/PATCH operations for users to change their passwords (TODO #7097)
24
    url(r'^user/me$', calamari_rest.views.v1.UserMe.as_view()),
25 22
    # In v1 this required a POST but also allowed GET for some reason
26 23
    # In v2 it's post only
27 24
    url(r'^auth/login', calamari_rest.views.v1.login),
rest-api/calamari_rest/urls/v2.py
20 20
    url(r'^info', calamari_rest.views.v1.Info.as_view()),
21 21

  
22 22
    # Wrapping django auth
23
    url(r'^user/me', calamari_rest.views.v1.UserMe.as_view()),
24 23
    url(r'^auth/login', calamari_rest.views.v1.login),
25 24
    url(r'^auth/logout', calamari_rest.views.v1.logout),
26 25

  
rest-api/calamari_rest/views/v1.py
1 1

  
2 2
import logging
3
from django.core.exceptions import ValidationError, PermissionDenied
3 4
from django.core.urlresolvers import reverse
5
from django.http import Http404
4 6
import pytz
5 7
import socket
6 8

  
7 9
from rest_framework import viewsets
10
from rest_framework.exceptions import AuthenticationFailed
8 11
from rest_framework.response import Response
9 12
from rest_framework.decorators import api_view
10 13
from rest_framework.decorators import permission_classes
......
178 181

  
179 182
class UserViewSet(viewsets.ModelViewSet):
180 183
    """
181
    The Calamari UI/API user account information
182
    """
183
    queryset = User.objects.all()
184
    serializer_class = UserSerializer
184
    The Calamari UI/API user account information.
185 185

  
186
    You may pass 'me' as the user ID to refer to the currently logged in user,
187
    otherwise the user ID is a numeric ID.
186 188

  
187
class UserMe(APIView):
189
    Because all users are superusers, everybody can see each others accounts
190
    using this resource.  However, users can only modify their own account (i.e.
191
    the user being modified must be the user associated with the current login session).
188 192
    """
189
Return information about the current user. If the user is not authenticated
190
(i.e. an anonymous user), then 401 is returned with an error message.
191
    """
192
    permission_classes = (AllowAny,)
193
    queryset = User.objects.all()
193 194
    serializer_class = UserSerializer
194 195

  
195
    @never_cache
196
    def get(self, request):
197
        if request.user.is_authenticated():
198
            return Response(self.serializer_class(request.user).data)
199
        return Response({
200
            'message': 'Session expired or invalid',
201
        }, status.HTTP_401_UNAUTHORIZED)
196
    def _get_user(self, request, user_id):
197
        if user_id == "me":
198
            if request.user.is_authenticated():
199
                return request.user
200
            else:
201
                raise AuthenticationFailed()
202
        else:
203
            try:
204
                user = self.queryset.get(pk=user_id)
205
            except User.DoesNotExist:
206
                raise Http404("User not found")
207
            else:
208
                return user
209

  
210
    def update(self, request, *args, **kwargs):
211
        # Note that unlike the parent update() we do not support
212
        # creating users with PUT.
213
        partial = kwargs.pop('partial', False)
214
        user = self.get_object()
215

  
216
        if user.id != self.request.user.id:
217
            raise PermissionDenied("May not change another user's password")
218

  
219
        serializer = self.get_serializer(user, data=request.DATA, partial=partial)
220

  
221
        if serializer.is_valid():
222
            try:
223
                self.pre_save(serializer.object)
224
            except ValidationError as err:
225
                # full_clean on model instance may be called in pre_save, so we
226
                # have to handle eventual errors.
227
                return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
228
            user = serializer.save(force_update=True)
229
            log.debug("saved user %s" % user)
230
            # self.post_save(user, created=False)
231
            return Response(serializer.data, status=status.HTTP_200_OK)
232
        else:
233
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
234

  
235
    def get_object(self, queryset=None):
236
        user = self._get_user(self.request, self.kwargs['pk'])
237
        if self.kwargs['pk'] == 'me':
238
            self.kwargs['pk'] = user.id
239
        return user
202 240

  
203 241

  
204 242
@api_view(['GET', 'POST'])
rest-api/tests/rest_api_unit_test.py
1

  
2
from django.contrib.auth.models import User
3
from rest_framework.test import APIClient
4
from django.test import TestCase
5
import mock
6

  
7
import calamari_rest.views.rpc_view
8

  
9

  
10
class RestApiUnitTest(TestCase):
11
    login = True  # Should setUp log in for us?
12
    USERNAME = 'admin'
13
    PASSWORD = 'admin'
14

  
15
    def setUp(self):
16
        # Patch in a mock for RPCs
17
        rpc = mock.Mock()
18
        self.rpc = rpc
19
        old_init = calamari_rest.views.rpc_view.RPCViewSet.__init__
20

  
21
        def init(_self, *args, **kwargs):
22
            old_init(_self, *args, **kwargs)
23
            _self.client = rpc
24

  
25
        self._old_init = old_init
26
        calamari_rest.views.rpc_view.RPCViewSet.__init__ = init
27

  
28
        # Create a user to log in as
29
        User.objects.create_superuser(self.USERNAME, 'admin@admin.com', self.PASSWORD)
30

  
31
        # A client for performing requests to the API
32
        self.client = APIClient()
33
        if self.login:
34
            self.client.login(username='admin', password='admin')
35

  
36
    def tearDown(self):
37
        calamari_rest.views.rpc_view.RPCViewSet.__init__ = self._old_init
38

  
39
    def assertStatus(self, response, status_code):
40
        self.assertEqual(response.status_code, status_code, "Bad status %s, wanted %s (%s)" % (
41
            response.status_code, status_code, response.data
42
        ))
rest-api/tests/test_auth.py
1
from django.contrib.auth.models import User
2
from rest_framework import status
3
from tests.rest_api_unit_test import RestApiUnitTest
4

  
5

  
6
class TestAuthentication(RestApiUnitTest):
7
    login = False
8

  
9
    # A URI that should require login
10
    SOMETHING_PRIVILEGED = "/api/v2/grains"
11

  
12
    def test_login(self):
13
        # I should get a 403 accessing something privileged
14
        response = self.client.get(self.SOMETHING_PRIVILEGED)
15
        self.assertStatus(response, status.HTTP_403_FORBIDDEN)
16

  
17
        # I should get a 403 trying to read /user/me
18
        response = self.client.get("/api/v2/user/me")
19
        self.assertStatus(response, status.HTTP_403_FORBIDDEN)
20

  
21
        # Now log in with valid credentials
22
        response = self.client.post("/api/v2/auth/login", {
23
            'username': self.USERNAME,
24
            'password': self.PASSWORD
25
        }, format="json")
26
        self.assertStatus(response, status.HTTP_200_OK)
27

  
28
        # I should be able to access something privileged
29
        response = self.client.get(self.SOMETHING_PRIVILEGED)
30
        self.assertStatus(response, status.HTTP_200_OK)
31

  
32
        # Now log out
33
        response = self.client.post("/api/v2/auth/logout")
34
        self.assertStatus(response, status.HTTP_200_OK)
35

  
36
        # I should have lost the ability to access something privileged
37
        response = self.client.get(self.SOMETHING_PRIVILEGED)
38
        self.assertStatus(response, status.HTTP_403_FORBIDDEN)
39

  
40

  
41
class TestUserChanges(RestApiUnitTest):
42
    login = True
43

  
44
    def test_change_my_password(self):
45
        """
46
        That users can change their own password
47
        """
48
        new_password = 'bob'
49
        self.assertNotEqual(new_password, self.PASSWORD)
50

  
51
        # Do a read-write for PUT
52
        response = self.client.get("/api/v2/user/me")
53
        self.assertStatus(response, status.HTTP_200_OK)
54
        user = response.data
55
        user['password'] = new_password
56
        response = self.client.put("/api/v2/user/me", user)
57
        self.assertStatus(response, status.HTTP_200_OK)
58

  
59
        # Now log out
60
        response = self.client.post("/api/v2/auth/logout")
61
        self.assertStatus(response, status.HTTP_200_OK)
62

  
63
        # Try logging in with old password, should fail
64
        response = self.client.post("/api/v2/auth/login", {
65
            'username': self.USERNAME,
66
            'password': self.PASSWORD
67
        }, format="json")
68
        self.assertStatus(response, status.HTTP_401_UNAUTHORIZED)
69

  
70
        # Try logging in with new password, should succeed
71
        response = self.client.post("/api/v2/auth/login", {
72
            'username': self.USERNAME,
73
            'password': new_password
74
        }, format="json")
75
        self.assertStatus(response, status.HTTP_200_OK)
76

  
77
    def test_change_anothers_password(self):
78
        """
79
        That users cannot change one anothers' password.
80
        """
81
        stranger = User.objects.create_superuser("stranger", "stranger@admin.com", "stranger_pwd")
82
        me = User.objects.get(username=self.USERNAME)
83
        # I can't change his
84
        response = self.client.patch("/api/v2/user/%s" % stranger.id, {'password': 'new'})
85
        self.assertStatus(response, status.HTTP_403_FORBIDDEN)
86

  
87
        # But I can change my own
88
        response = self.client.patch("/api/v2/user/%s" % me.id, {'password': 'new'})
89
        self.assertStatus(response, status.HTTP_200_OK)
rest-api/tests/test_keys.py
1 1

  
2
from django.contrib.auth.models import User
3 2
from rest_framework import status
4
from rest_framework.test import APIClient
5
from django.test import TestCase
6 3
import mock
7 4

  
8
import calamari_rest.views.rpc_view
9

  
10

  
11
class RestApiUnitTest(TestCase):
12
    def setUp(self):
13
        # Patch in a mock for RPCs
14
        rpc = mock.Mock()
15
        self.rpc = rpc
16
        old_init = calamari_rest.views.rpc_view.RPCViewSet.__init__
17

  
18
        def init(_self, *args, **kwargs):
19
            old_init(_self, *args, **kwargs)
20
            _self.client = rpc
21

  
22
        self._old_init = old_init
23
        calamari_rest.views.rpc_view.RPCViewSet.__init__ = init
24

  
25
        # Create a user to log in as
26
        User.objects.create_superuser('admin', 'admin@admin.com', 'admin')
27

  
28
        # A client for performing requests to the API
29
        self.client = APIClient()
30
        self.client.login(username='admin', password='admin')
31

  
32
    def tearDown(self):
33
        calamari_rest.views.rpc_view.RPCViewSet.__init__ = self._old_init
5
from tests.rest_api_unit_test import RestApiUnitTest
34 6

  
35 7

  
36 8
class TestKey(RestApiUnitTest):

Also available in: Unified diff