2121
2222import datetime
2323from gettext import gettext as _
24+ from typing import Tuple
2425
2526from flask import abort , current_app , jsonify , render_template
2627from flask_jwt_extended import create_access_token , get_jwt , get_jwt_identity
2728from webargs import fields
2829
30+ from ...auth import SQLAuth
2931from ...auth .const import (
3032 CLAIM_LIMITED_SCOPE ,
33+ PERM_ADD_OTHER_TREE_USER ,
3134 PERM_ADD_USER ,
35+ PERM_DEL_OTHER_TREE_USER ,
3236 PERM_DEL_USER ,
37+ PERM_EDIT_OTHER_TREE_USER ,
38+ PERM_EDIT_OTHER_TREE_USER_ROLE ,
3339 PERM_EDIT_OTHER_USER ,
3440 PERM_EDIT_OWN_USER ,
3541 PERM_EDIT_USER_ROLE ,
42+ PERM_MAKE_ADMIN ,
43+ PERM_VIEW_OTHER_TREE_USER ,
3644 PERM_VIEW_OTHER_USER ,
45+ ROLE_ADMIN ,
3746 ROLE_DISABLED ,
3847 ROLE_OWNER ,
3948 ROLE_UNCONFIRMED ,
4049 SCOPE_CONF_EMAIL ,
41- SCOPE_CREATE_OWNER ,
50+ SCOPE_CREATE_ADMIN ,
4251 SCOPE_RESET_PW ,
4352)
44- from ..auth import require_permissions
53+ from ..auth import has_permissions , require_permissions
4554from ..ratelimiter import limiter
4655from ..tasks import (
4756 AsyncResult ,
5160 send_email_new_user ,
5261 send_email_reset_password ,
5362)
54- from ..util import use_args
63+ from ..util import get_tree_from_jwt , get_tree_id , use_args
5564from . import LimitedScopeProtectedResource , ProtectedResource , Resource
5665
5766
5867class UserChangeBase (ProtectedResource ):
5968 """Base class for user change endpoints."""
6069
61- def prepare_edit (self , user_name : str ):
70+ def prepare_edit (self , user_name : str ) -> Tuple [ SQLAuth , str , bool ] :
6271 """Cheks to do before processing the request."""
63- auth_provider = current_app .config .get ("AUTH_PROVIDER" )
72+ auth_provider : SQLAuth = current_app .config .get ("AUTH_PROVIDER" )
6473 if user_name == "-" :
6574 require_permissions ([PERM_EDIT_OWN_USER ])
6675 user_id = get_jwt_identity ()
6776 try :
6877 user_name = auth_provider .get_name (user_id )
6978 except ValueError :
7079 abort (401 )
80+ other_tree = False
7181 else :
72- require_permissions ([PERM_EDIT_OTHER_USER ])
73- return auth_provider , user_name
82+ try :
83+ user_id = auth_provider .get_guid (user_name )
84+ except ValueError ():
85+ abort (404 )
86+ source_tree = get_tree_from_jwt ()
87+ destination_tree = get_tree_id (user_id )
88+ if source_tree == destination_tree :
89+ require_permissions ([PERM_EDIT_OTHER_USER ])
90+ other_tree = False
91+ else :
92+ require_permissions ([PERM_EDIT_OTHER_TREE_USER ])
93+ other_tree = True
94+ return auth_provider , user_name , other_tree
7495
7596
7697class UsersResource (ProtectedResource ):
7798 """Resource for all users."""
7899
79100 def get (self ):
80101 """Get users' details."""
102+ auth_provider : SQLAuth = current_app .config .get ("AUTH_PROVIDER" )
103+ if has_permissions ([PERM_VIEW_OTHER_TREE_USER ]):
104+ # return all users from all trees
105+ return jsonify (auth_provider .get_all_user_details (tree = None )), 200
81106 require_permissions ([PERM_VIEW_OTHER_USER ])
82- auth_provider = current_app .config .get ("AUTH_PROVIDER" )
83- return jsonify (auth_provider .get_all_user_details ()), 200
107+ tree = get_tree_from_jwt ()
108+ # return only this tree's users
109+ return jsonify (auth_provider .get_all_user_details (tree = tree )), 200
84110
85111
86112class UserResource (UserChangeBase ):
87113 """Resource for a single user."""
88114
89115 def get (self , user_name : str ):
90116 """Get a user's details."""
91- auth_provider = current_app .config .get ("AUTH_PROVIDER" )
117+ auth_provider : SQLAuth = current_app .config .get ("AUTH_PROVIDER" )
92118 if user_name == "-" :
119+ # own user
93120 user_id = get_jwt_identity ()
94121 try :
95122 user_name = auth_provider .get_name (user_id )
96123 except ValueError :
97124 abort (401 )
98125 else :
99126 require_permissions ([PERM_VIEW_OTHER_USER ])
127+ if user_name != "_" and not has_permissions ([PERM_VIEW_OTHER_TREE_USER ]):
128+ # check if this is our tree
129+ try :
130+ user_id = auth_provider .get_guid (user_name )
131+ except ValueError :
132+ abort (404 )
133+ source_tree = get_tree_from_jwt ()
134+ destination_tree = get_tree_id (user_id )
135+ if source_tree != destination_tree :
136+ # user lives in other tree, not allowed to view
137+ abort (403 )
100138 details = auth_provider .get_user_details (user_name )
101139 if details is None :
102140 # user does not exist
@@ -113,9 +151,15 @@ def get(self, user_name: str):
113151 )
114152 def put (self , args , user_name : str ):
115153 """Update a user's details."""
116- auth_provider , user_name = self .prepare_edit (user_name )
154+ auth_provider , user_name , other_tree = self .prepare_edit (user_name )
117155 if "role" in args :
118- require_permissions ([PERM_EDIT_USER_ROLE ])
156+ if args ["role" ] >= ROLE_ADMIN :
157+ # only admins can elevate users to admins
158+ require_permissions ([PERM_MAKE_ADMIN ])
159+ if other_tree :
160+ require_permissions ([PERM_EDIT_OTHER_TREE_USER_ROLE ])
161+ else :
162+ require_permissions ([PERM_EDIT_USER_ROLE ])
119163 auth_provider .modify_user (
120164 name = user_name ,
121165 email = args .get ("email" ),
@@ -130,6 +174,7 @@ def put(self, args, user_name: str):
130174 "full_name" : fields .Str (required = True ),
131175 "password" : fields .Str (required = True ),
132176 "role" : fields .Int (required = True ),
177+ "tree" : fields .Str (required = False ),
133178 },
134179 location = "json" ,
135180 )
@@ -138,15 +183,24 @@ def post(self, args, user_name: str):
138183 if user_name == "-" :
139184 # Adding a new user does not make sense for "own" user
140185 abort (404 )
141- auth_provider = current_app .config .get ("AUTH_PROVIDER" )
142- require_permissions ([PERM_ADD_USER ])
186+ auth_provider : SQLAuth = current_app .config .get ("AUTH_PROVIDER" )
187+ if args ["role" ] >= ROLE_ADMIN :
188+ # only admins can create new admin users
189+ require_permissions ([PERM_MAKE_ADMIN ])
190+ tree = get_tree_from_jwt ()
191+ if not args .get ("tree" ) or tree == args .get ("tree" ):
192+ require_permissions ([PERM_ADD_USER ])
193+ else :
194+ require_permissions ([PERM_ADD_OTHER_TREE_USER ])
143195 try :
144196 auth_provider .add_user (
145197 name = user_name ,
146198 password = args ["password" ],
147199 email = args ["email" ],
148200 fullname = args ["full_name" ],
149201 role = args ["role" ],
202+ # use posting user's tree unless explicitly specified
203+ tree = args .get ("tree" ) or tree ,
150204 )
151205 except ValueError :
152206 abort (409 )
@@ -157,12 +211,19 @@ def delete(self, user_name: str):
157211 if user_name == "-" :
158212 # Deleting the own user is currently not allowed
159213 abort (404 )
160- auth_provider = current_app .config .get ("AUTH_PROVIDER" )
161- require_permissions ([ PERM_DEL_USER ])
214+ auth_provider : SQLAuth = current_app .config .get ("AUTH_PROVIDER" )
215+
162216 try :
163- auth_provider .delete_user (name = user_name )
217+ user_id = auth_provider .get_guid (name = user_name )
164218 except ValueError :
165219 abort (404 ) # user not found
220+ source_tree = get_tree_from_jwt ()
221+ destination_tree = get_tree_id (user_id )
222+ if source_tree == destination_tree :
223+ require_permissions ([PERM_DEL_USER ])
224+ else :
225+ require_permissions ([PERM_DEL_OTHER_TREE_USER ])
226+ auth_provider .delete_user (name = user_name )
166227 return "" , 200
167228
168229
@@ -175,6 +236,7 @@ class UserRegisterResource(Resource):
175236 "email" : fields .Str (required = True ),
176237 "full_name" : fields .Str (required = True ),
177238 "password" : fields .Str (required = True ),
239+ "tree" : fields .Str (required = False ),
178240 },
179241 location = "json" ,
180242 )
@@ -183,16 +245,20 @@ def post(self, args, user_name: str):
183245 if user_name == "-" :
184246 # Registering a new user does not make sense for "own" user
185247 abort (404 )
186- auth_provider = current_app .config .get ("AUTH_PROVIDER" )
187- # do not allow registration if no admin account exists!
188- if auth_provider .get_number_users (roles = (ROLE_OWNER ,)) == 0 :
248+ auth_provider : SQLAuth = current_app .config .get ("AUTH_PROVIDER" )
249+ # do not allow registration if no tree owner account exists!
250+ if (
251+ auth_provider .get_number_users (tree = args .get ("tree" ), roles = (ROLE_OWNER ,))
252+ == 0
253+ ):
189254 abort (405 )
190255 try :
191256 auth_provider .add_user (
192257 name = user_name ,
193258 password = args ["password" ],
194259 email = args ["email" ],
195260 fullname = args ["full_name" ],
261+ tree = args .get ("tree" ),
196262 role = ROLE_UNCONFIRMED ,
197263 )
198264 except ValueError :
@@ -212,14 +278,15 @@ def post(self, args, user_name: str):
212278
213279
214280class UserCreateOwnerResource (LimitedScopeProtectedResource ):
215- """Resource for creating an owner when the user database is empty."""
281+ """Resource for creating a site admin when the user database is empty."""
216282
217283 @limiter .limit ("1/second" )
218284 @use_args (
219285 {
220286 "email" : fields .Str (required = True ),
221287 "full_name" : fields .Str (required = True ),
222288 "password" : fields .Str (required = True ),
289+ "tree" : fields .Str (required = False ),
223290 },
224291 location = "json" ,
225292 )
@@ -228,20 +295,21 @@ def post(self, args, user_name: str):
228295 if user_name == "-" :
229296 # User name - is not allowed
230297 abort (404 )
231- auth_provider = current_app .config .get ("AUTH_PROVIDER" )
298+ auth_provider : SQLAuth = current_app .config .get ("AUTH_PROVIDER" )
232299 if auth_provider .get_number_users () > 0 :
233300 # there is already a user in the user DB
234301 abort (405 )
235302 claims = get_jwt ()
236- if claims [CLAIM_LIMITED_SCOPE ] != SCOPE_CREATE_OWNER :
303+ if claims [CLAIM_LIMITED_SCOPE ] != SCOPE_CREATE_ADMIN :
237304 # This is a wrong token!
238305 abort (403 )
239306 auth_provider .add_user (
240307 name = user_name ,
241308 password = args ["password" ],
242309 email = args ["email" ],
243310 fullname = args ["full_name" ],
244- role = ROLE_OWNER ,
311+ tree = args .get ("tree" ),
312+ role = ROLE_ADMIN ,
245313 )
246314 return "" , 201
247315
@@ -258,7 +326,7 @@ class UserChangePasswordResource(UserChangeBase):
258326 )
259327 def post (self , args , user_name : str ):
260328 """Post new password."""
261- auth_provider , user_name = self .prepare_edit (user_name )
329+ auth_provider , user_name , _ = self .prepare_edit (user_name )
262330 if len (args ["new_password" ]) == "" :
263331 abort (400 )
264332 if not auth_provider .authorized (user_name , args ["old_password" ]):
@@ -377,11 +445,13 @@ def get(self):
377445 if current_details ["role" ] == ROLE_UNCONFIRMED :
378446 # otherwise it has been confirmed already
379447 auth_provider .modify_user (name = username , role = ROLE_DISABLED )
448+ tree = get_tree_from_jwt ()
380449 run_task (
381450 send_email_new_user ,
382451 username = username ,
383452 fullname = current_details .get ("full_name" , "" ),
384453 email = claims ["email" ],
454+ tree = tree ,
385455 )
386456 title = _ ("E-mail address confirmation" )
387457 message = _ ("Thank you for confirming your e-mail address." )
0 commit comments