Skip to content

Commit fede86f

Browse files
authored
Implement deleting user and changing user role (fixes #205) (#220)
* Implement user deletion and role change * Fix email confirmation template
1 parent 3e0d5dd commit fede86f

File tree

5 files changed

+173
-6
lines changed

5 files changed

+173
-6
lines changed

gramps_webapi/api/resources/user.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@
3131
from ...auth.const import (
3232
CLAIM_LIMITED_SCOPE,
3333
PERM_ADD_USER,
34+
PERM_DEL_USER,
3435
PERM_EDIT_OTHER_USER,
3536
PERM_EDIT_OWN_USER,
37+
PERM_EDIT_USER_ROLE,
3638
PERM_VIEW_OTHER_USER,
3739
ROLE_DISABLED,
3840
ROLE_UNCONFIRMED,
@@ -98,16 +100,25 @@ def get(self, user_name: str):
98100
return jsonify(details), 200
99101

100102
@use_args(
101-
{"email": fields.Str(required=False), "full_name": fields.Str(required=False),},
103+
{
104+
"email": fields.Str(required=False),
105+
"full_name": fields.Str(required=False),
106+
"role": fields.Int(required=False),
107+
},
102108
location="json",
103109
)
104110
def put(self, args, user_name: str):
105111
"""Update a user's details."""
106112
auth_provider, user_name = self.prepare_edit(user_name)
113+
if "role" in args:
114+
require_permissions([PERM_EDIT_USER_ROLE])
107115
auth_provider.modify_user(
108-
name=user_name, email=args.get("email"), fullname=args.get("full_name")
116+
name=user_name,
117+
email=args.get("email"),
118+
fullname=args.get("full_name"),
119+
role=args.get("role"),
109120
)
110-
return "", 201
121+
return "", 200
111122

112123
@use_args(
113124
{
@@ -139,6 +150,21 @@ def post(self, args, user_name: str):
139150
abort(409)
140151
return "", 201
141152

153+
def delete(self, user_name: str):
154+
"""Delete a user."""
155+
if user_name == "-":
156+
# Deleting the own user is currently not allowed
157+
abort(404)
158+
auth_provider = current_app.config.get("AUTH_PROVIDER")
159+
if auth_provider is None:
160+
abort(405)
161+
require_permissions([PERM_DEL_USER])
162+
try:
163+
auth_provider.delete_user(name=user_name)
164+
except ValueError:
165+
abort(404) # user not found
166+
return "", 200
167+
142168

143169
class UserRegisterResource(Resource):
144170
"""Resource for registering a new user."""

gramps_webapi/auth/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
PERM_ADD_USER = "AddUser"
3434
PERM_EDIT_OWN_USER = "EditOwnUser"
3535
PERM_EDIT_OTHER_USER = "EditOtherUser"
36+
PERM_EDIT_USER_ROLE = "EditUserRole"
3637
PERM_VIEW_OTHER_USER = "ViewOtherUser"
3738
PERM_DEL_USER = "DeleteUser"
3839
PERM_VIEW_PRIVATE = "ViewPrivate"
@@ -46,6 +47,7 @@
4647
PERM_EDIT_OWN_USER,
4748
PERM_DEL_USER,
4849
PERM_EDIT_OTHER_USER,
50+
PERM_EDIT_USER_ROLE,
4951
PERM_VIEW_OTHER_USER,
5052
PERM_VIEW_PRIVATE,
5153
PERM_EDIT_OBJ,

gramps_webapi/data/apispec.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,16 @@ paths:
208208
full_name:
209209
description: "The new user's full name."
210210
type: string
211+
role:
212+
description: "An integer user role ID."
213+
type: number
211214
responses:
212215
200:
213216
description: "OK: Successful operation."
214217
401:
215218
description: "Unauthorized: Missing authorization header."
219+
403:
220+
description: "Unauthorized: insufficient permissions."
216221
404:
217222
description: "Not found: User does not exist."
218223
422:
@@ -251,6 +256,27 @@ paths:
251256
description: "Conflict: user already exists."
252257
422:
253258
description: "Unprocessable Entity: Invalid token."
259+
delete:
260+
tags:
261+
- users
262+
summary: "Delete the user."
263+
operationId: deleteUser
264+
security:
265+
- Bearer: []
266+
responses:
267+
200:
268+
description: "OK: Successful operation."
269+
400:
270+
description: "Bad Request: Malformed request could not be parsed."
271+
401:
272+
description: "Unauthorized: Missing authorization header."
273+
403:
274+
description: "Unauthorized: insufficient permissions."
275+
404:
276+
description: "Not found: user does not exist."
277+
422:
278+
description: "Unprocessable Entity: Invalid or bad parameter provided."
279+
254280

255281
/users/{user_name}/register/:
256282
post:

gramps_webapi/templates/confirmation.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{% block title %}{{title}}{% endblock %}
33

44
{% block main %}
5-
<div id="success" class="success" style="display:none;">
5+
<div id="success" class="success">
66
<img width="200" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaWQ9IkxheWVyXzEiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYxMiA3OTI7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA2MTIgNzkyIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiM0MUFENDk7fQo8L3N0eWxlPjxnPjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik01NjIsMzk2YzAtMTQxLjQtMTE0LjYtMjU2LTI1Ni0yNTZTNTAsMjU0LjYsNTAsMzk2czExNC42LDI1NiwyNTYsMjU2UzU2Miw1MzcuNCw1NjIsMzk2TDU2MiwzOTZ6ICAgIE01MDEuNywyOTYuM2wtMjQxLDI0MWwwLDBsLTE3LjIsMTcuMkwxMTAuMyw0MjEuM2w1OC44LTU4LjhsNzQuNSw3NC41bDE5OS40LTE5OS40TDUwMS43LDI5Ni4zTDUwMS43LDI5Ni4zeiIvPjwvZz48L3N2Zz4=">
77
<div>{{message}}</div>
88
</div>

tests/test_endpoints/test_user.py

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ def test_edit_user(self):
319319
headers={"Authorization": "Bearer {}".format(token_user)},
320320
json={"full_name": "My Name"},
321321
)
322-
assert rv.status_code == 201
322+
assert rv.status_code == 200
323323
rv = self.client.get(
324324
BASE_URL + "/users/-/",
325325
headers={"Authorization": "Bearer {}".format(token_user)},
@@ -348,7 +348,7 @@ def test_edit_user(self):
348348
headers={"Authorization": "Bearer {}".format(token_owner)},
349349
json={"full_name": "His Name"},
350350
)
351-
assert rv.status_code == 201
351+
assert rv.status_code == 200
352352
rv = self.client.get(
353353
BASE_URL + "/users/user/",
354354
headers={"Authorization": "Bearer {}".format(token_owner)},
@@ -570,3 +570,116 @@ def test_confirm_email(self):
570570
headers={"Authorization": "Bearer {}".format(token)},
571571
)
572572
self.assertEqual(rv.status_code, 401)
573+
574+
def test_delete_user(self):
575+
# get user token
576+
rv = self.client.post(
577+
BASE_URL + "/token/", json={"username": "user", "password": "123"}
578+
)
579+
assert rv.status_code == 200
580+
token_user = rv.json["access_token"]
581+
# get owner token
582+
rv = self.client.post(
583+
BASE_URL + "/token/", json={"username": "owner", "password": "123"},
584+
)
585+
assert rv.status_code == 200
586+
token_owner = rv.json["access_token"]
587+
# add user
588+
rv = self.client.post(
589+
BASE_URL + "/users/user_to_delete/",
590+
headers={"Authorization": "Bearer {}".format(token_owner)},
591+
json={
592+
"email": "[email protected]",
593+
"role": ROLE_MEMBER,
594+
"full_name": "To Delete",
595+
"password": "abc",
596+
},
597+
)
598+
assert rv.status_code == 201
599+
rv = self.client.get(
600+
BASE_URL + "/users/user_to_delete/",
601+
headers={"Authorization": "Bearer {}".format(token_owner)},
602+
)
603+
assert rv.status_code == 200
604+
# check token for new user
605+
rv = self.client.post(
606+
BASE_URL + "/token/", json={"username": "user_to_delete", "password": "abc"}
607+
)
608+
assert rv.status_code == 200
609+
# user cannot delete user
610+
rv = self.client.delete(
611+
BASE_URL + "/users/user_to_delete/",
612+
headers={"Authorization": "Bearer {}".format(token_user)},
613+
)
614+
assert rv.status_code == 403
615+
# owner can user
616+
rv = self.client.delete(
617+
BASE_URL + "/users/user_to_delete/",
618+
headers={"Authorization": "Bearer {}".format(token_owner)},
619+
)
620+
assert rv.status_code == 200
621+
# check user is gone
622+
rv = self.client.get(
623+
BASE_URL + "/users/user_to_delete/",
624+
headers={"Authorization": "Bearer {}".format(token_owner)},
625+
)
626+
assert rv.status_code == 404
627+
# check user can't get token
628+
rv = self.client.post(
629+
BASE_URL + "/token/", json={"username": "user_to_delete", "password": "abc"}
630+
)
631+
assert rv.status_code == 403
632+
633+
def test_change_user_role(self):
634+
# get user token
635+
rv = self.client.post(
636+
BASE_URL + "/token/", json={"username": "user", "password": "123"}
637+
)
638+
assert rv.status_code == 200
639+
token_user = rv.json["access_token"]
640+
# get owner token
641+
rv = self.client.post(
642+
BASE_URL + "/token/", json={"username": "owner", "password": "123"},
643+
)
644+
assert rv.status_code == 200
645+
token_owner = rv.json["access_token"]
646+
# add user
647+
rv = self.client.post(
648+
BASE_URL + "/users/user_change_role/",
649+
headers={"Authorization": "Bearer {}".format(token_owner)},
650+
json={
651+
"email": "[email protected]",
652+
"role": ROLE_MEMBER,
653+
"full_name": "Change Role",
654+
"password": "abc",
655+
},
656+
)
657+
assert rv.status_code == 201
658+
# get token for new user
659+
rv = self.client.post(
660+
BASE_URL + "/token/",
661+
json={"username": "user_change_role", "password": "abc"},
662+
)
663+
assert rv.status_code == 200
664+
token_new_user = rv.json["access_token"]
665+
# user can change own details
666+
rv = self.client.put(
667+
BASE_URL + "/users/-/",
668+
headers={"Authorization": "Bearer {}".format(token_new_user)},
669+
json={"full_name": "Change My Role"},
670+
)
671+
assert rv.status_code == 200
672+
# user cannot change own role
673+
rv = self.client.put(
674+
BASE_URL + "/users/-/",
675+
headers={"Authorization": "Bearer {}".format(token_new_user)},
676+
json={"role": ROLE_OWNER},
677+
)
678+
assert rv.status_code == 403
679+
# owner can change user role
680+
rv = self.client.put(
681+
BASE_URL + "/users/user_change_role/",
682+
headers={"Authorization": "Bearer {}".format(token_owner)},
683+
json={"role": ROLE_OWNER},
684+
)
685+
assert rv.status_code == 200

0 commit comments

Comments
 (0)