Skip to content

Commit 8835414

Browse files
authored
Allow multiple trees (#362)
* Allow multiple trees * Add warning
1 parent 1902409 commit 8835414

22 files changed

+289
-85
lines changed

gramps_webapi/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from .dbmanager import WebDbManager
3434
from .app import create_app
3535
from .auth import add_user, delete_user, fill_tree, user_db
36-
from .const import ENV_CONFIG_FILE
36+
from .const import ENV_CONFIG_FILE, TREE_MULTI
3737

3838
logging.basicConfig()
3939
LOG = logging.getLogger("gramps_webapi")
@@ -125,6 +125,8 @@ def migrate_db(ctx):
125125
def search(ctx, tree):
126126
app = ctx.obj["app"]
127127
if not tree:
128+
if app.config["TREE"] == TREE_MULTI:
129+
raise ValueError("`tree` is required when multi-tree support is enabled.")
128130
# needed for backwards compatibility!
129131
dbmgr = WebDbManager(name=app.config["TREE"], create_if_missing=False)
130132
tree = dbmgr.dirname

gramps_webapi/api/resources/trees.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from werkzeug.security import safe_join
3131

3232
from ...auth.const import PERM_ADD_TREE, PERM_EDIT_OTHER_TREE, PERM_EDIT_TREE
33+
from ...const import TREE_MULTI
3334
from ...dbmanager import WebDbManager
3435
from ..auth import require_permissions
3536
from ..util import get_tree_from_jwt, use_args
@@ -66,18 +67,12 @@ def validate_tree_id(tree_id: str) -> None:
6667
abort(422)
6768

6869

69-
def get_single_tree_id() -> str:
70-
"""Get the tree ID in the case of using app.config['TREE']."""
71-
dbmgr = WebDbManager(name=current_app.config["TREE"], create_if_missing=False)
72-
return dbmgr.dirname
73-
74-
7570
class TreesResource(ProtectedResource):
7671
"""Resource for getting info about trees."""
7772

7873
def get(self):
7974
"""Get info about all trees."""
80-
user_tree_id = get_tree_from_jwt() or get_single_tree_id()
75+
user_tree_id = get_tree_from_jwt()
8176
# only allowed to see details about our own tree
8277
tree_ids = [user_tree_id]
8378
return [get_tree_details(tree_id) for tree_id in tree_ids]
@@ -90,9 +85,10 @@ def get(self):
9085
)
9186
def post(self, args):
9287
"""Create a new tree."""
88+
if current_app.config["TREE"] != TREE_MULTI:
89+
abort(405)
9390
require_permissions([PERM_ADD_TREE])
9491
tree_id = str(uuid.uuid4())
95-
# TODO dbid
9692
dbmgr = WebDbManager(dirname=tree_id, name=args["name"], create_if_missing=True)
9793
return {"name": args["name"], "tree_id": dbmgr.dirname}, 201
9894

@@ -103,7 +99,7 @@ class TreeResource(ProtectedResource):
10399
def get(self, tree_id: str):
104100
"""Get info about a tree."""
105101
validate_tree_id(tree_id)
106-
user_tree_id = get_tree_from_jwt() or get_single_tree_id()
102+
user_tree_id = get_tree_from_jwt()
107103
if tree_id != user_tree_id:
108104
# only allowed to see details about our own tree
109105
abort(403)
@@ -117,7 +113,7 @@ def get(self, tree_id: str):
117113
)
118114
def put(self, args, tree_id: str):
119115
"""Modify a tree."""
120-
user_tree_id = get_tree_from_jwt() or get_single_tree_id()
116+
user_tree_id = get_tree_from_jwt()
121117
if tree_id == user_tree_id:
122118
require_permissions([PERM_EDIT_TREE])
123119
else:

gramps_webapi/api/resources/user.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from gettext import gettext as _
2424
from typing import Tuple
2525

26-
from flask import abort, jsonify, render_template
26+
from flask import abort, current_app, jsonify, render_template
2727
from flask_jwt_extended import create_access_token, get_jwt, get_jwt_identity
2828
from webargs import fields
2929

@@ -50,6 +50,7 @@
5050
PERM_EDIT_OTHER_USER,
5151
PERM_EDIT_OWN_USER,
5252
PERM_EDIT_USER_ROLE,
53+
PERM_EDIT_USER_TREE,
5354
PERM_MAKE_ADMIN,
5455
PERM_VIEW_OTHER_TREE_USER,
5556
PERM_VIEW_OTHER_USER,
@@ -61,6 +62,7 @@
6162
SCOPE_CREATE_ADMIN,
6263
SCOPE_RESET_PW,
6364
)
65+
from ...const import TREE_MULTI
6466
from ..auth import has_permissions, require_permissions
6567
from ..ratelimiter import limiter
6668
from ..tasks import (
@@ -71,7 +73,7 @@
7173
send_email_new_user,
7274
send_email_reset_password,
7375
)
74-
from ..util import get_tree_from_jwt, get_tree_id, use_args
76+
from ..util import get_tree_from_jwt, get_tree_id, tree_exists, use_args
7577
from . import LimitedScopeProtectedResource, ProtectedResource, Resource
7678

7779

@@ -154,6 +156,7 @@ def get(self, user_name: str):
154156
"email": fields.Str(required=False),
155157
"full_name": fields.Str(required=False),
156158
"role": fields.Int(required=False),
159+
"tree": fields.Str(required=False),
157160
},
158161
location="json",
159162
)
@@ -168,11 +171,16 @@ def put(self, args, user_name: str):
168171
require_permissions([PERM_EDIT_OTHER_TREE_USER_ROLE])
169172
else:
170173
require_permissions([PERM_EDIT_USER_ROLE])
174+
if "tree" in args:
175+
require_permissions([PERM_EDIT_USER_TREE])
176+
if not tree_exists(args["tree"]):
177+
abort(422)
171178
modify_user(
172179
name=user_name,
173180
email=args.get("email"),
174181
fullname=args.get("full_name"),
175182
role=args.get("role"),
183+
tree=args.get("tree"),
176184
)
177185
return "", 200
178186

@@ -199,6 +207,8 @@ def post(self, args, user_name: str):
199207
require_permissions([PERM_ADD_USER])
200208
else:
201209
require_permissions([PERM_ADD_OTHER_TREE_USER])
210+
if not tree_exists(args["tree"]):
211+
abort(422)
202212
try:
203213
add_user(
204214
name=user_name,
@@ -250,9 +260,14 @@ def post(self, args, user_name: str):
250260
if user_name == "-":
251261
# Registering a new user does not make sense for "own" user
252262
abort(404)
263+
if not args.get("tree") and current_app.config["TREE"] == TREE_MULTI:
264+
# if multi-tree is enabled, tree is required
265+
abort(422)
253266
# do not allow registration if no tree owner account exists!
254267
if get_number_users(tree=args.get("tree"), roles=(ROLE_OWNER,)) == 0:
255268
abort(405)
269+
if "tree" in args and not tree_exists(args["tree"]):
270+
abort(422)
256271
try:
257272
add_user(
258273
name=user_name,
@@ -292,17 +307,20 @@ class UserCreateOwnerResource(LimitedScopeProtectedResource):
292307
location="json",
293308
)
294309
def post(self, args, user_name: str):
295-
"""Create a user with owner permissions."""
310+
"""Create a user with admin permissions."""
296311
if user_name == "-":
297312
# User name - is not allowed
298313
abort(404)
314+
# FIXME what about multi-tree
299315
if get_number_users() > 0:
300316
# there is already a user in the user DB
301317
abort(405)
302318
claims = get_jwt()
303319
if claims[CLAIM_LIMITED_SCOPE] != SCOPE_CREATE_ADMIN:
304320
# This is a wrong token!
305321
abort(403)
322+
if "tree" in args and not tree_exists(args["tree"]):
323+
abort(422)
306324
add_user(
307325
name=user_name,
308326
password=args["password"],

gramps_webapi/api/util.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,22 @@
3131

3232
from flask import abort, current_app, g, jsonify, make_response, request
3333
from flask_jwt_extended import get_jwt
34-
from gramps.cli.clidbman import CLIDbManager
34+
from gramps.cli.clidbman import NAME_FILE, CLIDbManager
35+
from gramps.gen.config import config
3536
from gramps.gen.const import GRAMPS_LOCALE
3637
from gramps.gen.db.base import DbReadBase
38+
from gramps.gen.db.dbconst import DBBACKEND
3739
from gramps.gen.dbstate import DbState
3840
from gramps.gen.errors import HandleError
3941
from gramps.gen.proxy import PrivateProxyDb
4042
from gramps.gen.utils.grampslocale import GrampsLocale
4143
from marshmallow import RAISE
4244
from webargs.flaskparser import FlaskParser
45+
from werkzeug.security import safe_join
4346

4447
from ..auth import config_get, get_tree
4548
from ..auth.const import PERM_VIEW_PRIVATE
46-
from ..const import DB_CONFIG_ALLOWED_KEYS, LOCALE_MAP
49+
from ..const import DB_CONFIG_ALLOWED_KEYS, LOCALE_MAP, TREE_MULTI
4750
from ..dbmanager import WebDbManager
4851
from .auth import has_permissions
4952
from .search import SearchIndexer
@@ -310,7 +313,27 @@ def get_tree_id(guid: str) -> str:
310313
"""Get the appropriate tree ID for a user."""
311314
tree_id = get_tree(guid)
312315
if not tree_id:
313-
# needed for backwards compatibility!
316+
if current_app.config["TREE"] == TREE_MULTI:
317+
# multi-tree support enabled but user has no tree ID: forbidden!
318+
abort(403)
319+
# needed for backwards compatibility: single-tree mode but user without tree ID
314320
dbmgr = WebDbManager(name=current_app.config["TREE"], create_if_missing=False)
315321
tree_id = dbmgr.dirname
316322
return tree_id
323+
324+
325+
def tree_exists(tree_id: str) -> bool:
326+
"""Check if a tree exists."""
327+
dbdir = config.get("database.path")
328+
dir_path = safe_join(dbdir, tree_id)
329+
if not dir_path:
330+
return False
331+
if not os.path.isdir(dir_path):
332+
return False
333+
name_path = os.path.join(dir_path, NAME_FILE)
334+
if not os.path.isfile(name_path):
335+
return False
336+
dbid_path = os.path.join(dir_path, DBBACKEND)
337+
if not os.path.isfile(dbid_path):
338+
return False
339+
return True

gramps_webapi/app.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from .api.search import SearchIndexer
3636
from .auth import user_db
3737
from .config import DefaultConfig, DefaultConfigJWT
38-
from .const import API_PREFIX, ENV_CONFIG_FILE
38+
from .const import API_PREFIX, ENV_CONFIG_FILE, TREE_MULTI
3939
from .dbmanager import WebDbManager
4040
from .util.celery import create_celery
4141

@@ -107,8 +107,16 @@ def create_app(config: Optional[Dict[str, Any]] = None):
107107
if not app.config.get(option):
108108
raise ValueError(f"{option} must be specified")
109109

110-
# create database if missing
111-
WebDbManager(name=app.config["TREE"], create_if_missing=True)
110+
if app.config["TREE"] != TREE_MULTI:
111+
# create database if missing (only in single-tree mode)
112+
WebDbManager(name=app.config["TREE"], create_if_missing=True)
113+
114+
if app.config["TREE"] == TREE_MULTI and not app.config["MEDIA_PREFIX_TREE"]:
115+
warnings.warn(
116+
"You have enabled multi-tree support, but `MEDIA_PREFIX_TREE` is "
117+
"set to `False`. This is strongly discouraged as it exposes media "
118+
"files to users belonging to different trees!"
119+
)
112120

113121
# load JWT default settings
114122
app.config.from_object(DefaultConfigJWT)

gramps_webapi/auth/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
PERM_EDIT_OTHER_TREE_USER_ROLE = "EditOtherTreeUserRole"
3838
PERM_VIEW_OTHER_TREE_USER = "ViewOtherTreeUser"
3939
PERM_DEL_OTHER_TREE_USER = "DeleteOtherTreeUser"
40+
PERM_EDIT_USER_TREE = "EditUserTree"
4041
PERM_EDIT_OTHER_TREE = "EditOtherTree"
4142
PERM_ADD_TREE = "AddTree"
4243

@@ -100,6 +101,7 @@
100101
PERM_VIEW_OTHER_TREE_USER,
101102
PERM_EDIT_OTHER_TREE_USER,
102103
PERM_EDIT_OTHER_TREE_USER_ROLE,
104+
PERM_EDIT_USER_TREE,
103105
PERM_MAKE_ADMIN,
104106
PERM_DEL_OTHER_TREE_USER,
105107
PERM_VIEW_SETTINGS,

gramps_webapi/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727

2828
from ._version import __version__ as VERSION
2929

30+
# the value of the TREE config option that enables multi-tree support
31+
TREE_MULTI = "*"
32+
3033
# files
3134
TEST_CONFIG = resource_filename("gramps_webapi", "data/test.cfg")
3235
TEST_AUTH_CONFIG = resource_filename("gramps_webapi", "data/test_auth.cfg")
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
TREE="example_gramps"
1+
TREE="*"
22
CORS_ORIGINS="*"
33
SECRET_KEY="C2eAhXGrXVe-iljXTjnp4paeRT-m68pq"
44
USER_DB_URI="sqlite://"

gramps_webapi/data/test_auth.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
TREE="Test Web API"
1+
TREE="*"
22
CORS_ORIGINS="*"
33
SECRET_KEY="C2eAhXGrXVe-iljXTjnp4paeRT-m68pq"
44
USER_DB_URI="sqlite://"

tests/test_endpoints/__init__.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,18 @@ def setUpModule():
8181
TEST_CLIENT = test_app.test_client()
8282
with test_app.app_context():
8383
user_db.create_all()
84-
for role in TEST_USERS:
84+
db_manager = WebDbManager(name=test_db.name, create_if_missing=False)
85+
tree = db_manager.dirname
86+
87+
for role, user in TEST_USERS.items():
8588
add_user(
86-
name=TEST_USERS[role]["name"],
87-
password=TEST_USERS[role]["password"],
89+
name=user["name"],
90+
password=user["password"],
8891
role=role,
92+
tree=tree,
8993
)
90-
db_manager = WebDbManager(name=test_db.name, create_if_missing=False)
91-
db_state = db_manager.get_db()
92-
with test_app.app_context():
93-
tree = db_manager.dirname
94+
95+
db_state = db_manager.get_db()
9496
search_index = get_search_indexer(tree)
9597
db = db_state.db
9698
search_index.reindex_full(db)

0 commit comments

Comments
 (0)