Skip to content

Commit 87fe9d7

Browse files
committed
feat(controller): add "deis auth:passwd" to update password
1 parent 101301b commit 87fe9d7

6 files changed

Lines changed: 144 additions & 6 deletions

File tree

client/deis.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,10 +659,11 @@ def auth(self, args):
659659
Valid commands for auth:
660660
661661
auth:register register a new user
662-
auth:cancel remove the current account
663662
auth:login authenticate against a controller
664663
auth:logout clear the current user session
665-
auth:whoami display the authenticated user
664+
auth:passwd change the password for the current user
665+
auth:whoami display the current user
666+
auth:cancel remove the current user account
666667
667668
Use `deis help [command]` to learn more.
668669
"""
@@ -794,6 +795,40 @@ def auth_logout(self, args):
794795
self._settings.save()
795796
self._logger.info('Logged out')
796797

798+
def auth_passwd(self, args):
799+
"""
800+
Changes the password for the current user.
801+
802+
Usage: deis auth:passwd [options]
803+
804+
Options:
805+
--password=<password>
806+
provide the current password for the account.
807+
--new-password=<new-password>
808+
provide a new password for the account.
809+
"""
810+
if not self._settings.get('token'):
811+
raise EnvironmentError(
812+
'Could not find token. Use `deis login` or `deis register` to get started.')
813+
password = args.get('--password')
814+
if not password:
815+
password = getpass('current password: ')
816+
new_password = args.get('--new-password')
817+
if not new_password:
818+
new_password = getpass('new password: ')
819+
confirm = getpass('new password (confirm): ')
820+
if new_password != confirm:
821+
self._logger.error('Password mismatch, not changing.')
822+
sys.exit(1)
823+
payload = {'password': password, 'new_password': new_password}
824+
response = self._dispatch('post', "/v1/auth/passwd", json.dumps(payload))
825+
if response.status_code == requests.codes.ok:
826+
self._logger.info('Password change succeeded.')
827+
else:
828+
self._logger.info("Password change failed: {}".format(response.text))
829+
sys.exit(1)
830+
return True
831+
797832
def auth_whoami(self, args):
798833
"""
799834
Displays the currently logged in user.
@@ -1913,6 +1948,7 @@ def shortcuts(self, args):
19131948
('logout', 'auth:logout'),
19141949
('logs', 'apps:logs'),
19151950
('open', 'apps:open'),
1951+
('passwd', 'auth:passwd'),
19161952
('pull', 'builds:create'),
19171953
('register', 'auth:register'),
19181954
('rollback', 'releases:rollback'),

controller/api/tests/test_auth.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,49 @@ def test_cancel(self):
9999
response = self.client.delete(url,
100100
HTTP_AUTHORIZATION='token {}'.format(token))
101101
self.assertEqual(response.status_code, 204)
102+
103+
def test_passwd(self):
104+
"""Test that a registered user can change the password."""
105+
# test registration workflow
106+
username, password = 'newuser', 'password'
107+
first_name, last_name = 'Otto', 'Test'
108+
email = 'autotest@deis.io'
109+
submit = {
110+
'username': username,
111+
'password': password,
112+
'first_name': first_name,
113+
'last_name': last_name,
114+
'email': email,
115+
}
116+
url = '/v1/auth/register'
117+
response = self.client.post(url, json.dumps(submit), content_type='application/json')
118+
self.assertEqual(response.status_code, 201)
119+
# change password
120+
url = '/v1/auth/passwd'
121+
user = User.objects.get(username=username)
122+
token = Token.objects.get(user=user).key
123+
submit = {
124+
'password': 'password2',
125+
'new_password': password,
126+
}
127+
response = self.client.post(url, json.dumps(submit), content_type='application/json',
128+
HTTP_AUTHORIZATION='token {}'.format(token))
129+
self.assertEqual(response.status_code, 400)
130+
submit = {
131+
'password': password,
132+
'new_password': 'password2',
133+
}
134+
response = self.client.post(url, json.dumps(submit), content_type='application/json',
135+
HTTP_AUTHORIZATION='token {}'.format(token))
136+
self.assertEqual(response.status_code, 200)
137+
# test login with old password
138+
url = '/v1/auth/login/'
139+
payload = urllib.urlencode({'username': username, 'password': password})
140+
response = self.client.post(url, data=payload,
141+
content_type='application/x-www-form-urlencoded')
142+
self.assertEqual(response.status_code, 400)
143+
# test login with new password
144+
payload = urllib.urlencode({'username': username, 'password': 'password2'})
145+
response = self.client.post(url, data=payload,
146+
content_type='application/x-www-form-urlencoded')
147+
self.assertEqual(response.status_code, 200)

controller/api/urls.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,14 @@
169169
170170
Create a new User.
171171
172-
.. http:delete:: /v1/auth/register/
172+
.. http:delete:: /v1/auth/cancel/
173173
174174
Destroy the logged-in User.
175175
176+
.. http:post:: /v1/auth/passwd/
177+
178+
Update the password of the logged-in User.
179+
176180
.. http:get:: /v1/auth/login/
177181
178182
Generate an API key.
@@ -272,7 +276,9 @@
272276
url(r'^auth/register/?',
273277
views.UserRegistrationView.as_view({'post': 'create'})),
274278
url(r'^auth/cancel/?',
275-
views.UserCancellationView.as_view({'delete': 'destroy'})),
279+
views.UserManagementView.as_view({'delete': 'destroy'})),
280+
url(r'^auth/passwd/?',
281+
views.UserManagementView.as_view({'post': 'passwd'})),
276282
url(r'^auth/login/',
277283
'rest_framework.authtoken.views.obtain_auth_token'),
278284
# admin sharing

controller/api/views.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,20 @@ def pre_save(self, obj):
5858
obj.is_superuser = obj.is_staff = True
5959

6060

61-
class UserCancellationView(viewsets.GenericViewSet,
62-
viewsets.mixins.DestroyModelMixin):
61+
class UserManagementView(viewsets.GenericViewSet,
62+
viewsets.mixins.CreateModelMixin,
63+
viewsets.mixins.DestroyModelMixin):
6364
model = User
6465
permission_classes = (permissions.IsAuthenticated,)
6566

67+
def passwd(self, request, *args, **kwargs):
68+
obj = self.request.user
69+
if not obj.check_password(request.DATA['password']):
70+
return Response("Current password did not match", status=status.HTTP_400_BAD_REQUEST)
71+
obj.set_password(request.DATA['new_password'])
72+
obj.save()
73+
return Response({'status': 'password set'})
74+
6675
def destroy(self, request, *args, **kwargs):
6776
obj = self.request.user
6877
obj.delete()

tests/auth_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func TestAuth(t *testing.T) {
2020
authLogoutTest(t, params)
2121
authLoginTest(t, params)
2222
authWhoamiTest(t, params)
23+
authPasswdTest(t, params)
2324
authCancel(t, params)
2425
}
2526

@@ -44,6 +45,15 @@ func authLogoutTest(t *testing.T, params *utils.DeisTestConfig) {
4445
utils.Execute(t, authLogoutCmd, params, false, "")
4546
}
4647

48+
func authPasswdTest(t *testing.T, params *utils.DeisTestConfig) {
49+
password := "aNewPassword"
50+
utils.AuthPasswd(t, params, password)
51+
cmd := authLoginCmd
52+
utils.Execute(t, cmd, params, true, "400 BAD REQUEST")
53+
params.Password = password
54+
utils.Execute(t, cmd, params, false, "")
55+
}
56+
4757
func authRegisterTest(t *testing.T, params *utils.DeisTestConfig) {
4858
cmd := authRegisterCmd
4959
utils.Execute(t, cmd, params, false, "")

tests/utils/itutils.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,38 @@ func AuthCancel(t *testing.T, params *DeisTestConfig) {
149149
t.Fatalf("command executiuon failed\n%v", err)
150150
}
151151
child.Close()
152+
}
152153

154+
// AuthPasswd tests whether `deis auth:passwd` updates a user's password.
155+
func AuthPasswd(t *testing.T, params *DeisTestConfig, password string) {
156+
fmt.Println("deis auth:passwd")
157+
child, err := gexpect.Spawn(Deis + " auth:passwd")
158+
if err != nil {
159+
t.Fatalf("command not started\n%v", err)
160+
}
161+
fmt.Println("current password:")
162+
err = child.Expect("current password: ")
163+
if err != nil {
164+
t.Fatalf("expect password failed\n%v", err)
165+
}
166+
child.SendLine(params.Password)
167+
fmt.Println("new password:")
168+
err = child.Expect("new password: ")
169+
if err != nil {
170+
t.Fatalf("expect password failed\n%v", err)
171+
}
172+
child.SendLine(password)
173+
fmt.Println("new password (confirm):")
174+
err = child.Expect("new password (confirm): ")
175+
if err != nil {
176+
t.Fatalf("expect password failed\n%v", err)
177+
}
178+
child.SendLine(password)
179+
err = child.Expect("Password change succeeded")
180+
if err != nil {
181+
t.Fatalf("command executiuon failed\n%v", err)
182+
}
183+
child.Close()
153184
}
154185

155186
// CheckList executes a command and optionally tests whether its output does

0 commit comments

Comments
 (0)