From 99eba3afb8309ec1d906c35653940cee4d631565 Mon Sep 17 00:00:00 2001 From: Joao Victor Portal Date: Tue, 16 Aug 2022 11:45:55 -0300 Subject: [PATCH] Implement access control for FM API This commit implements the access control for all FM APIs. An incomplete list of FM APIs can be found at "https://docs.starlingx.io/api-ref/fault/api-ref-fm-v1-fault.html". Unit tests will be created in other task. All access control rules can be overwritten through file "/etc/fm/policy.yaml". Any change in file "/etc/fm/policy.yaml" is automatically detected by policy engine and the rules are updated. Differently from other APIs, which have as default rule to enforce that all users using the API are present in either project "admin" or "services", all read-only actions (GET requests) of FM API are allowed for any user, so it only requires "reader" role (that is the lowest role). Other actions require the user to have "admin" role and to be present in either project "admin" or "services". As all system users of StarlingX have "admin" role and are present in either project "admin" or "services", the default rules for FM API allows any system users to execute any action, so there should be no regression with the change introduced here. To test the access control of FM API, the following commands will be used: fm alarm-list fm alarm-show fm alarm-summary fm alarm-delete fm event-list fm event-show fm event-suppress --alarm_id fm event-suppress-list fm event-unsuppress --alarm_id fm event-unsuppress-all On test plan, these commands will be reffered as "test commands". Note: there is one FM API that is not tested by the commands above, that is the creation of alarms ("fm_api:alarm:create"). This API will be tested indirectly by observing the system successfully creating alarms in the deployed environment. Test Plan: PASS: Successfully deploy an AIO-SX using an Debian image with this commit present. Successfully create, through openstack CLI, the users: 'testreader' with role 'reader' in project 'admin', 'adminsvc' with role 'admin' in project 'services' and 'otheradmin' with role 'admin' in project 'notadminproject'. Create openrc files for all new users. Note: the other user that will be used is the already existing 'admin' with role 'admin' in project 'admin'. PASS: In the deployed AIO-SX, check the behavior of test commands through different users: for "admin" and "adminsvc" users, all commands are successful; for users "testreader" and "otheradmin", only the commands "alarm-delete", "event-suppress", "event-unsuppress" and "event-unsuppress-all" fail. Observe also that the system is able to create alarms during its operation. PASS: In the deployed AIO-SX, add the following lines in file "/etc/fm/policy.yaml": fm_api:alarm:create: role:admin fm_api:alarm:delete: role:admin fm_api:alarm:get: role:admin fm_api:alarm:modify: role:admin fm_api:event_log:get: role:admin fm_api:event_suppression:get: role:admin fm_api:event_suppression:modify: role:admin and check that all test commands are successful through user "otheradmin" and that all test commands fail through user "testreader". Observe also that the system is able to create alarms during its operation. PASS: In the deployed AIO-SX, to assert that public API works without authentication, execute the commands: "curl -v http://:18002/" and "curl -v http://:18002/v1/" and verify that they are accepted and that the HTTP response is 200, and execute the commands: "curl -v http://:18002/v1/alarms" and "curl -v http://:18002/v1/event_log" and verify that they are rejected and that the HTTP response is 401. PASS: In the deployed AIO-SX, check through Horizon interface that Fault Management works correctly (showing alarms and events, allowing events to be suppressed). PASS: Repeat all tests above changing the deploy to AIO-DX using an CentOS image. Story: 2010149 Task: 46123 Signed-off-by: Joao Victor Portal Change-Id: I3db6d0464d8d53c4dfbc761663be1712141b8b93 --- devstack/lib/fault | 8 +- fm-rest-api/centos/fm-rest-api.spec | 2 + .../debian/deb_folder/fm-rest-api.install | 1 + fm-rest-api/debian/deb_folder/postinst | 1 + fm-rest-api/debian/deb_folder/rules | 3 +- fm-rest-api/fm/fm/api/app.py | 5 +- fm-rest-api/fm/fm/api/config.py | 7 +- fm-rest-api/fm/fm/api/controllers/v1/alarm.py | 22 +++++- .../fm/fm/api/controllers/v1/event_log.py | 13 +++- .../api/controllers/v1/event_suppression.py | 17 ++++- fm-rest-api/fm/fm/api/hooks.py | 28 ++++++- fm-rest-api/fm/fm/api/policies/__init__.py | 21 ++++++ fm-rest-api/fm/fm/api/policies/alarm.py | 74 +++++++++++++++++++ fm-rest-api/fm/fm/api/policies/base.py | 30 ++++++++ fm-rest-api/fm/fm/api/policies/event_log.py | 36 +++++++++ .../fm/fm/api/policies/event_suppression.py | 44 +++++++++++ fm-rest-api/fm/fm/common/context.py | 7 +- fm-rest-api/fm/fm/common/exceptions.py | 6 +- fm-rest-api/fm/fm/common/policy.py | 68 +++++------------ fm-rest-api/fm/fm/policy.yaml | 16 ++++ fm-rest-api/fm/fm/tests/api/base.py | 7 +- fm-rest-api/opensuse/fm-rest-api.spec | 2 + .../fmclient/v1/event_suppression_shell.py | 6 +- 23 files changed, 352 insertions(+), 72 deletions(-) create mode 100644 fm-rest-api/fm/fm/api/policies/__init__.py create mode 100644 fm-rest-api/fm/fm/api/policies/alarm.py create mode 100644 fm-rest-api/fm/fm/api/policies/base.py create mode 100644 fm-rest-api/fm/fm/api/policies/event_log.py create mode 100644 fm-rest-api/fm/fm/api/policies/event_suppression.py create mode 100644 fm-rest-api/fm/fm/policy.yaml diff --git a/devstack/lib/fault b/devstack/lib/fault index 614fe31d..a1f0bb76 100644 --- a/devstack/lib/fault +++ b/devstack/lib/fault @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 # -# Copyright (C) 2019 Intel Corporation +# Copyright (c) 2019 Intel Corporation +# +# Copyright (c) 2022 Wind River Systems, Inc. # # lib/fault # Functions to control the configuration and operation of the **fault** service @@ -37,6 +39,7 @@ GITDIR["fm-core"]=$STX_FAULT_DIR/fm-common/sources FM_RESTAPI_CONF=$STX_FAULT_CONF_DIR/fm.conf FM_RESTAPI_PASTE_INI=$STX_FAULT_CONF_DIR/api-paste.ini FM_EVENT_YAML=$STX_FAULT_CONF_DIR/events.yaml +FM_POLICY_YAML=$STX_FAULT_CONF_DIR/policy.yaml FM_RESTAPI_AUTH_CACHE_DIR=${FM_RESTAPI_AUTH_CACHE_DIR:-/var/cache/fault} FM_RESTAPI_DIR=$STX_FAULT_DIR/fm-rest-api/fm @@ -191,7 +194,7 @@ function cleanup_fm_mgr { function cleanup_fm_rest_api { sudo pip uninstall -y fm - sudo rm -rf $FM_RESTAPI_AUTH_CACHE_DIR $FM_RESTAPI_CONF $FM_RESTAPI_PASTE_INI $FM_EVENT_YAML + sudo rm -rf $FM_RESTAPI_AUTH_CACHE_DIR $FM_RESTAPI_CONF $FM_RESTAPI_PASTE_INI $FM_EVENT_YAML $FM_POLICY_YAML dropdb -h 127.0.0.1 -Uroot fm } @@ -208,6 +211,7 @@ function configure_fm_rest_api { cp -p $STX_FAULT_DIR/devstack/files/api-paste.ini $FM_RESTAPI_PASTE_INI cp -p $STX_FAULT_DIR/fm-doc/fm_doc/events.yaml $FM_EVENT_YAML + cp -p $STX_FAULT_DIR/fm-rest-api/fm/fm/policy.yaml $FM_POLICY_YAML configure_auth_token_middleware $FM_RESTAPI_CONF fm $FM_RESTAPI_AUTH_CACHE_DIR diff --git a/fm-rest-api/centos/fm-rest-api.spec b/fm-rest-api/centos/fm-rest-api.spec index b28592bc..6eab40f1 100644 --- a/fm-rest-api/centos/fm-rest-api.spec +++ b/fm-rest-api/centos/fm-rest-api.spec @@ -69,6 +69,7 @@ install -p -D -m 644 fm-api-pmond.conf %{buildroot}%{local_etc_pmond}/fm-api.con # install default config files cd %{_builddir}/%{name}-%{version} && oslo-config-generator --config-file fm/config-generator.conf --output-file %{_builddir}/%{name}-%{version}/fm.conf.sample install -p -D -m 600 %{_builddir}/%{name}-%{version}/fm.conf.sample %{buildroot}%{_sysconfdir}/fm/fm.conf +install -p -D -m 600 fm/policy.yaml %{buildroot}%{_sysconfdir}/fm/policy.yaml %clean echo "CLEAN CALLED" @@ -90,6 +91,7 @@ rm -rf $RPM_BUILD_ROOT %{pythonroot}/fm-%{version}*.egg-info %config(noreplace) %attr(600,fm,fm)%{_sysconfdir}/fm/fm.conf +%config(noreplace) %attr(600,fm,fm)%{_sysconfdir}/fm/policy.yaml # systemctl service files %{_unitdir}/fm-api.service diff --git a/fm-rest-api/debian/deb_folder/fm-rest-api.install b/fm-rest-api/debian/deb_folder/fm-rest-api.install index 69e1eeb5..3e0c4c46 100644 --- a/fm-rest-api/debian/deb_folder/fm-rest-api.install +++ b/fm-rest-api/debian/deb_folder/fm-rest-api.install @@ -1,4 +1,5 @@ debian/systemd/00-fm-rest-api.preset etc/systemd/system-preset etc/fm/fm.conf +etc/fm/policy.yaml etc/init.d etc/pmon.d/fm-api.conf diff --git a/fm-rest-api/debian/deb_folder/postinst b/fm-rest-api/debian/deb_folder/postinst index ff26b856..e628dd62 100644 --- a/fm-rest-api/debian/deb_folder/postinst +++ b/fm-rest-api/debian/deb_folder/postinst @@ -3,5 +3,6 @@ set -e chown fm:fm /etc/fm/fm.conf +chown fm:fm /etc/fm/policy.yaml #DEBHELPER# diff --git a/fm-rest-api/debian/deb_folder/rules b/fm-rest-api/debian/deb_folder/rules index 1c17e506..81a031d2 100755 --- a/fm-rest-api/debian/deb_folder/rules +++ b/fm-rest-api/debian/deb_folder/rules @@ -18,11 +18,12 @@ override_dh_auto_install: oslo-config-generator --config-file fm/config-generator.conf --output-file fm.conf.sample install -d -m 755 $(FMCONFDIR) install -p -D -m 600 fm.conf.sample $(FMCONFDIR)/fm.conf + install -p -D -m 600 fm/policy.yaml $(FMCONFDIR)/policy.yaml dh_auto_install override_dh_fixperms: - dh_fixperms -Xfm.conf + dh_fixperms -Xfm.conf -Xpolicy.yaml override_dh_installsystemd: dh_installsystemd --no-enable --name fm-api diff --git a/fm-rest-api/fm/fm/api/app.py b/fm-rest-api/fm/fm/api/app.py index 7fd5a927..7e160db3 100644 --- a/fm-rest-api/fm/fm/api/app.py +++ b/fm-rest-api/fm/fm/api/app.py @@ -10,7 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Copyright (c) 2018 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -23,6 +23,7 @@ from oslo_log import log import pecan from fm.api import config +from fm.api import hooks from fm.api import middleware from fm.common import policy from fm.common.i18n import _ @@ -48,6 +49,8 @@ def setup_app(config=None): pecan.configuration.set_config(dict(config), overwrite=True) app_conf = dict(config.app) + if app_conf['enable_acl']: + app_conf['hooks'].append(hooks.AccessPolicyHook()) app = pecan.make_app( app_conf.pop('root'), diff --git a/fm-rest-api/fm/fm/api/config.py b/fm-rest-api/fm/fm/api/config.py index de6ccee7..7fc60422 100644 --- a/fm-rest-api/fm/fm/api/config.py +++ b/fm-rest-api/fm/fm/api/config.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2022 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -45,10 +45,7 @@ app = { hooks.DBHook(), hooks.AuditLogging(), ], - 'acl_public_routes': [ - '/', - '/v1', - ], + 'enable_acl': True } diff --git a/fm-rest-api/fm/fm/api/controllers/v1/alarm.py b/fm-rest-api/fm/fm/api/controllers/v1/alarm.py index aee0c808..dc2a2a78 100644 --- a/fm-rest-api/fm/fm/api/controllers/v1/alarm.py +++ b/fm-rest-api/fm/fm/api/controllers/v1/alarm.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018-2021 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -25,7 +25,9 @@ from fm.api.controllers.v1 import types from fm.api.controllers.v1 import utils as api_utils from fm.common import exceptions from fm.common import constants +from fm.common import policy from fm import objects +from fm.api.policies import alarm as alarm_policy from fm.api.controllers.v1.query import Query from fm_api import constants as fm_constants @@ -438,3 +440,21 @@ class AlarmController(rest.RestController): return err alarm_dict = alm.as_dict() return json.dumps({"uuid": alarm_dict['uuid']}) + + def enforce_policy(self, method_name, request): + """Check policy rules for each action of this controller.""" + context_dict = request.context.to_dict() + if method_name == "delete": + policy.authorize(alarm_policy.POLICY_ROOT % "delete", {}, + context_dict) + elif method_name in ["detail", "get_all", "get_one", "summary"]: + policy.authorize(alarm_policy.POLICY_ROOT % "get", {}, + context_dict) + elif method_name == "post": + policy.authorize(alarm_policy.POLICY_ROOT % "create", {}, + context_dict) + elif method_name == "put": + policy.authorize(alarm_policy.POLICY_ROOT % "modify", {}, + context_dict) + else: + raise exceptions.PolicyNotFound() diff --git a/fm-rest-api/fm/fm/api/controllers/v1/event_log.py b/fm-rest-api/fm/fm/api/controllers/v1/event_log.py index e5d012d5..4fe7a58f 100644 --- a/fm-rest-api/fm/fm/api/controllers/v1/event_log.py +++ b/fm-rest-api/fm/fm/api/controllers/v1/event_log.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018-2019 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -23,7 +23,9 @@ from fm.api.controllers.v1 import collection from fm.api.controllers.v1 import link from fm.api.controllers.v1.query import Query from fm.api.controllers.v1 import types +from fm.api.policies import event_log as event_log_policy from fm.common import exceptions +from fm.common import policy from fm.common.i18n import _ LOG = log.getLogger(__name__) @@ -292,3 +294,12 @@ class EventLogController(rest.RestController): pecan.request.context, id) return EventLog.convert_with_links(rpc_ilog) + + def enforce_policy(self, method_name, request): + """Check policy rules for each action of this controller.""" + context_dict = request.context.to_dict() + if method_name in ["detail", "get_all", "get_one"]: + policy.authorize(event_log_policy.POLICY_ROOT % "get", {}, + context_dict) + else: + raise exceptions.PolicyNotFound() diff --git a/fm-rest-api/fm/fm/api/controllers/v1/event_suppression.py b/fm-rest-api/fm/fm/api/controllers/v1/event_suppression.py index 8402256c..ee673f68 100644 --- a/fm-rest-api/fm/fm/api/controllers/v1/event_suppression.py +++ b/fm-rest-api/fm/fm/api/controllers/v1/event_suppression.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -20,7 +20,10 @@ from fm.api.controllers.v1 import link from fm.api.controllers.v1.query import Query from fm.api.controllers.v1 import types from fm.api.controllers.v1 import utils as api_utils +from fm.api.policies import event_suppression as event_suppression_policy from fm.common import constants +from fm.common import exceptions +from fm.common import policy from fm.common import utils as cutils from fm.common.i18n import _ @@ -213,3 +216,15 @@ class EventSuppressionController(rest.RestController): pecan.request.dbapi.event_suppression_update(uuid, updates) return EventSuppression.convert_with_links(updated_event_suppression) + + def enforce_policy(self, method_name, request): + """Check policy rules for each action of this controller.""" + context_dict = request.context.to_dict() + if method_name in ["get_all", "get_one"]: + policy.authorize(event_suppression_policy.POLICY_ROOT % "get", + {}, context_dict) + elif method_name == "patch": + policy.authorize(event_suppression_policy.POLICY_ROOT % "modify", + {}, context_dict) + else: + raise exceptions.PolicyNotFound() diff --git a/fm-rest-api/fm/fm/api/hooks.py b/fm-rest-api/fm/fm/api/hooks.py index 10c3488a..33bef849 100644 --- a/fm-rest-api/fm/fm/api/hooks.py +++ b/fm-rest-api/fm/fm/api/hooks.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2022 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -13,6 +13,7 @@ from oslo_config import cfg from oslo_log import log from oslo_serialization import jsonutils from oslo_utils import uuidutils +from webob import exc from fm.common import context from fm.db import api as dbapi @@ -59,7 +60,7 @@ class ContextHook(hooks.PecanHook): environ = state.request.environ user_name = headers.get('X-User-Name') user_id = headers.get('X-User-Id') - project = headers.get('X-Project-Name') + project_name = headers.get('X-Project-Name') project_id = headers.get('X-Project-Id') domain_id = headers.get('X-User-Domain-Id') domain_name = headers.get('X-User-Domain-Name') @@ -83,7 +84,7 @@ class ContextHook(hooks.PecanHook): auth_token_info=auth_token_info, user_name=user_name, user_id=user_id, - project_name=project, + project_name=project_name, project_id=project_id, domain_id=domain_id, domain_name=domain_name, @@ -146,7 +147,10 @@ class AuditLogging(hooks.PecanHook): def json_post_data(rest_state): if 'form-data' in rest_state.request.headers.get('Content-Type'): return " POST: {}".format(rest_state.request.params) - if not hasattr(rest_state.request, 'json'): + try: + if not hasattr(rest_state.request, 'json'): + return "" + except Exception: return "" return " POST: {}".format(rest_state.request.json) @@ -195,3 +199,19 @@ class AuditLogging(hooks.PecanHook): def on_error(self, state, e): auditLOG.exception("Exception in AuditLogging passed to event 'on_error': " + str(e)) + + +class AccessPolicyHook(hooks.PecanHook): + """Verify that the user has the needed privilege to execute the action.""" + def before(self, state): + is_public_api = state.request.environ.get('is_public_api', False) + if not is_public_api: + controller = state.controller.__self__ + if hasattr(controller, 'enforce_policy'): + try: + controller_method = state.controller.__name__ + controller.enforce_policy(controller_method, state.request) + except Exception: + raise exc.HTTPForbidden() + else: + raise exc.HTTPForbidden() diff --git a/fm-rest-api/fm/fm/api/policies/__init__.py b/fm-rest-api/fm/fm/api/policies/__init__.py new file mode 100644 index 00000000..c8007d7a --- /dev/null +++ b/fm-rest-api/fm/fm/api/policies/__init__.py @@ -0,0 +1,21 @@ +# +# Copyright (c) 2022 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import itertools + +from fm.api.policies import base +from fm.api.policies import alarm +from fm.api.policies import event_log +from fm.api.policies import event_suppression + + +def list_rules(): + return itertools.chain( + base.list_rules(), + alarm.list_rules(), + event_log.list_rules(), + event_suppression.list_rules() + ) diff --git a/fm-rest-api/fm/fm/api/policies/alarm.py b/fm-rest-api/fm/fm/api/policies/alarm.py new file mode 100644 index 00000000..0f92295f --- /dev/null +++ b/fm-rest-api/fm/fm/api/policies/alarm.py @@ -0,0 +1,74 @@ +# +# Copyright (c) 2022 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_policy import policy +from fm.api.policies import base + +POLICY_ROOT = 'fm_api:alarm:%s' + + +alarm_rules = [ + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'create', + check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + description="Create an alarm.", + operations=[ + { + 'method': 'POST', + 'path': '/v1/alarms' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'delete', + check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + description="Delete an alarm.", + operations=[ + { + 'method': 'DELETE', + 'path': '/v1/alarms/{alarm_uuid}' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'get', + check_str='role:reader', + description="Get alarms.", + operations=[ + { + 'method': 'GET', + 'path': '/v1/alarms' + }, + { + 'method': 'GET', + 'path': '/v1/alarms/{alarm_uuid}' + }, + { + 'method': 'GET', + 'path': '/v1/alarms/detail' + }, + { + 'method': 'GET', + 'path': '/v1/alarms/summary' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'modify', + check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + description="Modify an alarm.", + operations=[ + { + 'method': 'PUT', + 'path': '/v1/alarms' + } + ] + ) +] + + +def list_rules(): + return alarm_rules diff --git a/fm-rest-api/fm/fm/api/policies/base.py b/fm-rest-api/fm/fm/api/policies/base.py new file mode 100644 index 00000000..a4455a24 --- /dev/null +++ b/fm-rest-api/fm/fm/api/policies/base.py @@ -0,0 +1,30 @@ +# +# Copyright (c) 2022 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_policy import policy + +ADMIN_IN_SYSTEM_PROJECTS = 'admin_in_system_projects' +READER_IN_SYSTEM_PROJECTS = 'reader_in_system_projects' + + +base_rules = [ + policy.RuleDefault( + name=ADMIN_IN_SYSTEM_PROJECTS, + check_str='role:admin and (project_name:admin or ' + + 'project_name:services)', + description="Base rule.", + ), + policy.RuleDefault( + name=READER_IN_SYSTEM_PROJECTS, + check_str='role:reader and (project_name:admin or ' + + 'project_name:services)', + description="Base rule." + ) +] + + +def list_rules(): + return base_rules diff --git a/fm-rest-api/fm/fm/api/policies/event_log.py b/fm-rest-api/fm/fm/api/policies/event_log.py new file mode 100644 index 00000000..00a691ce --- /dev/null +++ b/fm-rest-api/fm/fm/api/policies/event_log.py @@ -0,0 +1,36 @@ +# +# Copyright (c) 2022 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_policy import policy + +POLICY_ROOT = 'fm_api:event_log:%s' + + +event_log_rules = [ + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'get', + check_str='role:reader', + description="Get event logs.", + operations=[ + { + 'method': 'GET', + 'path': '/v1/event_log' + }, + { + 'method': 'GET', + 'path': '/v1/event_log/{log_uuid}' + }, + { + 'method': 'GET', + 'path': '/v1/event_log/detail' + } + ] + ) +] + + +def list_rules(): + return event_log_rules diff --git a/fm-rest-api/fm/fm/api/policies/event_suppression.py b/fm-rest-api/fm/fm/api/policies/event_suppression.py new file mode 100644 index 00000000..9ce21dd5 --- /dev/null +++ b/fm-rest-api/fm/fm/api/policies/event_suppression.py @@ -0,0 +1,44 @@ +# +# Copyright (c) 2022 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_policy import policy +from fm.api.policies import base + +POLICY_ROOT = 'fm_api:event_suppression:%s' + + +event_suppression_rules = [ + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'get', + check_str='role:reader', + description="Get event suppressions.", + operations=[ + { + 'method': 'GET', + 'path': '/v1/event_suppression' + }, + { + 'method': 'GET', + 'path': '/v1/event_suppression/{event_suppression_uuid}' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'modify', + check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + description="Modify the value of an event suppression.", + operations=[ + { + 'method': 'PATCH', + 'path': '/v1/event_suppression/{event_suppression_uuid}' + } + ] + ) +] + + +def list_rules(): + return event_suppression_rules diff --git a/fm-rest-api/fm/fm/common/context.py b/fm-rest-api/fm/fm/common/context.py index 9dd811ef..c1e3652c 100644 --- a/fm-rest-api/fm/fm/common/context.py +++ b/fm-rest-api/fm/fm/common/context.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -9,6 +9,7 @@ from oslo_config import cfg from keystoneauth1 import plugin from keystoneauth1.access import service_catalog as k_service_catalog +from fm.api.policies import base as base_policy from fm.common import policy @@ -95,7 +96,9 @@ class RequestContext(context.RequestContext): self.user_auth_plugin = user_auth_plugin if is_admin is None: - self.is_admin = policy.check_is_admin(self) + self.is_admin = policy.authorize( + base_policy.ADMIN_IN_SYSTEM_PROJECTS, {}, self.to_dict(), + do_raise=False) else: self.is_admin = is_admin diff --git a/fm-rest-api/fm/fm/common/exceptions.py b/fm-rest-api/fm/fm/common/exceptions.py index 873f0910..b0780aa1 100644 --- a/fm-rest-api/fm/fm/common/exceptions.py +++ b/fm-rest-api/fm/fm/common/exceptions.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -104,6 +104,10 @@ class PolicyNotAuthorized(ApiError): code = webob.exc.HTTPUnauthorized.code +class PolicyNotFound(Invalid): + message = _("Policy not found for requested action.") + + class Conflict(ApiError): message = _('HTTP Conflict.') # 409 - HTTPConflict diff --git a/fm-rest-api/fm/fm/common/policy.py b/fm-rest-api/fm/fm/common/policy.py index 16a7320d..20c45579 100644 --- a/fm-rest-api/fm/fm/common/policy.py +++ b/fm-rest-api/fm/fm/common/policy.py @@ -13,77 +13,47 @@ # License for the specific language governing permissions and limitations # under the License. # -# Copyright (c) 2018 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # """Policy Engine For FM.""" from oslo_config import cfg - from oslo_policy import policy -from oslo_log import log +from fm.api import policies as controller_policies -base_rules = [ - policy.RuleDefault('admin_required', 'role:admin or is_admin:1', - description='Who is considered an admin'), - policy.RuleDefault('admin_api', 'is_admin_required:True', - description='admin API requirement'), - policy.RuleDefault('default', 'rule:admin_api', - description='default rule'), -] - CONF = cfg.CONF - - -LOG = log.getLogger(__name__) - _ENFORCER = None -# we can get a policy enforcer by this init. -# oslo policy support change policy rule dynamically. -# at present, policy.enforce will reload the policy rules when it checks -# the policy files have been touched. -def init(policy_file=None, rules=None, - default_rule=None, use_conf=True, overwrite=True): +def reset(): + """Discard current Enforcer object.""" + global _ENFORCER + _ENFORCER = None + + +def init(policy_file='policy.yaml'): """Init an Enforcer class. - :param policy_file: Custom policy file to use, if none is - specified, ``conf.policy_file`` will be - used. - :param rules: Default dictionary / Rules to use. It will be - considered just in the first instantiation. If - :meth:`load_rules` with ``force_reload=True``, - :meth:`clear` or :meth:`set_rules` with - ``overwrite=True`` is called this will be overwritten. - :param default_rule: Default rule to use, conf.default_rule will - be used if none is specified. - :param use_conf: Whether to load rules from cache or config file. - :param overwrite: Whether to overwrite existing rules when reload rules - from config file. + :param policy_file: Custom policy file to be used. + + :return: Returns a Enforcer instance. """ global _ENFORCER if not _ENFORCER: # https://docs.openstack.org/oslo.policy/latest/user/usage.html _ENFORCER = policy.Enforcer(CONF, policy_file=policy_file, - rules=rules, - default_rule=default_rule, - use_conf=use_conf, - overwrite=overwrite) - _ENFORCER.register_defaults(base_rules) + default_rule='default', + use_conf=True, + overwrite=True) + _ENFORCER.register_defaults(controller_policies.list_rules()) return _ENFORCER -def check_is_admin(context): - """Whether or not role contains 'admin' role according to policy setting. - - """ +def authorize(rule, target, creds, do_raise=True): + """A wrapper around 'authorize' from 'oslo_policy.policy'.""" init() - - target = {} - credentials = context.to_dict() - - return _ENFORCER.enforce('context_is_admin', target, credentials) + return _ENFORCER.authorize(rule, target, creds, do_raise=do_raise) diff --git a/fm-rest-api/fm/fm/policy.yaml b/fm-rest-api/fm/fm/policy.yaml new file mode 100644 index 00000000..510f9246 --- /dev/null +++ b/fm-rest-api/fm/fm/policy.yaml @@ -0,0 +1,16 @@ +--- +# The commented lines below contains the default values for presented rules. + +# admin_in_system_projects: role:admin and (project_name:admin or project_name:services) +# reader_in_system_projects: role:reader and (project_name:admin or project_name:services) + +# fm_api:alarm:create: rule:admin_in_system_projects +# fm_api:alarm:delete: rule:admin_in_system_projects +# fm_api:alarm:get: role:reader +# fm_api:alarm:modify: rule:admin_in_system_projects + +# fm_api:event_log:get: role:reader + +# fm_api:event_suppression:get: role:reader +# fm_api:event_suppression:modify: rule:admin_in_system_projects + diff --git a/fm-rest-api/fm/fm/tests/api/base.py b/fm-rest-api/fm/fm/tests/api/base.py index d8506099..cc318119 100644 --- a/fm-rest-api/fm/fm/tests/api/base.py +++ b/fm-rest-api/fm/fm/tests/api/base.py @@ -12,6 +12,11 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +# +# Copyright (c) 2022 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# """Base classes for API tests.""" from oslo_config import cfg @@ -45,7 +50,7 @@ class FunctionalTest(base.TestCase): 'app': { 'root': 'fm.api.controllers.root.RootController', 'modules': ['fm.api'], - 'acl_public_routes': ['/', '/v1'], + 'enable_acl': False }, } diff --git a/fm-rest-api/opensuse/fm-rest-api.spec b/fm-rest-api/opensuse/fm-rest-api.spec index ca06f9de..31625e33 100644 --- a/fm-rest-api/opensuse/fm-rest-api.spec +++ b/fm-rest-api/opensuse/fm-rest-api.spec @@ -79,6 +79,7 @@ install -m 640 fm/db/sqlalchemy/migrate_repo/migrate.cfg %{buildroot}%{pythonroo # install default config files oslo-config-generator --config-file fm/config-generator.conf --output-file %{_builddir}/fm.conf.sample install -p -D -m 644 %{_builddir}/fm.conf.sample %{buildroot}%{_sysconfdir}/fm/fm.conf +install -p -D -m 600 fm/policy.yaml %{buildroot}%{_sysconfdir}/fm/policy.yaml %fdupes %{buildroot}%{pythonroot}/fm @@ -99,6 +100,7 @@ install -p -D -m 644 %{_builddir}/fm.conf.sample %{buildroot}%{_sysconfdir}/fm/f %dir %{_sysconfdir}/fm %config(noreplace) %{_sysconfdir}/fm/fm.conf +%config(noreplace) %attr(600,fm,fm)%{_sysconfdir}/fm/policy.yaml # systemctl service files %{_unitdir}/fm-api.service diff --git a/python-fmclient/fmclient/fmclient/v1/event_suppression_shell.py b/python-fmclient/fmclient/fmclient/v1/event_suppression_shell.py index 7064dc17..f57ce23c 100644 --- a/python-fmclient/fmclient/fmclient/v1/event_suppression_shell.py +++ b/python-fmclient/fmclient/fmclient/v1/event_suppression_shell.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2018 Wind River Systems, Inc. +# Copyright (c) 2018-2022 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # @@ -96,10 +96,10 @@ def event_suppression_update(cc, data, suppress=False): patch = [] for event_id in event_suppression_list: if event_id.alarm_id in alarm_id_list: - print("Alarm ID: {} {}.".format(event_id.alarm_id, patch_value)) uuid = event_id.uuid patch.append(dict(path='/' + 'suppression_status', value=patch_value, op='replace')) cc.event_suppression.update(uuid, patch) + print("Alarm ID: {} {}.".format(event_id.alarm_id, patch_value)) @utils.arg('--include-unsuppressed', action='store_true', @@ -196,8 +196,8 @@ def do_event_unsuppress_all(cc, args): if suppression_status == 'suppressed': uuid = alarm_type.uuid patch.append(dict(path='/' + 'suppression_status', value='unsuppressed', op='replace')) - print("Alarm ID: {} unsuppressed.".format(alarm_type.alarm_id)) cc.event_suppression.update(uuid, patch) + print("Alarm ID: {} unsuppressed.".format(alarm_type.alarm_id)) no_paging = args.nopaging includeUUID = args.uuid