"""Tests for derived_roles cli commands.""" from pathlib import Path from typing import List, Optional from unittest import mock from unittest.mock import MagicMock import pytest import typer from pdp.cli.commands import derived_roles from pdp.cli.commands.derived_roles import ( derived_role_name_validator, filepath_validator, role_name_validator, ) def test_create_tenant_definition() -> None: """Test role is used for the derived role definition name and conditions.""" expected = { "name": "dog_whisperer", "parentRoles": ["user"], "condition": { "match": { "any": { "of": [ { "expr": "P.attr.tenants[V.resource_tenant].roles['dog_whisperer'] != null" # noqa: E501 }, { "expr": "hasIntersection(V.principal_dog_whisperer_tenant_list, V.resource_tenant_hierarchy)" # noqa: E501 }, ], }, }, }, } actual = derived_roles._create_tenant_definition("dog_whisperer") assert actual == derived_roles.DerivedRoleDefinition(**expected) def test_create_any_tenant_definition() -> None: """Test role is used to reference a variable.""" expected = { "name": "any_tenant_dog_whisperer", "parentRoles": ["user"], "condition": { "match": {"expr": "V.principal_dog_whisperer_tenant_list.size() > 0"}, }, } actual = derived_roles._create_any_tenant_definition("dog_whisperer") assert actual == derived_roles.DerivedRoleDefinition(**expected) def test_get_tenant_list_variables() -> None: """Test variables represent getting a tenant list by role.""" expected = { "principal_dog_whisperer_tenant_list": "P.attr.tenants.filter(t, P.attr.tenants[t].roles.exists(r, r == 'dog_whisperer')).map(t,t)", # noqa: E501 "principal_cat_scratcher_tenant_list": "P.attr.tenants.filter(t, P.attr.tenants[t].roles.exists(r, r == 'cat_scratcher')).map(t,t)", # noqa: E501 } actual = derived_roles._get_tenant_list_variables( ["dog_whisperer", "cat_scratcher"] ) assert actual == expected @mock.patch("pdp.cli.commands.derived_roles.yaml") def test_derived_roles_by_tenant(mock_yaml: MagicMock) -> None: """Test derived role policy is generated with tenant conditions.""" with mock.patch( "pdp.cli.commands.derived_roles.open", mock.mock_open(), create=True ) as mocked_open: derived_roles.derived_roles_by_tenant( filepath="my_roles.yaml", name="my_dream_jobs", roles=["dog_whisperer", "cat_scratcher"], ) mocked_open.assert_called_once_with("my_roles.yaml", mode="w") mock_file_handler = mocked_open() mock_yaml.dump.assert_called_once_with( { "apiVersion": "api.cerbos.dev/v1", "derivedRoles": { "name": "my_dream_jobs", "variables": { "import": ["common_variables"], "local": { "principal_dog_whisperer_tenant_list": "P.attr.tenants.filter(t, P.attr.tenants[t].roles.exists(r, r == 'dog_whisperer')).map(t,t)", # noqa: E501 "principal_cat_scratcher_tenant_list": "P.attr.tenants.filter(t, P.attr.tenants[t].roles.exists(r, r == 'cat_scratcher')).map(t,t)", # noqa: E501 }, }, "definitions": [ { "name": "dog_whisperer", "parentRoles": ["user"], "condition": { "match": { "any": { "of": [ { "expr": "P.attr.tenants[V.resource_tenant].roles['dog_whisperer'] != null" # noqa: E501 }, { "expr": "hasIntersection(V.principal_dog_whisperer_tenant_list, V.resource_tenant_hierarchy)" # noqa: E501 }, ] } } }, }, { "name": "cat_scratcher", "parentRoles": ["user"], "condition": { "match": { "any": { "of": [ { "expr": "P.attr.tenants[V.resource_tenant].roles['cat_scratcher'] != null" # noqa: E501 }, { "expr": "hasIntersection(V.principal_cat_scratcher_tenant_list, V.resource_tenant_hierarchy)" # noqa: E501 }, ] } } }, }, { "name": "any_tenant_dog_whisperer", "parentRoles": ["user"], "condition": { "match": { "expr": "V.principal_dog_whisperer_tenant_list.size() > 0" # noqa: E501 }, }, }, { "name": "any_tenant_cat_scratcher", "parentRoles": ["user"], "condition": { "match": { "expr": "V.principal_cat_scratcher_tenant_list.size() > 0" # noqa: E501 }, }, }, ], }, }, mock_file_handler, width=float("inf"), sort_keys=False, ) @pytest.mark.parametrize( "value,error_expected,expected_output", [ pytest.param( ["rolename", "role_name"], False, ["rolename", "role_name"], id="Input with valid format is OK.", ), pytest.param( ["roleNAME"], True, None, id="Input with upper case should fail.", ), pytest.param( ["role123"], True, None, id="Input with numbers should fail.", ), pytest.param( ["role!!!"], True, None, id="Input with special characters should fail.", ), pytest.param( ["role_"], True, None, id="Input with invalid snake_case format should fail..", ), ], ) def test_role_name_validator( value: List[str], error_expected: bool, expected_output: List[str] ) -> None: """Test role_name_validator.""" if error_expected: with pytest.raises(typer.BadParameter): role_name_validator(value) else: assert role_name_validator(value) == expected_output @pytest.mark.parametrize( "value,error_expected,expected_output", [ pytest.param( "my_roles", False, "my_roles", id="Input with valid format is OK.", ), pytest.param( "myroles", False, "myroles", id="Input with all lowercase but no underscore is OK.", ), pytest.param( "myROLES", True, None, id="Input with upper case should fail.", ), pytest.param( "myroles123", True, None, id="Input with numbers should fail.", ), pytest.param( "myroles!!!", True, None, id="Input with special characters should fail.", ), pytest.param( "my_roles_", True, None, id="Input with invalid snake_case is not allowed.", ), ], ) def test_derived_role_name_validator( value: str, error_expected: bool, expected_output: Optional[str] ) -> None: """Test role_name_validator.""" if error_expected: with pytest.raises(typer.BadParameter) as exception_info: derived_role_name_validator(value) assert ( "The NAME of the collection of derived roles must be a lowercase and snake_case" # noqa: E501 in str(exception_info.value) ) else: assert derived_role_name_validator(value) == expected_output @pytest.mark.parametrize( "value,error_expected,expected_output", [ pytest.param( "my_roles.yaml", False, "my_roles.yaml", id="Input with valid format with .yaml extension is OK.", ), pytest.param( "my_roles.yml", False, "my_roles.yml", id="Input with valid format with .yml extension is OK.", ), pytest.param( "myroles.yml", False, "myroles.yml", id="Input with all lowercase and no underscores is OK.", ), pytest.param( "myROLES.yml", True, None, id="Input with upper case should fail.", ), pytest.param( "myroles123.yml", True, None, id="Input with numbers should fail.", ), pytest.param( "myroles!!!.yml", True, None, id="Input with special characters should fail.", ), pytest.param( "my_roles_.yml", True, None, id="Input with invalid snake_case should fail.", ), pytest.param( "my_roles.sh", True, None, id="Input an invalid extension should fail.", ), ], ) def test_filepath_validator( value: str, error_expected: bool, expected_output: Optional[str], tmp_path: Path ) -> None: """Test role_name_validator.""" d = tmp_path / ".test_filepath_validator" d.mkdir() p = d / value if error_expected: with pytest.raises(typer.BadParameter): filepath_validator(str(p)) else: assert filepath_validator(str(p)) == str(p) def test_filepath_validator__invalid_path() -> None: """Test role_name_validator with an invalid path.""" with pytest.raises(typer.BadParameter) as exception_info: filepath_validator("/no/such/patch/roles.yml") assert "The FILEPATH directory does not exist" in str(exception_info.value)