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 <uuid>
fm alarm-summary
fm alarm-delete <uuid>
fm event-list
fm event-show <uuid>
fm event-suppress --alarm_id <alarm_id>
fm event-suppress-list
fm event-unsuppress --alarm_id <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://<MGMT_IP>:18002/" and
"curl -v http://<MGMT_IP>:18002/v1/" and
verify that they are accepted and that the HTTP response is 200,
and execute the commands:
"curl -v http://<MGMT_IP>:18002/v1/alarms" and
"curl -v http://<MGMT_IP>: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 <Joao.VictorPortal@windriver.com>
Change-Id: I3db6d0464d8d53c4dfbc761663be1712141b8b93
This commit is contained in:
Joao Victor Portal 2022-08-16 11:45:55 -03:00
parent 571b0665ae
commit 99eba3afb8
23 changed files with 352 additions and 72 deletions

View File

@ -2,7 +2,9 @@
# #
# SPDX-License-Identifier: Apache-2.0 # 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 # lib/fault
# Functions to control the configuration and operation of the **fault** service # 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_CONF=$STX_FAULT_CONF_DIR/fm.conf
FM_RESTAPI_PASTE_INI=$STX_FAULT_CONF_DIR/api-paste.ini FM_RESTAPI_PASTE_INI=$STX_FAULT_CONF_DIR/api-paste.ini
FM_EVENT_YAML=$STX_FAULT_CONF_DIR/events.yaml 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_AUTH_CACHE_DIR=${FM_RESTAPI_AUTH_CACHE_DIR:-/var/cache/fault}
FM_RESTAPI_DIR=$STX_FAULT_DIR/fm-rest-api/fm FM_RESTAPI_DIR=$STX_FAULT_DIR/fm-rest-api/fm
@ -191,7 +194,7 @@ function cleanup_fm_mgr {
function cleanup_fm_rest_api { function cleanup_fm_rest_api {
sudo pip uninstall -y fm 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 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/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-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 configure_auth_token_middleware $FM_RESTAPI_CONF fm $FM_RESTAPI_AUTH_CACHE_DIR

View File

@ -69,6 +69,7 @@ install -p -D -m 644 fm-api-pmond.conf %{buildroot}%{local_etc_pmond}/fm-api.con
# install default config files # 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 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 %{_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 %clean
echo "CLEAN CALLED" echo "CLEAN CALLED"
@ -90,6 +91,7 @@ rm -rf $RPM_BUILD_ROOT
%{pythonroot}/fm-%{version}*.egg-info %{pythonroot}/fm-%{version}*.egg-info
%config(noreplace) %attr(600,fm,fm)%{_sysconfdir}/fm/fm.conf %config(noreplace) %attr(600,fm,fm)%{_sysconfdir}/fm/fm.conf
%config(noreplace) %attr(600,fm,fm)%{_sysconfdir}/fm/policy.yaml
# systemctl service files # systemctl service files
%{_unitdir}/fm-api.service %{_unitdir}/fm-api.service

View File

@ -1,4 +1,5 @@
debian/systemd/00-fm-rest-api.preset etc/systemd/system-preset debian/systemd/00-fm-rest-api.preset etc/systemd/system-preset
etc/fm/fm.conf etc/fm/fm.conf
etc/fm/policy.yaml
etc/init.d etc/init.d
etc/pmon.d/fm-api.conf etc/pmon.d/fm-api.conf

View File

@ -3,5 +3,6 @@
set -e set -e
chown fm:fm /etc/fm/fm.conf chown fm:fm /etc/fm/fm.conf
chown fm:fm /etc/fm/policy.yaml
#DEBHELPER# #DEBHELPER#

View File

@ -18,11 +18,12 @@ override_dh_auto_install:
oslo-config-generator --config-file fm/config-generator.conf --output-file fm.conf.sample oslo-config-generator --config-file fm/config-generator.conf --output-file fm.conf.sample
install -d -m 755 $(FMCONFDIR) install -d -m 755 $(FMCONFDIR)
install -p -D -m 600 fm.conf.sample $(FMCONFDIR)/fm.conf 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 dh_auto_install
override_dh_fixperms: override_dh_fixperms:
dh_fixperms -Xfm.conf dh_fixperms -Xfm.conf -Xpolicy.yaml
override_dh_installsystemd: override_dh_installsystemd:
dh_installsystemd --no-enable --name fm-api dh_installsystemd --no-enable --name fm-api

View File

@ -10,7 +10,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # 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 # SPDX-License-Identifier: Apache-2.0
# #
@ -23,6 +23,7 @@ from oslo_log import log
import pecan import pecan
from fm.api import config from fm.api import config
from fm.api import hooks
from fm.api import middleware from fm.api import middleware
from fm.common import policy from fm.common import policy
from fm.common.i18n import _ from fm.common.i18n import _
@ -48,6 +49,8 @@ def setup_app(config=None):
pecan.configuration.set_config(dict(config), overwrite=True) pecan.configuration.set_config(dict(config), overwrite=True)
app_conf = dict(config.app) app_conf = dict(config.app)
if app_conf['enable_acl']:
app_conf['hooks'].append(hooks.AccessPolicyHook())
app = pecan.make_app( app = pecan.make_app(
app_conf.pop('root'), app_conf.pop('root'),

View File

@ -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 # SPDX-License-Identifier: Apache-2.0
# #
@ -45,10 +45,7 @@ app = {
hooks.DBHook(), hooks.DBHook(),
hooks.AuditLogging(), hooks.AuditLogging(),
], ],
'acl_public_routes': [ 'enable_acl': True
'/',
'/v1',
],
} }

View File

@ -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 # 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.api.controllers.v1 import utils as api_utils
from fm.common import exceptions from fm.common import exceptions
from fm.common import constants from fm.common import constants
from fm.common import policy
from fm import objects from fm import objects
from fm.api.policies import alarm as alarm_policy
from fm.api.controllers.v1.query import Query from fm.api.controllers.v1.query import Query
from fm_api import constants as fm_constants from fm_api import constants as fm_constants
@ -438,3 +440,21 @@ class AlarmController(rest.RestController):
return err return err
alarm_dict = alm.as_dict() alarm_dict = alm.as_dict()
return json.dumps({"uuid": alarm_dict['uuid']}) 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()

View File

@ -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 # 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 import link
from fm.api.controllers.v1.query import Query from fm.api.controllers.v1.query import Query
from fm.api.controllers.v1 import types 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 exceptions
from fm.common import policy
from fm.common.i18n import _ from fm.common.i18n import _
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -292,3 +294,12 @@ class EventLogController(rest.RestController):
pecan.request.context, id) pecan.request.context, id)
return EventLog.convert_with_links(rpc_ilog) 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()

View File

@ -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 # 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.query import Query
from fm.api.controllers.v1 import types from fm.api.controllers.v1 import types
from fm.api.controllers.v1 import utils as api_utils 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 constants
from fm.common import exceptions
from fm.common import policy
from fm.common import utils as cutils from fm.common import utils as cutils
from fm.common.i18n import _ from fm.common.i18n import _
@ -213,3 +216,15 @@ class EventSuppressionController(rest.RestController):
pecan.request.dbapi.event_suppression_update(uuid, updates) pecan.request.dbapi.event_suppression_update(uuid, updates)
return EventSuppression.convert_with_links(updated_event_suppression) 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()

View File

@ -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 # SPDX-License-Identifier: Apache-2.0
# #
@ -13,6 +13,7 @@ from oslo_config import cfg
from oslo_log import log from oslo_log import log
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
from webob import exc
from fm.common import context from fm.common import context
from fm.db import api as dbapi from fm.db import api as dbapi
@ -59,7 +60,7 @@ class ContextHook(hooks.PecanHook):
environ = state.request.environ environ = state.request.environ
user_name = headers.get('X-User-Name') user_name = headers.get('X-User-Name')
user_id = headers.get('X-User-Id') 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') project_id = headers.get('X-Project-Id')
domain_id = headers.get('X-User-Domain-Id') domain_id = headers.get('X-User-Domain-Id')
domain_name = headers.get('X-User-Domain-Name') domain_name = headers.get('X-User-Domain-Name')
@ -83,7 +84,7 @@ class ContextHook(hooks.PecanHook):
auth_token_info=auth_token_info, auth_token_info=auth_token_info,
user_name=user_name, user_name=user_name,
user_id=user_id, user_id=user_id,
project_name=project, project_name=project_name,
project_id=project_id, project_id=project_id,
domain_id=domain_id, domain_id=domain_id,
domain_name=domain_name, domain_name=domain_name,
@ -146,7 +147,10 @@ class AuditLogging(hooks.PecanHook):
def json_post_data(rest_state): def json_post_data(rest_state):
if 'form-data' in rest_state.request.headers.get('Content-Type'): if 'form-data' in rest_state.request.headers.get('Content-Type'):
return " POST: {}".format(rest_state.request.params) 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 ""
return " POST: {}".format(rest_state.request.json) return " POST: {}".format(rest_state.request.json)
@ -195,3 +199,19 @@ class AuditLogging(hooks.PecanHook):
def on_error(self, state, e): def on_error(self, state, e):
auditLOG.exception("Exception in AuditLogging passed to event 'on_error': " + str(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()

View File

@ -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()
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 # SPDX-License-Identifier: Apache-2.0
# #
@ -9,6 +9,7 @@ from oslo_config import cfg
from keystoneauth1 import plugin from keystoneauth1 import plugin
from keystoneauth1.access import service_catalog as k_service_catalog from keystoneauth1.access import service_catalog as k_service_catalog
from fm.api.policies import base as base_policy
from fm.common import policy from fm.common import policy
@ -95,7 +96,9 @@ class RequestContext(context.RequestContext):
self.user_auth_plugin = user_auth_plugin self.user_auth_plugin = user_auth_plugin
if is_admin is None: 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: else:
self.is_admin = is_admin self.is_admin = is_admin

View File

@ -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 # SPDX-License-Identifier: Apache-2.0
# #
@ -104,6 +104,10 @@ class PolicyNotAuthorized(ApiError):
code = webob.exc.HTTPUnauthorized.code code = webob.exc.HTTPUnauthorized.code
class PolicyNotFound(Invalid):
message = _("Policy not found for requested action.")
class Conflict(ApiError): class Conflict(ApiError):
message = _('HTTP Conflict.') message = _('HTTP Conflict.')
# 409 - HTTPConflict # 409 - HTTPConflict

View File

@ -13,77 +13,47 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
# #
# Copyright (c) 2018 Wind River Systems, Inc. # Copyright (c) 2018-2022 Wind River Systems, Inc.
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
"""Policy Engine For FM.""" """Policy Engine For FM."""
from oslo_config import cfg from oslo_config import cfg
from oslo_policy import policy 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 CONF = cfg.CONF
LOG = log.getLogger(__name__)
_ENFORCER = None _ENFORCER = None
# we can get a policy enforcer by this init. def reset():
# oslo policy support change policy rule dynamically. """Discard current Enforcer object."""
# at present, policy.enforce will reload the policy rules when it checks global _ENFORCER
# the policy files have been touched. _ENFORCER = None
def init(policy_file=None, rules=None,
default_rule=None, use_conf=True, overwrite=True):
def init(policy_file='policy.yaml'):
"""Init an Enforcer class. """Init an Enforcer class.
:param policy_file: Custom policy file to use, if none is :param policy_file: Custom policy file to be used.
specified, ``conf.policy_file`` will be
used. :return: Returns a Enforcer instance.
: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.
""" """
global _ENFORCER global _ENFORCER
if not _ENFORCER: if not _ENFORCER:
# https://docs.openstack.org/oslo.policy/latest/user/usage.html # https://docs.openstack.org/oslo.policy/latest/user/usage.html
_ENFORCER = policy.Enforcer(CONF, _ENFORCER = policy.Enforcer(CONF,
policy_file=policy_file, policy_file=policy_file,
rules=rules, default_rule='default',
default_rule=default_rule, use_conf=True,
use_conf=use_conf, overwrite=True)
overwrite=overwrite) _ENFORCER.register_defaults(controller_policies.list_rules())
_ENFORCER.register_defaults(base_rules)
return _ENFORCER return _ENFORCER
def check_is_admin(context): def authorize(rule, target, creds, do_raise=True):
"""Whether or not role contains 'admin' role according to policy setting. """A wrapper around 'authorize' from 'oslo_policy.policy'."""
"""
init() init()
return _ENFORCER.authorize(rule, target, creds, do_raise=do_raise)
target = {}
credentials = context.to_dict()
return _ENFORCER.enforce('context_is_admin', target, credentials)

View File

@ -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

View File

@ -12,6 +12,11 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
"""Base classes for API tests.""" """Base classes for API tests."""
from oslo_config import cfg from oslo_config import cfg
@ -45,7 +50,7 @@ class FunctionalTest(base.TestCase):
'app': { 'app': {
'root': 'fm.api.controllers.root.RootController', 'root': 'fm.api.controllers.root.RootController',
'modules': ['fm.api'], 'modules': ['fm.api'],
'acl_public_routes': ['/', '/v1'], 'enable_acl': False
}, },
} }

View File

@ -79,6 +79,7 @@ install -m 640 fm/db/sqlalchemy/migrate_repo/migrate.cfg %{buildroot}%{pythonroo
# install default config files # install default config files
oslo-config-generator --config-file fm/config-generator.conf --output-file %{_builddir}/fm.conf.sample 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 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 %fdupes %{buildroot}%{pythonroot}/fm
@ -99,6 +100,7 @@ install -p -D -m 644 %{_builddir}/fm.conf.sample %{buildroot}%{_sysconfdir}/fm/f
%dir %{_sysconfdir}/fm %dir %{_sysconfdir}/fm
%config(noreplace) %{_sysconfdir}/fm/fm.conf %config(noreplace) %{_sysconfdir}/fm/fm.conf
%config(noreplace) %attr(600,fm,fm)%{_sysconfdir}/fm/policy.yaml
# systemctl service files # systemctl service files
%{_unitdir}/fm-api.service %{_unitdir}/fm-api.service

View File

@ -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 # SPDX-License-Identifier: Apache-2.0
# #
@ -96,10 +96,10 @@ def event_suppression_update(cc, data, suppress=False):
patch = [] patch = []
for event_id in event_suppression_list: for event_id in event_suppression_list:
if event_id.alarm_id in alarm_id_list: if event_id.alarm_id in alarm_id_list:
print("Alarm ID: {} {}.".format(event_id.alarm_id, patch_value))
uuid = event_id.uuid uuid = event_id.uuid
patch.append(dict(path='/' + 'suppression_status', value=patch_value, op='replace')) patch.append(dict(path='/' + 'suppression_status', value=patch_value, op='replace'))
cc.event_suppression.update(uuid, patch) cc.event_suppression.update(uuid, patch)
print("Alarm ID: {} {}.".format(event_id.alarm_id, patch_value))
@utils.arg('--include-unsuppressed', action='store_true', @utils.arg('--include-unsuppressed', action='store_true',
@ -196,8 +196,8 @@ def do_event_unsuppress_all(cc, args):
if suppression_status == 'suppressed': if suppression_status == 'suppressed':
uuid = alarm_type.uuid uuid = alarm_type.uuid
patch.append(dict(path='/' + 'suppression_status', value='unsuppressed', op='replace')) 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) cc.event_suppression.update(uuid, patch)
print("Alarm ID: {} unsuppressed.".format(alarm_type.alarm_id))
no_paging = args.nopaging no_paging = args.nopaging
includeUUID = args.uuid includeUUID = args.uuid