Debian: Remove sysinv references from sw-patch

sysinv files were being imported to provide authentication
features like policy enforcement and request contexts.

Those are now replaced with oslo imports.

Test Plan: (Debian)
 PASS: AIO-DX bootstrap/unlock
 PASS: CLI upload/apply/host-install RR patch
 PASS: Horizon patching operations work
 PASS: NFV patching operations work
 PASS: no (new) errors in patching logs

Story: 2009969
Task: 45998
Signed-off-by: Al Bailey <al.bailey@windriver.com>
Change-Id: I15d80441201755673f827529469a7f4feaa7f0ee
This commit is contained in:
Al Bailey 2022-08-04 18:25:32 +00:00
parent 0cada3c8ba
commit a4906a4f57
16 changed files with 289 additions and 124 deletions

View File

@ -1,5 +1,2 @@
{ {
"admin": "role:admin or role:administrator",
"admin_api": "is_admin:True",
"default": "rule:admin_api"
} }

View File

@ -1,16 +1,14 @@
""" #
Copyright (c) 2014-2017 Wind River Systems, Inc. # Copyright (c) 2014-2022 Wind River Systems, Inc.
#
SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
#
"""
"""Access Control Lists (ACL's) control access the API server."""
from cgcs_patch.authapi import auth_token from cgcs_patch.authapi import auth_token
OPT_GROUP_NAME = 'keystone_authtoken' OPT_GROUP_NAME = 'keystone_authtoken'
OPT_GROUP_PROVIDER = 'keystonemiddleware.auth_token'
"""Access Control Lists (ACL's) control access the API server."""
def install(app, conf, public_routes): def install(app, conf, public_routes):
@ -23,8 +21,7 @@ def install(app, conf, public_routes):
:return: The same WSGI application with ACL installed. :return: The same WSGI application with ACL installed.
""" """
keystone_config = dict(conf.get(OPT_GROUP_NAME))
keystone_config = dict(conf.items(OPT_GROUP_NAME))
return auth_token.AuthTokenMiddleware(app, return auth_token.AuthTokenMiddleware(app,
conf=keystone_config, conf=keystone_config,
public_api_routes=public_routes) public_api_routes=public_routes)

View File

@ -1,11 +1,10 @@
""" """
Copyright (c) 2014-2017 Wind River Systems, Inc. Copyright (c) 2014-2022 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
""" """
import configparser
from oslo_config import cfg from oslo_config import cfg
import pecan import pecan
@ -14,7 +13,6 @@ from cgcs_patch.authapi import config
from cgcs_patch.authapi import hooks from cgcs_patch.authapi import hooks
from cgcs_patch.authapi import policy from cgcs_patch.authapi import policy
auth_opts = [ auth_opts = [
cfg.StrOpt('auth_strategy', cfg.StrOpt('auth_strategy',
default='keystone', default='keystone',
@ -32,9 +30,6 @@ def get_pecan_config():
def setup_app(pecan_config=None, extra_hooks=None): def setup_app(pecan_config=None, extra_hooks=None):
config_parser = configparser.RawConfigParser()
config_parser.read('/etc/patching/patching.conf')
policy.init() policy.init()
app_hooks = [hooks.ConfigHook(), app_hooks = [hooks.ConfigHook(),
@ -47,7 +42,7 @@ def setup_app(pecan_config=None, extra_hooks=None):
pecan_config = get_pecan_config() pecan_config = get_pecan_config()
if pecan_config.app.enable_acl: if pecan_config.app.enable_acl:
app_hooks.append(hooks.AdminAuthHook()) app_hooks.append(hooks.AccessPolicyHook())
pecan.configuration.set_config(dict(pecan_config), overwrite=True) pecan.configuration.set_config(dict(pecan_config), overwrite=True)
@ -61,8 +56,10 @@ def setup_app(pecan_config=None, extra_hooks=None):
guess_content_type_from_ext=False, # Avoid mime-type lookup guess_content_type_from_ext=False, # Avoid mime-type lookup
) )
# config_parser must contain the keystone_auth
if pecan_config.app.enable_acl: if pecan_config.app.enable_acl:
return acl.install(app, config_parser, pecan_config.app.acl_public_routes) CONF.import_group(acl.OPT_GROUP_NAME, acl.OPT_GROUP_PROVIDER)
return acl.install(app, CONF, pecan_config.app.acl_public_routes)
return app return app

View File

@ -0,0 +1,39 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from oslo_context import context
# Patching calls into fault. so only FM service type
# needs to be preserved in the service catalog
REQUIRED_SERVICE_TYPES = ('faultmanagement',)
class RequestContext(context.RequestContext):
"""Extends security contexts from the OpenStack common library."""
def __init__(self, is_public_api=False, service_catalog=None, **kwargs):
"""Stores several additional request parameters:
"""
super(RequestContext, self).__init__(**kwargs)
self.is_public_api = is_public_api
if service_catalog:
# Only include required parts of service_catalog
self.service_catalog = [s for s in service_catalog
if s.get('type') in REQUIRED_SERVICE_TYPES]
else:
# if list is empty or none
self.service_catalog = []
def to_dict(self):
value = super(RequestContext, self).to_dict()
value.update({'is_public_api': self.is_public_api,
'service_catalog': self.service_catalog})
return value
def make_context(*args, **kwargs):
return RequestContext(*args, **kwargs)

View File

@ -21,12 +21,12 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils
from pecan import hooks from pecan import hooks
from webob import exc from webob import exc
from sysinv.common import context from cgcs_patch.authapi.context import RequestContext
from sysinv.openstack.common import policy from cgcs_patch.authapi import policy
from cgcs_patch import utils from cgcs_patch import utils
@ -56,6 +56,9 @@ class ContextHook(hooks.PecanHook):
The flag is set to True, if X-Roles contains either an administrator The flag is set to True, if X-Roles contains either an administrator
or admin substring. Otherwise it is set to False. or admin substring. Otherwise it is set to False.
X-Project-Name:
Used for context.project_name.
""" """
def __init__(self, public_api_routes): def __init__(self, public_api_routes):
self.public_api_routes = public_api_routes self.public_api_routes = public_api_routes
@ -66,36 +69,54 @@ class ContextHook(hooks.PecanHook):
user_id = state.request.headers.get('X-User', user_id) user_id = state.request.headers.get('X-User', user_id)
tenant = state.request.headers.get('X-Tenant-Id') tenant = state.request.headers.get('X-Tenant-Id')
tenant = state.request.headers.get('X-Tenant', tenant) tenant = state.request.headers.get('X-Tenant', tenant)
project_name = state.request.headers.get('X-Project-Name')
domain_id = state.request.headers.get('X-User-Domain-Id') domain_id = state.request.headers.get('X-User-Domain-Id')
domain_name = state.request.headers.get('X-User-Domain-Name') domain_name = state.request.headers.get('X-User-Domain-Name')
auth_token = state.request.headers.get('X-Auth-Token', None) auth_token = state.request.headers.get('X-Auth-Token', None)
creds = {'roles': state.request.headers.get('X-Roles', '').split(',')} creds = {'roles': state.request.headers.get('X-Roles', '').split(',')}
catalog_header = state.request.headers.get('X-Service-Catalog')
service_catalog = None
if catalog_header:
try:
service_catalog = jsonutils.loads(catalog_header)
except ValueError:
raise exc.HTTPInternalServerError(
'Invalid service catalog json.')
is_admin = policy.check('admin', state.request.headers, creds) is_admin = policy.authorize('admin_api', {}, creds, do_raise=False)
path = utils.safe_rstrip(state.request.path, '/') path = utils.safe_rstrip(state.request.path, '/')
is_public_api = path in self.public_api_routes is_public_api = path in self.public_api_routes
state.request.context = context.RequestContext( state.request.context = RequestContext(
auth_token=auth_token, auth_token=auth_token,
user=user_id, user=user_id,
tenant=tenant, tenant=tenant,
domain_id=domain_id, domain_id=domain_id,
domain_name=domain_name, domain_name=domain_name,
is_admin=is_admin, is_admin=is_admin,
is_public_api=is_public_api) is_public_api=is_public_api,
project_name=project_name,
roles=creds['roles'],
service_catalog=service_catalog)
class AdminAuthHook(hooks.PecanHook): class AccessPolicyHook(hooks.PecanHook):
"""Verify that the user has admin rights. """Verify that the user has the needed credentials
to execute the action.
Checks whether the request context is an admin context and
rejects the request otherwise.
""" """
def before(self, state): def before(self, state):
ctx = state.request.context controller = state.controller.__self__
is_admin_api = policy.check('admin_api', {}, ctx.to_dict()) if hasattr(controller, 'enforce_policy'):
controller_method = state.controller.__name__
if not is_admin_api and not ctx.is_public_api: controller.enforce_policy(controller_method,
raise exc.HTTPForbidden() state.request)
else:
context = state.request.context
is_admin_api = policy.authorize(
'admin_api',
{},
context.to_dict(),
do_raise=False)
if not is_admin_api and not context.is_public_api:
raise exc.HTTPForbidden()

View File

@ -16,104 +16,76 @@
# #
# Copyright (c) 2014-2022 Wind River Systems, Inc. # Copyright (c) 2014-2022 Wind River Systems, Inc.
# #
# SPDX-License-Identifier: Apache-2.0
#
"""Policy Engine For Patching.""" """Policy Engine For Patching."""
import os.path from oslo_config import cfg
from oslo_policy import policy
from sysinv.common import exception
from sysinv.openstack.common import policy
from cgcs_patch import utils
_POLICY_PATH = None base_rules = [
_POLICY_CACHE = {} policy.RuleDefault('admin_required', 'role:admin or is_admin:1',
description='Who is considered an admin'),
policy.RuleDefault('admin_api', 'rule:admin_required',
description='admin API requirement'),
policy.RuleDefault('default', 'rule:admin_api',
description='default rule'),
]
CONF = cfg.CONF
_ENFORCER = None
def reset(): def init(policy_file=None, rules=None,
global _POLICY_PATH default_rule=None, use_conf=True, overwrite=True):
global _POLICY_CACHE """Init an Enforcer class.
_POLICY_PATH = None
_POLICY_CACHE = {}
policy.reset()
oslo policy supports change policy rule dynamically.
policy.enforce will reload the policy rules if it detects
the policy files have been touched.
def init(): :param policy_file: Custom policy file to use, if none is
global _POLICY_PATH specified, ``conf.policy_file`` will be
global _POLICY_CACHE used.
if not _POLICY_PATH: :param rules: Default dictionary / Rules to use. It will be
_POLICY_PATH = '/etc/patching/policy.json' considered just in the first instantiation. If
if not os.path.exists(_POLICY_PATH): :meth:`load_rules` with ``force_reload=True``,
raise exception.ConfigNotFound(message='/etc/patching/policy.json') :meth:`clear` or :meth:`set_rules` with
utils.read_cached_file(_POLICY_PATH, _POLICY_CACHE, ``overwrite=True`` is called this will be overwritten.
reload_func=_set_rules) :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.
def _set_rules(data): :param overwrite: Whether to overwrite existing rules when reload rules
default_rule = "rule:admin_api" from config file.
rules = policy.Rules.load_rules(data, default_rule, [])
policy.set_rules(rules)
def enforce(context, action, target, do_raise=True):
"""Verifies that the action is valid on the target in this context.
:param context: sysinv context
:param action: string representing the action to be checked
this should be colon separated for clarity.
i.e. ``compute:create_instance``,
``compute:attach_volume``,
``volume:attach_volume``
:param target: dictionary representing the object of the action
for object creation this should be a dictionary representing the
location of the object e.g. ``{'project_id': context.project_id}``
:param do_raise: if True (the default), raises PolicyNotAuthorized;
if False, returns False
:raises sysinv.exception.PolicyNotAuthorized: if verification fails
and do_raise is True.
:return: returns a non-False value (not necessarily "True") if
authorized, and the exact value False if not authorized and
do_raise is False.
""" """
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)
return _ENFORCER
def authorize(rule, target, creds, do_raise=True):
"""A wrapper around 'authorize' from 'oslo_policy.policy'."""
init() init()
return _ENFORCER.authorize(rule, target, creds, do_raise=do_raise)
credentials = context.to_dict()
# Add the exception arguments if asked to do a raise def default_target(context):
extra = {} return {'project_id': context.project_id, 'user_id': context.user_id}
if do_raise:
extra.update(exc=exception.PolicyNotAuthorized, action=action)
return policy.check(action, target, credentials, **extra)
def check_is_admin(context): def check_is_admin(context):
"""Whether or not role contains 'admin' role according to policy setting. """Whether or not roles contains 'admin' role according to policy setting.
""" """
init() return authorize('context_is_admin', # rule
default_target(context), # target
credentials = context.to_dict() context) # creds
target = credentials
return policy.check('context_is_admin', target, credentials)
@policy.register('context_is_admin')
class IsAdminCheck(policy.Check):
"""An explicit check for is_admin."""
def __init__(self, kind, match):
"""Initialize the check."""
self.expected = (match.lower() == 'true')
super(IsAdminCheck, self).__init__(kind, str(self.expected))
def __call__(self, target, creds):
"""Determine whether is_admin matches the requested value."""
return creds['is_admin'] == self.expected

View File

@ -2588,6 +2588,13 @@ class PatchControllerMainThread(threading.Thread):
def main(): def main():
# The following call to CONF is to ensure the oslo config
# has been called to specify a valid config dir.
# Otherwise oslo_policy will fail when it looks for its files.
CONF(
(), # Required to load an anonymous configuration
default_config_files=['/etc/patching/patching.conf', ]
)
configure_logging() configure_logging()
cfg.read_config() cfg.read_config()

View File

@ -0,0 +1,23 @@
"""
Copyright (c) 2022 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
"""
# Server Specific Configurations
server = {
'port': '5487',
'host': '127.0.0.1'
}
# Pecan Application Configurations
app = {
'root': 'cgcs_patch.api.controllers.root.RootController',
'modules': ['cgcs_patch.authapi'],
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/../templates',
'debug': False,
'enable_acl': True,
'acl_public_routes': [],
}

View File

@ -0,0 +1,23 @@
"""
Copyright (c) 2022 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
"""
# Server Specific Configurations
server = {
'port': '5487',
'host': '127.0.0.1'
}
# Pecan Application Configurations
app = {
'root': 'cgcs_patch.api.controllers.root.RootController',
'modules': ['cgcs_patch.authapi'],
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/../templates',
'debug': False,
'enable_acl': False,
'acl_public_routes': [],
}

View File

@ -0,0 +1 @@
[DEFAULT]

View File

@ -0,0 +1,42 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import os
from oslo_config import cfg
from pecan import set_config
from pecan.testing import load_test_app
from unittest import TestCase
class SWPatchAPITest(TestCase):
"""API Tests for sw-patch"""
def setUp(self):
# trigger oslo_config to load a config file
# so that it can co-locate a policy file
config_file = os.path.join(os.path.dirname(__file__),
'patching.conf')
cfg.CONF((),
default_config_files=[config_file, ])
# config.py sets acl to False
self.app = load_test_app(os.path.join(
os.path.dirname(__file__),
'config.py'
))
def tearDown(self):
set_config({}, overwrite=True)
class TestRootController(SWPatchAPITest):
def test_get(self):
response = self.app.get('/')
assert response.status_int == 200
def test_get_not_found(self):
response = self.app.get('/a/bogus/url', expect_errors=True)
assert response.status_int == 404

View File

@ -0,0 +1,43 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import os
from oslo_config import cfg
from pecan import set_config
from pecan.testing import load_test_app
from unittest import TestCase
class SWPatchAuthAPITest(TestCase):
"""Auth API Tests for sw-patch"""
def setUp(self):
# trigger oslo_config to load a config file
# so that it can co-locate a policy file
config_file = os.path.join(os.path.dirname(__file__),
'patching.conf')
cfg.CONF((),
default_config_files=[config_file, ])
# auth_config.py sets acl to True
self.app = load_test_app(os.path.join(
os.path.dirname(__file__),
'auth_config.py'
))
def tearDown(self):
set_config({}, overwrite=True)
class TestRootControllerNoAuth(SWPatchAuthAPITest):
"""We are not authenticated. Commands return 401"""
def test_get(self):
response = self.app.get('/', expect_errors=True)
assert response.status_int == 401
def test_get_not_found(self):
response = self.app.get('/a/bogus/url', expect_errors=True)
assert response.status_int == 401

View File

@ -2,11 +2,15 @@
# of appearance. Changing the order has an impact on the overall integration # of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
keystoneauth1
keystonemiddleware keystonemiddleware
lxml lxml
oslo.config oslo.config
oslo.policy
oslo.serialization
netaddr netaddr
pecan pecan
pycryptodomex pycryptodomex
requests_toolbelt requests_toolbelt
sh sh
WebOb

View File

@ -17,7 +17,6 @@ basepython = python3
deps = -r{toxinidir}/requirements.txt deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt -r{toxinidir}/test-requirements.txt
-e{[tox]stxdir}/fault/fm-api/source -e{[tox]stxdir}/fault/fm-api/source
-e{[tox]stxdir}/config/sysinv/sysinv/sysinv
-e{[tox]stxdir}/config/tsconfig/tsconfig -e{[tox]stxdir}/config/tsconfig/tsconfig
install_command = pip install \ install_command = pip install \