Merge "Adding RBAC policy system and checks for identity"
This commit is contained in:
commit
d5166a6ff7
@ -73,6 +73,7 @@ the following topic guides.
|
|||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
topics/tables
|
topics/tables
|
||||||
|
topics/policy
|
||||||
topics/testing
|
topics/testing
|
||||||
|
|
||||||
API Reference
|
API Reference
|
||||||
@ -94,6 +95,7 @@ In-depth documentation for Horizon and its APIs.
|
|||||||
ref/decorators
|
ref/decorators
|
||||||
ref/exceptions
|
ref/exceptions
|
||||||
ref/test
|
ref/test
|
||||||
|
ref/policy
|
||||||
|
|
||||||
Source Code Reference
|
Source Code Reference
|
||||||
---------------------
|
---------------------
|
||||||
|
140
doc/source/topics/policy.rst
Normal file
140
doc/source/topics/policy.rst
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
============================================================
|
||||||
|
Horizon Policy Enforcement (RBAC: Role Based Access Control)
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
Horizon's policy enforcement builds on the oslo-incubator policy engine.
|
||||||
|
The basis of which is ``openstack_dashboard/openstack/common/policy.py``.
|
||||||
|
Services in OpenStack use the oslo policy engine to define policy rules
|
||||||
|
to limit access to APIs based primarily on role grants and resource
|
||||||
|
ownership.
|
||||||
|
|
||||||
|
The Keystone v3 API provides an interface for creating/reading/updating
|
||||||
|
policy files in the keystone database. However, at this time services
|
||||||
|
do not load the policy files into Keystone. Thus, the implementation in
|
||||||
|
Horizon is based on copies of policy.json files found in the service's
|
||||||
|
source code. The long-term goal is to read/utilize/update these policy
|
||||||
|
files in Horizon.
|
||||||
|
|
||||||
|
The service rules files are loaded into the policy engine to determine
|
||||||
|
access rights to actions and service APIs.
|
||||||
|
|
||||||
|
Horizon Settings
|
||||||
|
================
|
||||||
|
|
||||||
|
There are a few settings that must be in place for the Horizon policy
|
||||||
|
engine to work.
|
||||||
|
|
||||||
|
``POLICY_FILES_PATH``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Default: ``os.path.join(ROOT_PATH, "conf")``
|
||||||
|
|
||||||
|
Specifies where service based policy files are located. These are used to
|
||||||
|
define the policy rules actions are verified against. This value must contain
|
||||||
|
the files listed in ``POLICY_FILES`` or all policy checks will pass.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The path to deployment specific policy files can be specified in
|
||||||
|
``local_settings.py`` to override the default location.
|
||||||
|
|
||||||
|
|
||||||
|
``POLICY_FILES``
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Default: { 'identity': 'keystone_policy.json', 'compute': 'nova_policy.json'}
|
||||||
|
|
||||||
|
This should essentially be the mapping of the contents of ``POLICY_FILES_PATH``
|
||||||
|
to service types. When policy.json files are added to the directory
|
||||||
|
``POLICY_FILES_PATH``, they should be included here too. Without this mapping,
|
||||||
|
there is no way to map service types with policy rules, thus two policy.json
|
||||||
|
files containing a "default" rule would be ambiguous.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Deployment specific policy files can be specified in ``local_settings.py``
|
||||||
|
to override the default policy files. It is imperative that these policy
|
||||||
|
files match those deployed in the target OpenStack installation. Otherwise,
|
||||||
|
the displayed actions and the allowed action will not match.
|
||||||
|
|
||||||
|
``POLICY_CHECK_FUNCTION``
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Default: ``policy.check``
|
||||||
|
|
||||||
|
This value should not be changed, although removing it would be a means to
|
||||||
|
bypass all policy checks.
|
||||||
|
|
||||||
|
|
||||||
|
How user's roles are determined
|
||||||
|
===============================
|
||||||
|
|
||||||
|
Each policy check uses information about the user stored on the request to
|
||||||
|
determine the user's roles. This information was extracted from the scoped
|
||||||
|
token received from Keystone when authenticating.
|
||||||
|
|
||||||
|
Entity ownership is also a valid role. To verify access to specific entities
|
||||||
|
like a project, the target must be specified. See the section
|
||||||
|
:ref:`rule targets <rule_targets>` later in this document.
|
||||||
|
|
||||||
|
How to Utilize RBAC
|
||||||
|
===================
|
||||||
|
|
||||||
|
The primary way to add role based access control checks to panels is in the
|
||||||
|
definition of table actions. When implementing a derived action class,
|
||||||
|
setting the :attr:`~horizon.tables.Action.policy_rules` attribute to valid
|
||||||
|
policy rules will force a policy check before the
|
||||||
|
:meth:`horizon.tables.Action.allowed` method is called on the action. These
|
||||||
|
rules are defined in the the policy files point to by ``POLICY_PATH`` and
|
||||||
|
``POLICY_FILES``. The rules are role based, where entity owner is also a
|
||||||
|
role. The format for the ``policy_rules`` is a list of two item tuples. The
|
||||||
|
first component of the tuple is the scope of the policy rule, this is the
|
||||||
|
service type. This informs the policy engine which policy file to reference.
|
||||||
|
The second component is the rule to enforce from the policy file specified by
|
||||||
|
the scope. An example tuple is::
|
||||||
|
|
||||||
|
("identity", "identity:get_user")
|
||||||
|
|
||||||
|
x tuples can be added to enforce x rules.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If a rule specified is not found in the policy file. The policy check
|
||||||
|
will return False and the action will not be allowed.
|
||||||
|
|
||||||
|
The secondary way to add a role based check is to directly use the
|
||||||
|
:meth:`~openstack_dashboard.policy.check` method. The method takes a list
|
||||||
|
of actions, same format as the :attr:`~horizon.tables.Action.policy_rules`
|
||||||
|
attribute detailed above; the current request object; and a dictionary of
|
||||||
|
action targets. This is the method that :class:`horizon.tables.Action` class
|
||||||
|
utilizes.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Any time multiple rules are specified in a single `policy.check` method
|
||||||
|
call, the result is the logical `and` of each rule check. So, if any
|
||||||
|
rule fails verification, the result is `False`.
|
||||||
|
|
||||||
|
.. _rule_targets:
|
||||||
|
|
||||||
|
Rule Targets
|
||||||
|
============
|
||||||
|
|
||||||
|
Some rules allow access if the user owns the entity. Policy check targets
|
||||||
|
specify particular entities to check for user ownership. The target parameter
|
||||||
|
to the :meth:`~openstack_dashboard.policy.check` method is a simple dictionary.
|
||||||
|
For instance, the target for checking access a project looks like::
|
||||||
|
|
||||||
|
{"project_id": "0905760626534a74979afd3f4a9d67f1"}
|
||||||
|
|
||||||
|
If the value matches the ``project_id`` to which the user's token is scoped,
|
||||||
|
then access is allowed.
|
||||||
|
|
||||||
|
When deriving the :class:`horizon.tables.Action` class for use in a table, if
|
||||||
|
a policy check is desired for a particular target, the implementer should
|
||||||
|
override the :meth:`horizon.tables.Action.get_policy_target` method. This
|
||||||
|
allows a programatic way to specify the target based on the current datum. The
|
||||||
|
value returned should be the target dictionary.
|
@ -251,6 +251,23 @@ Default: ``20``
|
|||||||
Similar to ``API_RESULT_LIMIT``. This setting currently only controls the
|
Similar to ``API_RESULT_LIMIT``. This setting currently only controls the
|
||||||
Glance image list page size. It will be removed in a future version.
|
Glance image list page size. It will be removed in a future version.
|
||||||
|
|
||||||
|
``POLICY_FILES_PATH``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Default: ``os.path.join(ROOT_PATH, "conf")``
|
||||||
|
|
||||||
|
Specifies where service based policy files are located. These are used to
|
||||||
|
define the policy rules actions are verified against.
|
||||||
|
|
||||||
|
``POLICY_FILES``
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Default: { 'identity': 'keystone_policy.json', 'compute': 'nova_policy.json'}
|
||||||
|
|
||||||
|
This should essentially be the mapping of the contents of ``POLICY_FILES_PATH``
|
||||||
|
to service types. When policy.json files are added to ``POLICY_FILES_PATH``,
|
||||||
|
they should be included here too.
|
||||||
|
|
||||||
Django Settings (Partial)
|
Django Settings (Partial)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
@ -127,3 +127,30 @@ require data, such as :meth:`~horizon.tables.DataTable.get_object_display` or
|
|||||||
actions is that you can avoid having to do all the processing, API calls, etc.
|
actions is that you can avoid having to do all the processing, API calls, etc.
|
||||||
associated with loading data into the table for actions which don't require
|
associated with loading data into the table for actions which don't require
|
||||||
access to that information.
|
access to that information.
|
||||||
|
|
||||||
|
Policy checks on actions
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
The :attr:`~horizon.tables.Action.policy_rules` attribute, when set, will
|
||||||
|
validate access to the action using the policy rules specified. The attribute
|
||||||
|
is a list of scope/rule pairs. Where the scope is the service type defining
|
||||||
|
the rule and the rule is a rule from the corresponding service policy.json
|
||||||
|
file. The format of :attr:`horizon.tables.Action.policy_rules` looks like::
|
||||||
|
|
||||||
|
(("identity", "identity:get_user"),)
|
||||||
|
|
||||||
|
Multiple checks can be made for the same action by merely adding more tuples
|
||||||
|
to the list. The policy check will use information stored in the session
|
||||||
|
about the user and the result of
|
||||||
|
:meth:`~horizon.tables.Action.get_policy_target` (which can be overridden in
|
||||||
|
the derived action class) to determine if the user
|
||||||
|
can execute the action. If the user does not have access to the action, the
|
||||||
|
action is not added to the table.
|
||||||
|
|
||||||
|
If :attr:`~horizon.tables.Action.policy_rules` is not set, no policy checks
|
||||||
|
will be made to determine if the action should be visible and will be
|
||||||
|
displayed solely based on the result of
|
||||||
|
:meth:`~horizon.tables.Action.allowed`.
|
||||||
|
|
||||||
|
For more information on policy based Role Based Access Control see:
|
||||||
|
:doc:`Horizon Policy Enforcement (RBAC: Role Based Access Control) </topics/policy>`.
|
||||||
|
@ -43,6 +43,7 @@ class BaseAction(html.HTMLElement):
|
|||||||
handles_multiple = False
|
handles_multiple = False
|
||||||
requires_input = False
|
requires_input = False
|
||||||
preempt = False
|
preempt = False
|
||||||
|
policy_rules = None
|
||||||
|
|
||||||
def __init__(self, datum=None):
|
def __init__(self, datum=None):
|
||||||
super(BaseAction, self).__init__()
|
super(BaseAction, self).__init__()
|
||||||
@ -63,6 +64,14 @@ class BaseAction(html.HTMLElement):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_policy_target(self, request, datum):
|
||||||
|
""" Provide the target for a policy request.
|
||||||
|
|
||||||
|
This method is meant to be overridden to return target details when
|
||||||
|
one of the policy checks requires them. E.g., {"user_id": datum.id}
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
def allowed(self, request, datum):
|
||||||
""" Determine whether this action is allowed for the current request.
|
""" Determine whether this action is allowed for the current request.
|
||||||
|
|
||||||
@ -71,6 +80,12 @@ class BaseAction(html.HTMLElement):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _allowed(self, request, datum):
|
def _allowed(self, request, datum):
|
||||||
|
policy_check = getattr(settings, "POLICY_CHECK_FUNCTION", None)
|
||||||
|
|
||||||
|
if policy_check and self.policy_rules:
|
||||||
|
target = self.get_policy_target(request, datum)
|
||||||
|
return (policy_check(self.policy_rules, request, target) and
|
||||||
|
self.allowed(request, datum))
|
||||||
return self.allowed(request, datum)
|
return self.allowed(request, datum)
|
||||||
|
|
||||||
def update(self, request, datum):
|
def update(self, request, datum):
|
||||||
@ -156,6 +171,22 @@ class Action(BaseAction):
|
|||||||
Default to be an empty list (``[]``). When set to empty, the action
|
Default to be an empty list (``[]``). When set to empty, the action
|
||||||
will accept any kind of data.
|
will accept any kind of data.
|
||||||
|
|
||||||
|
.. attribute:: policy_rules
|
||||||
|
|
||||||
|
list of scope and rule tuples to do policy checks on, the
|
||||||
|
composition of which is (scope, rule)
|
||||||
|
|
||||||
|
scope: service type managing the policy for action
|
||||||
|
rule: string representing the action to be checked
|
||||||
|
|
||||||
|
for a policy that requires a single rule check:
|
||||||
|
policy_rules should look like
|
||||||
|
"(("compute", "compute:create_instance"),)"
|
||||||
|
for a policy that requires multiple rule checks:
|
||||||
|
rules should look like
|
||||||
|
"(("identity", "identity:list_users"),
|
||||||
|
("identity", "identity:list_roles"))"
|
||||||
|
|
||||||
At least one of the following methods must be defined:
|
At least one of the following methods must be defined:
|
||||||
|
|
||||||
.. method:: single(self, data_table, request, object_id)
|
.. method:: single(self, data_table, request, object_id)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
module=config
|
module=config
|
||||||
module=eventlet_backdoor
|
module=eventlet_backdoor
|
||||||
|
module=fileutils
|
||||||
module=install_venv_common
|
module=install_venv_common
|
||||||
module=notifier
|
module=notifier
|
||||||
|
module=policy
|
||||||
module=rpc
|
module=rpc
|
||||||
module=service
|
module=service
|
||||||
module=threadgroup
|
module=threadgroup
|
||||||
|
90
openstack_dashboard/conf/keystone_policy.json
Normal file
90
openstack_dashboard/conf/keystone_policy.json
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"admin_required": [["role:admin"], ["is_admin:1"]],
|
||||||
|
"service_role": [["role:service"]],
|
||||||
|
"service_or_admin": [["rule:admin_required"], ["rule:service_role"]],
|
||||||
|
"owner" : [["user_id:%(user_id)s"]],
|
||||||
|
"admin_or_owner": [["rule:admin_required"], ["rule:owner"]],
|
||||||
|
|
||||||
|
"default": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:get_service": [["rule:admin_required"]],
|
||||||
|
"identity:list_services": [["rule:admin_required"]],
|
||||||
|
"identity:create_service": [["rule:admin_required"]],
|
||||||
|
"identity:update_service": [["rule:admin_required"]],
|
||||||
|
"identity:delete_service": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:get_endpoint": [["rule:admin_required"]],
|
||||||
|
"identity:list_endpoints": [["rule:admin_required"]],
|
||||||
|
"identity:create_endpoint": [["rule:admin_required"]],
|
||||||
|
"identity:update_endpoint": [["rule:admin_required"]],
|
||||||
|
"identity:delete_endpoint": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:get_domain": [["rule:admin_required"]],
|
||||||
|
"identity:list_domains": [["rule:admin_required"]],
|
||||||
|
"identity:create_domain": [["rule:admin_required"]],
|
||||||
|
"identity:update_domain": [["rule:admin_required"]],
|
||||||
|
"identity:delete_domain": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:get_project": [["rule:admin_required"]],
|
||||||
|
"identity:list_projects": [["rule:admin_required"]],
|
||||||
|
"identity:list_user_projects": [["rule:admin_or_owner"]],
|
||||||
|
"identity:create_project": [["rule:admin_required"]],
|
||||||
|
"identity:update_project": [["rule:admin_required"]],
|
||||||
|
"identity:delete_project": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:get_user": [["rule:admin_required"]],
|
||||||
|
"identity:list_users": [["rule:admin_required"]],
|
||||||
|
"identity:create_user": [["rule:admin_required"]],
|
||||||
|
"identity:update_user": [["rule:admin_or_owner"]],
|
||||||
|
"identity:delete_user": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:get_group": [["rule:admin_required"]],
|
||||||
|
"identity:list_groups": [["rule:admin_required"]],
|
||||||
|
"identity:list_groups_for_user": [["rule:admin_or_owner"]],
|
||||||
|
"identity:create_group": [["rule:admin_required"]],
|
||||||
|
"identity:update_group": [["rule:admin_required"]],
|
||||||
|
"identity:delete_group": [["rule:admin_required"]],
|
||||||
|
"identity:list_users_in_group": [["rule:admin_required"]],
|
||||||
|
"identity:remove_user_from_group": [["rule:admin_required"]],
|
||||||
|
"identity:check_user_in_group": [["rule:admin_required"]],
|
||||||
|
"identity:add_user_to_group": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:get_credential": [["rule:admin_required"]],
|
||||||
|
"identity:list_credentials": [["rule:admin_required"]],
|
||||||
|
"identity:create_credential": [["rule:admin_required"]],
|
||||||
|
"identity:update_credential": [["rule:admin_required"]],
|
||||||
|
"identity:delete_credential": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:get_role": [["rule:admin_required"]],
|
||||||
|
"identity:list_roles": [["rule:admin_required"]],
|
||||||
|
"identity:create_role": [["rule:admin_required"]],
|
||||||
|
"identity:update_role": [["rule:admin_required"]],
|
||||||
|
"identity:delete_role": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:check_grant": [["rule:admin_required"]],
|
||||||
|
"identity:list_grants": [["rule:admin_required"]],
|
||||||
|
"identity:create_grant": [["rule:admin_required"]],
|
||||||
|
"identity:revoke_grant": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:list_role_assignments": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:get_policy": [["rule:admin_required"]],
|
||||||
|
"identity:list_policies": [["rule:admin_required"]],
|
||||||
|
"identity:create_policy": [["rule:admin_required"]],
|
||||||
|
"identity:update_policy": [["rule:admin_required"]],
|
||||||
|
"identity:delete_policy": [["rule:admin_required"]],
|
||||||
|
|
||||||
|
"identity:check_token": [["rule:admin_required"]],
|
||||||
|
"identity:validate_token": [["rule:service_or_admin"]],
|
||||||
|
"identity:validate_token_head": [["rule:service_or_admin"]],
|
||||||
|
"identity:revocation_list": [["rule:service_or_admin"]],
|
||||||
|
"identity:revoke_token": [["rule:admin_or_owner"]],
|
||||||
|
|
||||||
|
"identity:create_trust": [["user_id:%(trust.trustor_user_id)s"]],
|
||||||
|
"identity:get_trust": [["rule:admin_or_owner"]],
|
||||||
|
"identity:list_trusts": [["@"]],
|
||||||
|
"identity:list_roles_for_trust": [["@"]],
|
||||||
|
"identity:check_role_for_trust": [["@"]],
|
||||||
|
"identity:get_role_for_trust": [["@"]],
|
||||||
|
"identity:delete_trust": [["@"]]
|
||||||
|
}
|
261
openstack_dashboard/conf/nova_policy.json
Normal file
261
openstack_dashboard/conf/nova_policy.json
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
{
|
||||||
|
"context_is_admin": "role:admin",
|
||||||
|
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
|
||||||
|
"default": "rule:admin_or_owner",
|
||||||
|
|
||||||
|
"cells_scheduler_filter:TargetCellFilter": "is_admin:True",
|
||||||
|
|
||||||
|
"compute:create": "",
|
||||||
|
"compute:create:attach_network": "",
|
||||||
|
"compute:create:attach_volume": "",
|
||||||
|
"compute:create:forced_host": "is_admin:True",
|
||||||
|
"compute:get_all": "",
|
||||||
|
"compute:get_all_tenants": "",
|
||||||
|
"compute:unlock_override": "rule:admin_api",
|
||||||
|
|
||||||
|
"compute:shelve": "",
|
||||||
|
"compute:shelve_offload": "",
|
||||||
|
"compute:unshelve": "",
|
||||||
|
|
||||||
|
"admin_api": "is_admin:True",
|
||||||
|
"compute_extension:accounts": "rule:admin_api",
|
||||||
|
"compute_extension:admin_actions": "rule:admin_api",
|
||||||
|
"compute_extension:admin_actions:pause": "rule:admin_or_owner",
|
||||||
|
"compute_extension:admin_actions:unpause": "rule:admin_or_owner",
|
||||||
|
"compute_extension:admin_actions:suspend": "rule:admin_or_owner",
|
||||||
|
"compute_extension:admin_actions:resume": "rule:admin_or_owner",
|
||||||
|
"compute_extension:admin_actions:lock": "rule:admin_or_owner",
|
||||||
|
"compute_extension:admin_actions:unlock": "rule:admin_or_owner",
|
||||||
|
"compute_extension:admin_actions:resetNetwork": "rule:admin_api",
|
||||||
|
"compute_extension:admin_actions:injectNetworkInfo": "rule:admin_api",
|
||||||
|
"compute_extension:admin_actions:createBackup": "rule:admin_or_owner",
|
||||||
|
"compute_extension:admin_actions:migrateLive": "rule:admin_api",
|
||||||
|
"compute_extension:admin_actions:resetState": "rule:admin_api",
|
||||||
|
"compute_extension:admin_actions:migrate": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-admin-actions": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-admin-actions:pause": "rule:admin_or_owner",
|
||||||
|
"compute_extension:v3:os-admin-actions:unpause": "rule:admin_or_owner",
|
||||||
|
"compute_extension:v3:os-admin-actions:suspend": "rule:admin_or_owner",
|
||||||
|
"compute_extension:v3:os-admin-actions:resume": "rule:admin_or_owner",
|
||||||
|
"compute_extension:v3:os-admin-actions:lock": "rule:admin_or_owner",
|
||||||
|
"compute_extension:v3:os-admin-actions:unlock": "rule:admin_or_owner",
|
||||||
|
"compute_extension:v3:os-admin-actions:reset_network": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-admin-actions:inject_network_info": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-admin-actions:create_backup": "rule:admin_or_owner",
|
||||||
|
"compute_extension:v3:os-admin-actions:migrate_live": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-admin-actions:reset_state": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-admin-actions:migrate": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-admin-password": "",
|
||||||
|
"compute_extension:aggregates": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-aggregates": "rule:admin_api",
|
||||||
|
"compute_extension:agents": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-agents": "rule:admin_api",
|
||||||
|
"compute_extension:attach_interfaces": "",
|
||||||
|
"compute_extension:v3:os-attach-interfaces": "",
|
||||||
|
"compute_extension:baremetal_nodes": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-baremetal-nodes": "rule:admin_api",
|
||||||
|
"compute_extension:cells": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-cells": "rule:admin_api",
|
||||||
|
"compute_extension:certificates": "",
|
||||||
|
"compute_extension:v3:os-certificates": "",
|
||||||
|
"compute_extension:cloudpipe": "rule:admin_api",
|
||||||
|
"compute_extension:cloudpipe_update": "rule:admin_api",
|
||||||
|
"compute_extension:console_output": "",
|
||||||
|
"compute_extension:v3:consoles:discoverable": "",
|
||||||
|
"compute_extension:v3:os-console-output": "",
|
||||||
|
"compute_extension:consoles": "",
|
||||||
|
"compute_extension:v3:os-remote-consoles": "",
|
||||||
|
"compute_extension:coverage_ext": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-coverage": "rule:admin_api",
|
||||||
|
"compute_extension:createserverext": "",
|
||||||
|
"compute_extension:deferred_delete": "",
|
||||||
|
"compute_extension:v3:os-deferred-delete": "",
|
||||||
|
"compute_extension:disk_config": "",
|
||||||
|
"compute_extension:evacuate": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-evacuate": "rule:admin_api",
|
||||||
|
"compute_extension:extended_server_attributes": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-extended-server-attributes": "rule:admin_api",
|
||||||
|
"compute_extension:extended_status": "",
|
||||||
|
"compute_extension:v3:os-extended-status": "",
|
||||||
|
"compute_extension:extended_availability_zone": "",
|
||||||
|
"compute_extension:v3:os-extended-availability-zone": "",
|
||||||
|
"compute_extension:extended_ips": "",
|
||||||
|
"compute_extension:extended_ips_mac": "",
|
||||||
|
"compute_extension:extended_vif_net": "",
|
||||||
|
"compute_extension:v3:extension_info:discoverable": "",
|
||||||
|
"compute_extension:extended_volumes": "",
|
||||||
|
"compute_extension:v3:os-extended-volumes": "",
|
||||||
|
"compute_extension:v3:os-extended-volumes:attach": "",
|
||||||
|
"compute_extension:v3:os-extended-volumes:detach": "",
|
||||||
|
"compute_extension:fixed_ips": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-fixed-ips:discoverable": "",
|
||||||
|
"compute_extension:v3:os-fixed-ips": "rule:admin_api",
|
||||||
|
"compute_extension:flavor_access": "",
|
||||||
|
"compute_extension:v3:os-flavor-access": "",
|
||||||
|
"compute_extension:flavor_disabled": "",
|
||||||
|
"compute_extension:v3:os-flavor-disabled": "",
|
||||||
|
"compute_extension:flavor_rxtx": "",
|
||||||
|
"compute_extension:v3:os-flavor-rxtx": "",
|
||||||
|
"compute_extension:flavor_swap": "",
|
||||||
|
"compute_extension:flavorextradata": "",
|
||||||
|
"compute_extension:flavorextraspecs:index": "",
|
||||||
|
"compute_extension:flavorextraspecs:show": "",
|
||||||
|
"compute_extension:flavorextraspecs:create": "rule:admin_api",
|
||||||
|
"compute_extension:flavorextraspecs:update": "rule:admin_api",
|
||||||
|
"compute_extension:flavorextraspecs:delete": "rule:admin_api",
|
||||||
|
"compute_extension:v3:flavor-extra-specs:index": "",
|
||||||
|
"compute_extension:v3:flavor-extra-specs:show": "",
|
||||||
|
"compute_extension:v3:flavor-extra-specs:create": "rule:admin_api",
|
||||||
|
"compute_extension:v3:flavor-extra-specs:update": "rule:admin_api",
|
||||||
|
"compute_extension:v3:flavor-extra-specs:delete": "rule:admin_api",
|
||||||
|
"compute_extension:flavormanage": "rule:admin_api",
|
||||||
|
"compute_extension:floating_ip_dns": "",
|
||||||
|
"compute_extension:floating_ip_pools": "",
|
||||||
|
"compute_extension:floating_ips": "",
|
||||||
|
"compute_extension:floating_ips_bulk": "rule:admin_api",
|
||||||
|
"compute_extension:fping": "",
|
||||||
|
"compute_extension:fping:all_tenants": "rule:admin_api",
|
||||||
|
"compute_extension:hide_server_addresses": "is_admin:False",
|
||||||
|
"compute_extension:v3:os-hide-server-addresses": "is_admin:False",
|
||||||
|
"compute_extension:hosts": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-hosts": "rule:admin_api",
|
||||||
|
"compute_extension:hypervisors": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-hypervisors": "rule:admin_api",
|
||||||
|
"compute_extension:image_size": "",
|
||||||
|
"compute_extension:v3:os-image-metadata": "",
|
||||||
|
"compute_extension:v3:os-images": "",
|
||||||
|
"compute_extension:instance_actions": "",
|
||||||
|
"compute_extension:v3:os-instance-actions": "",
|
||||||
|
"compute_extension:instance_actions:events": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-instance-actions:events": "rule:admin_api",
|
||||||
|
"compute_extension:instance_usage_audit_log": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-instance-usage-audit-log": "rule:admin_api",
|
||||||
|
"compute_extension:v3:ips:discoverable": "",
|
||||||
|
"compute_extension:keypairs": "",
|
||||||
|
"compute_extension:keypairs:index": "",
|
||||||
|
"compute_extension:keypairs:show": "",
|
||||||
|
"compute_extension:keypairs:create": "",
|
||||||
|
"compute_extension:keypairs:delete": "",
|
||||||
|
"compute_extension:v3:os-keypairs:discoverable": "",
|
||||||
|
"compute_extension:v3:os-keypairs": "",
|
||||||
|
"compute_extension:v3:os-keypairs:index": "",
|
||||||
|
"compute_extension:v3:os-keypairs:show": "",
|
||||||
|
"compute_extension:v3:os-keypairs:create": "",
|
||||||
|
"compute_extension:v3:os-keypairs:delete": "",
|
||||||
|
"compute_extension:multinic": "",
|
||||||
|
"compute_extension:v3:os-multinic": "",
|
||||||
|
"compute_extension:networks": "rule:admin_api",
|
||||||
|
"compute_extension:networks:view": "",
|
||||||
|
"compute_extension:networks_associate": "rule:admin_api",
|
||||||
|
"compute_extension:quotas:show": "",
|
||||||
|
"compute_extension:quotas:update": "rule:admin_api",
|
||||||
|
"compute_extension:quotas:delete": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-quota-sets:show": "",
|
||||||
|
"compute_extension:v3:os-quota-sets:update": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-quota-sets:delete": "rule:admin_api",
|
||||||
|
"compute_extension:quota_classes": "",
|
||||||
|
"compute_extension:v3:os-quota-class-sets": "",
|
||||||
|
"compute_extension:rescue": "",
|
||||||
|
"compute_extension:v3:os-rescue": "",
|
||||||
|
"compute_extension:security_group_default_rules": "rule:admin_api",
|
||||||
|
"compute_extension:security_groups": "",
|
||||||
|
"compute_extension:v3:os-security-groups": "",
|
||||||
|
"compute_extension:server_diagnostics": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-server-diagnostics": "rule:admin_api",
|
||||||
|
"compute_extension:server_password": "",
|
||||||
|
"compute_extension:v3:os-server-password": "",
|
||||||
|
"compute_extension:server_usage": "",
|
||||||
|
"compute_extension:v3:os-server-usage": "",
|
||||||
|
"compute_extension:services": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-services": "rule:admin_api",
|
||||||
|
"compute_extension:v3:servers:discoverable": "",
|
||||||
|
"compute_extension:shelve": "",
|
||||||
|
"compute_extension:shelveOffload": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-shelve:shelve": "",
|
||||||
|
"compute_extension:v3:os-shelve:shelve_offload": "rule:admin_api",
|
||||||
|
"compute_extension:simple_tenant_usage:show": "rule:admin_or_owner",
|
||||||
|
"compute_extension:v3:os-simple-tenant-usage:show": "rule:admin_or_owner",
|
||||||
|
"compute_extension:simple_tenant_usage:list": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-simple-tenant-usage:list": "rule:admin_api",
|
||||||
|
"compute_extension:unshelve": "",
|
||||||
|
"compute_extension:v3:os-shelve:unshelve": "",
|
||||||
|
"compute_extension:users": "rule:admin_api",
|
||||||
|
"compute_extension:virtual_interfaces": "",
|
||||||
|
"compute_extension:virtual_storage_arrays": "",
|
||||||
|
"compute_extension:volumes": "",
|
||||||
|
"compute_extension:volume_attachments:index": "",
|
||||||
|
"compute_extension:volume_attachments:show": "",
|
||||||
|
"compute_extension:volume_attachments:create": "",
|
||||||
|
"compute_extension:volume_attachments:update": "",
|
||||||
|
"compute_extension:volume_attachments:delete": "",
|
||||||
|
"compute_extension:volumetypes": "",
|
||||||
|
"compute_extension:availability_zone:list": "",
|
||||||
|
"compute_extension:v3:os-availability-zone:list": "",
|
||||||
|
"compute_extension:availability_zone:detail": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-availability-zone:detail": "rule:admin_api",
|
||||||
|
"compute_extension:used_limits_for_admin": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-used-limits": "",
|
||||||
|
"compute_extension:v3:os-used-limits:tenant": "rule:admin_api",
|
||||||
|
"compute_extension:migrations:index": "rule:admin_api",
|
||||||
|
"compute_extension:v3:os-migrations:index": "rule:admin_api",
|
||||||
|
|
||||||
|
|
||||||
|
"volume:create": "",
|
||||||
|
"volume:get_all": "",
|
||||||
|
"volume:get_volume_metadata": "",
|
||||||
|
"volume:get_snapshot": "",
|
||||||
|
"volume:get_all_snapshots": "",
|
||||||
|
|
||||||
|
|
||||||
|
"volume_extension:types_manage": "rule:admin_api",
|
||||||
|
"volume_extension:types_extra_specs": "rule:admin_api",
|
||||||
|
"volume_extension:volume_admin_actions:reset_status": "rule:admin_api",
|
||||||
|
"volume_extension:snapshot_admin_actions:reset_status": "rule:admin_api",
|
||||||
|
"volume_extension:volume_admin_actions:force_delete": "rule:admin_api",
|
||||||
|
|
||||||
|
|
||||||
|
"network:get_all": "",
|
||||||
|
"network:get": "",
|
||||||
|
"network:create": "",
|
||||||
|
"network:delete": "",
|
||||||
|
"network:associate": "",
|
||||||
|
"network:disassociate": "",
|
||||||
|
"network:get_vifs_by_instance": "",
|
||||||
|
"network:allocate_for_instance": "",
|
||||||
|
"network:deallocate_for_instance": "",
|
||||||
|
"network:validate_networks": "",
|
||||||
|
"network:get_instance_uuids_by_ip_filter": "",
|
||||||
|
"network:get_instance_id_by_floating_address": "",
|
||||||
|
"network:setup_networks_on_host": "",
|
||||||
|
"network:get_backdoor_port": "",
|
||||||
|
|
||||||
|
"network:get_floating_ip": "",
|
||||||
|
"network:get_floating_ip_pools": "",
|
||||||
|
"network:get_floating_ip_by_address": "",
|
||||||
|
"network:get_floating_ips_by_project": "",
|
||||||
|
"network:get_floating_ips_by_fixed_address": "",
|
||||||
|
"network:allocate_floating_ip": "",
|
||||||
|
"network:deallocate_floating_ip": "",
|
||||||
|
"network:associate_floating_ip": "",
|
||||||
|
"network:disassociate_floating_ip": "",
|
||||||
|
"network:release_floating_ip": "",
|
||||||
|
"network:migrate_instance_start": "",
|
||||||
|
"network:migrate_instance_finish": "",
|
||||||
|
|
||||||
|
"network:get_fixed_ip": "",
|
||||||
|
"network:get_fixed_ip_by_address": "",
|
||||||
|
"network:add_fixed_ip_to_instance": "",
|
||||||
|
"network:remove_fixed_ip_from_instance": "",
|
||||||
|
"network:add_network_to_project": "",
|
||||||
|
"network:get_instance_nw_info": "",
|
||||||
|
|
||||||
|
"network:get_dns_domains": "",
|
||||||
|
"network:add_dns_entry": "",
|
||||||
|
"network:modify_dns_entry": "",
|
||||||
|
"network:delete_dns_entry": "",
|
||||||
|
"network:get_dns_entries_by_address": "",
|
||||||
|
"network:get_dns_entries_by_name": "",
|
||||||
|
"network:create_private_dns_domain": "",
|
||||||
|
"network:create_public_dns_domain": "",
|
||||||
|
"network:delete_dns_domain": ""
|
||||||
|
}
|
@ -52,6 +52,7 @@ class CreateDomainLink(tables.LinkAction):
|
|||||||
verbose_name = _("Create Domain")
|
verbose_name = _("Create Domain")
|
||||||
url = constants.DOMAINS_CREATE_URL
|
url = constants.DOMAINS_CREATE_URL
|
||||||
classes = ("ajax-modal", "btn-create")
|
classes = ("ajax-modal", "btn-create")
|
||||||
|
policy_rules = (('identity', 'identity:create_domain'),)
|
||||||
|
|
||||||
def allowed(self, request, domain):
|
def allowed(self, request, domain):
|
||||||
return api.keystone.keystone_can_edit_domain()
|
return api.keystone.keystone_can_edit_domain()
|
||||||
@ -62,6 +63,7 @@ class EditDomainLink(tables.LinkAction):
|
|||||||
verbose_name = _("Edit")
|
verbose_name = _("Edit")
|
||||||
url = constants.DOMAINS_UPDATE_URL
|
url = constants.DOMAINS_UPDATE_URL
|
||||||
classes = ("ajax-modal", "btn-edit")
|
classes = ("ajax-modal", "btn-edit")
|
||||||
|
policy_rules = (('identity', 'identity:update_domain'),)
|
||||||
|
|
||||||
def allowed(self, request, domain):
|
def allowed(self, request, domain):
|
||||||
return api.keystone.keystone_can_edit_domain()
|
return api.keystone.keystone_can_edit_domain()
|
||||||
@ -71,6 +73,7 @@ class DeleteDomainsAction(tables.DeleteAction):
|
|||||||
name = "delete"
|
name = "delete"
|
||||||
data_type_singular = _("Domain")
|
data_type_singular = _("Domain")
|
||||||
data_type_plural = _("Domains")
|
data_type_plural = _("Domains")
|
||||||
|
policy_rules = (('identity', 'identity:delete_domain'),)
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
def allowed(self, request, datum):
|
||||||
return api.keystone.keystone_can_edit_domain()
|
return api.keystone.keystone_can_edit_domain()
|
||||||
@ -111,6 +114,7 @@ class SetDomainContext(tables.Action):
|
|||||||
verbose_name = _("Set Domain Context")
|
verbose_name = _("Set Domain Context")
|
||||||
url = constants.DOMAINS_INDEX_URL
|
url = constants.DOMAINS_INDEX_URL
|
||||||
preempt = True
|
preempt = True
|
||||||
|
policy_rules = (('identity', 'admin_required'),)
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
def allowed(self, request, datum):
|
||||||
multidomain_support = getattr(settings,
|
multidomain_support = getattr(settings,
|
||||||
@ -145,6 +149,7 @@ class UnsetDomainContext(tables.Action):
|
|||||||
url = constants.DOMAINS_INDEX_URL
|
url = constants.DOMAINS_INDEX_URL
|
||||||
preempt = True
|
preempt = True
|
||||||
requires_input = False
|
requires_input = False
|
||||||
|
policy_rules = (('identity', 'admin_required'),)
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
def allowed(self, request, datum):
|
||||||
ctx = request.session.get("domain_context", None)
|
ctx = request.session.get("domain_context", None)
|
||||||
|
@ -40,6 +40,7 @@ class CreateGroupLink(tables.LinkAction):
|
|||||||
verbose_name = _("Create Group")
|
verbose_name = _("Create Group")
|
||||||
url = constants.GROUPS_CREATE_URL
|
url = constants.GROUPS_CREATE_URL
|
||||||
classes = ("ajax-modal", "btn-create")
|
classes = ("ajax-modal", "btn-create")
|
||||||
|
policy_rules = (("identity", "identity:create_group"),)
|
||||||
|
|
||||||
def allowed(self, request, group):
|
def allowed(self, request, group):
|
||||||
return api.keystone.keystone_can_edit_group()
|
return api.keystone.keystone_can_edit_group()
|
||||||
@ -50,6 +51,7 @@ class EditGroupLink(tables.LinkAction):
|
|||||||
verbose_name = _("Edit Group")
|
verbose_name = _("Edit Group")
|
||||||
url = constants.GROUPS_UPDATE_URL
|
url = constants.GROUPS_UPDATE_URL
|
||||||
classes = ("ajax-modal", "btn-edit")
|
classes = ("ajax-modal", "btn-edit")
|
||||||
|
policy_rules = (("identity", "identity:update_group"),)
|
||||||
|
|
||||||
def allowed(self, request, group):
|
def allowed(self, request, group):
|
||||||
return api.keystone.keystone_can_edit_group()
|
return api.keystone.keystone_can_edit_group()
|
||||||
@ -59,6 +61,7 @@ class DeleteGroupsAction(tables.DeleteAction):
|
|||||||
name = "delete"
|
name = "delete"
|
||||||
data_type_singular = _("Group")
|
data_type_singular = _("Group")
|
||||||
data_type_plural = _("Groups")
|
data_type_plural = _("Groups")
|
||||||
|
policy_rules = (("identity", "identity:delete_group"),)
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
def allowed(self, request, datum):
|
||||||
return api.keystone.keystone_can_edit_group()
|
return api.keystone.keystone_can_edit_group()
|
||||||
@ -73,6 +76,8 @@ class ManageUsersLink(tables.LinkAction):
|
|||||||
verbose_name = _("Modify Users")
|
verbose_name = _("Modify Users")
|
||||||
url = constants.GROUPS_MANAGE_URL
|
url = constants.GROUPS_MANAGE_URL
|
||||||
classes = ("btn-edit")
|
classes = ("btn-edit")
|
||||||
|
policy_rules = (("identity", "identity:get_group"),
|
||||||
|
("identity", "identity:list_users"),)
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
def allowed(self, request, datum):
|
||||||
return api.keystone.keystone_can_edit_group()
|
return api.keystone.keystone_can_edit_group()
|
||||||
@ -120,6 +125,7 @@ class RemoveMembers(tables.DeleteAction):
|
|||||||
action_past = _("Removed")
|
action_past = _("Removed")
|
||||||
data_type_singular = _("User")
|
data_type_singular = _("User")
|
||||||
data_type_plural = _("Users")
|
data_type_plural = _("Users")
|
||||||
|
policy_rules = (("identity", "identity:remove_user_from_group"),)
|
||||||
|
|
||||||
def allowed(self, request, user=None):
|
def allowed(self, request, user=None):
|
||||||
return api.keystone.keystone_can_edit_group()
|
return api.keystone.keystone_can_edit_group()
|
||||||
@ -142,6 +148,8 @@ class AddMembersLink(tables.LinkAction):
|
|||||||
verbose_name = _("Add...")
|
verbose_name = _("Add...")
|
||||||
classes = ("ajax-modal", "btn-create")
|
classes = ("ajax-modal", "btn-create")
|
||||||
url = constants.GROUPS_ADD_MEMBER_URL
|
url = constants.GROUPS_ADD_MEMBER_URL
|
||||||
|
policy_rules = (("identity", "identity:list_users"),
|
||||||
|
("identity", "identity:add_user_to_group"),)
|
||||||
|
|
||||||
def allowed(self, request, user=None):
|
def allowed(self, request, user=None):
|
||||||
return api.keystone.keystone_can_edit_group()
|
return api.keystone.keystone_can_edit_group()
|
||||||
@ -177,6 +185,7 @@ class AddMembers(tables.BatchAction):
|
|||||||
classes = ("btn-create", )
|
classes = ("btn-create", )
|
||||||
requires_input = True
|
requires_input = True
|
||||||
success_url = constants.GROUPS_MANAGE_URL
|
success_url = constants.GROUPS_MANAGE_URL
|
||||||
|
policy_rules = (("identity", "identity:add_user_to_group"),)
|
||||||
|
|
||||||
def allowed(self, request, user=None):
|
def allowed(self, request, user=None):
|
||||||
return api.keystone.keystone_can_edit_group()
|
return api.keystone.keystone_can_edit_group()
|
||||||
|
@ -9,7 +9,6 @@ from horizon import tables
|
|||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
from openstack_dashboard.api import keystone
|
from openstack_dashboard.api import keystone
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -18,6 +17,8 @@ class ViewMembersLink(tables.LinkAction):
|
|||||||
verbose_name = _("Modify Users")
|
verbose_name = _("Modify Users")
|
||||||
url = "horizon:admin:projects:update"
|
url = "horizon:admin:projects:update"
|
||||||
classes = ("ajax-modal", "btn-edit")
|
classes = ("ajax-modal", "btn-edit")
|
||||||
|
policy_rules = (("identity", "identity:list_users"),
|
||||||
|
("identity", "identity:list_roles"))
|
||||||
|
|
||||||
def get_link_url(self, project):
|
def get_link_url(self, project):
|
||||||
step = 'update_members'
|
step = 'update_members'
|
||||||
@ -47,6 +48,7 @@ class UsageLink(tables.LinkAction):
|
|||||||
verbose_name = _("View Usage")
|
verbose_name = _("View Usage")
|
||||||
url = "horizon:admin:projects:usage"
|
url = "horizon:admin:projects:usage"
|
||||||
classes = ("btn-stats",)
|
classes = ("btn-stats",)
|
||||||
|
policy_rules = (("compute", "compute_extension:simple_tenant_usage:show"),)
|
||||||
|
|
||||||
|
|
||||||
class CreateProject(tables.LinkAction):
|
class CreateProject(tables.LinkAction):
|
||||||
@ -54,6 +56,7 @@ class CreateProject(tables.LinkAction):
|
|||||||
verbose_name = _("Create Project")
|
verbose_name = _("Create Project")
|
||||||
url = "horizon:admin:projects:create"
|
url = "horizon:admin:projects:create"
|
||||||
classes = ("btn-launch", "ajax-modal",)
|
classes = ("btn-launch", "ajax-modal",)
|
||||||
|
policy_rules = (('identity', 'identity:create_project'),)
|
||||||
|
|
||||||
def allowed(self, request, project):
|
def allowed(self, request, project):
|
||||||
return api.keystone.keystone_can_edit_project()
|
return api.keystone.keystone_can_edit_project()
|
||||||
@ -64,6 +67,7 @@ class UpdateProject(tables.LinkAction):
|
|||||||
verbose_name = _("Edit Project")
|
verbose_name = _("Edit Project")
|
||||||
url = "horizon:admin:projects:update"
|
url = "horizon:admin:projects:update"
|
||||||
classes = ("ajax-modal", "btn-edit")
|
classes = ("ajax-modal", "btn-edit")
|
||||||
|
policy_rules = (('identity', 'identity:update_project'),)
|
||||||
|
|
||||||
def allowed(self, request, project):
|
def allowed(self, request, project):
|
||||||
return api.keystone.keystone_can_edit_project()
|
return api.keystone.keystone_can_edit_project()
|
||||||
@ -74,6 +78,7 @@ class ModifyQuotas(tables.LinkAction):
|
|||||||
verbose_name = "Modify Quotas"
|
verbose_name = "Modify Quotas"
|
||||||
url = "horizon:admin:projects:update"
|
url = "horizon:admin:projects:update"
|
||||||
classes = ("ajax-modal", "btn-edit")
|
classes = ("ajax-modal", "btn-edit")
|
||||||
|
policy_rules = (('compute', "compute_extension:quotas:update"),)
|
||||||
|
|
||||||
def get_link_url(self, project):
|
def get_link_url(self, project):
|
||||||
step = 'update_quotas'
|
step = 'update_quotas'
|
||||||
@ -85,6 +90,7 @@ class ModifyQuotas(tables.LinkAction):
|
|||||||
class DeleteTenantsAction(tables.DeleteAction):
|
class DeleteTenantsAction(tables.DeleteAction):
|
||||||
data_type_singular = _("Project")
|
data_type_singular = _("Project")
|
||||||
data_type_plural = _("Projects")
|
data_type_plural = _("Projects")
|
||||||
|
policy_rules = (("identity", "identity:delete_project"),)
|
||||||
|
|
||||||
def allowed(self, request, project):
|
def allowed(self, request, project):
|
||||||
return api.keystone.keystone_can_edit_project()
|
return api.keystone.keystone_can_edit_project()
|
||||||
|
@ -31,6 +31,7 @@ class CreateRoleLink(tables.LinkAction):
|
|||||||
verbose_name = _("Create Role")
|
verbose_name = _("Create Role")
|
||||||
url = "horizon:admin:roles:create"
|
url = "horizon:admin:roles:create"
|
||||||
classes = ("ajax-modal", "btn-create")
|
classes = ("ajax-modal", "btn-create")
|
||||||
|
policy_rules = (("identity", "identity:create_role"),)
|
||||||
|
|
||||||
def allowed(self, request, role):
|
def allowed(self, request, role):
|
||||||
return api.keystone.keystone_can_edit_role()
|
return api.keystone.keystone_can_edit_role()
|
||||||
@ -41,6 +42,7 @@ class EditRoleLink(tables.LinkAction):
|
|||||||
verbose_name = _("Edit")
|
verbose_name = _("Edit")
|
||||||
url = "horizon:admin:roles:update"
|
url = "horizon:admin:roles:update"
|
||||||
classes = ("ajax-modal", "btn-edit")
|
classes = ("ajax-modal", "btn-edit")
|
||||||
|
policy_rules = (("identity", "identity:update_role"),)
|
||||||
|
|
||||||
def allowed(self, request, role):
|
def allowed(self, request, role):
|
||||||
return api.keystone.keystone_can_edit_role()
|
return api.keystone.keystone_can_edit_role()
|
||||||
@ -49,6 +51,7 @@ class EditRoleLink(tables.LinkAction):
|
|||||||
class DeleteRolesAction(tables.DeleteAction):
|
class DeleteRolesAction(tables.DeleteAction):
|
||||||
data_type_singular = _("Role")
|
data_type_singular = _("Role")
|
||||||
data_type_plural = _("Roles")
|
data_type_plural = _("Roles")
|
||||||
|
policy_rules = (("identity", "identity:delete_role"),)
|
||||||
|
|
||||||
def allowed(self, request, role):
|
def allowed(self, request, role):
|
||||||
return api.keystone.keystone_can_edit_role()
|
return api.keystone.keystone_can_edit_role()
|
||||||
|
@ -8,7 +8,6 @@ from horizon import tables
|
|||||||
|
|
||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
ENABLE = 0
|
ENABLE = 0
|
||||||
@ -20,6 +19,10 @@ class CreateUserLink(tables.LinkAction):
|
|||||||
verbose_name = _("Create User")
|
verbose_name = _("Create User")
|
||||||
url = "horizon:admin:users:create"
|
url = "horizon:admin:users:create"
|
||||||
classes = ("ajax-modal", "btn-create")
|
classes = ("ajax-modal", "btn-create")
|
||||||
|
policy_rules = (('identity', 'identity:create_grant'),
|
||||||
|
("identity", "identity:create_user"),
|
||||||
|
("identity", "identity:list_roles"),
|
||||||
|
("identity", "identity:list_projects"),)
|
||||||
|
|
||||||
def allowed(self, request, user):
|
def allowed(self, request, user):
|
||||||
return api.keystone.keystone_can_edit_user()
|
return api.keystone.keystone_can_edit_user()
|
||||||
@ -30,6 +33,11 @@ class EditUserLink(tables.LinkAction):
|
|||||||
verbose_name = _("Edit")
|
verbose_name = _("Edit")
|
||||||
url = "horizon:admin:users:update"
|
url = "horizon:admin:users:update"
|
||||||
classes = ("ajax-modal", "btn-edit")
|
classes = ("ajax-modal", "btn-edit")
|
||||||
|
policy_rules = (("identity", "identity:update_user"),
|
||||||
|
("identity", "identity:list_projects"),)
|
||||||
|
|
||||||
|
def get_policy_target(self, request, user):
|
||||||
|
return {"user_id": user.id}
|
||||||
|
|
||||||
def allowed(self, request, user):
|
def allowed(self, request, user):
|
||||||
return api.keystone.keystone_can_edit_user()
|
return api.keystone.keystone_can_edit_user()
|
||||||
@ -42,6 +50,12 @@ class ToggleEnabled(tables.BatchAction):
|
|||||||
data_type_singular = _("User")
|
data_type_singular = _("User")
|
||||||
data_type_plural = _("Users")
|
data_type_plural = _("Users")
|
||||||
classes = ("btn-toggle",)
|
classes = ("btn-toggle",)
|
||||||
|
policy_rules = (("identity", "identity:update_user"),)
|
||||||
|
|
||||||
|
def get_policy_target(self, request, user=None):
|
||||||
|
if user:
|
||||||
|
return {"user_id": user.id}
|
||||||
|
return {}
|
||||||
|
|
||||||
def allowed(self, request, user=None):
|
def allowed(self, request, user=None):
|
||||||
if not api.keystone.keystone_can_edit_user():
|
if not api.keystone.keystone_can_edit_user():
|
||||||
@ -78,10 +92,11 @@ class ToggleEnabled(tables.BatchAction):
|
|||||||
class DeleteUsersAction(tables.DeleteAction):
|
class DeleteUsersAction(tables.DeleteAction):
|
||||||
data_type_singular = _("User")
|
data_type_singular = _("User")
|
||||||
data_type_plural = _("Users")
|
data_type_plural = _("Users")
|
||||||
|
policy_rules = (("identity", "identity:delete_user"),)
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
def allowed(self, request, datum):
|
||||||
if not api.keystone.keystone_can_edit_user() or \
|
if not api.keystone.keystone_can_edit_user() or \
|
||||||
(datum and datum.id == request.user.id):
|
(datum and datum.id == request.user.id):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -195,6 +195,19 @@ TIME_ZONE = "UTC"
|
|||||||
# 'reverse': False,
|
# 'reverse': False,
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
# The Horizon Policy Enforcement engine uses these values to load per service
|
||||||
|
# policy rule files. The content of these files should match the files the
|
||||||
|
# OpenStack services are using to determine role based access control in the
|
||||||
|
# target installation.
|
||||||
|
|
||||||
|
# Path to directory containing policy.json files
|
||||||
|
#POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf")
|
||||||
|
# Map of local copy of service policy files
|
||||||
|
#POLICY_FILES = {
|
||||||
|
# 'identity': 'keystone_policy.json',
|
||||||
|
# 'compute': 'nova_policy.json'
|
||||||
|
#}
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
# When set to True this will disable all logging except
|
# When set to True this will disable all logging except
|
||||||
|
110
openstack_dashboard/openstack/common/fileutils.py
Normal file
110
openstack_dashboard/openstack/common/fileutils.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack Foundation.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
|
||||||
|
from openstack_dashboard.openstack.common import excutils
|
||||||
|
from openstack_dashboard.openstack.common.gettextutils import _ # noqa
|
||||||
|
from openstack_dashboard.openstack.common import log as logging
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_FILE_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tree(path):
|
||||||
|
"""Create a directory (and any ancestor directories required)
|
||||||
|
|
||||||
|
:param path: Directory to create
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
os.makedirs(path)
|
||||||
|
except OSError as exc:
|
||||||
|
if exc.errno == errno.EEXIST:
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def read_cached_file(filename, force_reload=False):
|
||||||
|
"""Read from a file if it has been modified.
|
||||||
|
|
||||||
|
:param force_reload: Whether to reload the file.
|
||||||
|
:returns: A tuple with a boolean specifying if the data is fresh
|
||||||
|
or not.
|
||||||
|
"""
|
||||||
|
global _FILE_CACHE
|
||||||
|
|
||||||
|
if force_reload and filename in _FILE_CACHE:
|
||||||
|
del _FILE_CACHE[filename]
|
||||||
|
|
||||||
|
reloaded = False
|
||||||
|
mtime = os.path.getmtime(filename)
|
||||||
|
cache_info = _FILE_CACHE.setdefault(filename, {})
|
||||||
|
|
||||||
|
if not cache_info or mtime > cache_info.get('mtime', 0):
|
||||||
|
LOG.debug(_("Reloading cached file %s") % filename)
|
||||||
|
with open(filename) as fap:
|
||||||
|
cache_info['data'] = fap.read()
|
||||||
|
cache_info['mtime'] = mtime
|
||||||
|
reloaded = True
|
||||||
|
return (reloaded, cache_info['data'])
|
||||||
|
|
||||||
|
|
||||||
|
def delete_if_exists(path):
|
||||||
|
"""Delete a file, but ignore file not found error.
|
||||||
|
|
||||||
|
:param path: File to delete
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.ENOENT:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def remove_path_on_error(path):
|
||||||
|
"""Protect code that wants to operate on PATH atomically.
|
||||||
|
Any exception will cause PATH to be removed.
|
||||||
|
|
||||||
|
:param path: File to work with
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
delete_if_exists(path)
|
||||||
|
|
||||||
|
|
||||||
|
def file_open(*args, **kwargs):
|
||||||
|
"""Open file
|
||||||
|
|
||||||
|
see built-in file() documentation for more details
|
||||||
|
|
||||||
|
Note: The reason this is kept in a separate module is to easily
|
||||||
|
be able to provide a stub module that doesn't alter system
|
||||||
|
state at all (for unit tests)
|
||||||
|
"""
|
||||||
|
return file(*args, **kwargs)
|
852
openstack_dashboard/openstack/common/policy.py
Normal file
852
openstack_dashboard/openstack/common/policy.py
Normal file
@ -0,0 +1,852 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2012 OpenStack Foundation.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Common Policy Engine Implementation
|
||||||
|
|
||||||
|
Policies can be expressed in one of two forms: A list of lists, or a
|
||||||
|
string written in the new policy language.
|
||||||
|
|
||||||
|
In the list-of-lists representation, each check inside the innermost
|
||||||
|
list is combined as with an "and" conjunction--for that check to pass,
|
||||||
|
all the specified checks must pass. These innermost lists are then
|
||||||
|
combined as with an "or" conjunction. This is the original way of
|
||||||
|
expressing policies, but there now exists a new way: the policy
|
||||||
|
language.
|
||||||
|
|
||||||
|
In the policy language, each check is specified the same way as in the
|
||||||
|
list-of-lists representation: a simple "a:b" pair that is matched to
|
||||||
|
the correct code to perform that check. However, conjunction
|
||||||
|
operators are available, allowing for more expressiveness in crafting
|
||||||
|
policies.
|
||||||
|
|
||||||
|
As an example, take the following rule, expressed in the list-of-lists
|
||||||
|
representation::
|
||||||
|
|
||||||
|
[["role:admin"], ["project_id:%(project_id)s", "role:projectadmin"]]
|
||||||
|
|
||||||
|
In the policy language, this becomes::
|
||||||
|
|
||||||
|
role:admin or (project_id:%(project_id)s and role:projectadmin)
|
||||||
|
|
||||||
|
The policy language also has the "not" operator, allowing a richer
|
||||||
|
policy rule::
|
||||||
|
|
||||||
|
project_id:%(project_id)s and not role:dunce
|
||||||
|
|
||||||
|
Finally, two special policy checks should be mentioned; the policy
|
||||||
|
check "@" will always accept an access, and the policy check "!" will
|
||||||
|
always reject an access. (Note that if a rule is either the empty
|
||||||
|
list ("[]") or the empty string, this is equivalent to the "@" policy
|
||||||
|
check.) Of these, the "!" policy check is probably the most useful,
|
||||||
|
as it allows particular rules to be explicitly disabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import re
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
import six
|
||||||
|
|
||||||
|
from openstack_dashboard.openstack.common import fileutils
|
||||||
|
from openstack_dashboard.openstack.common.gettextutils import _ # noqa
|
||||||
|
from openstack_dashboard.openstack.common import jsonutils
|
||||||
|
from openstack_dashboard.openstack.common import log as logging
|
||||||
|
|
||||||
|
policy_opts = [
|
||||||
|
cfg.StrOpt('policy_file',
|
||||||
|
default='policy.json',
|
||||||
|
help=_('JSON file containing policy')),
|
||||||
|
cfg.StrOpt('policy_default_rule',
|
||||||
|
default='default',
|
||||||
|
help=_('Rule enforced when requested rule is not found')),
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opts(policy_opts)
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_checks = {}
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyNotAuthorized(Exception):
|
||||||
|
|
||||||
|
def __init__(self, rule):
|
||||||
|
msg = _("Policy doesn't allow %s to be performed.") % rule
|
||||||
|
super(PolicyNotAuthorized, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class Rules(dict):
|
||||||
|
"""A store for rules. Handles the default_rule setting directly."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_json(cls, data, default_rule=None):
|
||||||
|
"""Allow loading of JSON rule data."""
|
||||||
|
|
||||||
|
# Suck in the JSON data and parse the rules
|
||||||
|
rules = dict((k, parse_rule(v)) for k, v in
|
||||||
|
jsonutils.loads(data).items())
|
||||||
|
|
||||||
|
return cls(rules, default_rule)
|
||||||
|
|
||||||
|
def __init__(self, rules=None, default_rule=None):
|
||||||
|
"""Initialize the Rules store."""
|
||||||
|
|
||||||
|
super(Rules, self).__init__(rules or {})
|
||||||
|
self.default_rule = default_rule
|
||||||
|
|
||||||
|
def __missing__(self, key):
|
||||||
|
"""Implements the default rule handling."""
|
||||||
|
|
||||||
|
if isinstance(self.default_rule, dict):
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
# If the default rule isn't actually defined, do something
|
||||||
|
# reasonably intelligent
|
||||||
|
if not self.default_rule or self.default_rule not in self:
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
if isinstance(self.default_rule, BaseCheck):
|
||||||
|
return self.default_rule
|
||||||
|
elif isinstance(self.default_rule, six.string_types):
|
||||||
|
return self[self.default_rule]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Dumps a string representation of the rules."""
|
||||||
|
|
||||||
|
# Start by building the canonical strings for the rules
|
||||||
|
out_rules = {}
|
||||||
|
for key, value in self.items():
|
||||||
|
# Use empty string for singleton TrueCheck instances
|
||||||
|
if isinstance(value, TrueCheck):
|
||||||
|
out_rules[key] = ''
|
||||||
|
else:
|
||||||
|
out_rules[key] = str(value)
|
||||||
|
|
||||||
|
# Dump a pretty-printed JSON representation
|
||||||
|
return jsonutils.dumps(out_rules, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
class Enforcer(object):
|
||||||
|
"""Responsible for loading and enforcing rules.
|
||||||
|
|
||||||
|
: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
|
||||||
|
`load_rules(True)`, `clear()` or `set_rules(True)`
|
||||||
|
is called this will be overwritten.
|
||||||
|
:param default_rule: Default rule to use, CONF.default_rule will
|
||||||
|
be used if none is specified.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, policy_file=None, rules=None, default_rule=None):
|
||||||
|
self.rules = Rules(rules, default_rule)
|
||||||
|
self.default_rule = default_rule or CONF.policy_default_rule
|
||||||
|
|
||||||
|
self.policy_path = None
|
||||||
|
self.policy_file = policy_file or CONF.policy_file
|
||||||
|
|
||||||
|
def set_rules(self, rules, overwrite=True):
|
||||||
|
"""Create a new Rules object based on the provided dict of rules.
|
||||||
|
|
||||||
|
:param rules: New rules to use. It should be an instance of dict.
|
||||||
|
:param overwrite: Whether to overwrite current rules or update them
|
||||||
|
with the new rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(rules, dict):
|
||||||
|
raise TypeError(_("Rules must be an instance of dict or Rules, "
|
||||||
|
"got %s instead") % type(rules))
|
||||||
|
|
||||||
|
if overwrite:
|
||||||
|
self.rules = Rules(rules, self.default_rule)
|
||||||
|
else:
|
||||||
|
self.rules.update(rules)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Clears Enforcer rules, policy's cache and policy's path."""
|
||||||
|
self.set_rules({})
|
||||||
|
self.default_rule = None
|
||||||
|
self.policy_path = None
|
||||||
|
|
||||||
|
def load_rules(self, force_reload=False):
|
||||||
|
"""Loads policy_path's rules.
|
||||||
|
|
||||||
|
Policy file is cached and will be reloaded if modified.
|
||||||
|
|
||||||
|
:param force_reload: Whether to overwrite current rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.policy_path:
|
||||||
|
self.policy_path = self._get_policy_path()
|
||||||
|
|
||||||
|
reloaded, data = fileutils.read_cached_file(self.policy_path,
|
||||||
|
force_reload=force_reload)
|
||||||
|
if reloaded or not self.rules:
|
||||||
|
rules = Rules.load_json(data, self.default_rule)
|
||||||
|
self.set_rules(rules)
|
||||||
|
LOG.debug(_("Rules successfully reloaded"))
|
||||||
|
|
||||||
|
def _get_policy_path(self):
|
||||||
|
"""Locate the policy json data file.
|
||||||
|
|
||||||
|
:param policy_file: Custom policy file to locate.
|
||||||
|
|
||||||
|
:returns: The policy path
|
||||||
|
|
||||||
|
:raises: ConfigFilesNotFoundError if the file couldn't
|
||||||
|
be located.
|
||||||
|
"""
|
||||||
|
policy_file = CONF.find_file(self.policy_file)
|
||||||
|
|
||||||
|
if policy_file:
|
||||||
|
return policy_file
|
||||||
|
|
||||||
|
raise cfg.ConfigFilesNotFoundError(path=CONF.policy_file)
|
||||||
|
|
||||||
|
def enforce(self, rule, target, creds, do_raise=False,
|
||||||
|
exc=None, *args, **kwargs):
|
||||||
|
"""Checks authorization of a rule against the target and credentials.
|
||||||
|
|
||||||
|
:param rule: A string or BaseCheck instance specifying the rule
|
||||||
|
to evaluate.
|
||||||
|
:param target: As much information about the object being operated
|
||||||
|
on as possible, as a dictionary.
|
||||||
|
:param creds: As much information about the user performing the
|
||||||
|
action as possible, as a dictionary.
|
||||||
|
:param do_raise: Whether to raise an exception or not if check
|
||||||
|
fails.
|
||||||
|
:param exc: Class of the exception to raise if the check fails.
|
||||||
|
Any remaining arguments passed to check() (both
|
||||||
|
positional and keyword arguments) will be passed to
|
||||||
|
the exception class. If not specified, PolicyNotAuthorized
|
||||||
|
will be used.
|
||||||
|
|
||||||
|
:return: Returns False if the policy does not allow the action and
|
||||||
|
exc is not provided; otherwise, returns a value that
|
||||||
|
evaluates to True. Note: for rules using the "case"
|
||||||
|
expression, this True value will be the specified string
|
||||||
|
from the expression.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# NOTE(flaper87): Not logging target or creds to avoid
|
||||||
|
# potential security issues.
|
||||||
|
LOG.debug(_("Rule %s will be now enforced") % rule)
|
||||||
|
|
||||||
|
self.load_rules()
|
||||||
|
|
||||||
|
# Allow the rule to be a Check tree
|
||||||
|
if isinstance(rule, BaseCheck):
|
||||||
|
result = rule(target, creds, self)
|
||||||
|
elif not self.rules:
|
||||||
|
# No rules to reference means we're going to fail closed
|
||||||
|
result = False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# Evaluate the rule
|
||||||
|
result = self.rules[rule](target, creds, self)
|
||||||
|
except KeyError:
|
||||||
|
LOG.debug(_("Rule [%s] doesn't exist") % rule)
|
||||||
|
# If the rule doesn't exist, fail closed
|
||||||
|
result = False
|
||||||
|
|
||||||
|
# If it is False, raise the exception if requested
|
||||||
|
if do_raise and not result:
|
||||||
|
if exc:
|
||||||
|
raise exc(*args, **kwargs)
|
||||||
|
|
||||||
|
raise PolicyNotAuthorized(rule)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCheck(object):
|
||||||
|
"""Abstract base class for Check classes."""
|
||||||
|
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __str__(self):
|
||||||
|
"""String representation of the Check tree rooted at this node."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __call__(self, target, cred, enforcer):
|
||||||
|
"""Triggers if instance of the class is called.
|
||||||
|
|
||||||
|
Performs the check. Returns False to reject the access or a
|
||||||
|
true value (not necessary True) to accept the access.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FalseCheck(BaseCheck):
|
||||||
|
"""A policy check that always returns False (disallow)."""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
|
return "!"
|
||||||
|
|
||||||
|
def __call__(self, target, cred, enforcer):
|
||||||
|
"""Check the policy."""
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class TrueCheck(BaseCheck):
|
||||||
|
"""A policy check that always returns True (allow)."""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
|
return "@"
|
||||||
|
|
||||||
|
def __call__(self, target, cred, enforcer):
|
||||||
|
"""Check the policy."""
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Check(BaseCheck):
|
||||||
|
"""A base class to allow for user-defined policy checks."""
|
||||||
|
|
||||||
|
def __init__(self, kind, match):
|
||||||
|
"""Initiates Check instance.
|
||||||
|
|
||||||
|
:param kind: The kind of the check, i.e., the field before the
|
||||||
|
':'.
|
||||||
|
:param match: The match of the check, i.e., the field after
|
||||||
|
the ':'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.kind = kind
|
||||||
|
self.match = match
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
|
return "%s:%s" % (self.kind, self.match)
|
||||||
|
|
||||||
|
|
||||||
|
class NotCheck(BaseCheck):
|
||||||
|
"""Implements the "not" logical operator.
|
||||||
|
|
||||||
|
A policy check that inverts the result of another policy check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rule):
|
||||||
|
"""Initialize the 'not' check.
|
||||||
|
|
||||||
|
:param rule: The rule to negate. Must be a Check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.rule = rule
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
|
return "not %s" % self.rule
|
||||||
|
|
||||||
|
def __call__(self, target, cred, enforcer):
|
||||||
|
"""Check the policy.
|
||||||
|
|
||||||
|
Returns the logical inverse of the wrapped check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return not self.rule(target, cred, enforcer)
|
||||||
|
|
||||||
|
|
||||||
|
class AndCheck(BaseCheck):
|
||||||
|
"""Implements the "and" logical operator.
|
||||||
|
|
||||||
|
A policy check that requires that a list of other checks all return True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rules):
|
||||||
|
"""Initialize the 'and' check.
|
||||||
|
|
||||||
|
:param rules: A list of rules that will be tested.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.rules = rules
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
|
return "(%s)" % ' and '.join(str(r) for r in self.rules)
|
||||||
|
|
||||||
|
def __call__(self, target, cred, enforcer):
|
||||||
|
"""Check the policy.
|
||||||
|
|
||||||
|
Requires that all rules accept in order to return True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for rule in self.rules:
|
||||||
|
if not rule(target, cred, enforcer):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_check(self, rule):
|
||||||
|
"""Adds rule to be tested.
|
||||||
|
|
||||||
|
Allows addition of another rule to the list of rules that will
|
||||||
|
be tested. Returns the AndCheck object for convenience.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.rules.append(rule)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class OrCheck(BaseCheck):
|
||||||
|
"""Implements the "or" operator.
|
||||||
|
|
||||||
|
A policy check that requires that at least one of a list of other
|
||||||
|
checks returns True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rules):
|
||||||
|
"""Initialize the 'or' check.
|
||||||
|
|
||||||
|
:param rules: A list of rules that will be tested.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.rules = rules
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
|
return "(%s)" % ' or '.join(str(r) for r in self.rules)
|
||||||
|
|
||||||
|
def __call__(self, target, cred, enforcer):
|
||||||
|
"""Check the policy.
|
||||||
|
|
||||||
|
Requires that at least one rule accept in order to return True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for rule in self.rules:
|
||||||
|
if rule(target, cred, enforcer):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_check(self, rule):
|
||||||
|
"""Adds rule to be tested.
|
||||||
|
|
||||||
|
Allows addition of another rule to the list of rules that will
|
||||||
|
be tested. Returns the OrCheck object for convenience.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.rules.append(rule)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_check(rule):
|
||||||
|
"""Parse a single base check rule into an appropriate Check object."""
|
||||||
|
|
||||||
|
# Handle the special checks
|
||||||
|
if rule == '!':
|
||||||
|
return FalseCheck()
|
||||||
|
elif rule == '@':
|
||||||
|
return TrueCheck()
|
||||||
|
|
||||||
|
try:
|
||||||
|
kind, match = rule.split(':', 1)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception(_("Failed to understand rule %s") % rule)
|
||||||
|
# If the rule is invalid, we'll fail closed
|
||||||
|
return FalseCheck()
|
||||||
|
|
||||||
|
# Find what implements the check
|
||||||
|
if kind in _checks:
|
||||||
|
return _checks[kind](kind, match)
|
||||||
|
elif None in _checks:
|
||||||
|
return _checks[None](kind, match)
|
||||||
|
else:
|
||||||
|
LOG.error(_("No handler for matches of kind %s") % kind)
|
||||||
|
return FalseCheck()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_list_rule(rule):
|
||||||
|
"""Translates the old list-of-lists syntax into a tree of Check objects.
|
||||||
|
|
||||||
|
Provided for backwards compatibility.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Empty rule defaults to True
|
||||||
|
if not rule:
|
||||||
|
return TrueCheck()
|
||||||
|
|
||||||
|
# Outer list is joined by "or"; inner list by "and"
|
||||||
|
or_list = []
|
||||||
|
for inner_rule in rule:
|
||||||
|
# Elide empty inner lists
|
||||||
|
if not inner_rule:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle bare strings
|
||||||
|
if isinstance(inner_rule, basestring):
|
||||||
|
inner_rule = [inner_rule]
|
||||||
|
|
||||||
|
# Parse the inner rules into Check objects
|
||||||
|
and_list = [_parse_check(r) for r in inner_rule]
|
||||||
|
|
||||||
|
# Append the appropriate check to the or_list
|
||||||
|
if len(and_list) == 1:
|
||||||
|
or_list.append(and_list[0])
|
||||||
|
else:
|
||||||
|
or_list.append(AndCheck(and_list))
|
||||||
|
|
||||||
|
# If we have only one check, omit the "or"
|
||||||
|
if not or_list:
|
||||||
|
return FalseCheck()
|
||||||
|
elif len(or_list) == 1:
|
||||||
|
return or_list[0]
|
||||||
|
|
||||||
|
return OrCheck(or_list)
|
||||||
|
|
||||||
|
|
||||||
|
# Used for tokenizing the policy language
|
||||||
|
_tokenize_re = re.compile(r'\s+')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tokenize(rule):
|
||||||
|
"""Tokenizer for the policy language.
|
||||||
|
|
||||||
|
Most of the single-character tokens are specified in the
|
||||||
|
_tokenize_re; however, parentheses need to be handled specially,
|
||||||
|
because they can appear inside a check string. Thankfully, those
|
||||||
|
parentheses that appear inside a check string can never occur at
|
||||||
|
the very beginning or end ("%(variable)s" is the correct syntax).
|
||||||
|
"""
|
||||||
|
|
||||||
|
for tok in _tokenize_re.split(rule):
|
||||||
|
# Skip empty tokens
|
||||||
|
if not tok or tok.isspace():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle leading parens on the token
|
||||||
|
clean = tok.lstrip('(')
|
||||||
|
for i in range(len(tok) - len(clean)):
|
||||||
|
yield '(', '('
|
||||||
|
|
||||||
|
# If it was only parentheses, continue
|
||||||
|
if not clean:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
tok = clean
|
||||||
|
|
||||||
|
# Handle trailing parens on the token
|
||||||
|
clean = tok.rstrip(')')
|
||||||
|
trail = len(tok) - len(clean)
|
||||||
|
|
||||||
|
# Yield the cleaned token
|
||||||
|
lowered = clean.lower()
|
||||||
|
if lowered in ('and', 'or', 'not'):
|
||||||
|
# Special tokens
|
||||||
|
yield lowered, clean
|
||||||
|
elif clean:
|
||||||
|
# Not a special token, but not composed solely of ')'
|
||||||
|
if len(tok) >= 2 and ((tok[0], tok[-1]) in
|
||||||
|
[('"', '"'), ("'", "'")]):
|
||||||
|
# It's a quoted string
|
||||||
|
yield 'string', tok[1:-1]
|
||||||
|
else:
|
||||||
|
yield 'check', _parse_check(clean)
|
||||||
|
|
||||||
|
# Yield the trailing parens
|
||||||
|
for i in range(trail):
|
||||||
|
yield ')', ')'
|
||||||
|
|
||||||
|
|
||||||
|
class ParseStateMeta(type):
|
||||||
|
"""Metaclass for the ParseState class.
|
||||||
|
|
||||||
|
Facilitates identifying reduction methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, cls_dict):
|
||||||
|
"""Create the class.
|
||||||
|
|
||||||
|
Injects the 'reducers' list, a list of tuples matching token sequences
|
||||||
|
to the names of the corresponding reduction methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
reducers = []
|
||||||
|
|
||||||
|
for key, value in cls_dict.items():
|
||||||
|
if not hasattr(value, 'reducers'):
|
||||||
|
continue
|
||||||
|
for reduction in value.reducers:
|
||||||
|
reducers.append((reduction, key))
|
||||||
|
|
||||||
|
cls_dict['reducers'] = reducers
|
||||||
|
|
||||||
|
return super(ParseStateMeta, mcs).__new__(mcs, name, bases, cls_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def reducer(*tokens):
|
||||||
|
"""Decorator for reduction methods.
|
||||||
|
|
||||||
|
Arguments are a sequence of tokens, in order, which should trigger running
|
||||||
|
this reduction method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
# Make sure we have a list of reducer sequences
|
||||||
|
if not hasattr(func, 'reducers'):
|
||||||
|
func.reducers = []
|
||||||
|
|
||||||
|
# Add the tokens to the list of reducer sequences
|
||||||
|
func.reducers.append(list(tokens))
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class ParseState(object):
|
||||||
|
"""Implement the core of parsing the policy language.
|
||||||
|
|
||||||
|
Uses a greedy reduction algorithm to reduce a sequence of tokens into
|
||||||
|
a single terminal, the value of which will be the root of the Check tree.
|
||||||
|
|
||||||
|
Note: error reporting is rather lacking. The best we can get with
|
||||||
|
this parser formulation is an overall "parse failed" error.
|
||||||
|
Fortunately, the policy language is simple enough that this
|
||||||
|
shouldn't be that big a problem.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__metaclass__ = ParseStateMeta
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the ParseState."""
|
||||||
|
|
||||||
|
self.tokens = []
|
||||||
|
self.values = []
|
||||||
|
|
||||||
|
def reduce(self):
|
||||||
|
"""Perform a greedy reduction of the token stream.
|
||||||
|
|
||||||
|
If a reducer method matches, it will be executed, then the
|
||||||
|
reduce() method will be called recursively to search for any more
|
||||||
|
possible reductions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for reduction, methname in self.reducers:
|
||||||
|
if (len(self.tokens) >= len(reduction) and
|
||||||
|
self.tokens[-len(reduction):] == reduction):
|
||||||
|
# Get the reduction method
|
||||||
|
meth = getattr(self, methname)
|
||||||
|
|
||||||
|
# Reduce the token stream
|
||||||
|
results = meth(*self.values[-len(reduction):])
|
||||||
|
|
||||||
|
# Update the tokens and values
|
||||||
|
self.tokens[-len(reduction):] = [r[0] for r in results]
|
||||||
|
self.values[-len(reduction):] = [r[1] for r in results]
|
||||||
|
|
||||||
|
# Check for any more reductions
|
||||||
|
return self.reduce()
|
||||||
|
|
||||||
|
def shift(self, tok, value):
|
||||||
|
"""Adds one more token to the state. Calls reduce()."""
|
||||||
|
|
||||||
|
self.tokens.append(tok)
|
||||||
|
self.values.append(value)
|
||||||
|
|
||||||
|
# Do a greedy reduce...
|
||||||
|
self.reduce()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def result(self):
|
||||||
|
"""Obtain the final result of the parse.
|
||||||
|
|
||||||
|
Raises ValueError if the parse failed to reduce to a single result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(self.values) != 1:
|
||||||
|
raise ValueError("Could not parse rule")
|
||||||
|
return self.values[0]
|
||||||
|
|
||||||
|
@reducer('(', 'check', ')')
|
||||||
|
@reducer('(', 'and_expr', ')')
|
||||||
|
@reducer('(', 'or_expr', ')')
|
||||||
|
def _wrap_check(self, _p1, check, _p2):
|
||||||
|
"""Turn parenthesized expressions into a 'check' token."""
|
||||||
|
|
||||||
|
return [('check', check)]
|
||||||
|
|
||||||
|
@reducer('check', 'and', 'check')
|
||||||
|
def _make_and_expr(self, check1, _and, check2):
|
||||||
|
"""Create an 'and_expr'.
|
||||||
|
|
||||||
|
Join two checks by the 'and' operator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [('and_expr', AndCheck([check1, check2]))]
|
||||||
|
|
||||||
|
@reducer('and_expr', 'and', 'check')
|
||||||
|
def _extend_and_expr(self, and_expr, _and, check):
|
||||||
|
"""Extend an 'and_expr' by adding one more check."""
|
||||||
|
|
||||||
|
return [('and_expr', and_expr.add_check(check))]
|
||||||
|
|
||||||
|
@reducer('check', 'or', 'check')
|
||||||
|
def _make_or_expr(self, check1, _or, check2):
|
||||||
|
"""Create an 'or_expr'.
|
||||||
|
|
||||||
|
Join two checks by the 'or' operator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [('or_expr', OrCheck([check1, check2]))]
|
||||||
|
|
||||||
|
@reducer('or_expr', 'or', 'check')
|
||||||
|
def _extend_or_expr(self, or_expr, _or, check):
|
||||||
|
"""Extend an 'or_expr' by adding one more check."""
|
||||||
|
|
||||||
|
return [('or_expr', or_expr.add_check(check))]
|
||||||
|
|
||||||
|
@reducer('not', 'check')
|
||||||
|
def _make_not_expr(self, _not, check):
|
||||||
|
"""Invert the result of another check."""
|
||||||
|
|
||||||
|
return [('check', NotCheck(check))]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_text_rule(rule):
|
||||||
|
"""Parses policy to the tree.
|
||||||
|
|
||||||
|
Translates a policy written in the policy language into a tree of
|
||||||
|
Check objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Empty rule means always accept
|
||||||
|
if not rule:
|
||||||
|
return TrueCheck()
|
||||||
|
|
||||||
|
# Parse the token stream
|
||||||
|
state = ParseState()
|
||||||
|
for tok, value in _parse_tokenize(rule):
|
||||||
|
state.shift(tok, value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return state.result
|
||||||
|
except ValueError:
|
||||||
|
# Couldn't parse the rule
|
||||||
|
LOG.exception(_("Failed to understand rule %r") % rule)
|
||||||
|
|
||||||
|
# Fail closed
|
||||||
|
return FalseCheck()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rule(rule):
|
||||||
|
"""Parses a policy rule into a tree of Check objects."""
|
||||||
|
|
||||||
|
# If the rule is a string, it's in the policy language
|
||||||
|
if isinstance(rule, basestring):
|
||||||
|
return _parse_text_rule(rule)
|
||||||
|
return _parse_list_rule(rule)
|
||||||
|
|
||||||
|
|
||||||
|
def register(name, func=None):
|
||||||
|
"""Register a function or Check class as a policy check.
|
||||||
|
|
||||||
|
:param name: Gives the name of the check type, e.g., 'rule',
|
||||||
|
'role', etc. If name is None, a default check type
|
||||||
|
will be registered.
|
||||||
|
:param func: If given, provides the function or class to register.
|
||||||
|
If not given, returns a function taking one argument
|
||||||
|
to specify the function or class to register,
|
||||||
|
allowing use as a decorator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Perform the actual decoration by registering the function or
|
||||||
|
# class. Returns the function or class for compliance with the
|
||||||
|
# decorator interface.
|
||||||
|
def decorator(func):
|
||||||
|
_checks[name] = func
|
||||||
|
return func
|
||||||
|
|
||||||
|
# If the function or class is given, do the registration
|
||||||
|
if func:
|
||||||
|
return decorator(func)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@register("rule")
|
||||||
|
class RuleCheck(Check):
|
||||||
|
def __call__(self, target, creds, enforcer):
|
||||||
|
"""Recursively checks credentials based on the defined rules."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return enforcer.rules[self.match](target, creds, enforcer)
|
||||||
|
except KeyError:
|
||||||
|
# We don't have any matching rule; fail closed
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@register("role")
|
||||||
|
class RoleCheck(Check):
|
||||||
|
def __call__(self, target, creds, enforcer):
|
||||||
|
"""Check that there is a matching role in the cred dict."""
|
||||||
|
|
||||||
|
return self.match.lower() in [x.lower() for x in creds['roles']]
|
||||||
|
|
||||||
|
|
||||||
|
@register('http')
|
||||||
|
class HttpCheck(Check):
|
||||||
|
def __call__(self, target, creds, enforcer):
|
||||||
|
"""Check http: rules by calling to a remote server.
|
||||||
|
|
||||||
|
This example implementation simply verifies that the response
|
||||||
|
is exactly 'True'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = ('http:' + self.match) % target
|
||||||
|
data = {'target': jsonutils.dumps(target),
|
||||||
|
'credentials': jsonutils.dumps(creds)}
|
||||||
|
post_data = urllib.urlencode(data)
|
||||||
|
f = urllib2.urlopen(url, post_data)
|
||||||
|
return f.read() == "True"
|
||||||
|
|
||||||
|
|
||||||
|
@register(None)
|
||||||
|
class GenericCheck(Check):
|
||||||
|
def __call__(self, target, creds, enforcer):
|
||||||
|
"""Check an individual match.
|
||||||
|
|
||||||
|
Matches look like:
|
||||||
|
|
||||||
|
tenant:%(tenant_id)s
|
||||||
|
role:compute:admin
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO(termie): do dict inspection via dot syntax
|
||||||
|
match = self.match % target
|
||||||
|
if self.kind in creds:
|
||||||
|
return match == six.text_type(creds[self.kind])
|
||||||
|
return False
|
120
openstack_dashboard/policy.py
Normal file
120
openstack_dashboard/policy.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Policy engine for Horizon"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from django.conf import settings # noqa
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from openstack_auth import utils as auth_utils
|
||||||
|
|
||||||
|
from openstack_dashboard.openstack.common import policy
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
_ENFORCER = None
|
||||||
|
_BASE_PATH = getattr(settings, 'POLICY_FILES_PATH', '')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_enforcer():
|
||||||
|
global _ENFORCER
|
||||||
|
if not _ENFORCER:
|
||||||
|
_ENFORCER = {}
|
||||||
|
policy_files = getattr(settings, 'POLICY_FILES', {})
|
||||||
|
for service in policy_files.keys():
|
||||||
|
enforcer = policy.Enforcer()
|
||||||
|
enforcer.policy_path = os.path.join(_BASE_PATH,
|
||||||
|
policy_files[service])
|
||||||
|
if os.path.isfile(enforcer.policy_path):
|
||||||
|
LOG.debug("adding enforcer for service: %s" % service)
|
||||||
|
_ENFORCER[service] = enforcer
|
||||||
|
else:
|
||||||
|
LOG.warn("policy file for service: %s not found at %s" %
|
||||||
|
(service, enforcer.policy_path))
|
||||||
|
return _ENFORCER
|
||||||
|
|
||||||
|
|
||||||
|
def reset():
|
||||||
|
global _ENFORCER
|
||||||
|
_ENFORCER = None
|
||||||
|
|
||||||
|
|
||||||
|
def check(actions, request, target={}):
|
||||||
|
"""
|
||||||
|
Check if the user has permission to the action according
|
||||||
|
to policy setting.
|
||||||
|
|
||||||
|
:param actions: list of scope and action to do policy checks on, the
|
||||||
|
composition of which is (scope, action)
|
||||||
|
|
||||||
|
scope: service type managing the policy for action
|
||||||
|
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
|
||||||
|
|
||||||
|
for a policy action that requires a single action:
|
||||||
|
actions should look like "(("compute", "compute:create_instance"),)"
|
||||||
|
for a multiple action check:
|
||||||
|
actions should look like "(("identity", "identity:list_users"),
|
||||||
|
("identity", "identity:list_roles"))"
|
||||||
|
|
||||||
|
:param request: django http request object. If not specified, credentials
|
||||||
|
must be passed.
|
||||||
|
: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.
|
||||||
|
{'tenant_id': object.tenant_id}
|
||||||
|
:returns: boolean if the user has permission or not for the actions.
|
||||||
|
"""
|
||||||
|
user = auth_utils.get_user(request)
|
||||||
|
credentials = _user_to_credentials(request, user)
|
||||||
|
|
||||||
|
enforcer = _get_enforcer()
|
||||||
|
|
||||||
|
for action in actions:
|
||||||
|
scope, action = action[0], action[1]
|
||||||
|
if scope in enforcer:
|
||||||
|
# if any check fails return failure
|
||||||
|
if not enforcer[scope].enforce(action, target, credentials):
|
||||||
|
return False
|
||||||
|
# if no policy for scope, allow action, underlying API will
|
||||||
|
# ultimately block the action if not permitted, treat as though
|
||||||
|
# allowed
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _user_to_credentials(request, user):
|
||||||
|
if not hasattr(user, "_credentials"):
|
||||||
|
roles = [role['name'] for role in user.roles]
|
||||||
|
user._credentials = {'user_id': user.id,
|
||||||
|
'token': user.token,
|
||||||
|
'username': user.username,
|
||||||
|
'project_id': user.project_id,
|
||||||
|
'project_name': user.project_name,
|
||||||
|
'domain_id': user.user_domain_id,
|
||||||
|
'is_admin': user.is_superuser,
|
||||||
|
'roles': roles}
|
||||||
|
return user._credentials
|
@ -177,11 +177,21 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member'
|
|||||||
|
|
||||||
DEFAULT_EXCEPTION_REPORTER_FILTER = 'horizon.exceptions.HorizonReporterFilter'
|
DEFAULT_EXCEPTION_REPORTER_FILTER = 'horizon.exceptions.HorizonReporterFilter'
|
||||||
|
|
||||||
|
POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf")
|
||||||
|
# Map of local copy of service policy files
|
||||||
|
POLICY_FILES = {
|
||||||
|
'identity': 'keystone_policy.json',
|
||||||
|
'compute': 'nova_policy.json'
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from local.local_settings import * # noqa
|
from local.local_settings import * # noqa
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logging.warning("No local_settings file found.")
|
logging.warning("No local_settings file found.")
|
||||||
|
|
||||||
|
from openstack_dashboard import policy
|
||||||
|
POLICY_CHECK_FUNCTION = policy.check
|
||||||
|
|
||||||
# Add HORIZON_CONFIG to the context information for offline compression
|
# Add HORIZON_CONFIG to the context information for offline compression
|
||||||
COMPRESS_OFFLINE_CONTEXT = {
|
COMPRESS_OFFLINE_CONTEXT = {
|
||||||
'STATIC_URL': STATIC_URL,
|
'STATIC_URL': STATIC_URL,
|
||||||
|
@ -120,3 +120,9 @@ NOSE_ARGS = ['--nocapture',
|
|||||||
'--cover-package=openstack_dashboard',
|
'--cover-package=openstack_dashboard',
|
||||||
'--cover-inclusive',
|
'--cover-inclusive',
|
||||||
'--all-modules']
|
'--all-modules']
|
||||||
|
|
||||||
|
POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf")
|
||||||
|
POLICY_FILES = {
|
||||||
|
'identity': 'keystone_policy.json',
|
||||||
|
'compute': 'nova_policy.json'
|
||||||
|
}
|
||||||
|
79
openstack_dashboard/test/tests/policy.py
Normal file
79
openstack_dashboard/test/tests/policy.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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 openstack_dashboard import policy
|
||||||
|
from openstack_dashboard.test import helpers as test
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyTestCase(test.TestCase):
|
||||||
|
def test_policy_file_load(self):
|
||||||
|
policy.reset()
|
||||||
|
enforcer = policy._get_enforcer()
|
||||||
|
self.assertEqual(len(enforcer), 2)
|
||||||
|
self.assertTrue('identity' in enforcer)
|
||||||
|
self.assertTrue('compute' in enforcer)
|
||||||
|
|
||||||
|
def test_policy_reset(self):
|
||||||
|
policy._get_enforcer()
|
||||||
|
self.assertEqual(len(policy._ENFORCER), 2)
|
||||||
|
policy.reset()
|
||||||
|
self.assertEqual(policy._ENFORCER, None)
|
||||||
|
|
||||||
|
def test_check_admin_required_false(self):
|
||||||
|
policy.reset()
|
||||||
|
value = policy.check((("identity", "admin_required"),),
|
||||||
|
request=self.request)
|
||||||
|
self.assertFalse(value)
|
||||||
|
|
||||||
|
def test_check_nova_admin_required_false(self):
|
||||||
|
policy.reset()
|
||||||
|
value = policy.check((("compute", "admin__or_owner"),),
|
||||||
|
request=self.request)
|
||||||
|
self.assertFalse(value)
|
||||||
|
|
||||||
|
def test_compound_check_false(self):
|
||||||
|
policy.reset()
|
||||||
|
value = policy.check((("identity", "admin_required"),
|
||||||
|
("identity", "identity:default"),),
|
||||||
|
request=self.request)
|
||||||
|
self.assertFalse(value)
|
||||||
|
|
||||||
|
def test_scope_not_found(self):
|
||||||
|
policy.reset()
|
||||||
|
value = policy.check((("dummy", "default"),),
|
||||||
|
request=self.request)
|
||||||
|
self.assertTrue(value)
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyTestCaseAdmin(test.BaseAdminViewTests):
|
||||||
|
def test_check_admin_required_true(self):
|
||||||
|
policy.reset()
|
||||||
|
value = policy.check((("identity", "admin_required"),),
|
||||||
|
request=self.request)
|
||||||
|
self.assertTrue(value)
|
||||||
|
|
||||||
|
def test_compound_check_true(self):
|
||||||
|
policy.reset()
|
||||||
|
value = policy.check((("identity", "admin_required"),
|
||||||
|
("identity", "identity:default"),),
|
||||||
|
request=self.request)
|
||||||
|
self.assertTrue(value)
|
||||||
|
|
||||||
|
def test_check_nova_admin_required_true(self):
|
||||||
|
policy.reset()
|
||||||
|
value = policy.check((("compute", "admin__or_owner"),),
|
||||||
|
request=self.request)
|
||||||
|
self.assertTrue(value)
|
Loading…
Reference in New Issue
Block a user