Merge "Add allowlist and denylist for action providers and actions"
This commit is contained in:
commit
ffef2f4a79
@ -99,7 +99,17 @@ class LegacyActionProvider(ml_actions.ActionProvider):
|
|||||||
invoke_on_load=False
|
invoke_on_load=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
allowlist = CONF.legacy_action_provider.allowlist
|
||||||
|
denylist = CONF.legacy_action_provider.denylist
|
||||||
|
|
||||||
for action_name in ext_mgr.names():
|
for action_name in ext_mgr.names():
|
||||||
|
if allowlist:
|
||||||
|
if action_name not in allowlist:
|
||||||
|
continue
|
||||||
|
elif denylist:
|
||||||
|
if action_name in denylist:
|
||||||
|
continue
|
||||||
|
|
||||||
action_cls = ext_mgr[action_name].plugin
|
action_cls = ext_mgr[action_name].plugin
|
||||||
|
|
||||||
if CONF.legacy_action_provider.only_builtin_actions:
|
if CONF.legacy_action_provider.only_builtin_actions:
|
||||||
|
@ -64,6 +64,29 @@ auth_type_opt = cfg.StrOpt(
|
|||||||
help=_('Authentication type (valid options: keystone, keycloak-oidc)')
|
help=_('Authentication type (valid options: keystone, keycloak-oidc)')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
action_providers_opts = [
|
||||||
|
cfg.ListOpt(
|
||||||
|
'allowlist',
|
||||||
|
default=[],
|
||||||
|
help=_(
|
||||||
|
'Allowlist with action providers that is allowed to be '
|
||||||
|
'loaded from the entry point "mistral.action.providers", '
|
||||||
|
'if empty all action providers will be allowed unless '
|
||||||
|
'denylist is set.'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cfg.ListOpt(
|
||||||
|
'denylist',
|
||||||
|
default=[],
|
||||||
|
help=_(
|
||||||
|
'Denylist with action providers that is not allowed to '
|
||||||
|
'be loaded from the entry point "mistral.action.providers", '
|
||||||
|
'allowlist takes precendence, if empty all action providers '
|
||||||
|
'will be allowed.'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
legacy_action_provider_opts = [
|
legacy_action_provider_opts = [
|
||||||
cfg.BoolOpt(
|
cfg.BoolOpt(
|
||||||
'load_action_plugins',
|
'load_action_plugins',
|
||||||
@ -91,6 +114,25 @@ legacy_action_provider_opts = [
|
|||||||
'This property is needed mostly for testing.'
|
'This property is needed mostly for testing.'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
cfg.ListOpt(
|
||||||
|
'allowlist',
|
||||||
|
default=[],
|
||||||
|
help=_(
|
||||||
|
'Allowlist with actions that is allowed to be '
|
||||||
|
'loaded from the entry point "mistral.actions", '
|
||||||
|
'if empty all actions will be allowed.'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cfg.ListOpt(
|
||||||
|
'denylist',
|
||||||
|
default=[],
|
||||||
|
help=_(
|
||||||
|
'Denylist with actions that is not allowed to '
|
||||||
|
'be loaded from the entry point "mistral.actions", '
|
||||||
|
'allowlist takes precedence, if empty all actions '
|
||||||
|
'will be allowed.'
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
api_opts = [
|
api_opts = [
|
||||||
@ -755,6 +797,7 @@ healthcheck_opts = [
|
|||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
ACTION_PROVIDERS_GROUP = 'action_providers'
|
||||||
LEGACY_ACTION_PROVIDER_GROUP = 'legacy_action_provider'
|
LEGACY_ACTION_PROVIDER_GROUP = 'legacy_action_provider'
|
||||||
API_GROUP = 'api'
|
API_GROUP = 'api'
|
||||||
ENGINE_GROUP = 'engine'
|
ENGINE_GROUP = 'engine'
|
||||||
@ -785,6 +828,7 @@ CONF.register_opt(oslo_rpc_executor)
|
|||||||
CONF.register_opt(expiration_token_duration)
|
CONF.register_opt(expiration_token_duration)
|
||||||
CONF.register_opts(service_opts.service_opts)
|
CONF.register_opts(service_opts.service_opts)
|
||||||
|
|
||||||
|
CONF.register_opts(action_providers_opts, group=ACTION_PROVIDERS_GROUP)
|
||||||
CONF.register_opts(
|
CONF.register_opts(
|
||||||
legacy_action_provider_opts,
|
legacy_action_provider_opts,
|
||||||
group=LEGACY_ACTION_PROVIDER_GROUP
|
group=LEGACY_ACTION_PROVIDER_GROUP
|
||||||
@ -842,6 +886,7 @@ _DEFAULT_LOG_LEVELS = [
|
|||||||
|
|
||||||
def list_opts():
|
def list_opts():
|
||||||
return [
|
return [
|
||||||
|
(ACTION_PROVIDERS_GROUP, action_providers_opts),
|
||||||
(API_GROUP, api_opts),
|
(API_GROUP, api_opts),
|
||||||
(ENGINE_GROUP, engine_opts),
|
(ENGINE_GROUP, engine_opts),
|
||||||
(EXECUTOR_GROUP, executor_opts),
|
(EXECUTOR_GROUP, executor_opts),
|
||||||
|
@ -18,6 +18,7 @@ collection of functions for accessing information about actions
|
|||||||
available in the system.
|
available in the system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from stevedore import extension
|
from stevedore import extension
|
||||||
|
|
||||||
@ -39,7 +40,17 @@ def _get_registered_providers():
|
|||||||
invoke_on_load=False
|
invoke_on_load=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
allowlist = cfg.CONF.action_providers.allowlist
|
||||||
|
denylist = cfg.CONF.action_providers.denylist
|
||||||
|
|
||||||
for provider_name in mgr.names():
|
for provider_name in mgr.names():
|
||||||
|
if allowlist:
|
||||||
|
if provider_name not in allowlist:
|
||||||
|
continue
|
||||||
|
elif denylist:
|
||||||
|
if provider_name in denylist:
|
||||||
|
continue
|
||||||
|
|
||||||
provider_cls = mgr[provider_name].plugin
|
provider_cls = mgr[provider_name].plugin
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -202,3 +202,61 @@ class LegacyActionProviderTest(base.BaseTest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual('Goodbye, Lieutenant Dan!', goodbye_action.run(None))
|
self.assertEqual('Goodbye, Lieutenant Dan!', goodbye_action.run(None))
|
||||||
|
|
||||||
|
def test_allowlist(self):
|
||||||
|
self.override_config(
|
||||||
|
'load_action_generators',
|
||||||
|
False,
|
||||||
|
'legacy_action_provider'
|
||||||
|
)
|
||||||
|
self.override_config(
|
||||||
|
'allowlist', ['std.noop'],
|
||||||
|
'legacy_action_provider'
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = legacy.LegacyActionProvider()
|
||||||
|
action_descs = provider.find_all()
|
||||||
|
self.assertEqual(1, len(action_descs))
|
||||||
|
self.assertIsNotNone(provider.find('std.noop'))
|
||||||
|
|
||||||
|
def test_denylist(self):
|
||||||
|
self.override_config(
|
||||||
|
'load_action_generators',
|
||||||
|
False,
|
||||||
|
'legacy_action_provider'
|
||||||
|
)
|
||||||
|
self.override_config(
|
||||||
|
'denylist', ['std.noop'],
|
||||||
|
'legacy_action_provider'
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = legacy.LegacyActionProvider()
|
||||||
|
action_descs = provider.find_all()
|
||||||
|
self.assertTrue(
|
||||||
|
all(
|
||||||
|
[
|
||||||
|
a_d.name != 'std.noop'
|
||||||
|
for a_d in action_descs
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_allowlist_and_denylist(self):
|
||||||
|
self.override_config(
|
||||||
|
'load_action_generators',
|
||||||
|
False,
|
||||||
|
'legacy_action_provider'
|
||||||
|
)
|
||||||
|
self.override_config(
|
||||||
|
'allowlist', ['std.noop'],
|
||||||
|
'legacy_action_provider'
|
||||||
|
)
|
||||||
|
self.override_config(
|
||||||
|
'denylist', ['std.fail'],
|
||||||
|
'legacy_action_provider'
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = legacy.LegacyActionProvider()
|
||||||
|
action_descs = provider.find_all()
|
||||||
|
self.assertEqual(1, len(action_descs))
|
||||||
|
self.assertIsNotNone(provider.find('std.noop'))
|
||||||
|
71
mistral/tests/unit/services/test_actions.py
Normal file
71
mistral/tests/unit/services/test_actions.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Copyright 2025 Binero.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from mistral import config
|
||||||
|
from mistral.services.actions import _get_registered_providers
|
||||||
|
from mistral.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
class FakeExtManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.plugins = ['adhoc', 'dynamic', 'legacy']
|
||||||
|
|
||||||
|
def names(self):
|
||||||
|
return self.plugins
|
||||||
|
|
||||||
|
|
||||||
|
class ActionsTest(base.DbTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(ActionsTest, self).setUp()
|
||||||
|
|
||||||
|
@mock.patch('stevedore.enabled.ExtensionManager')
|
||||||
|
def test_get_registered_providers(self, mock_ext_mgr):
|
||||||
|
mock_ext_mgr.return_value = FakeExtManager()
|
||||||
|
providers = _get_registered_providers()
|
||||||
|
self.assertEqual(3, len(providers))
|
||||||
|
self.assertEqual('adhoc', providers[0].name)
|
||||||
|
self.assertEqual('dynamic', providers[1].name)
|
||||||
|
self.assertEqual('legacy', providers[2].name)
|
||||||
|
|
||||||
|
@mock.patch('stevedore.enabled.ExtensionManager')
|
||||||
|
def test_get_registered_providers_allowlist(self, mock_ext_mgr):
|
||||||
|
self.override_config(
|
||||||
|
'allowlist', ['dynamic'], config.ACTION_PROVIDERS_GROUP)
|
||||||
|
mock_ext_mgr.return_value = FakeExtManager()
|
||||||
|
providers = _get_registered_providers()
|
||||||
|
self.assertEqual(1, len(providers))
|
||||||
|
self.assertEqual('dynamic', providers[0].name)
|
||||||
|
|
||||||
|
@mock.patch('stevedore.enabled.ExtensionManager')
|
||||||
|
def test_get_registered_providers_denylist(self, mock_ext_mgr):
|
||||||
|
self.override_config(
|
||||||
|
'denylist', ['legacy'], config.ACTION_PROVIDERS_GROUP)
|
||||||
|
mock_ext_mgr.return_value = FakeExtManager()
|
||||||
|
providers = _get_registered_providers()
|
||||||
|
self.assertEqual(2, len(providers))
|
||||||
|
self.assertEqual('adhoc', providers[0].name)
|
||||||
|
self.assertEqual('dynamic', providers[1].name)
|
||||||
|
|
||||||
|
@mock.patch('stevedore.enabled.ExtensionManager')
|
||||||
|
def test_get_registered_providers_allow_and_deny(self, mock_ext_mgr):
|
||||||
|
self.override_config(
|
||||||
|
'allowlist', ['adhoc'], group=config.ACTION_PROVIDERS_GROUP)
|
||||||
|
self.override_config(
|
||||||
|
'denylist', ['dynamic'], group=config.ACTION_PROVIDERS_GROUP)
|
||||||
|
mock_ext_mgr.return_value = FakeExtManager()
|
||||||
|
providers = _get_registered_providers()
|
||||||
|
self.assertEqual(1, len(providers))
|
||||||
|
self.assertEqual('adhoc', providers[0].name)
|
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added mutually exclusive config options ``[action_providers]/allowlist``
|
||||||
|
and ``[action_providers]/denylist`` that can be used to filter what action
|
||||||
|
providers is loaded.
|
||||||
|
- |
|
||||||
|
Added mutually exclusive config options ``[legacy_action_provider]/allowlist``
|
||||||
|
and ``[legacy_action_provider]/denylist`` that can be used to filter what
|
||||||
|
actions is loaded by the legacy action provider.
|
Loading…
x
Reference in New Issue
Block a user