Skip to content

Commit 3626c88

Browse files
authored
Implement disable/enable tree (#382)
* Implement disable/enable tree * Update apispec
1 parent b48e366 commit 3626c88

File tree

9 files changed

+251
-11
lines changed

9 files changed

+251
-11
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Add trees.enabled
2+
3+
Revision ID: 22c8d1fba959
4+
Revises: 66e56620891a
5+
Create Date: 2023-06-19 15:31:06.235185
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '22c8d1fba959'
14+
down_revision = '66e56620891a'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.add_column('trees', sa.Column('enabled', sa.Integer(), server_default='1', nullable=True))
22+
# ### end Alembic commands ###
23+
24+
25+
def downgrade():
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.drop_column('trees', 'enabled')
28+
# ### end Alembic commands ###

gramps_webapi/api/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@
8585
)
8686
from .resources.transactions import TransactionsResource
8787
from .resources.translations import TranslationResource, TranslationsResource
88-
from .resources.trees import TreeResource, TreesResource
88+
from .resources.trees import (
89+
DisableTreeResource,
90+
EnableTreeResource,
91+
TreeResource,
92+
TreesResource,
93+
)
8994
from .resources.types import (
9095
CustomTypeResource,
9196
CustomTypesResource,
@@ -167,6 +172,8 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
167172
# Trees
168173
register_endpt(TreeResource, "/trees/<string:tree_id>", "tree")
169174
register_endpt(TreesResource, "/trees/", "trees")
175+
register_endpt(DisableTreeResource, "/trees/<string:tree_id>/disable", "disable_tree")
176+
register_endpt(EnableTreeResource, "/trees/<string:tree_id>/enable", "enable_tree")
170177
# Types
171178
register_endpt(CustomTypeResource, "/types/custom/<string:datatype>", "custom-type")
172179
register_endpt(CustomTypesResource, "/types/custom/", "custom-types")

gramps_webapi/api/resources/token.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
get_guid,
3636
get_name,
3737
get_permissions,
38+
is_tree_disabled,
3839
)
3940
from ...auth.const import CLAIM_LIMITED_SCOPE, SCOPE_CREATE_ADMIN, SCOPE_CREATE_OWNER
4041
from ...const import TREE_MULTI
@@ -80,9 +81,11 @@ def post(self, args):
8081
abort(401)
8182
if not authorized(args.get("username"), args.get("password")):
8283
abort(403)
83-
permissions = get_permissions(args["username"])
8484
user_id = get_guid(args["username"])
8585
tree_id = get_tree_id(user_id)
86+
if is_tree_disabled(tree=tree_id):
87+
abort(503)
88+
permissions = get_permissions(args["username"])
8689
return get_tokens(
8790
user_id=user_id,
8891
permissions=permissions,
@@ -102,8 +105,10 @@ def post(self):
102105
username = get_name(user_id)
103106
except ValueError:
104107
abort(401)
105-
permissions = get_permissions(username)
106108
tree_id = get_tree_id(user_id)
109+
if is_tree_disabled(tree=tree_id):
110+
abort(503)
111+
permissions = get_permissions(username)
107112
return get_tokens(
108113
user_id=user_id,
109114
permissions=permissions,

gramps_webapi/api/resources/trees.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@
2929
from webargs import fields
3030
from werkzeug.security import safe_join
3131

32-
from ...auth import get_tree_usage, set_tree_quota
32+
from ...auth import disable_enable_tree, get_tree_usage, set_tree_quota
3333
from ...auth.const import (
3434
PERM_ADD_TREE,
35+
PERM_DISABLE_TREE,
3536
PERM_EDIT_OTHER_TREE,
3637
PERM_EDIT_TREE,
3738
PERM_EDIT_TREE_QUOTA,
@@ -170,3 +171,36 @@ def put(self, args, tree_id: str):
170171
if args.get(quota) is not None:
171172
rv.update({quota: args[quota]})
172173
return rv
174+
175+
176+
class DisableEnableTreeResource(ProtectedResource):
177+
"""Base class for disabling or enabling a tree."""
178+
179+
def _post_disable_enable_tree(self, tree_id: str, disabled: bool):
180+
"""Disable or enable a tree."""
181+
if current_app.config["TREE"] != TREE_MULTI:
182+
abort(405)
183+
if tree_id == "-":
184+
# own tree
185+
tree_id = get_tree_from_jwt()
186+
if not tree_exists(tree_id):
187+
abort(404)
188+
require_permissions([PERM_DISABLE_TREE])
189+
disable_enable_tree(tree=tree_id, disabled=disabled)
190+
return "", 201
191+
192+
193+
class DisableTreeResource(DisableEnableTreeResource):
194+
"""Resource for disabling a tree."""
195+
196+
def post(self, tree_id: str):
197+
"""Disable a tree."""
198+
return self._post_disable_enable_tree(tree_id=tree_id, disabled=True)
199+
200+
201+
class EnableTreeResource(DisableEnableTreeResource):
202+
"""Resource for enabling a tree."""
203+
204+
def post(self, tree_id: str):
205+
"""Disable a tree."""
206+
return self._post_disable_enable_tree(tree_id=tree_id, disabled=False)

gramps_webapi/api/tasks.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
from .util import (
3838
check_quota_people,
3939
get_config,
40-
get_db_manager,
4140
get_db_outside_request,
4241
get_search_indexer,
4342
send_email,
@@ -93,8 +92,7 @@ def send_email_new_user(username: str, fullname: str, email: str, tree: str):
9392
def _search_reindex_full(tree) -> None:
9493
"""Rebuild the search index."""
9594
indexer = get_search_indexer(tree)
96-
db_manager = get_db_manager(tree)
97-
db = db_manager.get_db().db
95+
db = get_db_outside_request(tree=tree, view_private=True, readonly=True)
9896
try:
9997
indexer.reindex_full(db)
10098
finally:
@@ -110,8 +108,7 @@ def search_reindex_full(tree) -> None:
110108
def _search_reindex_incremental(tree) -> None:
111109
"""Run an incremental reindex of the search index."""
112110
indexer = get_search_indexer(tree)
113-
db_manager = get_db_manager(tree)
114-
db = db_manager.get_db().db
111+
db = get_db_outside_request(tree=tree, view_private=True, readonly=True)
115112
try:
116113
indexer.reindex_incremental(db)
117114
finally:
@@ -131,8 +128,7 @@ def import_file(tree: str, file_name: str, extension: str, delete: bool = True):
131128
if object_counts is None:
132129
raise ValueError(f"Failed importing {file_name}")
133130
check_quota_people(to_add=object_counts["people"], tree=tree)
134-
db_manager = get_db_manager(tree)
135-
db_handle = db_manager.get_db().db
131+
db_handle = get_db_outside_request(tree=tree, view_private=True, readonly=True)
136132
run_import(
137133
db_handle=db_handle,
138134
file_name=file_name,

gramps_webapi/auth/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,26 @@ def set_tree_quota(
351351
user_db.session.commit() # pylint: disable=no-member
352352

353353

354+
def disable_enable_tree(tree: str, disabled: bool) -> None:
355+
"""Disable or enable a tree."""
356+
query = user_db.session.query(Tree) # pylint: disable=no-member
357+
tree_obj = query.filter_by(id=tree).scalar()
358+
if not tree_obj:
359+
tree_obj = Tree(id=tree)
360+
tree_obj.enabled = 0 if disabled else 1
361+
user_db.session.add(tree_obj) # pylint: disable=no-member
362+
user_db.session.commit() # pylint: disable=no-member
363+
364+
365+
def is_tree_disabled(tree: str) -> bool:
366+
"""Check if tree is disabled."""
367+
query = user_db.session.query(Tree) # pylint: disable=no-member
368+
tree_obj = query.filter_by(id=tree).scalar()
369+
if not tree_obj:
370+
return False
371+
return tree_obj.enabled == 0
372+
373+
354374
class User(user_db.Model):
355375
"""User table class for sqlalchemy."""
356376

@@ -393,6 +413,7 @@ class Tree(user_db.Model):
393413
quota_people = sa.Column(sa.Integer)
394414
usage_media = sa.Column(sa.Integer)
395415
usage_people = sa.Column(sa.Integer)
416+
enabled = sa.Column(sa.Integer, default=1, server_default="1")
396417

397418
def __repr__(self):
398419
"""Return string representation of instance."""

gramps_webapi/auth/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
PERM_EDIT_OTHER_TREE = "EditOtherTree"
4242
PERM_ADD_TREE = "AddTree"
4343
PERM_EDIT_TREE_QUOTA = "EditTreeQuota"
44+
PERM_DISABLE_TREE = "DisableTree"
4445

4546
# User permissions
4647
PERM_ADD_USER = "AddUser"
@@ -110,6 +111,7 @@
110111
PERM_EDIT_OTHER_TREE,
111112
PERM_EDIT_TREE_QUOTA,
112113
PERM_ADD_TREE,
114+
PERM_DISABLE_TREE,
113115
}
114116

115117
# keys/values for user claims

gramps_webapi/data/apispec.yaml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6809,6 +6809,59 @@ paths:
68096809
422:
68106810
description: "Unprocessable Entity: Invalid token."
68116811

6812+
/trees/{tree_id}/disable:
6813+
parameters:
6814+
- name: tree_id
6815+
in: path
6816+
required: true
6817+
type: string
6818+
description: "The tree ID."
6819+
post:
6820+
tags:
6821+
- trees
6822+
summary: "Disable a tree."
6823+
operationId: disableTree
6824+
security:
6825+
- Bearer: []
6826+
responses:
6827+
201:
6828+
description: "OK: Successful operation."
6829+
401:
6830+
description: "Unauthorized: Missing authorization header."
6831+
403:
6832+
description: "Unauthorized: insufficient permissions."
6833+
404:
6834+
description: "Not found: tree does not exist."
6835+
405:
6836+
description: "Method Not Allowed in single-tree setup."
6837+
6838+
6839+
/trees/{tree_id}/enable:
6840+
parameters:
6841+
- name: tree_id
6842+
in: path
6843+
required: true
6844+
type: string
6845+
description: "The tree ID."
6846+
post:
6847+
tags:
6848+
- trees
6849+
summary: "Enable a tree."
6850+
operationId: enableTree
6851+
security:
6852+
- Bearer: []
6853+
responses:
6854+
201:
6855+
description: "OK: Successful operation."
6856+
401:
6857+
description: "Unauthorized: Missing authorization header."
6858+
403:
6859+
description: "Unauthorized: insufficient permissions."
6860+
404:
6861+
description: "Not found: tree does not exist."
6862+
405:
6863+
description: "Method Not Allowed in single-tree setup."
6864+
68126865

68136866
##############################################################################
68146867
# Model definitions

tests/test_endpoints/test_trees.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,97 @@ def test_rename_tree(self):
166166
)
167167
assert rv.status_code == 200
168168
assert rv.json == {"old_name": "my old name", "new_name": "my new name"}
169+
170+
def test_disable_tree(self):
171+
# fetch tokens
172+
rv = self.client.post(
173+
BASE_URL + "/token/", json={"username": "owner", "password": "123"}
174+
)
175+
assert rv.status_code == 200
176+
token_owner = rv.json["access_token"]
177+
token_owner_refresh = rv.json["refresh_token"]
178+
rv = self.client.post(
179+
BASE_URL + "/token/", json={"username": "admin", "password": "123"}
180+
)
181+
assert rv.status_code == 200
182+
token_admin = rv.json["access_token"]
183+
# owner can't disable
184+
rv = self.client.post(
185+
BASE_URL + f"/trees/{self.tree}/disable",
186+
headers={"Authorization": f"Bearer {token_owner}"},
187+
)
188+
assert rv.status_code == 403
189+
# admin can disable
190+
rv = self.client.post(
191+
BASE_URL + f"/trees/{self.tree}/disable",
192+
headers={"Authorization": f"Bearer {token_admin}"},
193+
)
194+
assert rv.status_code == 201
195+
# token does not work
196+
rv = self.client.post(
197+
BASE_URL + "/token/", json={"username": "owner", "password": "123"}
198+
)
199+
assert rv.status_code == 503
200+
rv = self.client.post(
201+
BASE_URL + "/token/refresh/",
202+
headers={"Authorization": f"Bearer {token_owner_refresh}"},
203+
)
204+
assert rv.status_code == 503
205+
rv = self.client.post(
206+
BASE_URL + f"/trees/{self.tree}/enable",
207+
headers={"Authorization": f"Bearer {token_admin}"},
208+
)
209+
assert rv.status_code == 201
210+
# works again
211+
rv = self.client.post(
212+
BASE_URL + "/token/", json={"username": "owner", "password": "123"}
213+
)
214+
assert rv.status_code == 200
215+
rv = self.client.post(
216+
BASE_URL + "/token/refresh/",
217+
headers={"Authorization": f"Bearer {token_owner_refresh}"},
218+
)
219+
assert rv.status_code == 200
220+
# and disable again
221+
rv = self.client.post(
222+
BASE_URL + f"/trees/{self.tree}/disable",
223+
headers={"Authorization": f"Bearer {token_admin}"},
224+
)
225+
assert rv.status_code == 201
226+
# token does not work
227+
rv = self.client.post(
228+
BASE_URL + "/token/", json={"username": "owner", "password": "123"}
229+
)
230+
assert rv.status_code == 503
231+
rv = self.client.post(
232+
BASE_URL + "/token/refresh/",
233+
headers={"Authorization": f"Bearer {token_owner_refresh}"},
234+
)
235+
assert rv.status_code == 503
236+
rv = self.client.post(
237+
BASE_URL + f"/trees/{self.tree}/enable",
238+
headers={"Authorization": f"Bearer {token_admin}"},
239+
)
240+
assert rv.status_code == 201
241+
# works again
242+
rv = self.client.post(
243+
BASE_URL + "/token/", json={"username": "owner", "password": "123"}
244+
)
245+
assert rv.status_code == 200
246+
rv = self.client.post(
247+
BASE_URL + "/token/refresh/",
248+
headers={"Authorization": f"Bearer {token_owner_refresh}"},
249+
)
250+
assert rv.status_code == 200
251+
252+
def test_disable_nonexistant_tree(self):
253+
rv = self.client.post(
254+
BASE_URL + "/token/", json={"username": "admin", "password": "123"}
255+
)
256+
assert rv.status_code == 200
257+
token_admin = rv.json["access_token"]
258+
rv = self.client.post(
259+
BASE_URL + "/trees/idontexist/disable",
260+
headers={"Authorization": f"Bearer {token_admin}"},
261+
)
262+
assert rv.status_code == 404

0 commit comments

Comments
 (0)