diff --git a/releasenotes/notes/add-configurable-policy-files-7f4f2630bd497c90.yaml b/releasenotes/notes/add-configurable-policy-files-7f4f2630bd497c90.yaml new file mode 100644 index 0000000..7693803 --- /dev/null +++ b/releasenotes/notes/add-configurable-policy-files-7f4f2630bd497c90.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Editable policy files are supported. Add policy yaml file name and path options + to the configuration file. Users can edit and modify the policy as needed. diff --git a/skyline_apiserver/api/v1/policy.py b/skyline_apiserver/api/v1/policy.py index 00fc936..9872cca 100644 --- a/skyline_apiserver/api/v1/policy.py +++ b/skyline_apiserver/api/v1/policy.py @@ -28,6 +28,7 @@ from skyline_apiserver.client.utils import generate_session, get_access, get_sys from skyline_apiserver.config import CONF from skyline_apiserver.log import LOG from skyline_apiserver.policy import ENFORCER, UserContext +from skyline_apiserver.types import constants router = APIRouter() @@ -111,11 +112,27 @@ async def list_policies( # user_context as is. LOG.debug("Keystone is not reachable. No privilege to access system scope.") target = _generate_target(profile) - result = [ - {"rule": rule, "allowed": ENFORCER.authorize(rule, target, user_context)} - for rule in ENFORCER.rules - ] - return schemas.Policies(**{"policies": result}) + + results = [] + services = constants.SUPPORTED_SERVICE_EPS.keys() + for service in services: + try: + enforcer = ENFORCER[service] + result = [ + { + "rule": f"{service}:{rule}", + "allowed": enforcer.authorize(rule, target, user_context), + } + for rule in enforcer.rules + ] + results.extend(result) + except Exception: + msg = "An error occurred when calling %(service)s enforcer." % { + "service": str(service) + } + LOG.warning(msg) + + return schemas.Policies(**{"policies": results}) @router.post( @@ -159,10 +176,14 @@ async def check_policies( target = _generate_target(profile) target.update(policy_rules.target if policy_rules.target else {}) try: - result = [ - {"rule": rule, "allowed": ENFORCER.authorize(rule, target, user_context)} - for rule in policy_rules.rules - ] + result = [] + for policy_rule in policy_rules.rules: + service = policy_rule.split(":", 1)[0] + rule = policy_rule.split(":", 1)[1] + enforcer = ENFORCER[service] + result.append( + {"rule": policy_rule, "allowed": enforcer.authorize(rule, target, user_context)} + ) except Exception as e: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/skyline_apiserver/config/default.py b/skyline_apiserver/config/default.py index a84abf2..cf88e0d 100644 --- a/skyline_apiserver/config/default.py +++ b/skyline_apiserver/config/default.py @@ -125,6 +125,21 @@ cafile = Opt( default="", ) +policy_file_suffix = Opt( + name="policy_file_suffix", + description="policy file suffix", + schema=StrictStr, + default="policy.yaml", +) + +policy_file_path = Opt( + name="policy_file_path", + description="A path to policy file", + schema=StrictStr, + default="/etc/skyline/policy", +) + + GROUP_NAME = __name__.split(".")[-1] ALL_OPTS = ( debug, @@ -142,6 +157,8 @@ ALL_OPTS = ( prometheus_enable_basic_auth, prometheus_basic_auth_user, prometheus_basic_auth_password, + policy_file_suffix, + policy_file_path, ) __all__ = ("GROUP_NAME", "ALL_OPTS") diff --git a/skyline_apiserver/policy/__init__.py b/skyline_apiserver/policy/__init__.py index 21e2414..beb8eef 100644 --- a/skyline_apiserver/policy/__init__.py +++ b/skyline_apiserver/policy/__init__.py @@ -19,27 +19,20 @@ from oslo_policy import _parser from .base import Enforcer, UserContext from .manager import get_service_rules -ENFORCER = Enforcer() +ENFORCER = {} def setup() -> None: service_rules = get_service_rules() - all_api_rules = [] for service, rules in service_rules.items(): api_rules = [] for rule in rules: - # Update rule name with prefix service. - rule.name = f"{service}:{rule.name}" - # Update check - rule.check_str = rule.check_str.replace("rule:", f"rule:{service}:") rule.check = _parser.parse_rule(rule.check_str) - # Update basic check - rule.basic_check_str = rule.basic_check_str.replace("rule:", f"rule:{service}:") rule.basic_check = _parser.parse_rule(rule.basic_check_str) api_rules.append(rule) - all_api_rules.extend(api_rules) - - ENFORCER.register_rules(all_api_rules) + enforcer = Enforcer(service=service) + enforcer.register_rules(api_rules) + ENFORCER[service] = enforcer __all__ = ( diff --git a/skyline_apiserver/policy/base.py b/skyline_apiserver/policy/base.py index 0da97a4..a982659 100644 --- a/skyline_apiserver/policy/base.py +++ b/skyline_apiserver/policy/base.py @@ -15,14 +15,16 @@ from __future__ import annotations from collections.abc import MutableMapping +from pathlib import Path from typing import Any, Dict, Iterator, List, Union import attr from immutables import Map from keystoneauth1.access.access import AccessInfoV3 -from oslo_policy._checks import _check +from oslo_policy import _cache_handler, _checks, policy from skyline_apiserver.config import CONF +from skyline_apiserver.log import LOG from .manager.base import APIRule, Rule @@ -94,7 +96,23 @@ class UserContext(MutableMapping): @attr.s(kw_only=True, repr=True, frozen=False, slots=True, auto_attribs=True) class Enforcer: + service: str = attr.ib(repr=True, init=True) rules: Map = attr.ib(factory=Map, repr=True, init=False) + file_rules: Dict[str, Any] = attr.ib(default={}, repr=True, init=True) + _file_cache: Dict[str, Any] = attr.ib(default={}, repr=True, init=True) + + def load_rules(self) -> None: + path = Path(CONF.default.policy_file_path).joinpath( + str(self.service + "_" + CONF.default.policy_file_suffix) + ) + if path.exists(): + reloaded, data = _cache_handler.read_cached_file( + self._file_cache, path, force_reload=False + ) + if reloaded or not self.file_rules: + self.file_rules = policy.Rules.load(data) + else: + self.file_rules = {} def register_rules(self, rules: List[Union[Rule, APIRule]]) -> None: rule_map = {} @@ -107,16 +125,25 @@ class Enforcer: self.rules = Map(rule_map) def authorize(self, rule: str, target: Dict[str, Any], context: UserContext) -> bool: - result = False - do_check = self.rules.get(rule) - if do_check is None: - raise ValueError(f"Policy {rule} not registered.") + try: + self.load_rules() + except Exception: + LOG.debug(f"Failed to load {self.service} rules.") + + do_check = self.file_rules.get(rule) or self.rules.get(rule) + if do_check is None: + LOG.debug(f"Policy {rule} not registered.") + return False + + try: + result = _checks._check( + rule=do_check, + target=target, + creds=context, + enforcer=self, + current_rule=rule, + ) + except Exception: + result = False - result = _check( - rule=do_check, - target=target, - creds=context, - enforcer=self, - current_rule=rule, - ) return result