Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • perun/perun-proxyidp/satosacontrib-perun
1 result
Show changes
Commits on Source (3)
Showing
with 428 additions and 105 deletions
# [4.2.0](https://gitlab.ics.muni.cz/perun-proxy-aai/python/satosacontrib-perun/compare/v4.1.4...v4.2.0) (2023-10-02)
### Features
* use new resource attributes for filtering groups ([2c87dd0](https://gitlab.ics.muni.cz/perun-proxy-aai/python/satosacontrib-perun/commit/2c87dd0e8f597f0140356411d1c8730f80800f19))
## [4.1.4](https://gitlab.ics.muni.cz/perun-proxy-aai/python/satosacontrib-perun/compare/v4.1.3...v4.1.4) (2023-09-29) ## [4.1.4](https://gitlab.ics.muni.cz/perun-proxy-aai/python/satosacontrib-perun/compare/v4.1.3...v4.1.4) (2023-09-29)
......
...@@ -7,15 +7,18 @@ proxy, made by the Perun team. ...@@ -7,15 +7,18 @@ proxy, made by the Perun team.
### Seznam backend ### Seznam backend
A backend for SATOSA which implements login using [Seznam OAuth](https://partner.seznam.cz/seznam-oauth/). A backend for SATOSA which implements login
using [Seznam OAuth](https://partner.seznam.cz/seznam-oauth/).
Use the [example config](./example/plugins/backends/seznam.yaml) and fill in your client ID and client secret. Use the [example config](./example/plugins/backends/seznam.yaml) and fill in your client
ID and client secret.
## Microservices ## Microservices
### AuthSwitcher Lite ### AuthSwitcher Lite
This request microservice takes ACRs (AuthnContextClassRefs in SAML, acr_values in OIDC) from a frontend This request microservice takes ACRs (AuthnContextClassRefs in SAML, acr_values in OIDC)
from a frontend
and sets them as requested ACRs for the backends. It relies and sets them as requested ACRs for the backends. It relies
on [SATOSA!419](https://github.com/IdentityPython/SATOSA/pull/419) on [SATOSA!419](https://github.com/IdentityPython/SATOSA/pull/419)
...@@ -36,16 +39,19 @@ also needed for the satosa package until they are incorporated into the upstream ...@@ -36,16 +39,19 @@ also needed for the satosa package until they are incorporated into the upstream
### Is banned microservice ### Is banned microservice
The microservice connects to database storing user bans and redirects banned users to configured URL. The microservice connects to database storing user bans and redirects banned users to
configured URL.
### Persist authorization params microservice ### Persist authorization params microservice
This request microservice retrieves configured parameters from GET or POST request (if available) and This request microservice retrieves configured parameters from GET or POST request (if
available) and
stores the values to internal context state. stores the values to internal context state.
### Forward authorization params microservice ### Forward authorization params microservice
This request microservice retrieves configured parameters from GET or POST request (if available) and This request microservice retrieves configured parameters from GET or POST request (if
available) and
forwards them through a context parameter. Optionally, `default_values` can be forwards them through a context parameter. Optionally, `default_values` can be
provided in the config file. These will be forwarded for configured SP and/or IdP if provided in the config file. These will be forwarded for configured SP and/or IdP if
not provided in the GET/POST request. If the parameter with preconfigured not provided in the GET/POST request. If the parameter with preconfigured
...@@ -55,7 +61,8 @@ used instead of the default. ...@@ -55,7 +61,8 @@ used instead of the default.
### Session started with microservice ### Session started with microservice
This Satosa microservice checks, if configured attribute's value is present This Satosa microservice checks, if configured attribute's value is present
in "session_started_with" values (retrieved by Persist authorization params microservice). in "session_started_with" values (retrieved by Persist authorization params
microservice).
If so, adds attribute with configured name. The value is expected to be converted If so, adds attribute with configured name. The value is expected to be converted
to boolean by Attribute typing microservice. to boolean by Attribute typing microservice.
...@@ -66,23 +73,33 @@ from the internal data and runs a function configured for the ...@@ -66,23 +73,33 @@ from the internal data and runs a function configured for the
given eligibility type. The config is of format `type: function_path`. given eligibility type. The config is of format `type: function_path`.
The function should have a following signature: The function should have a following signature:
`example_func(data: InternalData, *args, **kwargs) -> timestamp | bool`, and it either returns `False` or `example_func(data: InternalData, *args, **kwargs) -> timestamp | bool`, and it either
returns `False` or
a new timestamp, in which case the time in the dictionary is a new timestamp, in which case the time in the dictionary is
updated in internal data. It strongly relies on the PerunAttributes microservice to fill the dict updated in internal data. It strongly relies on the PerunAttributes microservice to fill
beforehand. If you want to update eligibility in the IDM, use the UpdateUserExtSource microservice. the dict
beforehand. If you want to update eligibility in the IDM, use the UpdateUserExtSource
microservice.
### NameID Attribute microservice ### NameID Attribute microservice
This microservice copies SAML NameID to an internal attribute with preconfigured name (and format). This microservice copies SAML NameID to an internal attribute with preconfigured name (
and format).
It has two modes of operation. The deprecated way of configuring the microservice requires the config to It has two modes of operation. The deprecated way of configuring the microservice
include `nameid_attribute: saml2_nameid_persistent`, where `saml2_nameid_persistent` stands for the name of the SAML NameID requires the config to
attribute in format: `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent`. The new way of configuring this include `nameid_attribute: saml2_nameid_persistent`, where `saml2_nameid_persistent`
stands for the name of the SAML NameID
attribute in format: `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent`. The new way
of configuring this
microservice expects a dictionary named `nameid_attributes` containing items microservice expects a dictionary named `nameid_attributes` containing items
like `saml2_nameid_format: saml2_nameid_attribute`. In case of the newer configuration, both the format and attribute need like `saml2_nameid_format: saml2_nameid_attribute`. In case of the newer configuration,
to be configured correctly in order for the NameID attribute to be copied to the internal attribute. both the format and attribute need
to be configured correctly in order for the NameID attribute to be copied to the
internal attribute.
If one of the ways to use the microservice is configured, it will be used. If both ways are configured, the newer way of If one of the ways to use the microservice is configured, it will be used. If both ways
are configured, the newer way of
configuration - dictionary, will be used. configuration - dictionary, will be used.
Demonstrations of both ways of configuration are shown in the example config. Demonstrations of both ways of configuration are shown in the example config.
...@@ -101,7 +118,8 @@ serialized with json. User ext source and user is found by mathing this hashes. ...@@ -101,7 +118,8 @@ serialized with json. User ext source and user is found by mathing this hashes.
If not even one hash can be created, user will be redirected to error page. If not even one hash can be created, user will be redirected to error page.
If user is not found, he will be redirected to registration page. If user is not found, he will be redirected to registration page.
This microservice does not update the user in Perun with new values. To update save freshly computed values for the This microservice does not update the user in Perun with new values. To update save
freshly computed values for the
current user, you need to run `update_user_ext_source` microservice. current user, you need to run `update_user_ext_source` microservice.
[RFC - eduTEAMS Identifier Generation](https://docs.google.com/document/d/1UwnEnzFG6SM9cv6gx1AsjDXw09ZkUmkyl-NqcXg0OVo/edit#heading=h.y5g6a74d5ukn) <br/> [RFC - eduTEAMS Identifier Generation](https://docs.google.com/document/d/1UwnEnzFG6SM9cv6gx1AsjDXw09ZkUmkyl-NqcXg0OVo/edit#heading=h.y5g6a74d5ukn) <br/>
...@@ -112,9 +130,41 @@ Differences between our soulution and RFC: ...@@ -112,9 +130,41 @@ Differences between our soulution and RFC:
- The user’s home IdP entity-id does not need to be part of selection - The user’s home IdP entity-id does not need to be part of selection
- hashed values can be scoped but does not have to be - hashed values can be scoped but does not have to be
### Entitlement
Calculates Perun user's entitlements by joining `eduPersonEntitlements`,
`forwardedEduPersonEntitlements`, resource capabilities and facility capabilities.
Without the `group_entitlement_disabled` attribute set, user's entitlements are
computed based on all user's groups on facility.
When `group_entitlement_disabled` is set, resources
which have this attribute set to `true` are excluded from the computation.
This means that for a group to be included in the computation,
it needs to be assigned through at least one resource without this attribute set to
`true`.
### SP Authorization
Provides group based access control.
Without the `proxy_access_control_disabled` attribute set, this microservice denies access
to users who are not members of any group assigned to any resource on the current
facility.
If the `proxy_access_control_disabled` attribute is defined in config, resources which
have this attribute set to true are excluded. Groups assigned to these resources are
not counted towards user's groups on the current facility. This means, that the user
has to be a member of at least one group assigned to a resource without this
attribute set.
This feature allows computing entitlements while excluding groups which should not be
allowed to access the current service.
## Addons for SATOSA-oidcop frontend ## Addons for SATOSA-oidcop frontend
### userinfo_perun_updates ### userinfo_perun_updates
SATOSA by default caches user data in subsequent calls of the userinfo endpoint. SATOSA by default caches user data in subsequent calls of the userinfo endpoint.
Thanks to this addon the data is loaded from Perun IDM on each call so they are always up to date. Thanks to this addon the data is loaded from Perun IDM on each call so they are always
up to date.
...@@ -14,4 +14,5 @@ config: ...@@ -14,4 +14,5 @@ config:
entitlement_prefix: "prefix:including:separator:" entitlement_prefix: "prefix:including:separator:"
entitlement_authority: authorityWithoutSeparator entitlement_authority: authorityWithoutSeparator
# release_forwarded_entitlement: True # release_forwarded_entitlement: True
forwarded_eduperson_entitlement: "perun:attr:with:forwardedEduPersonEntitlement" # required unless release_forwarded_entitlement is False forwarded_eduperson_entitlement: "perun:attr:with:forwardedEduPersonEntitlement" # REQUIRED unless release_forwarded_entitlement is False
group_entitlement_disabled: "perun:attr:with:groupEntitlementDisabled" # OPTIONAL - leaving it out will skip filtering based on this attribute
...@@ -14,6 +14,7 @@ config: ...@@ -14,6 +14,7 @@ config:
- registrar_url: registrar_url - registrar_url: registrar_url
- allow_registration_attr: allow_registration_attr - allow_registration_attr: allow_registration_attr
- registration_link_attr: registration_link_attr - registration_link_attr: registration_link_attr
- proxy_access_control_disabled: "perun:attr:with:proxyAccessControlDisabled" # OPTIONAL - leaving it out will skip filtering based on this attribute
- skip_notification_sps: - skip_notification_sps:
- sp1 - sp1
- sp2 - sp2
......
...@@ -29,5 +29,5 @@ class Entitlement(ResponseMicroService): ...@@ -29,5 +29,5 @@ class Entitlement(ResponseMicroService):
if not Utils.allow_by_requester(context, data, self.__allowed_requesters): if not Utils.allow_by_requester(context, data, self.__allowed_requesters):
return super().process(context, data) return super().process(context, data)
data = self.__utils.update_entitlements(self, data) data = self.__utils.update_entitlements(data)
return super().process(context, data) return super().process(context, data)
...@@ -39,6 +39,7 @@ class SpAuthorization(ResponseMicroService): ...@@ -39,6 +39,7 @@ class SpAuthorization(ResponseMicroService):
self.__VO_SHORT_NAMES_ATTR = "vo_short_names_attr" self.__VO_SHORT_NAMES_ATTR = "vo_short_names_attr"
self.__ALLOW_REGISTRATION_ATTR = "allow_registration_attr" self.__ALLOW_REGISTRATION_ATTR = "allow_registration_attr"
self.__REGISTRATION_LINK_ATTR = "registration_link_attr" self.__REGISTRATION_LINK_ATTR = "registration_link_attr"
self.__PROXY_ACCESS_CONTROL_DISABLED = "proxy_access_control_disabled"
self.__REGISTRAR_URL = "registrar_url" self.__REGISTRAR_URL = "registrar_url"
self.__SKIP_NOTIFICATION_SPS = "skip_notification_sps" self.__SKIP_NOTIFICATION_SPS = "skip_notification_sps"
self.__HANDLE_UNSATISFIED_MEMBERSHIP = "handle_unsatisfied_membership" self.__HANDLE_UNSATISFIED_MEMBERSHIP = "handle_unsatisfied_membership"
...@@ -70,6 +71,9 @@ class SpAuthorization(ResponseMicroService): ...@@ -70,6 +71,9 @@ class SpAuthorization(ResponseMicroService):
self.__registration_link_attr = self.__filter_config[ self.__registration_link_attr = self.__filter_config[
self.__REGISTRATION_LINK_ATTR self.__REGISTRATION_LINK_ATTR
] ]
self.__proxy_access_control_disabled = self.__filter_config.get(
self.__PROXY_ACCESS_CONTROL_DISABLED
)
self.__skip_notification_sps = self.__filter_config[ self.__skip_notification_sps = self.__filter_config[
self.__SKIP_NOTIFICATION_SPS self.__SKIP_NOTIFICATION_SPS
] ]
...@@ -81,7 +85,9 @@ class SpAuthorization(ResponseMicroService): ...@@ -81,7 +85,9 @@ class SpAuthorization(ResponseMicroService):
attrs_map = ConfigStore.get_attributes_map( attrs_map = ConfigStore.get_attributes_map(
self.__global_config["attrs_cfg_path"] self.__global_config["attrs_cfg_path"]
) )
self.__adapters_manager = AdaptersManager(adapters_manager_cfg, attrs_map) self.__adapters_manager = AdaptersManager(adapters_manager_cfg, attrs_map)
self.__utils = Utils(self.__adapters_manager)
is_missing_registration_data = not ( is_missing_registration_data = not (
self.__registration_link_attr and self.__registrar_url self.__registration_link_attr and self.__registrar_url
...@@ -122,9 +128,7 @@ class SpAuthorization(ResponseMicroService): ...@@ -122,9 +128,7 @@ class SpAuthorization(ResponseMicroService):
) )
return self.unauthorized() return self.unauthorized()
facility = self.__access_adapters_manager( facility = self.__adapters_manager.get_facility_by_rp_identifier(data_requester)
self.__adapters_manager.get_facility_by_rp_identifier, data_requester
)
if not facility: if not facility:
logger.debug( logger.debug(
...@@ -146,10 +150,10 @@ class SpAuthorization(ResponseMicroService): ...@@ -146,10 +150,10 @@ class SpAuthorization(ResponseMicroService):
logger.warning("Group membership check not requested by the service.") logger.warning("Group membership check not requested by the service.")
return return
user_groups = self.__access_adapters_manager( eligible_user_groups = self.__utils.get_eligible_groups(
self.__adapters_manager.get_users_groups_on_facility, facility, user_id facility, user_id, self.__proxy_access_control_disabled
) )
if not user_groups: if not eligible_user_groups:
self.handle_unsatisfied_membership( self.handle_unsatisfied_membership(
context, context,
data, data,
...@@ -584,7 +588,9 @@ class SpAuthorization(ResponseMicroService): ...@@ -584,7 +588,9 @@ class SpAuthorization(ResponseMicroService):
if group_name == PerunConstants.GROUP_MEMBERS or has_registration_form: if group_name == PerunConstants.GROUP_MEMBERS or has_registration_form:
registration_data.append(sp_group) registration_data.append(sp_group)
logger.debug( logger.debug(
f"Group '{sp_group.unique_name}' added to " "the registration list." f"Group '{sp_group.unique_name}' added to "
"the "
"registration list."
) )
return registration_data return registration_data
......
...@@ -145,8 +145,7 @@ class UpdateUserExtSource(ResponseMicroService): ...@@ -145,8 +145,7 @@ class UpdateUserExtSource(ResponseMicroService):
except KeyError: except KeyError:
self.__logger.warning( self.__logger.warning(
self.__class__.__name__ self.__class__.__name__
+ "Updating UES for user with userId:" + "Updating UES for user with userId: "
+ " "
+ str(user_id) + str(user_id)
+ "was not successful." + "was not successful."
) )
......
from perun.connector.utils.Logger import Logger import os
from perun.connector.adapters.AdaptersManager import AdaptersManager
from satosa.exception import SATOSAError
from re import sub from re import sub
from natsort import natsorted from typing import Optional
from urllib.parse import quote from urllib.parse import quote
import os
import yaml import yaml
from natsort import natsorted
from perun.connector import Facility, AdaptersManager
from perun.connector.utils.Logger import Logger
from satosa.exception import SATOSAError
from satosa.internal import InternalData
from satosacontrib.perun.utils.ConfigStore import ConfigStore from satosacontrib.perun.utils.ConfigStore import ConfigStore
from satosacontrib.perun.utils.Utils import Utils
def encode_entitlement(group_name): def encode_entitlement(group_name):
...@@ -29,18 +33,20 @@ class EntitlementUtils: ...@@ -29,18 +33,20 @@ class EntitlementUtils:
def __init__(self, config=None): def __init__(self, config=None):
self.__logger = Logger.get_logger(self.__class__.__name__) self.__logger = Logger.get_logger(self.__class__.__name__)
self.__DEBUG_PREFIX = self.__class__.__name__
self.OUTPUT_ATTR_NAME = "output_attr_name" self.OUTPUT_ATTR_NAME = "output_attr_name"
self.RELEASE_FORWARDED_ENTITLEMENT = "release_forwarded_entitlement" self.RELEASE_FORWARDED_ENTITLEMENT = "release_forwarded_entitlement"
self.FORWARDED_EDUPERSON_ENTITLEMENT = "forwarded_eduperson_entitlement" self.FORWARDED_EDUPERSON_ENTITLEMENT = "forwarded_eduperson_entitlement"
self.ENTITLEMENT_PREFIX_ATTR = "entitlement_prefix" self.ENTITLEMENT_PREFIX_ATTR = "entitlement_prefix"
self.ENTITLEMENT_AUTHORITY_ATTR = "entitlement_authority" self.ENTITLEMENT_AUTHORITY_ATTR = "entitlement_authority"
self.GROUP_NAME_AARC_ATTR = "group_name_AARC" self.GROUP_NAME_AARC_ATTR = "group_name_AARC"
self.GROUP_ENTITLEMENT_DISABLED_ATTR = "group_entitlement_disabled"
if config is None: if config is None:
config = self.__load_cfg() config = self.__load_cfg()
self.__config = config self.__config = config
self.__group_mapping = self.__config["group_mapping"] self.__group_mapping = self.__config.get("group_mapping")
self.__extended = self.__config.get("entitlement_extended", False) in [ self.__extended = self.__config.get("entitlement_extended", False) in [
True, True,
"True", "True",
...@@ -49,7 +55,7 @@ class EntitlementUtils: ...@@ -49,7 +55,7 @@ class EntitlementUtils:
1, 1,
] ]
self.__global_cfg = ConfigStore.get_global_cfg(config["global_cfg_path"]) self.__global_cfg = ConfigStore.get_global_cfg(config.get("global_cfg_path"))
self.__attr_map_cfg = ConfigStore.get_attributes_map( self.__attr_map_cfg = ConfigStore.get_attributes_map(
self.__global_cfg["attrs_cfg_path"] self.__global_cfg["attrs_cfg_path"]
...@@ -58,6 +64,10 @@ class EntitlementUtils: ...@@ -58,6 +64,10 @@ class EntitlementUtils:
self.__adapters_manager = AdaptersManager( self.__adapters_manager = AdaptersManager(
self.__global_cfg["adapters_manager"], self.__attr_map_cfg self.__global_cfg["adapters_manager"], self.__attr_map_cfg
) )
# TODO maybe use inheritance EntitlementUtils(Utils) instead of this instance
self.__utils = Utils(self.__adapters_manager)
self.__output_attr_name = self.__config.get( self.__output_attr_name = self.__config.get(
self.OUTPUT_ATTR_NAME, "eduperson_entitlement" self.OUTPUT_ATTR_NAME, "eduperson_entitlement"
) )
...@@ -70,13 +80,19 @@ class EntitlementUtils: ...@@ -70,13 +80,19 @@ class EntitlementUtils:
self.FORWARDED_EDUPERSON_ENTITLEMENT self.FORWARDED_EDUPERSON_ENTITLEMENT
) )
self.__group_name_aarc = self.__config[self.GROUP_NAME_AARC_ATTR] self.__group_name_aarc = self.__config.get(self.GROUP_NAME_AARC_ATTR)
self.__entitlement_prefix = self.__config.get(self.ENTITLEMENT_PREFIX_ATTR)
self.__entitlement_prefix = self.__config[self.ENTITLEMENT_PREFIX_ATTR] self.__entitlement_authority = self.__config.get(
self.ENTITLEMENT_AUTHORITY_ATTR
)
self.__entitlement_authority = self.__config[self.ENTITLEMENT_AUTHORITY_ATTR] self.__group_entitlement_disabled = self.__config.get(
self.GROUP_ENTITLEMENT_DISABLED_ATTR
)
def update_entitlements(self, data): def update_entitlements(self, data: InternalData):
""" """
This method updates all entitlement related data stored in `data` This method updates all entitlement related data stored in `data`
...@@ -121,6 +137,18 @@ class EntitlementUtils: ...@@ -121,6 +137,18 @@ class EntitlementUtils:
data.attributes[self.__output_attr_name] = values data.attributes[self.__output_attr_name] = values
return data return data
def __get_facility_from_sp(self, data: InternalData) -> Optional[Facility]:
rp_id = data.requester
facility = self.__adapters_manager.get_facility_by_rp_identifier(rp_id)
if not facility:
self.__logger.debug(
f"No facility found for SP '{data.requester}', skipping "
"processing filter."
)
return facility
def __get_eduperson_entitlement(self, data): def __get_eduperson_entitlement(self, data):
""" """
This method gets entitlements of groups stored in `data` This method gets entitlements of groups stored in `data`
...@@ -131,31 +159,39 @@ class EntitlementUtils: ...@@ -131,31 +159,39 @@ class EntitlementUtils:
eduperson_entitlement = [] eduperson_entitlement = []
groups = data.data["perun"]["groups"] facility = self.__get_facility_from_sp(data)
for group in groups: user_id = data.attributes.get(self.__global_cfg["perun_user_id_attribute"])
group_name = group.unique_name if facility and user_id:
group_name = sub(r"^(\w*):members$", r"\1", group_name) eligible_groups = self.__utils.get_eligible_groups(
facility, user_id, self.__group_entitlement_disabled
if self.__config["group_name_AARC"] or self.__group_name_aarc: )
if not self.__entitlement_authority or not self.__entitlement_prefix: for group in eligible_groups:
raise SATOSAError( group_name = group.unique_name
"perun:Entitlement: missing " group_name = sub(r"^(\w*):members$", r"\1", group_name)
"mandatory configuration options "
"'groupNameAuthority' " if self.__config["group_name_AARC"] or self.__group_name_aarc:
"or 'groupNamePrefix'." if (
) not self.__entitlement_authority
or not self.__entitlement_prefix
group_name = self.__group_name_wrapper(group_name) ):
else: raise SATOSAError(
group_name = self.__map_group_name(group_name, data.requester) "perun:Entitlement: missing "
"mandatory configuration options "
eduperson_entitlement.append(group_name) "'groupNameAuthority' "
"or 'groupNamePrefix'."
)
group_name = self.__group_name_wrapper(group_name)
else:
group_name = self.__map_group_name(group_name, data.requester)
eduperson_entitlement.append(group_name)
natsorted(eduperson_entitlement) natsorted(eduperson_entitlement)
return eduperson_entitlement return eduperson_entitlement
def __get_eduperson_entitlement_extended(self, data): def __get_eduperson_entitlement_extended(self, data: InternalData):
""" """
This method gets entitlements of groups stored in `data` This method gets entitlements of groups stored in `data`
in extended mode in extended mode
...@@ -315,9 +351,12 @@ class EntitlementUtils: ...@@ -315,9 +351,12 @@ class EntitlementUtils:
) )
def __group_entitlement_with_attributes_wrapper(self, group_uuid, group_name): def __group_entitlement_with_attributes_wrapper(self, group_uuid, group_name):
return "{prefix}groupAttributes:{uuid}?=displayName={name}#{authority}".format( return (
prefix=self.__entitlement_prefix, "{prefix}groupAttributes:{uuid}?=displayName={name}#{"
uuid=group_uuid, "authority}".format(
name=encode_name(group_name), prefix=self.__entitlement_prefix,
authority=self.__entitlement_authority, uuid=group_uuid,
name=encode_name(group_name),
authority=self.__entitlement_authority,
)
) )
...@@ -5,10 +5,12 @@ import logging ...@@ -5,10 +5,12 @@ import logging
import random import random
import string import string
import time import time
from typing import List
import requests import requests
from jwcrypto import jwk, jwt from jwcrypto import jwk, jwt
from jwcrypto.jwk import JWKSet, JWK from jwcrypto.jwk import JWKSet, JWK
from perun.connector import AdaptersManager, Resource, Facility, User, Group
from perun.connector.utils.Logger import Logger from perun.connector.utils.Logger import Logger
from satosa.context import Context from satosa.context import Context
from satosa.internal import InternalData from satosa.internal import InternalData
...@@ -16,6 +18,51 @@ from satosa.response import Redirect ...@@ -16,6 +18,51 @@ from satosa.response import Redirect
class Utils: class Utils:
def __init__(self, adapters_manager: AdaptersManager):
self.__adapters_manager = adapters_manager
def __is_resource_applicable(
self, resource: Resource, applicability_attribute: str
) -> bool:
result = self.__adapters_manager.get_resource_attributes(
resource, [applicability_attribute]
)
if not result:
return False
is_resource_disabled = list(result.values())[0]
return not is_resource_disabled
def get_eligible_groups(
self, facility: Facility, user: [User, int], applicability_attribute: str
) -> List[Group]:
user_groups = self.__adapters_manager.get_users_groups_on_facility(
facility, user
)
all_facility_resources = self.__adapters_manager.get_resources_for_facility(
facility
)
applicable_facility_resources = all_facility_resources
if applicability_attribute:
applicable_facility_resources = list(
filter(
lambda my_resource: self.__is_resource_applicable(
my_resource, applicability_attribute
),
all_facility_resources,
)
)
applicable_groups = []
for resource in applicable_facility_resources:
applicable_groups += self.__adapters_manager.get_groups_for_resource(
resource
)
return list(set(user_groups).intersection(set(applicable_groups)))
@staticmethod @staticmethod
def generate_nonce() -> str: def generate_nonce() -> str:
letters = string.ascii_lowercase letters = string.ascii_lowercase
...@@ -133,11 +180,13 @@ class Utils: ...@@ -133,11 +180,13 @@ class Utils:
) -> bool: ) -> bool:
""" """
Checks whether the requester for target entity is allowed to use Perun. Checks whether the requester for target entity is allowed to use Perun.
The rules are defined by either allow or deny list. All requesters not present The rules are defined by either allow or deny list. All requesters
not present
in the allow (deny) list are implicitly denied (allowed). in the allow (deny) list are implicitly denied (allowed).
@param data: the Internal Data @param data: the Internal Data
@param context: the request context @param context: the request context
@param allowed_cfg: the dictionary of either deny or allow requesters for @param allowed_cfg: the dictionary of either deny or allow
requesters for
given entity given entity
@return: True if allowed False otherwise @return: True if allowed False otherwise
""" """
...@@ -151,7 +200,8 @@ class Utils: ...@@ -151,7 +200,8 @@ class Utils:
allow_rules = target_specific_rules.get("allow") allow_rules = target_specific_rules.get("allow")
if allow_rules: if allow_rules:
logger.debug( logger.debug(
"Requester '{0}' is {2} allowed for '{1}' due to allow rules".format( "Requester '{0}' is {2} allowed for '{1}' due to allow "
"rules".format(
data.requester, data.requester,
target_entity_id, target_entity_id,
"" if data.requester in allow_rules else "not", "" if data.requester in allow_rules else "not",
...@@ -161,7 +211,8 @@ class Utils: ...@@ -161,7 +211,8 @@ class Utils:
deny_rules = target_specific_rules.get("deny") deny_rules = target_specific_rules.get("deny")
if deny_rules: if deny_rules:
logger.debug( logger.debug(
"Requester '{0}' is {2} allowed for '{1}' due to deny rules".format( "Requester '{0}' is {2} allowed for '{1}' due to deny "
"rules".format(
data.requester, data.requester,
target_entity_id, target_entity_id,
"not" if data.requester in deny_rules else "", "not" if data.requester in deny_rules else "",
...@@ -169,9 +220,8 @@ class Utils: ...@@ -169,9 +220,8 @@ class Utils:
) )
return data.requester not in deny_rules return data.requester not in deny_rules
logger.debug( logger.debug(
"Requester '{}' is not allowed for '{}' due to final deny all rule".format( "Requester '{}' is not allowed for '{}' due to final deny all "
data.requester, target_entity_id "rule".format(data.requester, target_entity_id)
)
) )
return False return False
......
[metadata] [metadata]
version = 4.1.4 version = 4.2.0
license_files = LICENSE license_files = LICENSE
...@@ -19,7 +19,7 @@ setup( ...@@ -19,7 +19,7 @@ setup(
"SATOSA~=8.1", "SATOSA~=8.1",
"pysaml2~=7.1", "pysaml2~=7.1",
"requests~=2.28", "requests~=2.28",
"perun.connector~=3.7", "perun.connector~=3.8",
"PyYAML~=6.0", "PyYAML~=6.0",
"SQLAlchemy~=2.0", "SQLAlchemy~=2.0",
"jwcrypto~=1.3", "jwcrypto~=1.3",
......
...@@ -248,7 +248,7 @@ def test_handle_user_expired_user( ...@@ -248,7 +248,7 @@ def test_handle_user_expired_user(
) )
EnsureMember.register = MagicMock(return_value=None) EnsureMember.register = MagicMock(return_value=None)
message = "perun:EnsureMember: User is expired " "- sending to registration." message = "perun:EnsureMember: User is expired - sending to registration."
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
result = TEST_INSTANCE._EnsureMember__handle_user( result = TEST_INSTANCE._EnsureMember__handle_user(
...@@ -384,7 +384,7 @@ def test_process_error(mock_request): ...@@ -384,7 +384,7 @@ def test_process_error(mock_request):
AdaptersManager.get_vo = MagicMock(return_value=None) AdaptersManager.get_vo = MagicMock(return_value=None)
expected_error_message = ( expected_error_message = (
"perun:EnsureMember: VO with" " vo_short_name 'vo_short_name' not found." "perun:EnsureMember: VO with vo_short_name 'vo_short_name' not found."
) )
with pytest.raises(SATOSAError) as error: with pytest.raises(SATOSAError) as error:
......
from unittest.mock import patch, MagicMock
import pytest
from perun.connector.adapters.AdaptersManager import AdaptersManager from perun.connector.adapters.AdaptersManager import AdaptersManager
from perun.connector.models.User import User
from perun.connector.models.Group import Group from perun.connector.models.Group import Group
from perun.connector.models.User import User
from perun.connector.models.VO import VO from perun.connector.models.VO import VO
from tests.test_microservice_loader import Loader, TestContext, TestData
from satosacontrib.perun.utils.ConfigStore import ConfigStore
from satosacontrib.perun.utils.ConfigStore import ConfigStore
from satosacontrib.perun.utils.EntitlementUtils import EntitlementUtils from satosacontrib.perun.utils.EntitlementUtils import EntitlementUtils
from unittest.mock import patch, MagicMock from satosacontrib.perun.utils.Utils import Utils
from tests.test_microservice_loader import Loader, TestContext, TestData
import pytest
TEST_VO = VO(1, "vo", "vo_short_name") TEST_VO = VO(1, "vo", "vo_short_name")
TEST_GROUP_1 = Group(1, TEST_VO, "uuid", "group1", "group1", "") TEST_GROUP_1 = Group(1, TEST_VO, "uuid", "group1", "group1", "")
...@@ -204,34 +204,46 @@ def test_get_eduperson_entitlement_extended(): ...@@ -204,34 +204,46 @@ def test_get_eduperson_entitlement_extended():
assert expected_result == result assert expected_result == result
def test_get_eduperson_entitlement(): @patch("satosacontrib.perun.utils.Utils.AdaptersManager.get_facility_by_rp_identifier")
@patch("satosacontrib.perun.utils.Utils.Utils.get_eligible_groups")
def test_get_eduperson_entitlement(mock_request1, mock_request2):
expected_result = ["prefix:group:group1#authority", "prefix:group:group2#authority"] expected_result = ["prefix:group:group1#authority", "prefix:group:group2#authority"]
AdaptersManager.get_facility_by_rp_identifier = MagicMock(
return_value="example facility"
)
Utils.get_eligible_groups = MagicMock(return_value=DATA["perun"]["groups"])
result = TEST_INSTANCE._EntitlementUtils__get_eduperson_entitlement( result = TEST_INSTANCE._EntitlementUtils__get_eduperson_entitlement(
TestData(DATA, None) TestData(DATA, {"example_user_id": "user_id_attr"})
) # noqa ) # noqa
assert expected_result == result assert result == expected_result
@patch("satosacontrib.perun.utils.Utils.AdaptersManager.get_facility_by_rp_identifier")
@patch("satosacontrib.perun.utils.Utils.Utils.get_eligible_groups")
def test_get_eduperson_entitlement_exception(mock_request1, mock_request2):
AdaptersManager.get_facility_by_rp_identifier = MagicMock(
return_value="example facility"
)
Utils.get_eligible_groups = MagicMock(return_value=DATA["perun"]["groups"])
def test_get_eduperson_entitlement_exception():
expected_error_message = ( expected_error_message = (
"perun:Entitlement: missing " "perun:Entitlement: missing "
"mandatory configuration options " "mandatory configuration options "
"'groupNameAuthority' " "'groupNameAuthority' "
"or 'groupNamePrefix'." "or 'groupNamePrefix'."
) )
with pytest.raises(Exception) as error: with pytest.raises(Exception) as error:
_ = TEST_INSTANCE_WITHOUT_PREFIX._EntitlementUtils__get_eduperson_entitlement( _ = TEST_INSTANCE_WITHOUT_PREFIX._EntitlementUtils__get_eduperson_entitlement(
TestData(DATA, None) TestData(DATA, {"example_user_id": "user_id_attr"})
) )
assert str(error.value.args[0]) == expected_error_message assert str(error.value.args[0]) == expected_error_message
with pytest.raises(Exception) as error: with pytest.raises(Exception) as error:
_ = TEST_INSTANCE_WITHOUT_AUTHORITY._EntitlementUtils__get_eduperson_entitlement( _ = TEST_INSTANCE_WITHOUT_AUTHORITY._EntitlementUtils__get_eduperson_entitlement(
TestData(DATA, None) TestData(DATA, {"example_user_id": "user_id_attr"})
) )
assert str(error.value.args[0]) == expected_error_message assert str(error.value.args[0]) == expected_error_message
...@@ -4,7 +4,6 @@ from unittest.mock import patch, MagicMock ...@@ -4,7 +4,6 @@ from unittest.mock import patch, MagicMock
import pytest import pytest
from perun.connector import MemberStatusEnum, Group, VO from perun.connector import MemberStatusEnum, Group, VO
from perun.connector.adapters.AdaptersManager import AdaptersManager
from satosa.context import Context from satosa.context import Context
from satosa.exception import SATOSAError from satosa.exception import SATOSAError
from satosa.internal import InternalData from satosa.internal import InternalData
...@@ -15,7 +14,7 @@ from satosacontrib.perun.micro_services.idm.sp_authorization import ( ...@@ -15,7 +14,7 @@ from satosacontrib.perun.micro_services.idm.sp_authorization import (
SpAuthorization, SpAuthorization,
) )
from satosacontrib.perun.utils.PerunConstants import PerunConstants from satosacontrib.perun.utils.PerunConstants import PerunConstants
from satosacontrib.perun.utils.Utils import Utils from satosacontrib.perun.utils.Utils import Utils, AdaptersManager
from tests.test_microservice_loader import Loader, TestContext from tests.test_microservice_loader import Loader, TestContext
MICROSERVICE_CONFIG = { MICROSERVICE_CONFIG = {
...@@ -28,6 +27,7 @@ MICROSERVICE_CONFIG = { ...@@ -28,6 +27,7 @@ MICROSERVICE_CONFIG = {
"registration_link_attr": "example_attr", "registration_link_attr": "example_attr",
"skip_notification_sps": ["sp1", "sp2", "sp3"], "skip_notification_sps": ["sp1", "sp2", "sp3"],
"handle_unsatisfied_membership": True, "handle_unsatisfied_membership": True,
"proxy_access_control_disabled": "example_attr",
}, },
"unauthorized_redirect_url": "example_url", "unauthorized_redirect_url": "example_url",
"single_group_registration_url": "example_url", "single_group_registration_url": "example_url",
...@@ -171,12 +171,10 @@ def test_process_group_membership_check_not_required( ...@@ -171,12 +171,10 @@ def test_process_group_membership_check_not_required(
@patch( @patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager" "satosacontrib.perun.utils.Utils.AdaptersManager" ".get_facility_by_rp_identifier"
".get_facility_by_rp_identifier"
) )
@patch( @patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager" "satosacontrib.perun.utils.Utils.AdaptersManager" ".get_users_groups_on_facility"
".get_users_groups_on_facility"
) )
@patch( @patch(
"satosacontrib.perun.micro_services.idm.sp_authorization.SpAuthorization" # noqa "satosacontrib.perun.micro_services.idm.sp_authorization.SpAuthorization" # noqa
...@@ -186,8 +184,9 @@ def test_process_group_membership_check_not_required( ...@@ -186,8 +184,9 @@ def test_process_group_membership_check_not_required(
"satosacontrib.perun.micro_services.idm.sp_authorization.SpAuthorization" # noqa "satosacontrib.perun.micro_services.idm.sp_authorization.SpAuthorization" # noqa
".handle_unsatisfied_membership" ".handle_unsatisfied_membership"
) )
@patch("satosacontrib.perun.utils.Utils.Utils.get_eligible_groups")
def test_process_no_users_groups_found( def test_process_no_users_groups_found(
mock_request_1, mock_request_2, mock_request_3, mock_request_4 mock_request_1, mock_request_2, mock_request_3, mock_request_4, mock_request_5
): ):
data_user_without_groups = InternalData() data_user_without_groups = InternalData()
data_user_without_groups.attributes["example_user_id"] = "example user" data_user_without_groups.attributes["example_user_id"] = "example user"
...@@ -199,6 +198,7 @@ def test_process_no_users_groups_found( ...@@ -199,6 +198,7 @@ def test_process_no_users_groups_found(
return_value={"check_group_membership": True} return_value={"check_group_membership": True}
) )
AdaptersManager.get_users_groups_on_facility = MagicMock(return_value=[]) AdaptersManager.get_users_groups_on_facility = MagicMock(return_value=[])
Utils.get_eligible_groups = MagicMock(return_value=[])
SpAuthorization.handle_unsatisfied_membership = MagicMock(return_value=None) SpAuthorization.handle_unsatisfied_membership = MagicMock(return_value=None)
result = MICROSERVICE.process(TestContext(), data_user_without_groups) result = MICROSERVICE.process(TestContext(), data_user_without_groups)
...@@ -207,12 +207,10 @@ def test_process_no_users_groups_found( ...@@ -207,12 +207,10 @@ def test_process_no_users_groups_found(
@patch( @patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager" "satosacontrib.perun.utils.Utils.AdaptersManager" ".get_facility_by_rp_identifier"
".get_facility_by_rp_identifier"
) )
@patch( @patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager" "satosacontrib.perun.utils.Utils.AdaptersManager" ".get_users_groups_on_facility"
".get_users_groups_on_facility"
) )
@patch( @patch(
"satosacontrib.perun.micro_services.idm.sp_authorization.SpAuthorization" # noqa "satosacontrib.perun.micro_services.idm.sp_authorization.SpAuthorization" # noqa
...@@ -222,6 +220,7 @@ def test_process_no_users_groups_found( ...@@ -222,6 +220,7 @@ def test_process_no_users_groups_found(
"satosacontrib.perun.micro_services.idm.sp_authorization.SpAuthorization" # noqa "satosacontrib.perun.micro_services.idm.sp_authorization.SpAuthorization" # noqa
".handle_unsatisfied_membership" ".handle_unsatisfied_membership"
) )
@patch("satosacontrib.perun.utils.Utils.Utils.get_eligible_groups")
@patch("satosa.micro_services.base.MicroService.process") @patch("satosa.micro_services.base.MicroService.process")
def test_process_users_groups_found( def test_process_users_groups_found(
mock_request_1, mock_request_1,
...@@ -229,6 +228,7 @@ def test_process_users_groups_found( ...@@ -229,6 +228,7 @@ def test_process_users_groups_found(
mock_request_3, mock_request_3,
mock_request_4, mock_request_4,
mock_request_5, mock_request_5,
mock_request_6,
caplog, caplog,
): ):
user_satisfies_check_message = "User satisfies the group membership check." user_satisfies_check_message = "User satisfies the group membership check."
...@@ -244,6 +244,7 @@ def test_process_users_groups_found( ...@@ -244,6 +244,7 @@ def test_process_users_groups_found(
AdaptersManager.get_users_groups_on_facility = MagicMock( AdaptersManager.get_users_groups_on_facility = MagicMock(
return_value=["group1", "group2"] return_value=["group1", "group2"]
) )
Utils.get_eligible_groups = MagicMock(return_value=["group1"])
MicroService.process = MagicMock(return_value=None) MicroService.process = MagicMock(return_value=None)
with caplog.at_level(logging.INFO): with caplog.at_level(logging.INFO):
...@@ -268,7 +269,7 @@ def test_unauthorized_access(): ...@@ -268,7 +269,7 @@ def test_unauthorized_access():
) )
def test_handle_unsatisfied_membership_disabled(mock_request_1, caplog): def test_handle_unsatisfied_membership_disabled(mock_request_1, caplog):
handling_disabled_message = ( handling_disabled_message = (
"Handling unsatisfied membership is disabled, redirecting to" " unauthorized" "Handling unsatisfied membership is disabled, redirecting to unauthorized"
) )
custom_config = copy.deepcopy(MICROSERVICE_CONFIG) custom_config = copy.deepcopy(MICROSERVICE_CONFIG)
custom_config["filter_config"]["handle_unsatisfied_membership"] = False custom_config["filter_config"]["handle_unsatisfied_membership"] = False
...@@ -289,7 +290,7 @@ def test_handle_unsatisfied_membership_disabled(mock_request_1, caplog): ...@@ -289,7 +290,7 @@ def test_handle_unsatisfied_membership_disabled(mock_request_1, caplog):
) )
def test_handle_unsatisfied_membership_registration_unspecified(mock_request_1, caplog): def test_handle_unsatisfied_membership_registration_unspecified(mock_request_1, caplog):
handling_disabled_message = ( handling_disabled_message = (
"Handling unsatisfied membership is disabled, redirecting to" " unauthorized" "Handling unsatisfied membership is disabled, redirecting to unauthorized"
) )
custom_config = copy.deepcopy(MICROSERVICE_CONFIG) custom_config = copy.deepcopy(MICROSERVICE_CONFIG)
custom_config["filter_config"]["handle_unsatisfied_membership"] = False custom_config["filter_config"]["handle_unsatisfied_membership"] = False
......
...@@ -81,7 +81,7 @@ def test_get_attributes_from_perun_error(mock_request_1): ...@@ -81,7 +81,7 @@ def test_get_attributes_from_perun_error(mock_request_1):
attrs_without_name = {"attr": 1} attrs_without_name = {"attr": 1}
error_message = ( error_message = (
"UpdateUserExtSource" + "Getting attributes for UES " "was not successful." "UpdateUserExtSource" + "Getting attributes for UES was not successful."
) )
with pytest.raises(SATOSAError) as error: with pytest.raises(SATOSAError) as error:
......
from collections import Counter
from typing import List, Any
from unittest.mock import patch, MagicMock
from perun.connector import Group, VO, Resource, Facility, User, AdaptersManager
from satosacontrib.perun.utils.Utils import Utils
TEST_VO = VO(1, "vo", "vo_short_name")
GROUP_1 = Group(1, TEST_VO, "uuid1", "group1", "group_1", "")
GROUP_2 = Group(2, TEST_VO, "uuid2", "group2", "group_2", "")
GROUP_3 = Group(3, TEST_VO, "uuid3", "group3", "group_3", "")
USERS_GROUPS_ON_FACILITY = (GROUP_1, GROUP_2)
USER = User(1, "John Doe")
FACILITY = Facility(1, "example_name", "example_description", "example_rp_id", [])
APPLICABLE_RESOURCE_1 = Resource(1, TEST_VO, FACILITY, "applicable_1")
APPLICABLE_RESOURCE_2 = Resource(2, TEST_VO, FACILITY, "applicable_2")
APPLICABLE_RESOURCE_3 = Resource(3, TEST_VO, FACILITY, "applicable_3")
NON_APPLICABLE_RESOURCE_1 = Resource(4, TEST_VO, FACILITY, "non_applicable_1")
RESOURCES_ON_FACILITY = [
APPLICABLE_RESOURCE_1,
APPLICABLE_RESOURCE_2,
NON_APPLICABLE_RESOURCE_1,
]
@patch("perun.connector.adapters.AdaptersManager.AdaptersManager.__init__")
def create_mocked_utils_instance(mock_request_1):
AdaptersManager.__init__ = MagicMock(return_value=None)
adapters_manager_mock = AdaptersManager({}, {})
return Utils(adapters_manager_mock)
TEST_INSTANCE = create_mocked_utils_instance()
def are_equal_lists(lst1: List[Any], lst2: List[Any]) -> bool:
return Counter(lst1) == Counter(lst2)
def get_resource_applicability_attr(resource: Resource, _: str):
is_disabled = "non_applicable" in resource.name
return {"isDisabled": is_disabled}
def get_groups_for_resource(resouce: Resource):
resource_groups = {
APPLICABLE_RESOURCE_1: [GROUP_1],
APPLICABLE_RESOURCE_2: [GROUP_3],
NON_APPLICABLE_RESOURCE_1: [GROUP_2],
}
return resource_groups[resouce]
"""
Legend:
APPLICABLE RESOURCE - R_APP
NON-APPLICABLE RESOURCE (DISABLED BY ATTRIBUTE) - R_NON
GROUP - G
We test all possibilities in this test:
- facility has associated resources [R_APP_1, R_APP_2, R_APP_3, R_NON_1]
- resources are associated with groups {
R_APP_1: G_1, ✓
R_APP_2: G_3, ✓
R_NON_1: G_2 ˟
}
- groups sourced from resources which are not disabled are [G_1, G_3] (marked with ✓)
- user belongs to groups [G_1, G_2]
- groups passing the filter are {G_1, G_2} ∩ {G_1, G_3} = {G_1}
- {G_1} is the only eligible group passing all the restrictions
"""
@patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager"
".get_users_groups_on_facility"
)
@patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager"
".get_resources_for_facility"
)
@patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager"
".get_resource_attributes"
)
@patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager"
".get_groups_for_resource"
)
def test_get_eligible_groups_attribute_provided_filtered_groups(
mock_request_1, mock_request_2, mock_request_3, mock_request_4
):
AdaptersManager.get_users_groups_on_facility = MagicMock(
return_value=USERS_GROUPS_ON_FACILITY
)
AdaptersManager.get_resources_for_facility = MagicMock(
return_value=RESOURCES_ON_FACILITY
)
AdaptersManager.get_resource_attributes = MagicMock(
side_effect=get_resource_applicability_attr
)
AdaptersManager.get_groups_for_resource = MagicMock(
side_effect=get_groups_for_resource
)
eligible_groups = TEST_INSTANCE.get_eligible_groups(
FACILITY, USER, "mock_applicability_attr_name"
)
eligible_groups = [group.name for group in eligible_groups]
expected_eligible_groups = [GROUP_1.name]
assert Counter(expected_eligible_groups) == Counter(eligible_groups)
@patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager"
".get_users_groups_on_facility"
)
@patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager"
".get_resources_for_facility"
)
@patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager"
".get_resource_attributes"
)
@patch(
"perun.connector.adapters.AdaptersManager.AdaptersManager"
".get_groups_for_resource"
)
def test_get_eligible_groups_attribute_not_provided_filtering_skipped(
mock_request_1, mock_request_2, mock_request_3, mock_request_4
):
AdaptersManager.get_users_groups_on_facility = MagicMock(
return_value=USERS_GROUPS_ON_FACILITY
)
AdaptersManager.get_resources_for_facility = MagicMock(
return_value=RESOURCES_ON_FACILITY
)
AdaptersManager.get_resource_attributes = MagicMock(
side_effect=get_resource_applicability_attr
)
AdaptersManager.get_groups_for_resource = MagicMock(
side_effect=get_groups_for_resource
)
eligible_groups = TEST_INSTANCE.get_eligible_groups(FACILITY, USER, None)
eligible_groups = [group.name for group in eligible_groups]
expected_eligible_groups = [GROUP_1.name, GROUP_2.name]
assert Counter(expected_eligible_groups) == Counter(eligible_groups)