Skip to content
2 changes: 1 addition & 1 deletion bluesky_httpserver/authorization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
DictionaryAPIAccessControl,
ServerBasedAPIAccessControl,
)
from .resource_access import DefaultResourceAccessControl # noqa: F401
from .resource_access import DefaultResourceAccessControl, SingleGroupResourceAccessControl # noqa: F401
54 changes: 52 additions & 2 deletions bluesky_httpserver/authorization/resource_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -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#
Expand Down Expand Up @@ -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.

Expand All @@ -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
16 changes: 8 additions & 8 deletions bluesky_httpserver/routers/core_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 ...")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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})

Expand Down Expand Up @@ -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:
Expand Down
23 changes: 22 additions & 1 deletion bluesky_httpserver/tests/test_access_policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
DefaultResourceAccessControl,
DictionaryAPIAccessControl,
ServerBasedAPIAccessControl,
SingleGroupResourceAccessControl,
)
from bluesky_httpserver.authorization._defaults import (
_DEFAULT_RESOURCE_ACCESS_GROUP,
Expand Down Expand Up @@ -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
29 changes: 28 additions & 1 deletion docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Loading