"""Commands to generate derived role policy.""" import os import re from typing import Any, Dict, List import typer import yaml from pydantic import BaseModel, Field from typing_extensions import Annotated cli: typer.Typer = typer.Typer( short_help="Commands to generate derived role policy", no_args_is_help=True ) class DerivedRoleDefinition(BaseModel): """Object for conditions of a derived role.""" name: str parentRoles: List[str] = ["user"] condition: Dict[str, Any] = {} class DerivedRoleVariables(BaseModel): """Object for variables.""" import_variables: List[str] = Field(alias="import", default=["common_variables"]) local: Dict[str, Any] class DerivedRole(BaseModel): """Object for a derived role.""" name: str variables: DerivedRoleVariables definitions: List[DerivedRoleDefinition] = [] class DerivedRolePolicy(BaseModel): """Object for a derived role policy.""" apiVersion: str = "api.cerbos.dev/v1" derivedRoles: DerivedRole VALID_CLI_INPUT = re.compile(r"^[a-z]+(_[a-z]+)*$").match VALID_FILENAME_INPUT = re.compile(r"^[a-z]+(_[a-z]+)*\.(yaml|yml)$").match def role_name_validator(roles: List[str]) -> List[str]: """Validate that the input params are strings with lowercase characters and underscore (_). """ for role in roles: if not VALID_CLI_INPUT(role): raise typer.BadParameter( f"A role must be a lowercase and snake_case string. Found: '{role}'" ) return roles def derived_role_name_validator(value: str) -> str: """Validate the derived role name input value.""" if not VALID_CLI_INPUT(value): raise typer.BadParameter( f"The NAME of the collection of derived roles must be a lowercase and snake_case " # noqa: E501 f"string. Found: '{value}'" ) return value def filepath_validator(value: str) -> str: """Validate the filepath input.""" path = os.path.realpath(value) dirpath, filename = os.path.split(path) if not os.path.isdir(dirpath): raise typer.BadParameter(f"The FILEPATH directory does not exist: {dirpath}") if not VALID_FILENAME_INPUT(filename): raise typer.BadParameter( f"The filename must be a lowercase and snake_case string with yml extension. Found: '{filename}'" # noqa: E501 ) return value @cli.command("by_tenant", short_help="Create derived roles by tenant") def derived_roles_by_tenant( filepath: Annotated[ str, typer.Option( help="File path to write the YAML-formatted derived roles.", callback=filepath_validator, ), ], name: Annotated[ str, typer.Option( help="""The NAME of the collection of derived roles. \b Required to import the derived roles into a resource policy.""", callback=derived_role_name_validator, ), ], roles: Annotated[ List[str], typer.Option( "--role", help="""A ROLE to create in the derived roles file. \b - The role should be a lowercase and snake_case string (e.g., `audience_development_admin`). \b - Use unambiguous role names. For example, use `namespace_admin` instead of `admin`. """, # noqa: E501 callback=role_name_validator, ), ], ) -> None: """Command to create derived roles by tenant.""" typer.secho(f"Writing derived roles {name} to file {filepath}", fg="green") definitions = [_create_tenant_definition(role) for role in roles] any_tenant_definitions = [_create_any_tenant_definition(role) for role in roles] all_definitions = definitions + any_tenant_definitions variables = DerivedRoleVariables(local=_get_tenant_list_variables(roles)) derived_role_policy = DerivedRolePolicy( derivedRoles=DerivedRole( name=name, variables=variables, definitions=all_definitions ), ) with open(filepath, mode="w") as output: output.write( "# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json\n" # noqa: E501 ) output.write("---\n") yaml.dump( derived_role_policy.model_dump(by_alias=True), output, width=float("inf"), sort_keys=False, ) typer.secho("Done writing derived roles :)", fg="green") def _create_tenant_definition(role: str) -> DerivedRoleDefinition: """Create tenant and tenant hierarchy condition per role.""" condition = { "match": { "any": { "of": [ { "expr": f"P.attr.tenants[V.resource_tenant].roles['{role}'] != null" # noqa: E501 }, { "expr": f"hasIntersection(V.principal_{role}_tenant_list, V.resource_tenant_hierarchy)" # noqa:E501 }, ] } } } return DerivedRoleDefinition(name=role, condition=condition) def _create_any_tenant_definition(role: str) -> DerivedRoleDefinition: """Create any_tenant role.""" condition = {"match": {"expr": f"V.principal_{role}_tenant_list.size() > 0"}} return DerivedRoleDefinition(name=f"any_tenant_{role}", condition=condition) def _get_tenant_list_variables(roles: List[str]) -> Dict[str, Any]: """Generate variables representing tenant_list.""" variables: Dict[str, Any] = {} for role in roles: variables[f"principal_{role}_tenant_list"] = ( f"P.attr.tenants.filter(t, P.attr.tenants[t].roles.exists(r, r == '{role}')).map(t,t)" # noqa: E501 ) return variables