Repository: deis/deis

Issue:
Request: `deis auth:passwd` takes --user argument for admins to change user passwords
Changing your own password with `deis auth:passwd` is easy.
Changing the password for a user who has forgot their password is not.

http://stackoverflow.com/questions/1873806/changing-password-in-django

```
$ docker exec -ti deis-controller ./manage.py shell
WARNING Cannot synchronize with etcd cluster
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> u = User.objects.get(username__exact='theirusername')
>>> u.set_password('iforgotmypassword')
>>> u.save()
```

There really should be an easier way.



Discussion:
if we were to implement #3200 we could send out an email with a reset code.

:+1: I think this is a fairly easy and reasonable feature request.


Resolving patch:
diff --git a/client/deis.py b/client/deis.py
--- a/client/deis.py
+++ b/client/deis.py
@@ -895,9 +895,11 @@ def auth_passwd(self, args):
 
         Options:
           --password=<password>
-            provide the current password for the account.
+            the current password for the account.
           --new-password=<new-password>
-            provide a new password for the account.
+            the new password for the account.
+          --username=<username>
+            the account's username.
         """
         if not self._settings.get('token'):
             raise EnvironmentError(
@@ -912,7 +914,11 @@ def auth_passwd(self, args):
             if new_password != confirm:
                 self._logger.error('Password mismatch, not changing.')
                 sys.exit(1)
-        payload = {'password': password, 'new_password': new_password}
+        payload = {
+            'password': password,
+            'new_password': new_password,
+            'username': args.get('--username', self._settings['username']),
+        }
         response = self._dispatch('post', "/v1/auth/passwd", json.dumps(payload))
         if response.status_code == requests.codes.ok:
             self._logger.info('Password change succeeded.')
diff --git a/controller/api/views.py b/controller/api/views.py
--- a/controller/api/views.py
+++ b/controller/api/views.py
@@ -37,6 +37,12 @@ def get_object(self):
 
     def passwd(self, request, **kwargs):
         obj = self.get_object()
+        if request.data.get('username'):
+            # if you "accidentally" target yourself, that should be fine
+            if obj.username == request.data['username'] or obj.is_superuser:
+                obj = get_object_or_404(User, username=request.data['username'])
+            else:
+                raise PermissionDenied()
         if not obj.check_password(request.data['password']):
             return Response({'detail': 'Current password does not match'},
                             status=status.HTTP_400_BAD_REQUEST)


Test patch:
diff --git a/controller/api/fixtures/test_auth.json b/controller/api/fixtures/test_auth.json
--- a/controller/api/fixtures/test_auth.json
+++ b/controller/api/fixtures/test_auth.json
@@ -16,5 +16,41 @@
         "email": "autotest@deis.io",
         "date_joined": "2013-05-10T16:08:09.357Z"
     }
+},
+{
+    "pk": 8,
+    "model": "auth.user",
+    "fields": {
+        "username": "autotest2",
+        "first_name": "Otto",
+        "last_name": "Test",
+        "is_active": true,
+        "is_superuser": false,
+        "is_staff": false,
+        "last_login": "2013-05-10T16:08:09.357Z",
+        "groups": [],
+        "user_permissions": [],
+        "password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=",
+        "email": "autotest@deis.io",
+        "date_joined": "2013-05-10T16:08:09.357Z"
+    }
+},
+{
+    "pk": 9,
+    "model": "auth.user",
+    "fields": {
+        "username": "autotest3",
+        "first_name": "Otto",
+        "last_name": "Test",
+        "is_active": true,
+        "is_superuser": false,
+        "is_staff": false,
+        "last_login": "2013-05-10T16:08:09.357Z",
+        "groups": [],
+        "user_permissions": [],
+        "password": "pbkdf2_sha256$10000$5Uoq7dl61vnN$gQhDpc2q2Rkn16VdPC+pNNEQcKpy+LGe29Zkad+2/m4=",
+        "email": "autotest@deis.io",
+        "date_joined": "2013-05-10T16:08:09.357Z"
+    }
 }
 ]
diff --git a/controller/api/tests/test_auth.py b/controller/api/tests/test_auth.py
--- a/controller/api/tests/test_auth.py
+++ b/controller/api/tests/test_auth.py
@@ -21,6 +21,14 @@ class AuthTest(TestCase):
 
     """Tests user registration, authentication and authorization"""
 
+    def setUp(self):
+        self.admin = User.objects.get(username='autotest')
+        self.admin_token = Token.objects.get(user=self.admin).key
+        self.user1 = User.objects.get(username='autotest2')
+        self.user1_token = Token.objects.get(user=self.user1).key
+        self.user2 = User.objects.get(username='autotest3')
+        self.user2_token = Token.objects.get(user=self.user2).key
+
     def test_auth(self):
         """
         Test that a user can register using the API, login and logout
@@ -98,10 +106,6 @@ def test_auth_registration_admin_only_fails_if_not_admin(self):
     @override_settings(REGISTRATION_MODE="admin_only")
     def test_auth_registration_admin_only_works(self):
         """test that a superuser can register when registration is admin only."""
-
-        user = User.objects.get(username='autotest')
-        token = Token.objects.get(user=user)
-
         url = '/v1/auth/register'
 
         username, password = 'newuser_by_admin', 'password'
@@ -119,7 +123,7 @@ def test_auth_registration_admin_only_works(self):
             'is_staff': True,
         }
         response = self.client.post(url, json.dumps(submit), content_type='application/json',
-                                    HTTP_AUTHORIZATION='token {}'.format(token))
+                                    HTTP_AUTHORIZATION='token {}'.format(self.admin_token))
 
         self.assertEqual(response.status_code, 201)
         for key in response.data:
@@ -236,3 +240,41 @@ def test_passwd(self):
         response = self.client.post(url, data=payload,
                                     content_type='application/x-www-form-urlencoded')
         self.assertEqual(response.status_code, 200)
+
+    def test_change_user_passwd(self):
+        """
+        Test that an administrator can change a user's password, while a regular user cannot.
+        """
+        # change password
+        url = '/v1/auth/passwd'
+        old_password = self.user1.password
+        new_password = 'password'
+        submit = {
+            'username': self.user1.username,
+            'password': old_password,
+            'new_password': new_password,
+        }
+        response = self.client.post(url, json.dumps(submit), content_type='application/json',
+                                    HTTP_AUTHORIZATION='token {}'.format(self.admin_token))
+        self.assertEqual(response.status_code, 400)
+        # test login with old password
+        url = '/v1/auth/login/'
+        payload = urllib.urlencode({'username': self.user1.username, 'password': old_password})
+        response = self.client.post(url, data=payload,
+                                    content_type='application/x-www-form-urlencoded')
+        self.assertEqual(response.status_code, 400)
+        # test login with new password
+        payload = urllib.urlencode({'username': self.user1.username, 'password': new_password})
+        response = self.client.post(url, data=payload,
+                                    content_type='application/x-www-form-urlencoded')
+        self.assertEqual(response.status_code, 200)
+        # try to change back password with a regular user
+        submit['password'], submit['new_password'] = submit['new_password'], submit['password']
+        url = '/v1/auth/passwd'
+        response = self.client.post(url, json.dumps(submit), content_type='application/json',
+                                    HTTP_AUTHORIZATION='token {}'.format(self.user2_token))
+        self.assertEqual(response.status_code, 403)
+        # however, targeting yourself should be fine.
+        response = self.client.post(url, json.dumps(submit), content_type='application/json',
+                                    HTTP_AUTHORIZATION='token {}'.format(self.user1_token))
+        self.assertEqual(response.status_code, 200)