Skip to content

Commit 3b54111

Browse files
authored
Merge branch 'master' into fix-authomatic
2 parents 5de124a + c349520 commit 3b54111

11 files changed

+438
-147
lines changed

CHANGES.rst

+6
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ Changes
1111

1212
Features / Changes
1313
~~~~~~~~~~~~~~~~~~~~~
14+
* Add CLI helper ``batch_update_permissions`` that allows registering one or more `Permission` configuration files
15+
against a running `Magpie` instance.
1416
* Security fix: bump Docker base ``python:3.11-alpine3.19``.
1517
* Update ``authomatic[OpenID]==1.3.0`` to resolve temporary workarounds
1618
(relates to `authomatic/authomatic#195 <https://github.com/authomatic/authomatic/issues/195>`_
1719
and `authomatic/authomatic#233 <https://github.com/authomatic/authomatic/issues/233>`_,
1820
fixes `#583 <https://github.com/Ouranosinc/Magpie/issues/583>`_).
1921

22+
Bug Fixes
23+
~~~~~~~~~
24+
* Fix `Permission` update from configuration file using the ``requests`` code path.
25+
2026
.. _changes_4.0.0:
2127

2228
`4.0.0 <https://github.com/Ouranosinc/Magpie/tree/4.0.0>`_ (2024-04-26)

docs/conf.py

+2
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ def ignore_down_providers():
211211
# ignore false-positive broken links to local doc files used for rendering on GitHub
212212
"CHANGES.rst",
213213
r"docs/\w+.rst",
214+
"https://wso2.com/", # sporadic broken (probably robots or similar)
215+
"https://docs.wso2.com/*",
214216
"https://pcmdi.llnl.gov/", # works, but very often causes false-positive 'broken' links
215217
] + ignore_down_providers()
216218
linkcheck_anchors_ignore = [
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Magpie helper to create or delete a set of permissions.
4+
5+
When parsing permissions to create, any underlying user, group, or intermediate resource
6+
that are missing, but that can be resolved with reasonable defaults or with an explicit
7+
definition in the configuration file, will be dynamically created prior to setting the
8+
corresponding permission on it. All referenced services should exist beforehand. Consider
9+
using the 'register_providers' utility to register services beforehand as needed.
10+
11+
See https://pavics-magpie.readthedocs.io/en/latest/configuration.html#file-permissions-cfg for more details.
12+
"""
13+
import argparse
14+
from typing import TYPE_CHECKING
15+
16+
from magpie.cli.utils import make_logging_options, setup_logger_from_options
17+
from magpie.register import magpie_register_permissions_from_config
18+
from magpie.utils import get_logger
19+
20+
if TYPE_CHECKING:
21+
from typing import Any, Optional, Sequence
22+
23+
LOGGER = get_logger(__name__,
24+
message_format="%(asctime)s - %(levelname)s - %(message)s",
25+
datetime_format="%d-%b-%y %H:%M:%S", force_stdout=False)
26+
27+
ERROR_PARAMS = 2
28+
ERROR_EXEC = 1
29+
30+
31+
def make_parser():
32+
# type: () -> argparse.ArgumentParser
33+
parser = argparse.ArgumentParser(description=__doc__)
34+
parser.add_argument("-u", "--url", "--magpie-url", help=(
35+
"URL used to access the magpie service (if omitted, will try using 'MAGPIE_URL' environment variable)."
36+
))
37+
parser.add_argument("-U", "--username", "--magpie-admin-user", help=(
38+
"Admin username for magpie login (if omitted, will try using 'MAGPIE_ADMIN_USER' environment variable)."
39+
))
40+
parser.add_argument("-P", "--password", "--magpie-admin-password", help=(
41+
"Admin password for magpie login (if omitted, will try using 'MAGPIE_ADMIN_PASSWORD' environment variable)."
42+
))
43+
parser.add_argument("-c", "--config", required=True, nargs="+", action="append", help=(
44+
"Path to a single configuration file or a directory containing configuration file that contains permissions. "
45+
"The option can be specified multiple times to provide multiple lookup directories or specific files to load. "
46+
"Configuration files must be in JSON or YAML format, with their respective extensions, or the '.cfg' extension."
47+
))
48+
make_logging_options(parser)
49+
return parser
50+
51+
52+
def main(args=None, parser=None, namespace=None):
53+
# type: (Optional[Sequence[str]], Optional[argparse.ArgumentParser], Optional[argparse.Namespace]) -> Any
54+
if not parser:
55+
parser = make_parser()
56+
ns = parser.parse_args(args=args, namespace=namespace)
57+
setup_logger_from_options(LOGGER, ns)
58+
59+
all_configs = [cfg for cfg_args in ns.config for cfg in cfg_args]
60+
try:
61+
for config in all_configs:
62+
LOGGER.info("Processing: [%s]", config)
63+
magpie_register_permissions_from_config(config)
64+
except Exception as exc:
65+
LOGGER.error("Failed permissions parsing and update from specified configurations [%s].", str(exc))
66+
return ERROR_EXEC
67+
return 0
68+
69+
70+
if __name__ == "__main__":
71+
main()

magpie/constants.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def get_constant(constant_name, # type: Str
206206
5. search in environment variables
207207
208208
Parameter :paramref:`constant_name` is expected to have the format ``MAGPIE_[VARIABLE_NAME]`` although any value can
209-
be passed to retrieve generic settings from all above mentioned search locations.
209+
be passed to retrieve generic settings from all above-mentioned search locations.
210210
211211
If :paramref:`settings_name` is provided as alternative name, it is used as is to search for results if
212212
:paramref:`constant_name` was not found. Otherwise, ``magpie.[variable_name]`` is used for additional search when

magpie/register.py

+88-25
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import subprocess # nosec
66
import time
77
from tempfile import NamedTemporaryFile
8-
from typing import TYPE_CHECKING
8+
from typing import TYPE_CHECKING, overload
99

1010
import requests
1111
import six
@@ -56,18 +56,21 @@
5656
AnyCookiesType,
5757
AnyResolvedSettings,
5858
AnyResponseType,
59+
AnySettingsContainer,
5960
CombinedConfig,
6061
CookiesOrSessionType,
6162
GroupsConfig,
6263
GroupsSettings,
64+
Literal,
6365
MultiConfigs,
6466
PermissionConfigItem,
6567
PermissionsConfig,
6668
ServicesConfig,
6769
ServicesSettings,
6870
Str,
6971
UsersConfig,
70-
UsersSettings
72+
UsersSettings,
73+
WebhooksConfig
7174
)
7275

7376

@@ -572,6 +575,36 @@ def _load_config(path_or_dict, section, allow_missing=False):
572575
CONFIG_KNOWN_EXTENSIONS = frozenset([".cfg", ".json", ".yml", ".yaml"])
573576

574577

578+
@overload
579+
def get_all_configs(path_or_dict, section, allow_missing=False):
580+
# type: (Union[Str, CombinedConfig], Literal["groups"], bool) -> GroupsConfig
581+
...
582+
583+
584+
@overload
585+
def get_all_configs(path_or_dict, section, allow_missing=False):
586+
# type: (Union[Str, CombinedConfig], Literal["users"], bool) -> UsersConfig
587+
...
588+
589+
590+
@overload
591+
def get_all_configs(path_or_dict, section, allow_missing=False):
592+
# type: (Union[Str, CombinedConfig], Literal["permissions"], bool) -> PermissionsConfig
593+
...
594+
595+
596+
@overload
597+
def get_all_configs(path_or_dict, section, allow_missing=False):
598+
# type: (Union[Str, CombinedConfig], Literal["services"], bool) -> ServicesConfig
599+
...
600+
601+
602+
@overload
603+
def get_all_configs(path_or_dict, section, allow_missing=False):
604+
# type: (Union[Str, CombinedConfig], Literal["webhooks"], bool) -> WebhooksConfig
605+
...
606+
607+
575608
def get_all_configs(path_or_dict, section, allow_missing=False):
576609
# type: (Union[Str, CombinedConfig], Str, bool) -> MultiConfigs
577610
"""
@@ -776,10 +809,9 @@ def _parse_resource_path(permission_config_entry, # type: PermissionConfigItem
776809

777810
res_path = None
778811
if _use_request(cookies_or_session):
779-
res_path = get_magpie_url() + ServiceResourcesAPI.path.format(service_name=svc_name)
812+
res_path = magpie_url + ServiceResourcesAPI.path.format(service_name=svc_name)
780813
res_resp = requests.get(res_path, cookies=cookies_or_session, timeout=5)
781-
svc_json = get_json(res_resp)[svc_name] # type: JSON
782-
res_dict = svc_json["resources"]
814+
res_dict = get_json(res_resp)[svc_name] # type: JSON
783815
else:
784816
from magpie.api.management.service.service_formats import format_service_resources
785817
svc = models.Service.by_service_name(svc_name, db_session=cookies_or_session)
@@ -860,16 +892,16 @@ def _apply_request(_usr_name=None, _grp_name=None):
860892
Apply operation using HTTP request.
861893
"""
862894
action_oper = None
863-
if usr_name:
864-
action_oper = UserResourcePermissionsAPI.format(user_name=_usr_name, resource_id=resource_id)
865-
if grp_name:
866-
action_oper = GroupResourcePermissionsAPI.format(group_name=_grp_name, resource_id=resource_id)
895+
if _usr_name:
896+
action_oper = UserResourcePermissionsAPI.path.format(user_name=_usr_name, resource_id=resource_id)
897+
if _grp_name:
898+
action_oper = GroupResourcePermissionsAPI.path.format(group_name=_grp_name, resource_id=resource_id)
867899
if not action_oper:
868900
return None
869901
action_func = requests.post if create_perm else requests.delete
870902
action_body = {"permission": perm.json()}
871903
action_path = "{url}{path}".format(url=magpie_url, path=action_oper)
872-
action_resp = action_func(action_path, json=action_body, cookies=cookies_or_session)
904+
action_resp = action_func(action_path, json=action_body, cookies=cookies_or_session, timeout=5)
873905
return action_resp
874906

875907
def _apply_session(_usr_name=None, _grp_name=None):
@@ -921,10 +953,10 @@ def _apply_profile(_usr_name=None, _grp_name=None):
921953
if _use_request(cookies_or_session):
922954
if _usr_name:
923955
path = "{url}{path}".format(url=magpie_url, path=UsersAPI.path)
924-
return requests.post(path, json=usr_data, timeout=5)
956+
return requests.post(path, json=usr_data, cookies=cookies_or_session, timeout=5)
925957
if _grp_name:
926958
path = "{url}{path}".format(url=magpie_url, path=GroupsAPI.path)
927-
return requests.post(path, json=grp_data, timeout=5)
959+
return requests.post(path, json=grp_data, cookies=cookies_or_session, timeout=5)
928960
else:
929961
if _usr_name:
930962
from magpie.api.management.user.user_utils import create_user
@@ -988,13 +1020,19 @@ def _validate_response(operation, is_create, item_type="Permission"):
9881020
_validate_response(lambda: _apply_session(usr_name, None), is_create=create_perm)
9891021

9901022

991-
def magpie_register_permissions_from_config(permissions_config, magpie_url=None, db_session=None, raise_errors=False):
992-
# type: (Union[Str, PermissionsConfig], Optional[Str], Optional[Session], bool) -> None
1023+
def magpie_register_permissions_from_config(
1024+
permissions_config, # type: Union[Str, PermissionsConfig]
1025+
settings=None, # type: Optional[AnySettingsContainer]
1026+
db_session=None, # type: Optional[Session]
1027+
raise_errors=False, # type: bool
1028+
): # type: (...) -> None
9931029
"""
9941030
Applies `permissions` specified in configuration(s) defined as file, directory with files or literal configuration.
9951031
9961032
:param permissions_config: file/dir path to `permissions` config or JSON/YAML equivalent pre-loaded.
997-
:param magpie_url: URL to magpie instance (when using requests; default: `magpie.url` from this app's config).
1033+
:param settings: Magpie settings to resolve an instance session when using requests instead of DB session.
1034+
Will look for ``magpie.url``, ``magpie.admin_user`` and ``magpie.admin_password`` by default, or any
1035+
corresponding environment variable resolution if omitted in the settings.
9981036
:param db_session: db session to use instead of requests to directly create/remove permissions with config.
9991037
:param raise_errors: raises errors related to permissions, instead of just logging the info.
10001038
@@ -1003,9 +1041,9 @@ def magpie_register_permissions_from_config(permissions_config, magpie_url=None,
10031041
"""
10041042
LOGGER.info("Starting permissions processing.")
10051043

1044+
magpie_url = None
10061045
if _use_request(db_session):
1007-
magpie_url = magpie_url or get_magpie_url()
1008-
settings = {"magpie.url": magpie_url}
1046+
magpie_url = get_magpie_url(settings)
10091047
LOGGER.debug("Editing permissions using requests to [%s]...", magpie_url)
10101048
err_msg = "Invalid credentials to register Magpie permissions."
10111049
cookies_or_session = get_admin_cookies(settings, raise_message=err_msg)
@@ -1014,19 +1052,36 @@ def magpie_register_permissions_from_config(permissions_config, magpie_url=None,
10141052
cookies_or_session = db_session
10151053

10161054
LOGGER.debug("Loading configurations.")
1017-
permissions = get_all_configs(permissions_config, "permissions") # type: List[PermissionsConfig]
1055+
if isinstance(permissions_config, list):
1056+
permissions = [permissions_config]
1057+
else:
1058+
permissions = get_all_configs(permissions_config, "permissions")
10181059
perms_cfg_count = len(permissions)
10191060
LOGGER.log(logging.INFO if perms_cfg_count else logging.WARNING,
10201061
"Found %s permissions configurations.", perms_cfg_count)
10211062
users_settings = groups_settings = None
10221063
if perms_cfg_count:
1023-
users = get_all_configs(permissions_config, "users", allow_missing=True) # type: List[UsersConfig]
1024-
groups = get_all_configs(permissions_config, "groups", allow_missing=True) # type: List[GroupsConfig]
1064+
if isinstance(permissions_config, str):
1065+
users = get_all_configs(permissions_config, "users", allow_missing=True)
1066+
else:
1067+
users = []
1068+
if isinstance(permissions_config, str):
1069+
groups = get_all_configs(permissions_config, "groups", allow_missing=True)
1070+
else:
1071+
groups = []
10251072
users_settings = _resolve_config_registry(users, "username") or {}
10261073
groups_settings = _resolve_config_registry(groups, "name") or {}
10271074
for i, perms in enumerate(permissions):
10281075
LOGGER.info("Processing permissions from configuration (%s/%s).", i + 1, perms_cfg_count)
1029-
_process_permissions(perms, magpie_url, cookies_or_session, users_settings, groups_settings, raise_errors)
1076+
_process_permissions(
1077+
perms,
1078+
magpie_url,
1079+
cookies_or_session,
1080+
users_settings,
1081+
groups_settings,
1082+
settings,
1083+
raise_errors,
1084+
)
10301085
LOGGER.info("All permissions processed.")
10311086

10321087

@@ -1055,16 +1110,23 @@ def _resolve_config_registry(config_files, key):
10551110
return config_map
10561111

10571112

1058-
def _process_permissions(permissions, magpie_url, cookies_or_session, users=None, groups=None, raise_errors=False):
1059-
# type: (PermissionsConfig, Str, Session, Optional[UsersSettings], Optional[GroupsSettings], bool) -> None
1113+
def _process_permissions(
1114+
permissions, # type: PermissionsConfig
1115+
magpie_url, # type: Str
1116+
cookies_or_session, # type: Session
1117+
users=None, # type: Optional[UsersSettings]
1118+
groups=None, # type: Optional[GroupsSettings]
1119+
settings=None, # type: Optional[AnySettingsContainer]
1120+
raise_errors=False, # type: bool
1121+
): # type: (...) -> None
10601122
"""
10611123
Processes a single `permissions` configuration.
10621124
"""
10631125
if not permissions:
10641126
LOGGER.warning("Permissions configuration are empty.")
10651127
return
10661128

1067-
anon_user = get_constant("MAGPIE_ANONYMOUS_USER")
1129+
anon_user = get_constant("MAGPIE_ANONYMOUS_USER", settings)
10681130
perm_count = len(permissions)
10691131
LOGGER.log(logging.INFO if perm_count else logging.WARNING,
10701132
"Found %s permissions to evaluate from configuration.", perm_count)
@@ -1103,7 +1165,8 @@ def _process_permissions(permissions, magpie_url, cookies_or_session, users=None
11031165
if svc_resp.status_code != 200:
11041166
_handle_permission("Unknown service [{!s}]".format(svc_name), i, raise_errors=raise_errors)
11051167
continue
1106-
service_info = get_json(svc_resp)[svc_name]
1168+
service_json = get_json(svc_resp)
1169+
service_info = service_json.get(svc_name) or service_json.get("service") # format depends on magpie version
11071170
else:
11081171
transaction.commit() # force any pending transaction to be applied to find possible dependencies
11091172
svc = models.Service.by_service_name(svc_name, db_session=cookies_or_session)

setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ exclude_lines =
9595
LOGGER.error
9696
LOGGER.exception
9797
LOGGER.log
98+
@overload
99+
...
98100

99101
[tool:pytest]
100102
addopts =

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ def _extra_requirements(base_requirements, other_requirements):
247247
"console_scripts": [
248248
"magpie_cli = magpie.cli:magpie_helper_cli", # redirect to others below
249249
"magpie_helper = magpie.cli:magpie_helper_cli", # alias to helper
250+
"magpie_batch_update_permissions = magpie.cli.batch_update_permissions:main",
250251
"magpie_batch_update_users = magpie.cli.batch_update_users:main",
251252
"magpie_register_defaults = magpie.cli.register_defaults:main",
252253
"magpie_register_providers = magpie.cli.register_providers:main",

tests/interfaces.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2780,7 +2780,7 @@ def test_GetUserResourcePermissions_MultipleGroupPermissions(self):
27802780
[(effect_perm1_deny, grp1_reason), (effect_perm2_deny, PERMISSION_REASON_DEFAULT)]
27812781
)
27822782

2783-
# apply allow user permission on parent service (on level above, not same resource as previous tests)
2783+
# apply 'allow' user permission on parent service (on level above, not same resource as previous tests)
27842784
# allow user permission takes priority over deny from second group, but only during effective resolution
27852785
# even if second group deny still exists, the user allow permission takes priority as it is more specific
27862786
# during check of local inherited permissions (no recursive considered), second group deny remains the result

0 commit comments

Comments
 (0)