Revision 0d573e18
b/snf-pithos-backend/pithos/backends/modular.py | ||
---|---|---|
37 | 37 |
import hashlib |
38 | 38 |
import binascii |
39 | 39 |
|
40 |
from collections import defaultdict |
|
40 | 41 |
from functools import wraps, partial |
41 | 42 |
from traceback import format_exc |
42 | 43 |
|
... | ... | |
164 | 165 |
return wrapper |
165 | 166 |
|
166 | 167 |
|
168 |
def check_allowed_paths(action): |
|
169 |
"""Decorator for backend methods checking path access granted to user. |
|
170 |
|
|
171 |
The 1st argument of the decorated method is expected to be a |
|
172 |
ModularBackend instance, the 2nd the user performing the request and |
|
173 |
the path join of the rest arguments is supposed to be the requested path. |
|
174 |
|
|
175 |
The decorator checks whether the requested path is among the user's allowed |
|
176 |
cached paths. |
|
177 |
If this is the case, the decorator returns immediately to reduce the |
|
178 |
interactions with the database. |
|
179 |
Otherwise, it proceeds with the execution of the decorated method and if |
|
180 |
the method returns successfully (no exceptions are raised), the requested |
|
181 |
path is added to the user's cached allowed paths. |
|
182 |
|
|
183 |
:param action: (int) 0 for reads / 1 for writes |
|
184 |
:raises NotAllowedError: the user does not have access to the path |
|
185 |
""" |
|
186 |
def decorator(func): |
|
187 |
@wraps(func) |
|
188 |
def wrapper(self, *args): |
|
189 |
user = args[0] |
|
190 |
if action == self.READ: |
|
191 |
d = self.read_allowed_paths |
|
192 |
else: |
|
193 |
d = self.write_allowed_paths |
|
194 |
path = '/'.join(args[1:]) |
|
195 |
if path in d.get(user, []): |
|
196 |
return # access is already checked |
|
197 |
else: |
|
198 |
func(self, *args) # proceed with access check |
|
199 |
d[user].add(path) # add path in the allowed user paths |
|
200 |
return wrapper |
|
201 |
return decorator |
|
202 |
|
|
203 |
|
|
204 |
def list_method(func): |
|
205 |
@wraps(func) |
|
206 |
def wrapper(self, *args, **kw): |
|
207 |
marker = kw.get('marker') |
|
208 |
limit = kw.get('limit') |
|
209 |
result = func(self, *args, **kw) |
|
210 |
start, limit = self._list_limits(result, marker, limit) |
|
211 |
return result[start:start + limit] |
|
212 |
return wrapper |
|
213 |
|
|
214 |
|
|
167 | 215 |
class ModularBackend(BaseBackend): |
168 | 216 |
"""A modular backend. |
169 | 217 |
|
... | ... | |
282 | 330 |
|
283 | 331 |
self.in_transaction = False |
284 | 332 |
|
333 |
self._reset_allowed_paths() |
|
334 |
|
|
285 | 335 |
def pre_exec(self, lock_container_path=False): |
286 | 336 |
self.lock_container_path = lock_container_path |
287 | 337 |
self.wrapper.execute() |
288 | 338 |
self.serials = [] |
339 |
self._reset_allowed_paths() |
|
289 | 340 |
self.in_transaction = True |
290 | 341 |
|
291 | 342 |
def post_exec(self, success_status=True): |
... | ... | |
353 | 404 |
include_user_defined=True): |
354 | 405 |
"""Return a dictionary with the account metadata for the domain.""" |
355 | 406 |
|
407 |
self._can_read_account(user, account) |
|
356 | 408 |
path, node = self._lookup_account(account, user == account) |
357 | 409 |
if user != account: |
358 |
if until or (node is None) or (account not |
|
359 |
in self._allowed_accounts(user)): |
|
410 |
if until or (node is None): |
|
360 | 411 |
raise NotAllowedError |
361 | 412 |
try: |
362 | 413 |
props = self._get_properties(node, until) |
... | ... | |
394 | 445 |
def update_account_meta(self, user, account, domain, meta, replace=False): |
395 | 446 |
"""Update the metadata associated with the account for the domain.""" |
396 | 447 |
|
397 |
if user != account: |
|
398 |
raise NotAllowedError |
|
448 |
self._can_write_account(user, account) |
|
399 | 449 |
path, node = self._lookup_account(account, True) |
400 | 450 |
self._put_metadata(user, node, domain, meta, replace, |
401 | 451 |
update_statistics_ancestors_depth=-1) |
... | ... | |
405 | 455 |
def get_account_groups(self, user, account): |
406 | 456 |
"""Return a dictionary with the user groups defined for the account.""" |
407 | 457 |
|
458 |
self._can_read_account(user, account) |
|
408 | 459 |
if user != account: |
409 |
if account not in self._allowed_accounts(user): |
|
410 |
raise NotAllowedError |
|
411 | 460 |
return {} |
412 | 461 |
self._lookup_account(account, True) |
413 | 462 |
return self.permissions.group_dict(account) |
... | ... | |
417 | 466 |
def update_account_groups(self, user, account, groups, replace=False): |
418 | 467 |
"""Update the groups associated with the account.""" |
419 | 468 |
|
420 |
if user != account: |
|
421 |
raise NotAllowedError |
|
469 |
self._can_write_account(user, account) |
|
422 | 470 |
self._lookup_account(account, True) |
423 | 471 |
self._check_groups(groups) |
424 | 472 |
if replace: |
... | ... | |
434 | 482 |
def get_account_policy(self, user, account): |
435 | 483 |
"""Return a dictionary with the account policy.""" |
436 | 484 |
|
485 |
self._can_read_account(user, account) |
|
437 | 486 |
if user != account: |
438 |
if account not in self._allowed_accounts(user): |
|
439 |
raise NotAllowedError |
|
440 | 487 |
return {} |
441 | 488 |
path, node = self._lookup_account(account, True) |
442 | 489 |
policy = self._get_policy(node, is_account_policy=True) |
... | ... | |
450 | 497 |
def update_account_policy(self, user, account, policy, replace=False): |
451 | 498 |
"""Update the policy associated with the account.""" |
452 | 499 |
|
453 |
if user != account: |
|
454 |
raise NotAllowedError |
|
500 |
self._can_write_account(user, account) |
|
455 | 501 |
path, node = self._lookup_account(account, True) |
456 | 502 |
self._check_policy(policy, is_account_policy=True) |
457 | 503 |
self._put_policy(node, policy, replace, is_account_policy=True) |
... | ... | |
462 | 508 |
"""Create a new account with the given name.""" |
463 | 509 |
|
464 | 510 |
policy = policy or {} |
465 |
if user != account: |
|
466 |
raise NotAllowedError |
|
511 |
self._can_write_account(user, account) |
|
467 | 512 |
node = self.node.node_lookup(account) |
468 | 513 |
if node is not None: |
469 | 514 |
raise AccountExists('Account already exists') |
... | ... | |
478 | 523 |
def delete_account(self, user, account): |
479 | 524 |
"""Delete the account with the given name.""" |
480 | 525 |
|
481 |
if user != account: |
|
482 |
raise NotAllowedError |
|
526 |
self._can_write_account(user, account) |
|
483 | 527 |
node = self.node.node_lookup(account) |
484 | 528 |
if node is None: |
485 | 529 |
return |
... | ... | |
488 | 532 |
raise AccountNotEmpty('Account is not empty') |
489 | 533 |
self.permissions.group_destroy(account) |
490 | 534 |
|
535 |
# remove all the cached allowed paths |
|
536 |
# removing the specific path could be more expensive |
|
537 |
self._reset_allowed_paths() |
|
538 |
|
|
491 | 539 |
@debug_method |
492 | 540 |
@backend_method |
493 | 541 |
def list_containers(self, user, account, marker=None, limit=10000, |
494 | 542 |
shared=False, until=None, public=False): |
495 | 543 |
"""Return a list of containers existing under an account.""" |
496 | 544 |
|
545 |
self._can_read_account(user, account) |
|
497 | 546 |
if user != account: |
498 |
if until or account not in self._allowed_accounts(user):
|
|
547 |
if until: |
|
499 | 548 |
raise NotAllowedError |
500 | 549 |
allowed = self._allowed_containers(user, account) |
501 | 550 |
start, limit = self._list_limits(allowed, marker, limit) |
... | ... | |
524 | 573 |
until=None): |
525 | 574 |
"""Return a list of the container's object meta keys for a domain.""" |
526 | 575 |
|
576 |
self._can_read_container(user, account, container) |
|
527 | 577 |
allowed = [] |
528 | 578 |
if user != account: |
529 | 579 |
if until: |
530 | 580 |
raise NotAllowedError |
531 |
allowed = self.permissions.access_list_paths( |
|
532 |
user, '/'.join((account, container))) |
|
533 |
if not allowed: |
|
534 |
raise NotAllowedError |
|
535 | 581 |
path, node = self._lookup_container(account, container) |
536 | 582 |
before = until if until is not None else inf |
537 | 583 |
allowed = self._get_formatted_paths(allowed) |
... | ... | |
544 | 590 |
include_user_defined=True): |
545 | 591 |
"""Return a dictionary with the container metadata for the domain.""" |
546 | 592 |
|
593 |
self._can_read_container(user, account, container) |
|
547 | 594 |
if user != account: |
548 |
if until or container not in self._allowed_containers(user, |
|
549 |
account): |
|
595 |
if until: |
|
550 | 596 |
raise NotAllowedError |
551 | 597 |
path, node = self._lookup_container(account, container) |
552 | 598 |
props = self._get_properties(node, until) |
... | ... | |
579 | 625 |
replace=False): |
580 | 626 |
"""Update the metadata associated with the container for the domain.""" |
581 | 627 |
|
582 |
if user != account: |
|
583 |
raise NotAllowedError |
|
628 |
self._can_write_container(user, account, container) |
|
584 | 629 |
path, node = self._lookup_container(account, container) |
585 | 630 |
src_version_id, dest_version_id = self._put_metadata( |
586 | 631 |
user, node, domain, meta, replace, |
... | ... | |
597 | 642 |
def get_container_policy(self, user, account, container): |
598 | 643 |
"""Return a dictionary with the container policy.""" |
599 | 644 |
|
645 |
self._can_read_container(user, account, container) |
|
600 | 646 |
if user != account: |
601 |
if container not in self._allowed_containers(user, account): |
|
602 |
raise NotAllowedError |
|
603 | 647 |
return {} |
604 | 648 |
path, node = self._lookup_container(account, container) |
605 | 649 |
return self._get_policy(node, is_account_policy=False) |
... | ... | |
610 | 654 |
replace=False): |
611 | 655 |
"""Update the policy associated with the container.""" |
612 | 656 |
|
613 |
if user != account: |
|
614 |
raise NotAllowedError |
|
657 |
self._can_write_container(user, account, container) |
|
615 | 658 |
path, node = self._lookup_container(account, container) |
616 | 659 |
self._check_policy(policy, is_account_policy=False) |
617 | 660 |
self._put_policy(node, policy, replace, is_account_policy=False) |
... | ... | |
622 | 665 |
"""Create a new container with the given name.""" |
623 | 666 |
|
624 | 667 |
policy = policy or {} |
625 |
if user != account: |
|
626 |
raise NotAllowedError |
|
668 |
self._can_write_container(user, account, container) |
|
627 | 669 |
try: |
628 | 670 |
path, node = self._lookup_container(account, container) |
629 | 671 |
except NameError: |
... | ... | |
644 | 686 |
delimiter=None): |
645 | 687 |
"""Delete/purge the container with the given name.""" |
646 | 688 |
|
647 |
if user != account: |
|
648 |
raise NotAllowedError |
|
689 |
self._can_write_container(user, account, container) |
|
649 | 690 |
path, node = self._lookup_container(account, container) |
650 | 691 |
|
651 | 692 |
if until is not None: |
... | ... | |
714 | 755 |
paths.append(path) |
715 | 756 |
self.permissions.access_clear_bulk(paths) |
716 | 757 |
|
758 |
# remove all the cached allowed paths |
|
759 |
# removing the specific path could be more expensive |
|
760 |
self._reset_allowed_paths() |
|
761 |
|
|
717 | 762 |
def _list_objects(self, user, account, container, prefix, delimiter, |
718 | 763 |
marker, limit, virtual, domain, keys, shared, until, |
719 | 764 |
size_range, all_props, public): |
... | ... | |
878 | 923 |
version=None, include_user_defined=True): |
879 | 924 |
"""Return a dictionary with the object metadata for the domain.""" |
880 | 925 |
|
881 |
self._can_read(user, account, container, name) |
|
926 |
self._can_read_object(user, account, container, name)
|
|
882 | 927 |
path, node = self._lookup_object(account, container, name) |
883 | 928 |
props = self._get_version(node, version) |
884 | 929 |
if version is None: |
... | ... | |
916 | 961 |
replace=False): |
917 | 962 |
"""Update object metadata for a domain and return the new version.""" |
918 | 963 |
|
919 |
self._can_write(user, account, container, name) |
|
964 |
self._can_write_object(user, account, container, name)
|
|
920 | 965 |
|
921 | 966 |
path, node = self._lookup_object(account, container, name, |
922 | 967 |
lock_container=True) |
... | ... | |
999 | 1044 |
self._report_sharing_change(user, account, path, {'members': |
1000 | 1045 |
self.permissions.access_members(path)}) |
1001 | 1046 |
|
1047 |
# remove all the cached allowed paths |
|
1048 |
# filtering out only those affected could be more expensive |
|
1049 |
self._reset_allowed_paths() |
|
1050 |
|
|
1002 | 1051 |
@debug_method |
1003 | 1052 |
@backend_method |
1004 | 1053 |
def get_object_public(self, user, account, container, name): |
1005 | 1054 |
"""Return the public id of the object if applicable.""" |
1006 | 1055 |
|
1007 |
self._can_read(user, account, container, name) |
|
1056 |
self._can_read_object(user, account, container, name)
|
|
1008 | 1057 |
path = self._lookup_object(account, container, name)[0] |
1009 | 1058 |
p = self.permissions.public_get(path) |
1010 | 1059 |
return p |
... | ... | |
1014 | 1063 |
def update_object_public(self, user, account, container, name, public): |
1015 | 1064 |
"""Update the public status of the object.""" |
1016 | 1065 |
|
1017 |
self._can_write(user, account, container, name) |
|
1066 |
self._can_write_object(user, account, container, name)
|
|
1018 | 1067 |
path = self._lookup_object(account, container, name, |
1019 | 1068 |
lock_container=True)[0] |
1020 | 1069 |
if not public: |
... | ... | |
1028 | 1077 |
def get_object_hashmap(self, user, account, container, name, version=None): |
1029 | 1078 |
"""Return the object's size and a list with partial hashes.""" |
1030 | 1079 |
|
1031 |
self._can_read(user, account, container, name) |
|
1080 |
self._can_read_object(user, account, container, name)
|
|
1032 | 1081 |
path, node = self._lookup_object(account, container, name) |
1033 | 1082 |
props = self._get_version(node, version) |
1034 | 1083 |
if props[self.HASH] is None: |
... | ... | |
1042 | 1091 |
is_copy=False, report_size_change=True): |
1043 | 1092 |
if permissions is not None and user != account: |
1044 | 1093 |
raise NotAllowedError |
1045 |
self._can_write(user, account, container, name) |
|
1094 |
self._can_write_object(user, account, container, name)
|
|
1046 | 1095 |
if permissions is not None: |
1047 | 1096 |
path = '/'.join((account, container, name)) |
1048 | 1097 |
self._check_permissions(path, permissions) |
... | ... | |
1142 | 1191 |
|
1143 | 1192 |
# Update objects with greater version and same hashmap |
1144 | 1193 |
# and size (fix metadata updates). |
1145 |
self._can_write(user, account, container, name) |
|
1194 |
self._can_write_object(user, account, container, name)
|
|
1146 | 1195 |
path, node = self._lookup_object(account, container, name, |
1147 | 1196 |
lock_container=True) |
1148 | 1197 |
props = self._get_version(node, version) |
... | ... | |
1163 | 1212 |
report_size_change = not is_move |
1164 | 1213 |
dest_meta = dest_meta or {} |
1165 | 1214 |
dest_version_ids = [] |
1166 |
self._can_read(user, src_account, src_container, src_name) |
|
1215 |
self._can_read_object(user, src_account, src_container, src_name)
|
|
1167 | 1216 |
|
1168 | 1217 |
src_container_path = '/'.join((src_account, src_container)) |
1169 | 1218 |
dest_container_path = '/'.join((dest_account, dest_container)) |
... | ... | |
1353 | 1402 |
paths.append(path) |
1354 | 1403 |
self.permissions.access_clear_bulk(paths) |
1355 | 1404 |
|
1405 |
# remove all the cached allowed paths |
|
1406 |
# removing the specific path could be more expensive |
|
1407 |
self._reset_allowed_paths() |
|
1408 |
|
|
1356 | 1409 |
@debug_method |
1357 | 1410 |
@backend_method |
1358 | 1411 |
def delete_object(self, user, account, container, name, until=None, |
... | ... | |
1366 | 1419 |
def list_versions(self, user, account, container, name): |
1367 | 1420 |
"""Return a list of all object (version, version_timestamp) tuples.""" |
1368 | 1421 |
|
1369 |
self._can_read(user, account, container, name) |
|
1422 |
self._can_read_object(user, account, container, name)
|
|
1370 | 1423 |
path, node = self._lookup_object(account, container, name) |
1371 | 1424 |
versions = self.node.node_get_versions(node) |
1372 | 1425 |
return [[x[self.SERIAL], x[self.MTIME]] for x in versions if |
... | ... | |
1383 | 1436 |
path, serial = info |
1384 | 1437 |
account, container, name = path.split('/', 2) |
1385 | 1438 |
if check_permissions: |
1386 |
self._can_read(user, account, container, name) |
|
1439 |
self._can_read_object(user, account, container, name)
|
|
1387 | 1440 |
return (account, container, name) |
1388 | 1441 |
|
1389 | 1442 |
@debug_method |
... | ... | |
1395 | 1448 |
if path is None: |
1396 | 1449 |
raise NameError |
1397 | 1450 |
account, container, name = path.split('/', 2) |
1398 |
self._can_read(user, account, container, name) |
|
1451 |
self._can_read_object(user, account, container, name)
|
|
1399 | 1452 |
return (account, container, name) |
1400 | 1453 |
|
1401 | 1454 |
def get_block(self, hash): |
... | ... | |
1807 | 1860 |
|
1808 | 1861 |
return None |
1809 | 1862 |
|
1810 |
def _can_read(self, user, account, container, name): |
|
1863 |
def _reset_allowed_paths(self): |
|
1864 |
self.read_allowed_paths = defaultdict(set) |
|
1865 |
self.write_allowed_paths = defaultdict(set) |
|
1866 |
|
|
1867 |
@check_allowed_paths(action=0) |
|
1868 |
def _can_read_account(self, user, account): |
|
1869 |
if user != account: |
|
1870 |
if account not in self._allowed_accounts(user): |
|
1871 |
raise NotAllowedError |
|
1872 |
|
|
1873 |
@check_allowed_paths(action=1) |
|
1874 |
def _can_write_account(self, user, account): |
|
1875 |
if user != account: |
|
1876 |
raise NotAllowedError |
|
1877 |
|
|
1878 |
@check_allowed_paths(action=0) |
|
1879 |
def _can_read_container(self, user, account, container): |
|
1880 |
if user != account: |
|
1881 |
if container not in self._allowed_containers(user, account): |
|
1882 |
raise NotAllowedError |
|
1883 |
|
|
1884 |
@check_allowed_paths(action=1) |
|
1885 |
def _can_write_container(self, user, account, container): |
|
1886 |
if user != account: |
|
1887 |
raise NotAllowedError |
|
1888 |
|
|
1889 |
@check_allowed_paths(action=0) |
|
1890 |
def _can_read_object(self, user, account, container, name): |
|
1811 | 1891 |
if user == account: |
1812 | 1892 |
return True |
1813 | 1893 |
path = '/'.join((account, container, name)) |
... | ... | |
1820 | 1900 |
self.permissions.access_check(path, self.WRITE, user)): |
1821 | 1901 |
raise NotAllowedError |
1822 | 1902 |
|
1823 |
def _can_write(self, user, account, container, name): |
|
1903 |
@check_allowed_paths(action=1) |
|
1904 |
def _can_write_object(self, user, account, container, name): |
|
1824 | 1905 |
if user == account: |
1825 | 1906 |
return True |
1826 | 1907 |
path = '/'.join((account, container, name)) |
... | ... | |
1833 | 1914 |
def _allowed_accounts(self, user): |
1834 | 1915 |
allow = set() |
1835 | 1916 |
for path in self.permissions.access_list_paths(user): |
1836 |
allow.add(path.split('/', 1)[0]) |
|
1917 |
p = path.split('/', 1)[0] |
|
1918 |
allow.add(p) |
|
1919 |
self.read_allowed_paths[user] |= allow |
|
1837 | 1920 |
return sorted(allow) |
1838 | 1921 |
|
1839 | 1922 |
def _allowed_containers(self, user, account): |
1840 | 1923 |
allow = set() |
1841 | 1924 |
for path in self.permissions.access_list_paths(user, account): |
1842 |
allow.add(path.split('/', 2)[1]) |
|
1925 |
p = path.split('/', 2)[1] |
|
1926 |
allow.add(p) |
|
1927 |
self.read_allowed_paths[user] |= allow |
|
1843 | 1928 |
return sorted(allow) |
1844 | 1929 |
|
1845 | 1930 |
# Domain functions |
Also available in: Unified diff