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:
parent
0cada3c8ba
commit
a4906a4f57
@ -1,5 +1,2 @@
|
||||
{
|
||||
"admin": "role:admin or role:administrator",
|
||||
"admin_api": "is_admin:True",
|
||||
"default": "rule:admin_api"
|
||||
}
|
||||
|
@ -1,16 +1,14 @@
|
||||
"""
|
||||
Copyright (c) 2014-2017 Wind River Systems, Inc.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
#
|
||||
# Copyright (c) 2014-2022 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
"""Access Control Lists (ACL's) control access the API server."""
|
||||
from cgcs_patch.authapi import auth_token
|
||||
|
||||
OPT_GROUP_NAME = 'keystone_authtoken'
|
||||
|
||||
|
||||
"""Access Control Lists (ACL's) control access the API server."""
|
||||
OPT_GROUP_PROVIDER = 'keystonemiddleware.auth_token'
|
||||
|
||||
|
||||
def install(app, conf, public_routes):
|
||||
@ -23,8 +21,7 @@ def install(app, conf, public_routes):
|
||||
:return: The same WSGI application with ACL installed.
|
||||
|
||||
"""
|
||||
|
||||
keystone_config = dict(conf.items(OPT_GROUP_NAME))
|
||||
keystone_config = dict(conf.get(OPT_GROUP_NAME))
|
||||
return auth_token.AuthTokenMiddleware(app,
|
||||
conf=keystone_config,
|
||||
public_api_routes=public_routes)
|
||||
|
@ -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
|
||||
|
||||
"""
|
||||
|
||||
import configparser
|
||||
from oslo_config import cfg
|
||||
import pecan
|
||||
|
||||
@ -14,7 +13,6 @@ from cgcs_patch.authapi import config
|
||||
from cgcs_patch.authapi import hooks
|
||||
from cgcs_patch.authapi import policy
|
||||
|
||||
|
||||
auth_opts = [
|
||||
cfg.StrOpt('auth_strategy',
|
||||
default='keystone',
|
||||
@ -32,9 +30,6 @@ def get_pecan_config():
|
||||
|
||||
|
||||
def setup_app(pecan_config=None, extra_hooks=None):
|
||||
config_parser = configparser.RawConfigParser()
|
||||
config_parser.read('/etc/patching/patching.conf')
|
||||
|
||||
policy.init()
|
||||
|
||||
app_hooks = [hooks.ConfigHook(),
|
||||
@ -47,7 +42,7 @@ def setup_app(pecan_config=None, extra_hooks=None):
|
||||
pecan_config = get_pecan_config()
|
||||
|
||||
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)
|
||||
|
||||
@ -61,8 +56,10 @@ def setup_app(pecan_config=None, extra_hooks=None):
|
||||
guess_content_type_from_ext=False, # Avoid mime-type lookup
|
||||
)
|
||||
|
||||
# config_parser must contain the keystone_auth
|
||||
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
|
||||
|
||||
|
39
sw-patch/cgcs-patch/cgcs_patch/authapi/context.py
Normal file
39
sw-patch/cgcs-patch/cgcs_patch/authapi/context.py
Normal 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)
|
@ -21,12 +21,12 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
from oslo_config import cfg
|
||||
from oslo_serialization import jsonutils
|
||||
from pecan import hooks
|
||||
from webob import exc
|
||||
|
||||
from sysinv.common import context
|
||||
from sysinv.openstack.common import policy
|
||||
|
||||
from cgcs_patch.authapi.context import RequestContext
|
||||
from cgcs_patch.authapi import policy
|
||||
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
|
||||
or admin substring. Otherwise it is set to False.
|
||||
|
||||
X-Project-Name:
|
||||
Used for context.project_name.
|
||||
|
||||
"""
|
||||
def __init__(self, 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)
|
||||
tenant = state.request.headers.get('X-Tenant-Id')
|
||||
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_name = state.request.headers.get('X-User-Domain-Name')
|
||||
auth_token = state.request.headers.get('X-Auth-Token', None)
|
||||
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, '/')
|
||||
is_public_api = path in self.public_api_routes
|
||||
|
||||
state.request.context = context.RequestContext(
|
||||
state.request.context = RequestContext(
|
||||
auth_token=auth_token,
|
||||
user=user_id,
|
||||
tenant=tenant,
|
||||
domain_id=domain_id,
|
||||
domain_name=domain_name,
|
||||
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):
|
||||
"""Verify that the user has admin rights.
|
||||
|
||||
Checks whether the request context is an admin context and
|
||||
rejects the request otherwise.
|
||||
|
||||
class AccessPolicyHook(hooks.PecanHook):
|
||||
"""Verify that the user has the needed credentials
|
||||
to execute the action.
|
||||
"""
|
||||
def before(self, state):
|
||||
ctx = state.request.context
|
||||
is_admin_api = policy.check('admin_api', {}, ctx.to_dict())
|
||||
|
||||
if not is_admin_api and not ctx.is_public_api:
|
||||
controller = state.controller.__self__
|
||||
if hasattr(controller, 'enforce_policy'):
|
||||
controller_method = state.controller.__name__
|
||||
controller.enforce_policy(controller_method,
|
||||
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()
|
||||
|
@ -16,104 +16,76 @@
|
||||
#
|
||||
# Copyright (c) 2014-2022 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
"""Policy Engine For Patching."""
|
||||
|
||||
import os.path
|
||||
|
||||
from sysinv.common import exception
|
||||
from sysinv.openstack.common import policy
|
||||
|
||||
from cgcs_patch import utils
|
||||
from oslo_config import cfg
|
||||
from oslo_policy import policy
|
||||
|
||||
|
||||
_POLICY_PATH = None
|
||||
_POLICY_CACHE = {}
|
||||
base_rules = [
|
||||
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():
|
||||
global _POLICY_PATH
|
||||
global _POLICY_CACHE
|
||||
_POLICY_PATH = None
|
||||
_POLICY_CACHE = {}
|
||||
policy.reset()
|
||||
def init(policy_file=None, rules=None,
|
||||
default_rule=None, use_conf=True, overwrite=True):
|
||||
"""Init an Enforcer class.
|
||||
|
||||
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():
|
||||
global _POLICY_PATH
|
||||
global _POLICY_CACHE
|
||||
if not _POLICY_PATH:
|
||||
_POLICY_PATH = '/etc/patching/policy.json'
|
||||
if not os.path.exists(_POLICY_PATH):
|
||||
raise exception.ConfigNotFound(message='/etc/patching/policy.json')
|
||||
utils.read_cached_file(_POLICY_PATH, _POLICY_CACHE,
|
||||
reload_func=_set_rules)
|
||||
|
||||
|
||||
def _set_rules(data):
|
||||
default_rule = "rule:admin_api"
|
||||
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.
|
||||
: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.
|
||||
"""
|
||||
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()
|
||||
return _ENFORCER.authorize(rule, target, creds, do_raise=do_raise)
|
||||
|
||||
credentials = context.to_dict()
|
||||
|
||||
# Add the exception arguments if asked to do a raise
|
||||
extra = {}
|
||||
if do_raise:
|
||||
extra.update(exc=exception.PolicyNotAuthorized, action=action)
|
||||
|
||||
return policy.check(action, target, credentials, **extra)
|
||||
def default_target(context):
|
||||
return {'project_id': context.project_id, 'user_id': context.user_id}
|
||||
|
||||
|
||||
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()
|
||||
|
||||
credentials = context.to_dict()
|
||||
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
|
||||
return authorize('context_is_admin', # rule
|
||||
default_target(context), # target
|
||||
context) # creds
|
||||
|
@ -2588,6 +2588,13 @@ class PatchControllerMainThread(threading.Thread):
|
||||
|
||||
|
||||
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()
|
||||
|
||||
cfg.read_config()
|
||||
|
23
sw-patch/cgcs-patch/cgcs_patch/tests/api/auth_config.py
Normal file
23
sw-patch/cgcs-patch/cgcs_patch/tests/api/auth_config.py
Normal 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': [],
|
||||
}
|
23
sw-patch/cgcs-patch/cgcs_patch/tests/api/config.py
Normal file
23
sw-patch/cgcs-patch/cgcs_patch/tests/api/config.py
Normal 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': [],
|
||||
}
|
1
sw-patch/cgcs-patch/cgcs_patch/tests/api/patching.conf
Normal file
1
sw-patch/cgcs-patch/cgcs_patch/tests/api/patching.conf
Normal file
@ -0,0 +1 @@
|
||||
[DEFAULT]
|
42
sw-patch/cgcs-patch/cgcs_patch/tests/api/test_api.py
Normal file
42
sw-patch/cgcs-patch/cgcs_patch/tests/api/test_api.py
Normal 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
|
43
sw-patch/cgcs-patch/cgcs_patch/tests/api/test_authapi.py
Normal file
43
sw-patch/cgcs-patch/cgcs_patch/tests/api/test_authapi.py
Normal 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
|
@ -2,11 +2,15 @@
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
keystoneauth1
|
||||
keystonemiddleware
|
||||
lxml
|
||||
oslo.config
|
||||
oslo.policy
|
||||
oslo.serialization
|
||||
netaddr
|
||||
pecan
|
||||
pycryptodomex
|
||||
requests_toolbelt
|
||||
sh
|
||||
WebOb
|
||||
|
@ -17,7 +17,6 @@ basepython = python3
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
-e{[tox]stxdir}/fault/fm-api/source
|
||||
-e{[tox]stxdir}/config/sysinv/sysinv/sysinv
|
||||
-e{[tox]stxdir}/config/tsconfig/tsconfig
|
||||
|
||||
install_command = pip install \
|
||||
|
Loading…
Reference in New Issue
Block a user