From 11a7fada1b91c9110e22a456c3bfb2006225b45f Mon Sep 17 00:00:00 2001 From: aldbr Date: Tue, 17 Sep 2024 11:17:06 +0200 Subject: [PATCH 1/4] feat: add tests for specific properties --- diracx-routers/tests/auth/test_standard.py | 48 ++++++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/diracx-routers/tests/auth/test_standard.py b/diracx-routers/tests/auth/test_standard.py index e9f7a4dc..6d0d18a0 100644 --- a/diracx-routers/tests/auth/test_standard.py +++ b/diracx-routers/tests/auth/test_standard.py @@ -793,30 +793,61 @@ def _get_and_check_token_response(test_client, request_data): @pytest.mark.parametrize( "vos, groups, scope, expected", [ + # Classic use case, we ask for a vo and a group, we get the properties of the group [ - ["lhcb"], - ["lhcb_user"], + {"lhcb": {"default_group": "lhcb_user"}}, + {"lhcb_user": {"properties": ["NormalUser"]}}, "vo:lhcb group:lhcb_user", {"group": "lhcb_user", "properties": ["NormalUser"], "vo": "lhcb"}, ], + # We ask for a vo and a group with additional property + # We get the properties of the group + the additional property + # Authorization to access the additional property is checked later when user effectively requests a token + [ + {"lhcb": {"default_group": "lhcb_user"}}, + { + "lhcb_user": {"properties": ["NormalUser"]}, + "lhcb_admin": {"properties": ["AdminUser"]}, + }, + "vo:lhcb group:lhcb_user property:AdminUser", + { + "group": "lhcb_user", + "properties": ["NormalUser", "AdminUser"], + "vo": "lhcb", + }, + ], + # We ask for a vo, no group, and an additional existing property + # We get the default group but not its properties: only the one we asked for + # Authorization to access the additional property is checked later when user effectively requests a token + [ + {"lhcb": {"default_group": "lhcb_user"}}, + { + "lhcb_user": {"properties": ["NormalUser"]}, + "lhcb_admin": {"properties": ["AdminUser"]}, + }, + "vo:lhcb property:AdminUser", + {"group": "lhcb_user", "properties": ["AdminUser"], "vo": "lhcb"}, + ], ], ) def test_parse_scopes(vos, groups, scope, expected): - # TODO: Extend test for extra properties config = Config.model_validate( { "DIRAC": {}, "Registry": { - vo: { - "DefaultGroup": "lhcb_user", + vo_name: { + "DefaultGroup": vo_conf["default_group"], "IdP": {"URL": "https://idp.invalid", "ClientID": "test-idp"}, "Users": {}, "Groups": { - group: {"Properties": ["NormalUser"], "Users": []} - for group in groups + group_name: { + "Properties": group_conf["properties"], + "Users": [], + } + for group_name, group_conf in groups.items() }, } - for vo in vos + for vo_name, vo_conf in vos.items() }, "Operations": {"Defaults": {}}, } @@ -855,7 +886,6 @@ def test_parse_scopes(vos, groups, scope, expected): ], ) def test_parse_scopes_invalid(vos, groups, scope, expected_error): - # TODO: Extend test for extra properties config = Config.model_validate( { "DIRAC": {}, From 107f3872187b40d58249c73e004009b55625c8eb Mon Sep 17 00:00:00 2001 From: natthan-pigoux Date: Tue, 17 Sep 2024 13:14:23 +0200 Subject: [PATCH 2/4] Allow user to ask for specific properties only --- diracx-routers/src/diracx/routers/auth/token.py | 10 +++++++++- diracx-routers/src/diracx/routers/auth/utils.py | 6 ------ diracx-routers/src/diracx/routers/utils/users.py | 14 +++++++++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/diracx-routers/src/diracx/routers/auth/token.py b/diracx-routers/src/diracx/routers/auth/token.py index 625b56be..203865ee 100644 --- a/diracx-routers/src/diracx/routers/auth/token.py +++ b/diracx-routers/src/diracx/routers/auth/token.py @@ -23,7 +23,7 @@ from ..dependencies import AuthDB, AvailableSecurityProperties, Config from ..fastapi_classes import DiracxRouter -from ..utils.users import AuthSettings +from ..utils.users import AuthSettings, get_allowed_user_properties from .utils import ( parse_and_validate_scope, verify_dirac_refresh_token, @@ -363,6 +363,7 @@ async def exchange_token( parsed_scope = parse_and_validate_scope(scope, config, available_properties) vo = parsed_scope["vo"] dirac_group = parsed_scope["group"] + properties = parsed_scope["properties"] # Extract attributes from the OIDC token details sub = oidc_token_info["sub"] @@ -383,6 +384,13 @@ async def exchange_token( f"User is not a member of the requested group ({preferred_username}, {dirac_group})" ) + allowed_user_properties = get_allowed_user_properties(config, user_info, vo) + if not set(properties).issubset(allowed_user_properties): + raise ValueError( + f"{set(properties) - allowed_user_properties} are not valid properties for user {preferred_username}, " + f"available values: {' '.join(allowed_user_properties)}" + ) + # Merge the VO with the subject to get a unique DIRAC sub sub = f"{vo}:{sub}" diff --git a/diracx-routers/src/diracx/routers/auth/utils.py b/diracx-routers/src/diracx/routers/auth/utils.py index c75c99c1..f4f50f3c 100644 --- a/diracx-routers/src/diracx/routers/auth/utils.py +++ b/diracx-routers/src/diracx/routers/auth/utils.py @@ -226,12 +226,6 @@ def parse_and_validate_scope( f"{set(properties)-set(available_properties)} are not valid properties" ) - if not set(properties).issubset(allowed_properties): - raise PermissionError( - f"Attempted to access properties {set(properties)-set(allowed_properties)} which are not allowed." - f" Allowed properties are: {allowed_properties}" - ) - return { "group": group, "properties": sorted(properties), diff --git a/diracx-routers/src/diracx/routers/utils/users.py b/diracx-routers/src/diracx/routers/utils/users.py index 0130f199..81d52754 100644 --- a/diracx-routers/src/diracx/routers/utils/users.py +++ b/diracx-routers/src/diracx/routers/utils/users.py @@ -8,10 +8,11 @@ from pydantic import BaseModel, Field from pydantic_settings import SettingsConfigDict +from diracx.core.config.schema import UserConfig from diracx.core.models import UserInfo from diracx.core.properties import SecurityProperty from diracx.core.settings import FernetKey, ServiceSettingsBase, TokenSigningKey -from diracx.routers.dependencies import add_settings_annotation +from diracx.routers.dependencies import Config, add_settings_annotation # auto_error=False is used to avoid raising the wrong exception when the token is missing # The error is handled in the verify_dirac_access_token function @@ -117,3 +118,14 @@ async def verify_dirac_access_token( vo=token["vo"], policies=token.get("dirac_policies", {}), ) + + +def get_allowed_user_properties( + config: Config, user_info: UserConfig, vo: str +) -> set[SecurityProperty]: + """Retrieve all properties of groups a user is registered in.""" + allowed_user_properties = set() + for group in config.Registry[vo].Groups: + if user_info.PreferedUsername in config.Registry[vo].Groups[group].Users: + allowed_user_properties.update(config.Registry[vo].Groups[group].Properties) + return allowed_user_properties From 2b410da3fc7bec79e30ab58dea60267f7916ff9d Mon Sep 17 00:00:00 2001 From: natthan-pigoux Date: Tue, 17 Sep 2024 14:04:13 +0200 Subject: [PATCH 3/4] Fix use of sub instead of prefered_name --- diracx-routers/src/diracx/routers/auth/token.py | 2 +- diracx-routers/src/diracx/routers/utils/users.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/diracx-routers/src/diracx/routers/auth/token.py b/diracx-routers/src/diracx/routers/auth/token.py index 203865ee..b64e792d 100644 --- a/diracx-routers/src/diracx/routers/auth/token.py +++ b/diracx-routers/src/diracx/routers/auth/token.py @@ -384,7 +384,7 @@ async def exchange_token( f"User is not a member of the requested group ({preferred_username}, {dirac_group})" ) - allowed_user_properties = get_allowed_user_properties(config, user_info, vo) + allowed_user_properties = get_allowed_user_properties(config, sub, vo) if not set(properties).issubset(allowed_user_properties): raise ValueError( f"{set(properties) - allowed_user_properties} are not valid properties for user {preferred_username}, " diff --git a/diracx-routers/src/diracx/routers/utils/users.py b/diracx-routers/src/diracx/routers/utils/users.py index 81d52754..6346509d 100644 --- a/diracx-routers/src/diracx/routers/utils/users.py +++ b/diracx-routers/src/diracx/routers/utils/users.py @@ -8,7 +8,6 @@ from pydantic import BaseModel, Field from pydantic_settings import SettingsConfigDict -from diracx.core.config.schema import UserConfig from diracx.core.models import UserInfo from diracx.core.properties import SecurityProperty from diracx.core.settings import FernetKey, ServiceSettingsBase, TokenSigningKey @@ -120,12 +119,10 @@ async def verify_dirac_access_token( ) -def get_allowed_user_properties( - config: Config, user_info: UserConfig, vo: str -) -> set[SecurityProperty]: +def get_allowed_user_properties(config: Config, sub, vo: str) -> set[SecurityProperty]: """Retrieve all properties of groups a user is registered in.""" allowed_user_properties = set() for group in config.Registry[vo].Groups: - if user_info.PreferedUsername in config.Registry[vo].Groups[group].Users: + if sub in config.Registry[vo].Groups[group].Users: allowed_user_properties.update(config.Registry[vo].Groups[group].Properties) return allowed_user_properties From c694a7e173097d9e85ed0282df9dec70697b6c7b Mon Sep 17 00:00:00 2001 From: aldbr Date: Tue, 17 Sep 2024 16:52:46 +0200 Subject: [PATCH 4/4] fix(auth): adjust tests with unallowed properties --- .../src/diracx/routers/auth/token.py | 26 +-- .../src/diracx/routers/auth/utils.py | 8 +- .../tests/auth/test_legacy_exchange.py | 4 +- diracx-routers/tests/auth/test_standard.py | 151 +++++++++++++++--- diracx-testing/src/diracx/testing/__init__.py | 4 + 5 files changed, 152 insertions(+), 41 deletions(-) diff --git a/diracx-routers/src/diracx/routers/auth/token.py b/diracx-routers/src/diracx/routers/auth/token.py index b64e792d..14103add 100644 --- a/diracx-routers/src/diracx/routers/auth/token.py +++ b/diracx-routers/src/diracx/routers/auth/token.py @@ -98,7 +98,6 @@ async def token( raise NotImplementedError(f"Grant type not implemented {grant_type}") # Get a TokenResponse to return to the user - return await exchange_token( auth_db, scope, @@ -360,7 +359,13 @@ async def exchange_token( ) -> TokenResponse: """Method called to exchange the OIDC token for a DIRAC generated access token.""" # Extract dirac attributes from the OIDC scope - parsed_scope = parse_and_validate_scope(scope, config, available_properties) + try: + parsed_scope = parse_and_validate_scope(scope, config, available_properties) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=e.args[0], + ) from e vo = parsed_scope["vo"] dirac_group = parsed_scope["group"] properties = parsed_scope["properties"] @@ -380,15 +385,18 @@ async def exchange_token( # Check that the subject is part of the dirac users if sub not in config.Registry[vo].Groups[dirac_group].Users: - raise ValueError( - f"User is not a member of the requested group ({preferred_username}, {dirac_group})" + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"User is not a member of the requested group ({preferred_username}, {dirac_group})", ) + # Check that the user properties are valid allowed_user_properties = get_allowed_user_properties(config, sub, vo) - if not set(properties).issubset(allowed_user_properties): - raise ValueError( - f"{set(properties) - allowed_user_properties} are not valid properties for user {preferred_username}, " - f"available values: {' '.join(allowed_user_properties)}" + if not properties.issubset(allowed_user_properties): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"{' '.join(properties - allowed_user_properties)} are not valid properties " + f"for user {preferred_username}, available values: {' '.join(allowed_user_properties)}", ) # Merge the VO with the subject to get a unique DIRAC sub @@ -420,7 +428,7 @@ async def exchange_token( "sub": sub, "vo": vo, "iss": issuer, - "dirac_properties": parsed_scope["properties"], + "dirac_properties": list(properties), "jti": str(uuid4()), "preferred_username": preferred_username, "dirac_group": dirac_group, diff --git a/diracx-routers/src/diracx/routers/auth/utils.py b/diracx-routers/src/diracx/routers/auth/utils.py index f4f50f3c..3b881361 100644 --- a/diracx-routers/src/diracx/routers/auth/utils.py +++ b/diracx-routers/src/diracx/routers/auth/utils.py @@ -36,7 +36,7 @@ class GrantType(StrEnum): class ScopeInfoDict(TypedDict): group: str - properties: list[str] + properties: set[str] vo: str @@ -217,9 +217,7 @@ def parse_and_validate_scope( raise ValueError(f"{group} not in {vo} groups") allowed_properties = config.Registry[vo].Groups[group].Properties - if not properties: - # If there are no properties set get the defaults from the CS - properties = [str(p) for p in allowed_properties] + properties.extend([str(p) for p in allowed_properties]) if not set(properties).issubset(available_properties): raise ValueError( @@ -228,7 +226,7 @@ def parse_and_validate_scope( return { "group": group, - "properties": sorted(properties), + "properties": set(sorted(properties)), "vo": vo, } diff --git a/diracx-routers/tests/auth/test_legacy_exchange.py b/diracx-routers/tests/auth/test_legacy_exchange.py index 62a4acdf..551e3133 100644 --- a/diracx-routers/tests/auth/test_legacy_exchange.py +++ b/diracx-routers/tests/auth/test_legacy_exchange.py @@ -79,7 +79,9 @@ async def test_valid(test_client, legacy_credentials, expires_seconds): assert user_info["sub"] == "lhcb:b824d4dc-1f9d-4ee8-8df5-c0ae55d46041" assert user_info["vo"] == "lhcb" assert user_info["dirac_group"] == "lhcb_user" - assert user_info["properties"] == ["NormalUser", "PrivateLimitedDelegation"] + assert sorted(user_info["properties"]) == sorted( + ["PrivateLimitedDelegation", "NormalUser"] + ) async def test_refresh_token(test_client, legacy_credentials): diff --git a/diracx-routers/tests/auth/test_standard.py b/diracx-routers/tests/auth/test_standard.py index 6d0d18a0..8b2003fc 100644 --- a/diracx-routers/tests/auth/test_standard.py +++ b/diracx-routers/tests/auth/test_standard.py @@ -121,6 +121,12 @@ async def test_authorization_flow(test_client, auth_httpx_mock: HTTPXMock): .replace("=", "") ) + # The scope is valid and should return a token with the following claims + # vo:lhcb group:lhcb_user (default group) property:[NormalUser,ProductionManagement] + # Note: the property ProductionManagement is not part of the lhcb_user group properties + # but the user has the right to have it. + scope = "vo:lhcb property:ProductionManagement" + # Initiate the authorization flow with a wrong client ID # Check that the client ID is not recognized r = test_client.get( @@ -131,7 +137,7 @@ async def test_authorization_flow(test_client, auth_httpx_mock: HTTPXMock): "code_challenge_method": "S256", "client_id": "Unknown client ID", "redirect_uri": "http://diracx.test.invalid:8000/api/docs/oauth2-redirect", - "scope": "vo:lhcb property:NormalUser", + "scope": scope, "state": "external-state", }, follow_redirects=False, @@ -148,7 +154,7 @@ async def test_authorization_flow(test_client, auth_httpx_mock: HTTPXMock): "code_challenge_method": "S256", "client_id": DIRAC_CLIENT_ID, "redirect_uri": "http://diracx.test.unrecognized:8000/api/docs/oauth2-redirect", - "scope": "vo:lhcb property:NormalUser", + "scope": scope, "state": "external-state", }, follow_redirects=False, @@ -164,7 +170,7 @@ async def test_authorization_flow(test_client, auth_httpx_mock: HTTPXMock): "code_challenge_method": "S256", "client_id": DIRAC_CLIENT_ID, "redirect_uri": "http://diracx.test.invalid:8000/api/docs/oauth2-redirect", - "scope": "vo:lhcb property:NormalUser", + "scope": scope, "state": "external-state", }, follow_redirects=False, @@ -243,13 +249,19 @@ async def test_authorization_flow(test_client, auth_httpx_mock: HTTPXMock): async def test_device_flow(test_client, auth_httpx_mock: HTTPXMock): + # The scope is valid and should return a token with the following claims + # vo:lhcb group:lhcb_user (default group) property:[NormalUser,ProductionManagement] + # Note: the property ProductionManagement is not part of the lhcb_user group properties + # but the user has the right to have it. + scope = "vo:lhcb property:ProductionManagement" + # Initiate the device flow with a wrong client ID # Check that the client ID is not recognized r = test_client.post( "/api/auth/device", params={ "client_id": "Unknown client ID", - "scope": "vo:lhcb property:NormalUser", + "scope": scope, }, ) assert r.status_code == 400, r.json() @@ -259,7 +271,7 @@ async def test_device_flow(test_client, auth_httpx_mock: HTTPXMock): "/api/auth/device", params={ "client_id": DIRAC_CLIENT_ID, - "scope": "vo:lhcb group:lhcb_user property:NormalUser", + "scope": scope, }, ) assert r.status_code == 200, r.json() @@ -337,11 +349,14 @@ async def test_device_flow(test_client, auth_httpx_mock: HTTPXMock): assert r.json()["detail"] == "Code was already used" -async def test_flows_with_unallowed_properties(test_client): +async def test_authorization_flow_with_unallowed_properties( + test_client, auth_httpx_mock: HTTPXMock +): """Test the authorization flow and the device flow with unallowed properties.""" - unallowed_property = "FileCatalogManagement" + # ProxyManagement is a valid property but not allowed for the user + unallowed_property = "ProxyManagement" - # Initiate the authorization flow + # Initiate the authorization flow: should not fail code_verifier = secrets.token_hex() code_challenge = ( base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()) @@ -361,12 +376,43 @@ async def test_flows_with_unallowed_properties(test_client): }, follow_redirects=False, ) + assert r.status_code == 307, r.json() + query_parameters = parse_qs(urlparse(r.headers["Location"]).query) + redirect_uri = query_parameters["redirect_uri"][0] + state = query_parameters["state"][0] + + r = test_client.get( + redirect_uri, + params={"code": "valid-code", "state": state}, + follow_redirects=False, + ) + assert r.status_code == 307, r.text + query_parameters = parse_qs(urlparse(r.headers["Location"]).query) + code = query_parameters["code"][0] + + request_data = { + "grant_type": "authorization_code", + "code": code, + "state": state, + "client_id": DIRAC_CLIENT_ID, + "redirect_uri": "http://diracx.test.invalid:8000/api/docs/oauth2-redirect", + "code_verifier": code_verifier, + } + # Ensure the token request doesn't work because of the unallowed property + r = test_client.post("/api/auth/token", data=request_data) assert r.status_code == 403, r.json() assert ( - f"Attempted to access properties {{'{unallowed_property}'}} which are not allowed." - in r.json()["detail"] + f"{unallowed_property} are not valid properties for user" in r.json()["detail"] ) + +async def test_device_flow_with_unallowed_properties( + test_client, auth_httpx_mock: HTTPXMock +): + """Test the authorization flow and the device flow with unallowed properties.""" + # ProxyManagement is a valid property but not allowed for the user + unallowed_property = "ProxyManagement" + # Initiate the device flow r = test_client.post( "/api/auth/device", @@ -375,10 +421,36 @@ async def test_flows_with_unallowed_properties(test_client): "scope": f"vo:lhcb group:lhcb_user property:{unallowed_property} property:NormalUser", }, ) + assert r.status_code == 200, r.json() + + data = r.json() + assert data["user_code"] + assert data["device_code"] + assert data["verification_uri_complete"] + assert data["verification_uri"] + assert data["expires_in"] == 600 + + r = test_client.get(data["verification_uri_complete"], follow_redirects=False) + assert r.status_code == 307, r.text + login_url = r.headers["Location"] + query_parameters = parse_qs(urlparse(login_url).query) + redirect_uri = query_parameters["redirect_uri"][0] + state = query_parameters["state"][0] + + r = test_client.get(redirect_uri, params={"code": "valid-code", "state": state}) + assert r.status_code == 200, r.text + + request_data = { + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + "device_code": data["device_code"], + "client_id": DIRAC_CLIENT_ID, + } + + # Ensure the token request doesn't work a second time + r = test_client.post("/api/auth/token", data=request_data) assert r.status_code == 403, r.json() assert ( - f"Attempted to access properties {{'{unallowed_property}'}} which are not allowed." - in r.json()["detail"] + f"{unallowed_property} are not valid properties for user" in r.json()["detail"] ) @@ -793,40 +865,61 @@ def _get_and_check_token_response(test_client, request_data): @pytest.mark.parametrize( "vos, groups, scope, expected", [ - # Classic use case, we ask for a vo and a group, we get the properties of the group + # We ask for a vo, we get the properties of the default group + [ + {"lhcb": {"default_group": "lhcb_user"}}, + { + "lhcb_user": {"properties": ["NormalUser"]}, + "lhcb_admin": {"properties": ["ProxyManagement"]}, + "lhcb_production": {"properties": ["ProductionManagement"]}, + }, + "vo:lhcb", + {"group": "lhcb_user", "properties": {"NormalUser"}, "vo": "lhcb"}, + ], + # We ask for a vo and a group, we get the properties of the group [ {"lhcb": {"default_group": "lhcb_user"}}, - {"lhcb_user": {"properties": ["NormalUser"]}}, - "vo:lhcb group:lhcb_user", - {"group": "lhcb_user", "properties": ["NormalUser"], "vo": "lhcb"}, + { + "lhcb_user": {"properties": ["NormalUser"]}, + "lhcb_admin": {"properties": ["ProxyManagement"]}, + "lhcb_production": {"properties": ["ProductionManagement"]}, + }, + "vo:lhcb group:lhcb_admin", + {"group": "lhcb_admin", "properties": {"ProxyManagement"}, "vo": "lhcb"}, ], - # We ask for a vo and a group with additional property - # We get the properties of the group + the additional property + # We ask for a vo, no group, and an additional existing property + # We get the default group with its properties along with with the extra properties we asked for # Authorization to access the additional property is checked later when user effectively requests a token [ {"lhcb": {"default_group": "lhcb_user"}}, { "lhcb_user": {"properties": ["NormalUser"]}, - "lhcb_admin": {"properties": ["AdminUser"]}, + "lhcb_admin": {"properties": ["ProxyManagement"]}, + "lhcb_production": {"properties": ["ProductionManagement"]}, }, - "vo:lhcb group:lhcb_user property:AdminUser", + "vo:lhcb property:ProxyManagement", { "group": "lhcb_user", - "properties": ["NormalUser", "AdminUser"], + "properties": {"NormalUser", "ProxyManagement"}, "vo": "lhcb", }, ], - # We ask for a vo, no group, and an additional existing property - # We get the default group but not its properties: only the one we asked for + # We ask for a vo and a group with additional property + # We get the properties of the group + the additional property # Authorization to access the additional property is checked later when user effectively requests a token [ {"lhcb": {"default_group": "lhcb_user"}}, { "lhcb_user": {"properties": ["NormalUser"]}, - "lhcb_admin": {"properties": ["AdminUser"]}, + "lhcb_admin": {"properties": ["ProxyManagement"]}, + "lhcb_production": {"properties": ["ProductionManagement"]}, + }, + "vo:lhcb group:lhcb_admin property:ProductionManagement", + { + "group": "lhcb_admin", + "properties": {"ProductionManagement", "ProxyManagement"}, + "vo": "lhcb", }, - "vo:lhcb property:AdminUser", - {"group": "lhcb_user", "properties": ["AdminUser"], "vo": "lhcb"}, ], ], ) @@ -865,6 +958,12 @@ def test_parse_scopes(vos, groups, scope, expected): "group:lhcb_user undefinedscope:undefined", "Unrecognised scopes", ], + [ + ["lhcb"], + ["lhcb_user", "lhcb_admin"], + "vo:lhcb group:lhcb_user property:undefined_property", + "{'undefined_property'} are not valid properties", + ], [ ["lhcb"], ["lhcb_user"], diff --git a/diracx-testing/src/diracx/testing/__init__.py b/diracx-testing/src/diracx/testing/__init__.py index 2f91710b..6ced3e77 100644 --- a/diracx-testing/src/diracx/testing/__init__.py +++ b/diracx-testing/src/diracx/testing/__init__.py @@ -444,6 +444,10 @@ def with_config_repo(tmp_path_factory): "c935e5ed-2g0e-5ff9-9eg6-d1bf66e57152", ], }, + "lhcb_prmgr": { + "Properties": ["NormalUser", "ProductionManagement"], + "Users": ["b824d4dc-1f9d-4ee8-8df5-c0ae55d46041"], + }, "lhcb_tokenmgr": { "Properties": ["NormalUser", "ProxyManagement"], "Users": ["c935e5ed-2g0e-5ff9-9eg6-d1bf66e57152"],