Adding RBAC policy system and checks for identity
Adding file based RBAC engine for Horizon using copies of nova and keystone policy.json files Policy engine builds on top of oslo incubator policy.py, fileutils was also pulled from oslo incubator as a dependency of policy.py When Horizon runs and a policy check is made, a path and mapping of services to policy files is used to load the rules into the policy engine. Each check is mapped to a service type and validated. This extra level of mapping is required because the policy.json files may each contain a 'default' rule or unqualified (no service name include) rule. Additionally, maintaining separate policy.json files per service will allow easier syncing with the service projects. The engine allows for compound 'and' checks at this time. E.g., the way the Create User action is written, multiple APIs are called to read data (roles, projects) and more are required to update data (grants, user). Other workflows e.g., Edit Project, should have separate save actions per step as they are unrelated. Only the applicable policy checks to that step were added. The separating unrelated steps saves will should be future work. The underlying engine supports more rule types that are used in the underlying policy.json files. Policy checks were added for all actions on tables in the Identity Panel only. And the service policy files imported are limited in this commit to reduce scope of the change. Additionally, changes were made to the base action class to add support or setting policy rules and an overridable method for determining the policy check target. This reduces the need for redundant code in each action policy check. Note, the benefit Horizon has is that the underlying APIs will correct us if we get it wrong, so if a policy file is not found for a particular service, permission is assumed and the actual API call to the service will fail if the action isn't authorized for that user. Finally, adding documentation regarding policy enforcement. Implements: blueprint rbac Change-Id: I4a4a71163186b973229a0461b165c16936bc10e5
This commit is contained in:
parent
71ddcb0025
commit
5984e34862
@ -73,6 +73,7 @@ the following topic guides.
|
||||
:maxdepth: 1
|
||||
|
||||
topics/tables
|
||||
topics/policy
|
||||
topics/testing
|
||||
|
||||
API Reference
|
||||
@ -94,6 +95,7 @@ In-depth documentation for Horizon and its APIs.
|
||||
ref/decorators
|
||||
ref/exceptions
|
||||
ref/test
|
||||
ref/policy
|
||||
|
||||
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
|
||||
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)
|
||||
=========================
|
||||
|
||||
|
@ -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.
|
||||
associated with loading data into the table for actions which don't require
|
||||
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
|
||||
requires_input = False
|
||||
preempt = False
|
||||
policy_rules = None
|
||||
|
||||
def __init__(self, datum=None):
|
||||
super(BaseAction, self).__init__()
|
||||
@ -63,6 +64,14 @@ class BaseAction(html.HTMLElement):
|
||||
return False
|
||||
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):
|
||||
""" Determine whether this action is allowed for the current request.
|
||||
|
||||
@ -71,6 +80,12 @@ class BaseAction(html.HTMLElement):
|
||||
return True
|
||||
|
||||
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)
|
||||
|
||||
def update(self, request, datum):
|
||||
@ -156,6 +171,22 @@ class Action(BaseAction):
|
||||
Default to be an empty list (``[]``). When set to empty, the action
|
||||
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:
|
||||
|
||||
.. method:: single(self, data_table, request, object_id)
|
||||
|
@ -1,8 +1,10 @@
|
||||
[DEFAULT]
|
||||
module=config
|
||||
module=eventlet_backdoor
|
||||
module=fileutils
|
||||
module=install_venv_common
|
||||
module=notifier
|
||||
module=policy
|
||||
module=rpc
|
||||
module=service
|
||||
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": ""
|
||||
}
|
@ -37,6 +37,7 @@ class CreateDomainLink(tables.LinkAction):
|
||||
verbose_name = _("Create Domain")
|
||||
url = constants.DOMAINS_CREATE_URL
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
policy_rules = (('identity', 'identity:create_domain'),)
|
||||
|
||||
def allowed(self, request, domain):
|
||||
return api.keystone.keystone_can_edit_domain()
|
||||
@ -47,6 +48,7 @@ class EditDomainLink(tables.LinkAction):
|
||||
verbose_name = _("Edit")
|
||||
url = constants.DOMAINS_UPDATE_URL
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
policy_rules = (('identity', 'identity:update_domain'),)
|
||||
|
||||
def allowed(self, request, domain):
|
||||
return api.keystone.keystone_can_edit_domain()
|
||||
@ -56,6 +58,7 @@ class DeleteDomainsAction(tables.DeleteAction):
|
||||
name = "delete"
|
||||
data_type_singular = _("Domain")
|
||||
data_type_plural = _("Domains")
|
||||
policy_rules = (('identity', 'identity:delete_domain'),)
|
||||
|
||||
def allowed(self, request, datum):
|
||||
return api.keystone.keystone_can_edit_domain()
|
||||
@ -96,6 +99,7 @@ class SetDomainContext(tables.Action):
|
||||
verbose_name = _("Set Domain Context")
|
||||
url = constants.DOMAINS_INDEX_URL
|
||||
preempt = True
|
||||
policy_rules = (('identity', 'admin_required'),)
|
||||
|
||||
def allowed(self, request, datum):
|
||||
multidomain_support = getattr(settings,
|
||||
@ -130,6 +134,7 @@ class UnsetDomainContext(tables.Action):
|
||||
url = constants.DOMAINS_INDEX_URL
|
||||
preempt = True
|
||||
requires_input = False
|
||||
policy_rules = (('identity', 'admin_required'),)
|
||||
|
||||
def allowed(self, request, datum):
|
||||
ctx = request.session.get("domain_context", None)
|
||||
|
@ -40,6 +40,7 @@ class CreateGroupLink(tables.LinkAction):
|
||||
verbose_name = _("Create Group")
|
||||
url = constants.GROUPS_CREATE_URL
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
policy_rules = (("identity", "identity:create_group"),)
|
||||
|
||||
def allowed(self, request, group):
|
||||
return api.keystone.keystone_can_edit_group()
|
||||
@ -50,6 +51,7 @@ class EditGroupLink(tables.LinkAction):
|
||||
verbose_name = _("Edit Group")
|
||||
url = constants.GROUPS_UPDATE_URL
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
policy_rules = (("identity", "identity:update_group"),)
|
||||
|
||||
def allowed(self, request, group):
|
||||
return api.keystone.keystone_can_edit_group()
|
||||
@ -59,6 +61,7 @@ class DeleteGroupsAction(tables.DeleteAction):
|
||||
name = "delete"
|
||||
data_type_singular = _("Group")
|
||||
data_type_plural = _("Groups")
|
||||
policy_rules = (("identity", "identity:delete_group"),)
|
||||
|
||||
def allowed(self, request, datum):
|
||||
return api.keystone.keystone_can_edit_group()
|
||||
@ -73,6 +76,8 @@ class ManageUsersLink(tables.LinkAction):
|
||||
verbose_name = _("Modify Users")
|
||||
url = constants.GROUPS_MANAGE_URL
|
||||
classes = ("btn-edit")
|
||||
policy_rules = (("identity", "identity:get_group"),
|
||||
("identity", "identity:list_users"),)
|
||||
|
||||
def allowed(self, request, datum):
|
||||
return api.keystone.keystone_can_edit_group()
|
||||
@ -120,6 +125,7 @@ class RemoveMembers(tables.DeleteAction):
|
||||
action_past = _("Removed")
|
||||
data_type_singular = _("User")
|
||||
data_type_plural = _("Users")
|
||||
policy_rules = (("identity", "identity:remove_user_from_group"),)
|
||||
|
||||
def allowed(self, request, user=None):
|
||||
return api.keystone.keystone_can_edit_group()
|
||||
@ -142,6 +148,8 @@ class AddMembersLink(tables.LinkAction):
|
||||
verbose_name = _("Add...")
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
url = constants.GROUPS_ADD_MEMBER_URL
|
||||
policy_rules = (("identity", "identity:list_users"),
|
||||
("identity", "identity:add_user_to_group"),)
|
||||
|
||||
def allowed(self, request, user=None):
|
||||
return api.keystone.keystone_can_edit_group()
|
||||
@ -177,6 +185,7 @@ class AddMembers(tables.BatchAction):
|
||||
classes = ("btn-create", )
|
||||
requires_input = True
|
||||
success_url = constants.GROUPS_MANAGE_URL
|
||||
policy_rules = (("identity", "identity:add_user_to_group"),)
|
||||
|
||||
def allowed(self, request, user=None):
|
||||
return api.keystone.keystone_can_edit_group()
|
||||
|
@ -9,7 +9,6 @@ from horizon import tables
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import keystone
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -18,6 +17,8 @@ class ViewMembersLink(tables.LinkAction):
|
||||
verbose_name = _("Modify Users")
|
||||
url = "horizon:admin:projects:update"
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
policy_rules = (("identity", "identity:list_users"),
|
||||
("identity", "identity:list_roles"))
|
||||
|
||||
def get_link_url(self, project):
|
||||
step = 'update_members'
|
||||
@ -47,6 +48,7 @@ class UsageLink(tables.LinkAction):
|
||||
verbose_name = _("View Usage")
|
||||
url = "horizon:admin:projects:usage"
|
||||
classes = ("btn-stats",)
|
||||
policy_rules = (("compute", "compute_extension:simple_tenant_usage:show"),)
|
||||
|
||||
|
||||
class CreateProject(tables.LinkAction):
|
||||
@ -54,6 +56,7 @@ class CreateProject(tables.LinkAction):
|
||||
verbose_name = _("Create Project")
|
||||
url = "horizon:admin:projects:create"
|
||||
classes = ("btn-launch", "ajax-modal",)
|
||||
policy_rules = (('identity', 'identity:create_project'),)
|
||||
|
||||
def allowed(self, request, project):
|
||||
return api.keystone.keystone_can_edit_project()
|
||||
@ -64,6 +67,7 @@ class UpdateProject(tables.LinkAction):
|
||||
verbose_name = _("Edit Project")
|
||||
url = "horizon:admin:projects:update"
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
policy_rules = (('identity', 'identity:update_project'),)
|
||||
|
||||
def allowed(self, request, project):
|
||||
return api.keystone.keystone_can_edit_project()
|
||||
@ -74,6 +78,7 @@ class ModifyQuotas(tables.LinkAction):
|
||||
verbose_name = "Modify Quotas"
|
||||
url = "horizon:admin:projects:update"
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
policy_rules = (('compute', "compute_extension:quotas:update"),)
|
||||
|
||||
def get_link_url(self, project):
|
||||
step = 'update_quotas'
|
||||
@ -85,6 +90,7 @@ class ModifyQuotas(tables.LinkAction):
|
||||
class DeleteTenantsAction(tables.DeleteAction):
|
||||
data_type_singular = _("Project")
|
||||
data_type_plural = _("Projects")
|
||||
policy_rules = (("identity", "identity:delete_project"),)
|
||||
|
||||
def allowed(self, request, project):
|
||||
return api.keystone.keystone_can_edit_project()
|
||||
|
@ -31,6 +31,7 @@ class CreateRoleLink(tables.LinkAction):
|
||||
verbose_name = _("Create Role")
|
||||
url = "horizon:admin:roles:create"
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
policy_rules = (("identity", "identity:create_role"),)
|
||||
|
||||
def allowed(self, request, role):
|
||||
return api.keystone.keystone_can_edit_role()
|
||||
@ -41,6 +42,7 @@ class EditRoleLink(tables.LinkAction):
|
||||
verbose_name = _("Edit")
|
||||
url = "horizon:admin:roles:update"
|
||||
classes = ("ajax-modal", "btn-edit")
|
||||
policy_rules = (("identity", "identity:update_role"),)
|
||||
|
||||
def allowed(self, request, role):
|
||||
return api.keystone.keystone_can_edit_role()
|
||||
@ -49,6 +51,7 @@ class EditRoleLink(tables.LinkAction):
|
||||
class DeleteRolesAction(tables.DeleteAction):
|
||||
data_type_singular = _("Role")
|
||||
data_type_plural = _("Roles")
|
||||
policy_rules = (("identity", "identity:delete_role"),)
|
||||
|
||||
def allowed(self, request, role):
|
||||
return api.keystone.keystone_can_edit_role()
|
||||
|
@ -8,7 +8,6 @@ from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
ENABLE = 0
|
||||
@ -20,6 +19,10 @@ class CreateUserLink(tables.LinkAction):
|
||||
verbose_name = _("Create User")
|
||||
url = "horizon:admin:users: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):
|
||||
return api.keystone.keystone_can_edit_user()
|
||||
@ -30,6 +33,11 @@ class EditUserLink(tables.LinkAction):
|
||||
verbose_name = _("Edit")
|
||||
url = "horizon:admin:users:update"
|
||||
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):
|
||||
return api.keystone.keystone_can_edit_user()
|
||||
@ -42,6 +50,12 @@ class ToggleEnabled(tables.BatchAction):
|
||||
data_type_singular = _("User")
|
||||
data_type_plural = _("Users")
|
||||
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):
|
||||
if not api.keystone.keystone_can_edit_user():
|
||||
@ -78,10 +92,11 @@ class ToggleEnabled(tables.BatchAction):
|
||||
class DeleteUsersAction(tables.DeleteAction):
|
||||
data_type_singular = _("User")
|
||||
data_type_plural = _("Users")
|
||||
policy_rules = (("identity", "identity:delete_user"),)
|
||||
|
||||
def allowed(self, request, datum):
|
||||
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 True
|
||||
|
||||
|
@ -194,6 +194,19 @@ TIME_ZONE = "UTC"
|
||||
# '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 = {
|
||||
'version': 1,
|
||||
# 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
|
@ -178,11 +178,21 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member'
|
||||
|
||||
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:
|
||||
from local.local_settings import * # noqa
|
||||
except ImportError:
|
||||
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
|
||||
COMPRESS_OFFLINE_CONTEXT = {
|
||||
'STATIC_URL': STATIC_URL,
|
||||
|
@ -119,3 +119,9 @@ NOSE_ARGS = ['--nocapture',
|
||||
'--cover-package=openstack_dashboard',
|
||||
'--cover-inclusive',
|
||||
'--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