diff --git a/bluesky_httpserver/authorization/__init__.py b/bluesky_httpserver/authorization/__init__.py index f76bbd6..bed077f 100644 --- a/bluesky_httpserver/authorization/__init__.py +++ b/bluesky_httpserver/authorization/__init__.py @@ -4,4 +4,4 @@ DictionaryAPIAccessControl, ServerBasedAPIAccessControl, ) -from .resource_access import DefaultResourceAccessControl # noqa: F401 +from .resource_access import DefaultResourceAccessControl, SingleGroupResourceAccessControl # noqa: F401 diff --git a/bluesky_httpserver/authorization/resource_access.py b/bluesky_httpserver/authorization/resource_access.py index a287742..f91f706 100644 --- a/bluesky_httpserver/authorization/resource_access.py +++ b/bluesky_httpserver/authorization/resource_access.py @@ -2,7 +2,7 @@ import yaml from ..config_schemas.loading import ConfigError -from ._defaults import _DEFAULT_RESOURCE_ACCESS_GROUP +from ._defaults import _DEFAULT_RESOURCE_ACCESS_GROUP, _DEFAULT_ROLE_PUBLIC, _DEFAULT_ROLE_SINGLE_USER _schema_DefaultResourceAccessControl = """ $schema": http://json-schema.org/draft-07/schema# @@ -61,7 +61,7 @@ def __init__(self, *, default_group=None): default_group = default_group or _DEFAULT_RESOURCE_ACCESS_GROUP self._default_group = default_group - def get_resource_group(self, username): + def get_resource_group(self, username, group): """ Returns the name of the user group based on the user name. @@ -76,3 +76,53 @@ def get_resource_group(self, username): Name of the user group. """ return self._default_group + + +class SingleGroupResourceAccessControl(DefaultResourceAccessControl): + """ + Single group resource access policy. + The resource access policy associates users with its correspondent first user group. + The groups define the resources, such as plans and devices users can access. The + single group policy assumes that one user belong to a single group or if they are + unauthenticated or have authenticated with a single-user API key, it uses the default + user group. + The arguments of the class constructor are the same as the one specified in the + DefaultResourceAccessControl configuration ile as shown in the example below. + + Parameters + ---------- + default_group: str + The name of the group returned by the access manager by default. + + Examples + -------- + Configure ``SingleGroupResourceAccessControl`` policy. The default group name is + ``test_user``. + + .. code-block:: + + resource_access: + policy: bluesky_httpserver.authorization:SingleGroupResourceAccessControl + args: + default_group: test_user + """ + + def get_resource_group(self, username, group): + """ + Returns the name of the user group based on the user name. + + Parameters + ---------- + username: str + User name. + + Returns + ------- + str + Name of the user group. + """ + if isinstance(group, list): + group = group[-1] + if group in [_DEFAULT_ROLE_PUBLIC, _DEFAULT_ROLE_SINGLE_USER]: + return self._default_group + return group diff --git a/bluesky_httpserver/routers/core_api.py b/bluesky_httpserver/routers/core_api.py index 3cd00aa..0389ade 100644 --- a/bluesky_httpserver/routers/core_api.py +++ b/bluesky_httpserver/routers/core_api.py @@ -199,7 +199,7 @@ async def queue_item_add_handler( principal=principal, settings=settings, api_access_manager=api_access_manager )[0] displayed_name = api_access_manager.get_displayed_user_name(username) - user_group = resource_access_manager.get_resource_group(username) + user_group = resource_access_manager.get_resource_group(username, principal.roles) payload.update({"user": displayed_name, "user_group": user_group}) if "item" not in payload: @@ -228,7 +228,7 @@ async def queue_item_execute_handler( principal=principal, settings=settings, api_access_manager=api_access_manager )[0] displayed_name = api_access_manager.get_displayed_user_name(username) - user_group = resource_access_manager.get_resource_group(username) + user_group = resource_access_manager.get_resource_group(username, principal.roles) payload.update({"user": displayed_name, "user_group": user_group}) if "item" not in payload: @@ -257,7 +257,7 @@ async def queue_item_add_batch_handler( principal=principal, settings=settings, api_access_manager=api_access_manager )[0] displayed_name = api_access_manager.get_displayed_user_name(username) - user_group = resource_access_manager.get_resource_group(username) + user_group = resource_access_manager.get_resource_group(username, principal.roles) payload.update({"user": displayed_name, "user_group": user_group}) if "items" not in payload: @@ -330,7 +330,7 @@ async def queue_upload_spreadsheet( principal=principal, settings=settings, api_access_manager=api_access_manager )[0] displayed_name = api_access_manager.get_displayed_user_name(username) - user_group = resource_access_manager.get_resource_group(username) + user_group = resource_access_manager.get_resource_group(username, principal.roles) if custom_module: logger.info("Processing spreadsheet using function from external module ...") @@ -399,7 +399,7 @@ async def queue_item_update_handler( principal=principal, settings=settings, api_access_manager=api_access_manager )[0] displayed_name = api_access_manager.get_displayed_user_name(username) - user_group = resource_access_manager.get_resource_group(username) + user_group = resource_access_manager.get_resource_group(username, principal.roles) payload.update({"user": displayed_name, "user_group": user_group}) msg = await SR.RM.item_update(**payload) @@ -719,7 +719,7 @@ async def plans_allowed_handler( username = get_current_username( principal=principal, settings=settings, api_access_manager=api_access_manager )[0] - user_group = resource_access_manager.get_resource_group(username) + user_group = resource_access_manager.get_resource_group(username, principal.roles) if "reduced" in payload: reduced = payload["reduced"] @@ -751,7 +751,7 @@ async def devices_allowed_handler( username = get_current_username( principal=principal, settings=settings, api_access_manager=api_access_manager )[0] - user_group = resource_access_manager.get_resource_group(username) + user_group = resource_access_manager.get_resource_group(username, principal.roles) payload.update({"user_group": user_group}) @@ -866,7 +866,7 @@ async def function_execute_handler( principal=principal, settings=settings, api_access_manager=api_access_manager )[0] displayed_name = api_access_manager.get_displayed_user_name(username) - user_group = resource_access_manager.get_resource_group(username) + user_group = resource_access_manager.get_resource_group(username, principal.roles) payload.update({"user": displayed_name, "user_group": user_group}) if "item" not in payload: diff --git a/bluesky_httpserver/tests/test_access_policies.py b/bluesky_httpserver/tests/test_access_policies.py index 374d711..4af592a 100644 --- a/bluesky_httpserver/tests/test_access_policies.py +++ b/bluesky_httpserver/tests/test_access_policies.py @@ -14,6 +14,7 @@ DefaultResourceAccessControl, DictionaryAPIAccessControl, ServerBasedAPIAccessControl, + SingleGroupResourceAccessControl, ) from bluesky_httpserver.authorization._defaults import ( _DEFAULT_RESOURCE_ACCESS_GROUP, @@ -546,7 +547,27 @@ def test_DefaultResourceAccessControl_01(params, group, success): """ if success: manager = DefaultResourceAccessControl(**params) - assert manager.get_resource_group("arbitrary_user_name") == group + assert manager.get_resource_group("arbitrary_user_name", group) == group else: with pytest.raises(ConfigError): DefaultResourceAccessControl(**params) + + +# fmt: off +@pytest.mark.parametrize("params, role, group, success", [ + ({"default_group": "expert"}, [_DEFAULT_ROLE_PUBLIC], "expert", True), + ({"default_group": "user"}, _DEFAULT_ROLE_SINGLE_USER, "user", True), + ({"default_group": "user"}, _DEFAULT_ROLE_SINGLE_USER, _DEFAULT_ROLE_SINGLE_USER, False), + ({"default_group": "user"}, ["expert"], "expert", True), + ({"default_group": "user"}, "advanced", "advanced", True), + ({"default_group": "user"}, "advanced", "user", False), +]) +# fmt: on +def test_SingleGroupResourceAccessControl_01(params, role, group, success): + """ + SingleGroupResourceAccessControl: basic tests. + """ + manager = SingleGroupResourceAccessControl(**params) + result = manager.get_resource_group("arbitrary_user_name", role) == group + print(manager.get_resource_group("arbitrary_user_name", role)) + assert result == success diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 0ed8796..2b16336 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -457,7 +457,6 @@ is passed to Run Engine Manager in some API calls. Default Resource Access Policy ++++++++++++++++++++++++++++++ -Only the default policy ``DefaultResourceAccessControl`` is currently implemented. This is a simple policy, which associates one fixed group name with all users. The group name used by default is ``'primary'``. ``DefaultResourceAccessControl`` with default settings is activated by default if no other policy is selected @@ -482,3 +481,31 @@ See the documentation on ``DefaultResourceAccessControl`` for more details. :toctree: generated authorization.DefaultResourceAccessControl + + +Single Group Resource Access Policy ++++++++++++++++++++++++++++++++++++ + +This is a policy that associates one group name with one user, based on the +specified user group in the access policy. +The default group name is defined in the same way as the +``DefaultResourceAccessControl``. +This functionality can be very useful in order to provide different levels +of access to different users directly in the server so all the clients +can receive the same plans and devices for a specific user. + +The default group name can be changed in the policy configuration. For example, +the following policy configuration sets the returned group name to ``test_user``:: + + resource_access: + policy: bluesky_httpserver.authorization:SingleGroupResourceAccessControl + args: + default_group: test_user + +See the documentation on ``SingleGroupResourceAccessControl`` for more details. + +.. autosummary:: + :nosignatures: + :toctree: generated + + authorization.SingleGroupResourceAccessControl