Skip to content

Commit 8d0637c

Browse files
author
Matthew Fisher
committed
Merge pull request #4147 from Joshua-Anderson/admin-user-cancelation
feat(controller): allow admins to delete users
2 parents e73447a + f923e49 commit 8d0637c

9 files changed

Lines changed: 146 additions & 27 deletions

File tree

client-go/cmd/auth.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,12 @@ func Cancel(username string, password string, yes bool) error {
199199
return err
200200
}
201201

202-
fmt.Println("Please log in again in order to cancel this account")
202+
if username == "" || password != "" {
203+
fmt.Println("Please log in again in order to cancel this account")
203204

204-
if err = Login(c.ControllerURL.String(), username, password, c.SSLVerify); err != nil {
205-
return err
205+
if err = Login(c.ControllerURL.String(), username, password, c.SSLVerify); err != nil {
206+
return err
207+
}
206208
}
207209

208210
if yes == false {
@@ -214,7 +216,13 @@ func Cancel(username string, password string, yes bool) error {
214216
return err
215217
}
216218

217-
fmt.Printf("cancel account %s at %s? (y/N): ", c.Username, c.ControllerURL.String())
219+
deletedUser := username
220+
221+
if deletedUser == "" {
222+
deletedUser = c.Username
223+
}
224+
225+
fmt.Printf("cancel account %s at %s? (y/N): ", deletedUser, c.ControllerURL.String())
218226
fmt.Scanln(&confirm)
219227

220228
if strings.ToLower(confirm) == "y" {
@@ -227,14 +235,17 @@ func Cancel(username string, password string, yes bool) error {
227235
return nil
228236
}
229237

230-
err = auth.Delete(c)
238+
err = auth.Delete(c, username)
231239

232240
if err != nil {
233241
return err
234242
}
235243

236-
if err := client.Delete(); err != nil {
237-
return err
244+
// If user targets themselves, logout.
245+
if username != "" || c.Username == username {
246+
if err := client.Delete(); err != nil {
247+
return err
248+
}
238249
}
239250

240251
fmt.Println("Account cancelled")

client-go/controller/api/auth.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ type AuthRegenerateRequest struct {
3131
All bool `json:"all,omitempty"`
3232
}
3333

34+
// AuthCancelRequest is the definition of POST /v1/auth/cancel/.
35+
type AuthCancelRequest struct {
36+
Username string `json:"username"`
37+
}
38+
3439
// AuthRegenerateResponse is the definition of /v1/auth/tokens/.
3540
type AuthRegenerateResponse tokenResponse
3641

client-go/controller/models/auth/auth.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,20 @@ func Login(c *client.Client, username, password string) (string, error) {
4444
}
4545

4646
// Delete deletes a user.
47-
func Delete(c *client.Client) error {
48-
_, err := c.BasicRequest("DELETE", "/v1/auth/cancel/", nil)
47+
func Delete(c *client.Client, username string) error {
48+
var body []byte
49+
var err error
50+
51+
if username != "" {
52+
req := api.AuthCancelRequest{Username: username}
53+
body, err = json.Marshal(req)
54+
55+
if err != nil {
56+
return err
57+
}
58+
}
59+
60+
_, err = c.BasicRequest("DELETE", "/v1/auth/cancel/", body)
4961
return err
5062
}
5163

client-go/controller/models/auth/auth_test.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ const loginExpected string = `{"username":"test","password":"opensesame"}`
1717
const passwdExpected string = `{"username":"test","password":"old","new_password":"new"}`
1818
const regenAllExpected string = `{"all":true}`
1919
const regenUserExpected string = `{"username":"test"}`
20+
const cancelUserExpected string = `{"username":"foo"}`
2021

2122
type fakeHTTPServer struct {
2223
regenBodyEmpty bool
2324
regenBodyAll bool
2425
regenBodyUsername bool
26+
cancelEmpty bool
27+
cancelUsername bool
2528
}
2629

27-
func (f fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
30+
func (f *fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
2831
res.Header().Add("DEIS_API_VERSION", version.APIVersion)
2932

3033
if req.URL.Path == "/v1/auth/register/" && req.Method == "POST" {
@@ -55,6 +58,7 @@ func (f fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
5558
fmt.Println(err)
5659
res.WriteHeader(http.StatusInternalServerError)
5760
res.Write(nil)
61+
return
5862
}
5963

6064
if string(body) != loginExpected {
@@ -119,7 +123,28 @@ func (f fakeHTTPServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
119123
}
120124

121125
if req.URL.Path == "/v1/auth/cancel/" && req.Method == "DELETE" {
122-
res.WriteHeader(http.StatusNoContent)
126+
body, err := ioutil.ReadAll(req.Body)
127+
128+
if err != nil {
129+
fmt.Println(err)
130+
res.WriteHeader(http.StatusInternalServerError)
131+
res.Write(nil)
132+
}
133+
134+
if string(body) == cancelUserExpected && !f.cancelUsername {
135+
f.cancelUsername = true
136+
res.WriteHeader(http.StatusNoContent)
137+
res.Write(nil)
138+
return
139+
} else if string(body) == "" && !f.cancelEmpty {
140+
f.cancelEmpty = true
141+
res.WriteHeader(http.StatusNoContent)
142+
res.Write(nil)
143+
return
144+
}
145+
146+
fmt.Printf("%s is not a valid body.", body)
147+
res.WriteHeader(http.StatusInternalServerError)
123148
res.Write(nil)
124149
return
125150
}
@@ -204,7 +229,7 @@ func TestPasswd(t *testing.T) {
204229
func TestDelete(t *testing.T) {
205230
t.Parallel()
206231

207-
handler := fakeHTTPServer{}
232+
handler := fakeHTTPServer{cancelUsername: false, cancelEmpty: false}
208233
server := httptest.NewServer(&handler)
209234
defer server.Close()
210235

@@ -217,7 +242,11 @@ func TestDelete(t *testing.T) {
217242
httpClient := client.CreateHTTPClient(false)
218243
client := client.Client{HTTPClient: httpClient, ControllerURL: *u}
219244

220-
if err := Delete(&client); err != nil {
245+
if err := Delete(&client, "foo"); err != nil {
246+
t.Error(err)
247+
}
248+
249+
if err := Delete(&client, ""); err != nil {
221250
t.Error(err)
222251
}
223252
}

client/deis.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -827,20 +827,26 @@ def auth_cancel(self, args):
827827
if not controller:
828828
self._logger.error('Not logged in to a Deis controller')
829829
sys.exit(1)
830-
self._logger.info('Please log in again in order to cancel this account')
831-
args['<controller>'] = controller
832-
username = self.auth_login(args)
830+
username = args.get('--username')
831+
832+
if not username and not args.get('--password'):
833+
self._logger.info('Please log in again in order to cancel this account')
834+
args['<controller>'] = controller
835+
username = self.auth_login(args)
836+
833837
if username:
834838
confirm = args.get('--yes')
835839
if not confirm:
836840
confirm = raw_input(
837841
"Cancel account \"{}\" at {}? (y/N) ".format(username, controller))
838842
if confirm in ['y', True]:
839-
response = self._dispatch('delete', '/v1/auth/cancel')
843+
response = self._dispatch('delete', '/v1/auth/cancel',
844+
json.dumps({'username': username}))
840845
if response.status_code == requests.codes.no_content:
841-
self._settings['controller'] = None
842-
self._settings['token'] = None
843-
self._settings.save()
846+
if not args.get('--username'):
847+
self._settings['controller'] = None
848+
self._settings['token'] = None
849+
self._settings.save()
844850
self._logger.info('Account cancelled')
845851
else:
846852
self._logger.info('Account not changed')

controller/api/tests/test_auth.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,21 +170,31 @@ def test_cancel(self):
170170
"""Test that a registered user can cancel her account."""
171171
# test registration workflow
172172
username, password = 'newuser', 'password'
173-
first_name, last_name = 'Otto', 'Test'
174-
email = 'autotest@deis.io'
175173
submit = {
176174
'username': username,
177175
'password': password,
178-
'first_name': first_name,
179-
'last_name': last_name,
180-
'email': email,
176+
'first_name': 'Otto',
177+
'last_name': 'Test',
178+
'email': 'autotest@deis.io',
181179
# try to abuse superuser/staff level perms
182180
'is_superuser': True,
183181
'is_staff': True,
184182
}
183+
184+
other_username, other_password = 'newuser2', 'password'
185+
other_submit = {
186+
'username': other_username,
187+
'password': other_password,
188+
'first_name': 'Test',
189+
'last_name': 'Tester',
190+
'email': 'autotest-2@deis.io',
191+
'is_superuser': False,
192+
'is_staff': False,
193+
}
185194
url = '/v1/auth/register'
186195
response = self.client.post(url, json.dumps(submit), content_type='application/json')
187196
self.assertEqual(response.status_code, 201)
197+
188198
# cancel the account
189199
url = '/v1/auth/cancel'
190200
user = User.objects.get(username=username)
@@ -193,6 +203,25 @@ def test_cancel(self):
193203
HTTP_AUTHORIZATION='token {}'.format(token))
194204
self.assertEqual(response.status_code, 204)
195205

206+
url = '/v1/auth/register'
207+
response = self.client.post(url, json.dumps(other_submit), content_type='application/json')
208+
self.assertEqual(response.status_code, 201)
209+
210+
# normal user can't delete another user
211+
url = '/v1/auth/cancel'
212+
other_user = User.objects.get(username=other_username)
213+
other_token = Token.objects.get(user=other_user).key
214+
response = self.client.delete(url, json.dumps({'username': self.admin.username}),
215+
content_type='application/json',
216+
HTTP_AUTHORIZATION='token {}'.format(other_token))
217+
self.assertEqual(response.status_code, 403)
218+
219+
# admin can delete another user
220+
response = self.client.delete(url, json.dumps({'username': other_username}),
221+
content_type='application/json',
222+
HTTP_AUTHORIZATION='token {}'.format(self.admin_token))
223+
self.assertEqual(response.status_code, 204)
224+
196225
def test_passwd(self):
197226
"""Test that a registered user can change the password."""
198227
# test registration workflow

controller/api/views.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ class UserRegistrationViewSet(GenericViewSet,
2626
serializer_class = serializers.UserSerializer
2727

2828

29-
class UserManagementViewSet(GenericViewSet,
30-
mixins.DestroyModelMixin):
29+
class UserManagementViewSet(GenericViewSet):
3130
serializer_class = serializers.UserSerializer
3231

3332
def get_queryset(self):
@@ -36,6 +35,20 @@ def get_queryset(self):
3635
def get_object(self):
3736
return self.get_queryset()[0]
3837

38+
def destroy(self, request, **kwargs):
39+
calling_obj = self.get_object()
40+
target_obj = calling_obj
41+
42+
if request.data.get('username'):
43+
# if you "accidentally" target yourself, that should be fine
44+
if calling_obj.username == request.data['username'] or calling_obj.is_superuser:
45+
target_obj = get_object_or_404(User, username=request.data['username'])
46+
else:
47+
raise PermissionDenied()
48+
49+
target_obj.delete()
50+
return Response(status=status.HTTP_204_NO_CONTENT)
51+
3952
def passwd(self, request, **kwargs):
4053
caller_obj = self.get_object()
4154
target_obj = self.get_object()

docs/reference/api-v1.6.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ What's New
1414

1515
**New!** administrators no longer have to supply a password when changing another user's password.
1616

17+
**New!** administrators no longer have to supply a password when deleting another user.
18+
1719
Authentication
1820
--------------
1921

tests/auth_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ var (
1313
authLogoutCmd = "auth:logout"
1414
authRegisterCmd = "auth:register http://deis.{{.Domain}} --username={{.UserName}} --password={{.Password}} --email={{.Email}}"
1515
authCancelCmd = "auth:cancel --username={{.UserName}} --password={{.Password}} --yes"
16+
authCancelAdminCmd = "auth:cancel --username={{.UserName}} --yes"
1617
authRegenerateCmd = "auth:regenerate"
1718
authRegenerateUsrCmd = "auth:regenerate -u {{.UserName}}"
1819
authRegenerateAllCmd = "auth:regenerate --all"
1920
checkTokenCmd = "apps:list"
2021
authPasswdCmd = "auth:passwd --username={{.UserName}} --password={{.Password}} --new-password={{.NewPassword}}"
22+
authWhoamiCmd = "auth:whoami"
2123
)
2224

2325
func TestAuth(t *testing.T) {
@@ -39,6 +41,16 @@ func authSetup(t *testing.T) *utils.DeisTestConfig {
3941

4042
func authCancel(t *testing.T, params *utils.DeisTestConfig) {
4143
utils.Execute(t, authCancelCmd, params, false, "Account cancelled")
44+
user := utils.GetGlobalConfig()
45+
46+
// Admins can delete other users.
47+
user.UserName, user.Password = "cancel-test", "test"
48+
utils.Execute(t, authRegisterCmd, user, false, "")
49+
admin := utils.GetGlobalConfig()
50+
utils.Execute(t, authLoginCmd, admin, false, "")
51+
utils.Execute(t, authCancelAdminCmd, user, false, "Account cancelled")
52+
// Make sure the admin is still logged in
53+
utils.CheckList(t, authWhoamiCmd, admin, admin.UserName, false)
4254
}
4355

4456
func authLoginTest(t *testing.T, params *utils.DeisTestConfig) {

0 commit comments

Comments
 (0)