用法

要在项目中使用的 oslo.policy,请导入相关的模块。例如

from oslo_policy import policy

迁移到 oslo.policy

除了更改库的导入方式外,使用 Oslo 中孵化版本策略代码的应用程序可能需要进行一些额外的更改。

整合 oslo.policy 工具

oslo.policy 库提供了一个生成器,项目可以使用它来渲染示例策略文件、检查冗余规则或策略等等。这不仅对于管理策略的运维人员有用,也对于希望自动化描述项目默认策略文档的开发人员有用。

本文档的这一部分描述了如何将这些功能整合到您的项目中。假设我们正在开发一个名为 foo 的类似 OpenStack 的项目。此服务的策略在项目的通用模块中以代码形式注册。

首先,您需要在项目的 setup.cfg 中暴露几个入口点

[entry_points]
oslo.policy.policies =
    foo = foo.common.policies:list_rules

oslo.policy.enforcer =
    foo = foo.common.policy:get_enforcer

oslo.policy 库使用项目命名空间来调用 list_rules,该函数应返回一个 oslo.policy 对象列表,这些对象是 RuleDefaultDocumentedRuleDefault 的实例。

第二个入口点允许 oslo.policy 从磁盘上现有策略文件提供的覆盖中生成完整的策略。这对于希望向 Horizon 提供策略文件或用于安全合规性(包括对该部署重要的覆盖)的运维人员来说很有用。 get_enforcer 方法应返回 oslo.policy.policy:Enforcer 的实例。传递给 Enforcer 构造函数的的信息应解析磁盘上的任何覆盖。对于项目 foo 的示例可能如下所示

from oslo_config import cfg
from oslo_policy import policy

from foo.common import policies

CONF = cfg.CONF
_ENFORCER = None

def get_enforcer():
    CONF([], project='foo')
    global _ENFORCER
    if not _ENFORCER:
        _ENFORCER = policy.Enforcer(CONF)
        _ENFORCER.register_defaults(policies.list_rules())
    return _ENFORCER

请注意,如果您正在将此功能整合到已经以某种形式使用 oslo.policy 的项目中,则可能需要对其进行更改以适应该项目的结构。

接下来,您可以为专门为项目 foo 生成策略创建一个配置文件。此文件可以命名为 foo-policy-generator.conf,可以将其保存在项目的版本控制之下

[DEFAULT]
output_file = etc/foo/policy.yaml.sample
namespace = foo

如果项目 foo 使用 tox,这将更容易创建一个专门用于在 tox.ini 中生成示例配置文件的 tox 环境

[testenv:genpolicy]
commands = oslopolicy-sample-generator --config-file etc/foo/foo-policy-generator.conf

Enforcer 初始化更改

oslo.policy 库不再假定全局配置对象可用。相反,oslo_policy.policy.Enforcer 类已更改为期望消耗应用程序传入一个 oslo.config 配置对象。

在使用 oslo-incubator 中的策略时

enforcer = policy.Enforcer(policy_file=_POLICY_PATH)

在使用 oslo.policy 时

from oslo_config import cfg
CONF = cfg.CONF
enforcer = policy.Enforcer(CONF, policy_file=_POLICY_PATH)

在代码中注册策略默认值

项目可以在他们的代码中注册策略默认值,这会带来一些好处。

  • 部署者只需要添加一个策略文件,如果他们希望覆盖项目默认值。

  • 项目可以使用 Enforcer.authorize 来确保正在对已注册的策略进行策略检查。这可用于确保使用的所有策略都已注册。Enforcer.authorize 的签名与 Enforcer.enforce 匹配。

  • 项目可以将策略注册为 DocumentedRuleDefault 对象,这些对象需要对应策略的方法和路径。这有助于策略阅读器了解哪个路径映射到特定的策略,最终提供更好的文档。

  • 可以基于已注册的策略生成示例策略文件,而无需手动维护。

  • 可以生成一个策略文件,该文件是已注册默认值和从文件中加载的策略的合并。这显示了正在使用的有效策略。

  • 可以生成一个列表,其中包含在文件中定义的与代码中注册的默认值匹配的策略。这些是可从文件中删除以保持其小巧和易于理解的候选策略。

如何注册

from oslo_config import cfg
CONF = cfg.CONF
enforcer = policy.Enforcer(CONF, policy_file=_POLICY_PATH)

base_rules = [
    policy.RuleDefault('admin_required', 'role:admin or is_admin:1',
                       description='Who is considered an admin'),
    policy.RuleDefault('service_role', 'role:service',
                       description='service role'),
]

enforcer.register_defaults(base_rules)
enforcer.register_default(policy.RuleDefault('identity:create_region',
                                             'rule:admin_required',
                                             description='helpful text'))

要提供有关策略的更多信息,请使用 DocumentedRuleDefault

enforcer.register_default(
    policy.DocumentedRuleDefault(
        'identity:create_region',
        'rule:admin_required',
        'helpful text',
        [{'path': '/regions/{region_id}', 'method': 'POST'}]
    )
)

DocumentedRuleDefault 类继承自 RuleDefault 实现,但必须提供 description 属性才能使用。此外,DocumentedRuleDefault 类需要一个新的 operations 属性,该属性是一个字典列表。每个字典必须具有 pathmethod 键。path 应映射到用于与策略保护的资源交互的路径。method 应是与 path 对应的 HTTP 动词。operations 列表可以使用多个字典,如果策略用于保护多个路径。

命名策略

策略名称是理解 OpenStack 策略引擎如何工作的关键信息。开发人员使用策略名称来保护 API。运维人员使用策略名称来覆盖其部署中的策略。在 OpenStack 服务中拥有一致的策略名称对于提供愉快的用户体验至关重要。以下规则是指导方针,可以帮助您作为开发人员构建独特且描述性的策略名称。

服务类型

策略名称应明确使用它们的服务。服务类型还应遵循已知的标准,即 service-types 权威机构。使用现有的标准可以避免通过重用已建立的参考来混淆用户。例如,不要使用 keystone 作为策略名称中的服务,而应使用 identity,因为它不是特定于一种实现的。 它也更具体地说明了服务提供的功能,而不是让读者维护服务代码名称与它提供的功能之间的心理映射。

资源和子资源

用户可能会与服务 API 暴露的资源交互。您应在策略名称中包含资源的名称,并且应为单数。例如,保护用户 API 的策略应使用 identity:user,而不是 identity:users

某些服务可能有子资源。例如,固定的 IP 地址可以被认为是 IP 地址的子资源。您应使用连字符而不是下划线分隔开放形式的复合词。这种间距约定与 service-types 权威机构使用的间距保持一致。例如,使用 ip-address 而不是 ip_address。在单个约定中采用多种分隔复合词的方式会令人困惑,并且容易意外引入不一致。

资源名称应简约,仅包含描述资源所需的字符。应从资源中省略额外的信息。使用 agent 而不是 os-agents,即使资源的 URL 路径使用 /os-agents

操作和子操作

操作是用户可以对资源执行的特定操作。典型的操作是 creategetlistupdatedelete。这些操作定义与用于实现其底层 API 的 HTTP 方法无关,这是故意的。这种独立性很重要,因为两个不同的服务可能会使用两个不同的 HTTP 方法来实现相同的操作。例如,使用 compute:server:list 作为列出服务器的策略名称,而不是 compute:server:get_allcompute:server:get-all。在策略名称中使用 all 本身意味着返回所有可能的实体,而实际响应可能会根据用户的权限进行过滤。换句话说,管理域内许多不同项目的域管理员列出服务器与项目成员列出单个项目拥有的服务器可能会非常不同。

某些服务具有以更多细节列出资源的权限。根据上下文,这些额外的细节可能很敏感,需要比 list 更严格的 RBAC 权限。在这种情况下,使用 compute:server:list-detail 而不是 compute:server:detail。通过使用复合词,我们更详细地描述了 detail 的实际含义。

子操作是您可以选择性地添加以阐明资源操作的选项。例如,compute:server:resize:confirm 是您如何将操作(resize)与子操作(confirm)组合以明确命名策略的示例。

开放形式的复合词操作应使用连字符而不是下划线进行间距。这种间距与 service-types 权威机构和开放形式复合词的资源名称保持一致。例如,使用 compute:server:resize-state 而不是 compute:server:resize_state

资源属性

资源属性可以用于策略名称,并且完全是可选的。如果您需要在名称中包含资源的属性,则应将其放置在资源或子资源部分之后。例如,使用 compute:flavor:private:list 来命名列出所有私有风味的策略。

组合在一起

现在您知道在策略名称的上下文中什么是服务类型、资源、属性和操作,就可以确定您应该使用的顺序。策略名称应随着阅读而增加细节。这会导致以下语法

<service-type>:<resource>[:<subresource>][:<attribute>]:<action>[:<subaction>]

您应使用冒号 (:) 分隔名称的每个部分。以下是现有 OpenStack API 的示例

identity:user:list
block-storage:volume:extend
compute:server:resize:confirm
compute:flavor:private:list
network:ip-address:fixed-ip-address:create

设置范围

RuleDefaultDocumentedRuleDefault 对象具有一个专用于操作预期范围的属性,称为 scope_types。此属性只能在规则定义时设置,而不能通过策略文件覆盖。此变量旨在保存策略应运行的范围。在执行期间,scope_types 中的信息与用于请求的令牌的范围进行比较。它旨在匹配从 keystone 可用的令牌范围,即 systemdomainproject。以下示例将展示与系统和项目 API 一起使用的情况。将 scope_types 设置为这些值以外的任何值不受支持。

例如,用于保护在项目中跟踪的资源的策略应需要项目范围的令牌。可以使用 scope_types 如下表达

policy.DocumentedRuleDefault(
    name='service:create_foo',
    check_str='role:admin',
    scope_types=['project'],
    description='Creates a foo resource',
    operations=[
        {
            'path': '/v1/foos/',
            'method': 'POST'
        }
    ]
)

可以使用相同的模式保护系统级资源

policy.DocumentedRuleDefault(
    name='service:update_bar',
    check_str='role:admin',
    scope_types=['system'],
    description='Updates a bar resource',
    operations=[
        {
            'path': '/v1/bars/{bar_id}',
            'method': 'PATCH'
        }
    ]
)

scope_types 属性确保用于发出请求的令牌已正确范围,并传递 check_str。这很强大,因为它允许在不损害 API 的情况下在不同的授权级别重用角色。例如,上述示例中的 admin 角色在项目级别和系统级别用于保护两个不同的资源。如果我们只检查令牌是否包含 admin 角色,那么拥有项目范围令牌的用户就有可能访问系统级 API。

scope_types 整合到 OpenStack 服务中的开发人员应注意他们正在保护的 API 与策略运行的资源级别之间的关系,无论它是系统级别还是项目级别。

示例文件生成

在正在使用 oslo.policy 的项目的 setup.cfg 中

[entry_points]
oslo.policy.policies =
    nova = nova.policy:list_policies

其中 list_policies 是一种返回 policy.RuleDefault 对象列表的方法。

使用一些配置选项运行 oslopolicy-sample-generator 脚本

oslopolicy-sample-generator --namespace nova --output-file policy-sample.yaml

或者

oslopolicy-sample-generator --config-file policy-generator.conf

其中 policy-generator.conf 如下所示

[DEFAULT]
output_file = policy-sample.yaml
namespace = nova

如果省略 output_file,则示例文件将发送到 stdout。

合并文件生成

这将输出一个策略文件,其中包含所有已注册的策略默认值和使用策略文件配置的所有策略。此文件显示了项目正在使用的有效策略。

在正在使用 oslo.policy 的项目的 setup.cfg 中

[entry_points]
oslo.policy.enforcer =
    nova = nova.policy:get_enforcer

其中 get_enforcer 是一种返回已配置的 oslo_policy.policy.Enforcer 对象的方法。此对象应设置为它用于实际策略执行的方式,如果不同,则生成的策略文件可能与现实不符。

使用一些配置选项运行 oslopolicy-policy-generator 脚本

oslopolicy-policy-generator --namespace nova --output-file policy-merged.yaml

或者

oslopolicy-policy-generator --config-file policy-merged-generator.conf

其中 policy-merged-generator.conf 如下所示

[DEFAULT]
output_file = policy-merged.yaml
namespace = nova

如果省略 output_file,则该文件将发送到 stdout。

冗余配置列表

这将输出一个列表,其中包含在配置文件中定义的策略规则,其中该规则与已注册的默认规则不同。这些是从配置文件中删除而不会更改有效策略的规则。

在正在使用 oslo.policy 的项目的 setup.cfg 中

[entry_points]
oslo.policy.enforcer =
    nova = nova.policy:get_enforcer

其中 get_enforcer 是一种返回已配置的 oslo_policy.policy.Enforcer 对象的方法。此对象应设置为它用于实际策略执行的方式,如果不同,则生成的策略文件可能与现实不符。

运行 oslopolicy-list-redundant 脚本

oslopolicy-list-redundant --namespace nova

或者

oslopolicy-list-redundant --config-file policy-redundant.conf

其中 policy-redundant.conf 如下所示

[DEFAULT]
namespace = nova

输出将发送到 stdout。

测试默认策略

开发人员需要可靠地单元测试用于保护 API 的策略。拥有强大的单元测试覆盖率可以增加对更改不会对用户体验产生负面影响的信心。本文档旨在帮助您了解您可能在服务中找到的历史测试实践背景。更重要的是,它将描述您可以用来增加策略测试和覆盖率信心的测试模式。

历史

在能够注册代码中的策略之前,开发人员在策略文件中维护策略,其中包括服务使用的所有策略。开发人员将策略文件保存在项目源代码中,其中包含服务的默认策略。

一旦可以注册代码中的策略,策略文件就变得无关紧要,因为您可以生成它们。从代码生成策略文件可以更轻松地维护策略文档,并允许单一的事实来源。注册代码中的策略也意味着测试不再需要策略文件,因为默认策略位于服务本身中。

在这一点上,重要的是要注意策略执行需要基于发出请求的用户(例如,用户是否有权执行他们请求的操作)的授权上下文。在 OpenStack 中,此授权上下文通过用于调用 API 的令牌传递给服务,该令牌来自 OpenStack 身份服务。从最纯粹的形式来看,您可以将授权上下文视为用户在项目、域或系统上拥有的角色。服务可以将授权上下文馈送到策略执行中,以确定用户是否有权执行某项操作。

授权上下文与策略执行机制之间的耦合提高了有效测试策略和 API 的门槛。服务开发人员希望确保特定于其服务的功能正常工作,而不要纠结于授权系统的实现细节。此外,他们希望保持单元测试的轻量级,而不是需要一个单独的系统来颁发用于授权的令牌,从而将单元测试的边界扩展到集成测试。

因此,您通常会看到 OpenStack 服务在策略和测试代码方面采用以下两种方法之一。

一种方法是为测试提供一个策略文件,该文件会覆盖示例策略文件或代码中的默认策略。该文件主要包含没有适当检查字符串的策略,从而放宽了使用 oslo.policy 服务强制执行的授权。如果没有适当的检查字符串,则有可能在不构建上下文对象或使用来自身份服务的令牌的情况下访问 API。

另一种方法是模拟策略执行以无条件成功。由于开发人员绕过了策略引擎内的代码,因此提供适当的授权上下文对测试用例中使用的 API 没有影响。

这两种方法都让开发人员专注于验证其服务的特定领域功能,而无需理解策略执行的复杂性。不幸的是,绕过 API 授权测试会以牺牲默认策略可能在新更改中意外中断为代价。如果测试没有断言默认行为,那么看似简单的更改可能会对用户或操作员产生负面影响,无论开发人员的意图如何。

测试策略

幸运的是,您可以通过直接使用上下文对象(特别是 RequestContext 对象)来测试策略,而无需处理令牌。您的服务可能已经在 API 前面使用这些对象来表示来自中间件的信息。使用上下文进行授权在集成测试和行使足够的授权以确保策略充分保护 API 之间取得了完美的平衡。oslo.policy 库也接受上下文对象,并自动将属性转换为在评估策略时使用的值,这使得使用它们更加自然。

要有效地使用 RequestContext 对象,您需要了解正在测试的策略。然后,您可以为测试用例相应地建模一个上下文对象。其思想是构建一个上下文对象,用于在请求中,要么使策略执行失败,要么使策略执行成功。例如,假设您正在测试如下默认策略

from oslo_config import cfg

CONF = cfg.CONF
enforcer = policy.Enforcer(CONF, policy_file=_POLICY_PATH)

enforcer.register_default(
    policy.RuleDefault('identity:create_region', 'role:admin')
)

这里的执行很简单,即具有名为 admin 角色的用户可以访问此 API。您可以通过显式设置这些属性在请求上下文中对此进行建模

from oslo_context import context

context = context.RequestContext()
context.roles = ['admin']

根据您的服务在单元测试中部署 API 的方式,您可以提供一个假的上下文,就像您提供请求一样,或者模拟上下文的返回值以返回您构建的上下文。

您还可以为具有复杂检查字符串或使用作用域类型的策略提供作用域信息。例如,考虑以下默认策略

from oslo_config import cfg

CONF = cfg.CONF
enforcer = policy.Enforcer(CONF, policy_file=_POLICY_PATH)

enforcer.register_default(
    policy.RuleDefault('identity:create_region', 'role:admin',
    scope_types=['system'])
)

我们可以使用以下请求上下文对象对其进行建模,其中包括作用域

from oslo_context import context

context = context.RequestContext()
context.roles = ['admin']
context.system_scope = 'all'

请注意,all 是一个唯一的系统作用域目标,表示用户有权在部署系统上运行。相反,以下是建模项目范围令牌的上下文的示例

import uuid
from oslo_context import context

context = context.RequestContext()
context.roles = ['admin']
context.project_id = uuid.uuid4().hex

这里的意义在于部署系统上的管理员授权与项目上的管理员授权之间的区别。