"""Cerbos policy parser and metadata models.""" import json import logging from pathlib import Path from typing import Optional import yaml from pydantic import BaseModel logger = logging.getLogger(__name__) class ResourcePolicyMetadata(BaseModel): """Metadata captured from a Cerbos resource policy file.""" resource_type: str requires_account_feature_controls: bool class PolicyMetadataDatabase: """In-memory store of resource policy metadata with JSON serialization support.""" def __init__(self) -> None: self._policies: dict[str, ResourcePolicyMetadata] = {} def add_policy(self, metadata: ResourcePolicyMetadata) -> None: self._policies[metadata.resource_type] = metadata def get_policy(self, resource_type: str) -> Optional[ResourcePolicyMetadata]: return self._policies.get(resource_type) def requires_account_feature_controls(self, resource_type: str) -> bool: policy = self.get_policy(resource_type) return policy.requires_account_feature_controls if policy else False def to_json(self) -> str: data = {rt: meta.model_dump() for rt, meta in self._policies.items()} return json.dumps(data) @classmethod def from_json(cls, raw: str) -> "PolicyMetadataDatabase": db = cls() for rt, meta_dict in json.loads(raw).items(): db.add_policy(ResourcePolicyMetadata(**meta_dict)) return db class CerbosPolicyParser: """Walks policy files and builds resource policy metadata database.""" def __init__(self, policies_dir: str = "cerbos/policies") -> None: """Initialize parser with a policies directory path. policies_dir may be a relative or absolute path. Relative paths are resolved from the process current working directory. """ self.policies_dir = Path(policies_dir) def parse_resource_policy( self, policy_file: Path ) -> Optional[ResourcePolicyMetadata]: """Parse a Cerbos resource policy file and extract metadata. Reads a YAML policy file and extracts the resource type and feature control requirements. Returns None if the file cannot be parsed or does not contain a valid resource policy. Args: policy_file: Path to the policy YAML file. Returns: ResourcePolicyMetadata if successfully parsed, None otherwise. """ try: with policy_file.open() as f: policy_data = yaml.safe_load(f) if not isinstance(policy_data, dict): return None if "resourcePolicy" not in policy_data: return None resource_policy = policy_data["resourcePolicy"] if not isinstance(resource_policy, dict): return None resource_type = resource_policy.get("resource") if not resource_type: logger.warning("No resource type found in %s", policy_file) return None variables_import = resource_policy.get("variables", {}).get("import", []) return ResourcePolicyMetadata( resource_type=resource_type, requires_account_feature_controls="account_feature_controls" in variables_import, ) except Exception as e: logger.error("Error parsing policy file %s: %s", policy_file, e) return None def build_database(self) -> PolicyMetadataDatabase: """Build a policy metadata database by scanning all policy files. Walks the policies directory recursively, parses all YAML files (*.yaml and *.yml), and aggregates their metadata into a single database. Files are processed in sorted order for consistency. Returns: PolicyMetadataDatabase containing metadata for all valid policies. """ database = PolicyMetadataDatabase() if not self.policies_dir.exists(): logger.error("Policies directory does not exist: %s", self.policies_dir) return database policy_files = list(self.policies_dir.rglob("*.yaml")) + list( self.policies_dir.rglob("*.yml") ) if not policy_files: logger.error("No policy files found in %s", self.policies_dir) return database for policy_file in sorted(policy_files): metadata = self.parse_resource_policy(policy_file) if metadata: database.add_policy(metadata) return database