From 41ef2adbab3c711e464ffe9157b72cf33c54bed9 Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Thu, 4 Sep 2025 10:13:45 -0300 Subject: [PATCH 01/10] Create SingleGroupResourceAccessControl This resource access control returns the first user group setted to the current user on the APIAccess policy --- .../authorization/resource_access.py | 53 ++++++++++++++++++- bluesky_httpserver/routers/core_api.py | 16 +++--- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/bluesky_httpserver/authorization/resource_access.py b/bluesky_httpserver/authorization/resource_access.py index a287742..a1aab3d 100644 --- a/bluesky_httpserver/authorization/resource_access.py +++ b/bluesky_httpserver/authorization/resource_access.py @@ -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,54 @@ 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 + file 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): + if group[0] in ['unauthenticated_public', 'unauthenticated_single_user']: + return self.get_resource_group(username, group) + return group[0] + 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: From c2354843c6fc22de5d75deb9cc95d27f0f2bfd2c Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Thu, 4 Sep 2025 10:23:39 -0300 Subject: [PATCH 02/10] Format code style to fit the precommit standard --- .../authorization/resource_access.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/bluesky_httpserver/authorization/resource_access.py b/bluesky_httpserver/authorization/resource_access.py index a1aab3d..b4ec88e 100644 --- a/bluesky_httpserver/authorization/resource_access.py +++ b/bluesky_httpserver/authorization/resource_access.py @@ -81,12 +81,13 @@ def get_resource_group(self, username, 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 - file as shown in the example below. + 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 ---------- @@ -95,7 +96,8 @@ class SingleGroupResourceAccessControl(DefaultResourceAccessControl): Examples -------- - Configure ``SingleGroupResourceAccessControl`` policy. The default group name is ``test_user``. + Configure ``SingleGroupResourceAccessControl`` policy. The default group name is + ``test_user``. .. code-block:: @@ -120,10 +122,7 @@ def get_resource_group(self, username, group): Name of the user group. """ if isinstance(group, list): - if group[0] in ['unauthenticated_public', 'unauthenticated_single_user']: + if group[0] in ["unauthenticated_public", "unauthenticated_single_user"]: return self.get_resource_group(username, group) return group[0] return group - - - From 2e9262840590c107b9377e70f99d15e200b1cafc Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Thu, 4 Sep 2025 10:59:39 -0300 Subject: [PATCH 03/10] Add SingleGroupResourceAccessControl to the authorization component --- bluesky_httpserver/authorization/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From fe9ff8351e42c232878487eca6629365a315ed9b Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Thu, 4 Sep 2025 13:36:37 -0300 Subject: [PATCH 04/10] Get first group setted to the user in the configuration file --- bluesky_httpserver/authorization/resource_access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bluesky_httpserver/authorization/resource_access.py b/bluesky_httpserver/authorization/resource_access.py index b4ec88e..c5a6d53 100644 --- a/bluesky_httpserver/authorization/resource_access.py +++ b/bluesky_httpserver/authorization/resource_access.py @@ -122,7 +122,7 @@ def get_resource_group(self, username, group): Name of the user group. """ if isinstance(group, list): - if group[0] in ["unauthenticated_public", "unauthenticated_single_user"]: + if group[-1] in ["unauthenticated_public", "unauthenticated_single_user"]: return self.get_resource_group(username, group) - return group[0] + return group[-1] return group From 5e6fd90944ed0a68ced8f2f59a0ce21ca3394d1b Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Thu, 4 Sep 2025 13:39:02 -0300 Subject: [PATCH 05/10] Fix recursion bug --- bluesky_httpserver/authorization/resource_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluesky_httpserver/authorization/resource_access.py b/bluesky_httpserver/authorization/resource_access.py index c5a6d53..d7c6eb8 100644 --- a/bluesky_httpserver/authorization/resource_access.py +++ b/bluesky_httpserver/authorization/resource_access.py @@ -123,6 +123,6 @@ def get_resource_group(self, username, group): """ if isinstance(group, list): if group[-1] in ["unauthenticated_public", "unauthenticated_single_user"]: - return self.get_resource_group(username, group) + return self._default_group return group[-1] return group From 323340cce516c2978721935f11fcf2ce58475e76 Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Thu, 4 Sep 2025 14:00:14 -0300 Subject: [PATCH 06/10] Fix linting --- bluesky_httpserver/authorization/resource_access.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bluesky_httpserver/authorization/resource_access.py b/bluesky_httpserver/authorization/resource_access.py index d7c6eb8..44ad91e 100644 --- a/bluesky_httpserver/authorization/resource_access.py +++ b/bluesky_httpserver/authorization/resource_access.py @@ -81,12 +81,12 @@ def get_resource_group(self, username, group): class SingleGroupResourceAccessControl(DefaultResourceAccessControl): """ Single group resource access policy. - The resource access policy associates users with its correspondent first user group. + 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 + 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 + 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 @@ -96,7 +96,7 @@ class SingleGroupResourceAccessControl(DefaultResourceAccessControl): Examples -------- - Configure ``SingleGroupResourceAccessControl`` policy. The default group name is + Configure ``SingleGroupResourceAccessControl`` policy. The default group name is ``test_user``. .. code-block:: From 313acff54663f6541db21fb32bbeeb23f47d4872 Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Thu, 4 Sep 2025 14:19:11 -0300 Subject: [PATCH 07/10] Add Single Group Resource Access Policy documentation --- docs/source/configuration.rst | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) 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 From a82ce64e0df7892c1262ebf3bb1e78f478918929 Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Fri, 5 Sep 2025 08:53:22 -0300 Subject: [PATCH 08/10] Fix DefaultResourceAccessControl unit test --- bluesky_httpserver/tests/test_access_policies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bluesky_httpserver/tests/test_access_policies.py b/bluesky_httpserver/tests/test_access_policies.py index 374d711..9b8da03 100644 --- a/bluesky_httpserver/tests/test_access_policies.py +++ b/bluesky_httpserver/tests/test_access_policies.py @@ -546,7 +546,7 @@ 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) From 5753d944b91d7f3e7e0b39fe87cbeceee5170338 Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Fri, 5 Sep 2025 08:58:49 -0300 Subject: [PATCH 09/10] Use default names in the defaults file --- bluesky_httpserver/authorization/resource_access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bluesky_httpserver/authorization/resource_access.py b/bluesky_httpserver/authorization/resource_access.py index 44ad91e..38964e2 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# @@ -122,7 +122,7 @@ def get_resource_group(self, username, group): Name of the user group. """ if isinstance(group, list): - if group[-1] in ["unauthenticated_public", "unauthenticated_single_user"]: + if group[-1] in [_DEFAULT_ROLE_PUBLIC, _DEFAULT_ROLE_SINGLE_USER]: return self._default_group return group[-1] return group From 5e337538cd15b4f08aaee21983d60e61cb3b399d Mon Sep 17 00:00:00 2001 From: "rafael.lyra" Date: Fri, 5 Sep 2025 09:30:54 -0300 Subject: [PATCH 10/10] Add SingleGroupResourceAccessControl unit tests --- .../authorization/resource_access.py | 6 +++--- .../tests/test_access_policies.py | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/bluesky_httpserver/authorization/resource_access.py b/bluesky_httpserver/authorization/resource_access.py index 38964e2..f91f706 100644 --- a/bluesky_httpserver/authorization/resource_access.py +++ b/bluesky_httpserver/authorization/resource_access.py @@ -122,7 +122,7 @@ def get_resource_group(self, username, group): Name of the user group. """ if isinstance(group, list): - if group[-1] in [_DEFAULT_ROLE_PUBLIC, _DEFAULT_ROLE_SINGLE_USER]: - return self._default_group - return group[-1] + group = group[-1] + if group in [_DEFAULT_ROLE_PUBLIC, _DEFAULT_ROLE_SINGLE_USER]: + return self._default_group return group diff --git a/bluesky_httpserver/tests/test_access_policies.py b/bluesky_httpserver/tests/test_access_policies.py index 9b8da03..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, @@ -550,3 +551,23 @@ def test_DefaultResourceAccessControl_01(params, group, success): 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