5c17e4c7ef
Instead of a default policy.json file, policy defaults are now defined in code. An operator need not supply policy.json data except to the extent they want to override the defaults. Currently an empty policy.json is still shipped because it is expected by devstack, but this can be removed later. A sample policy.yaml file can be generated using the genpolicy tox environment. This partly fulfils the requirements of the policy in code goal[1]. However, because policies don't map 1:1 with APIs, it will not be possible to fully document the policies until changes are made in how policies are applied as proposed in https://review.opendev.org/528021 Due to the fact that existing policy files may rely on a rule named "default" to specifiy policies not explicitly listed in the policy.json file, all policies that are not admin-only by default now default to "rule:default", so that the "default" rule will continue to apply to those policies that are not listed in policy.json. To ensure that this yields the expected policy in a standard policy-in-code config file, the default value of the "default" rule is now the empty string "". This is a change; between the Queens release and now the default was set to "role:admin" to match the value specified in the default policy.json file. An installation relying on both the "default" rule for some policies and the default value of the default rule may end up with a more permissive policy after upgrading. It's likely that no such policies exist in the wild, because prior to the Queens release the default value for the "default" rule was "@" (allow all requests), so anybody relying on this rule will surely have specified it explicitly in their policy.json. Policies whose default is "role:admin" no longer use the "default" rule. Therefore existing policy.json files that rely on the "default" rule for those policies, and who have specified a value for the "default" rule that is more permissive, will result in a more restrictive policy after upgrading. It is unlikely that any of these policies exist in the wild either. [1] https://governance.openstack.org/tc/goals/selected/queens/policy-in-code.html Change-Id: I8d1ccf5844078cc0b1652fb1130794daf07cedbc
250 lines
9.4 KiB
Python
250 lines
9.4 KiB
Python
# Copyright 2013 Rackspace
|
|
#
|
|
# 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 re
|
|
import sys
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_policy import policy
|
|
from six.moves import configparser
|
|
|
|
import glance.api.policy
|
|
from glance.common import exception
|
|
from glance.i18n import _, _LE, _LW
|
|
|
|
# SafeConfigParser was deprecated in Python 3.2
|
|
if sys.version_info >= (3, 2):
|
|
CONFIG = configparser.ConfigParser()
|
|
else:
|
|
CONFIG = configparser.SafeConfigParser()
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
property_opts = [
|
|
cfg.StrOpt('property_protection_file',
|
|
help=_("""
|
|
The location of the property protection file.
|
|
|
|
Provide a valid path to the property protection file which contains
|
|
the rules for property protections and the roles/policies associated
|
|
with them.
|
|
|
|
A property protection file, when set, restricts the Glance image
|
|
properties to be created, read, updated and/or deleted by a specific
|
|
set of users that are identified by either roles or policies.
|
|
If this configuration option is not set, by default, property
|
|
protections won't be enforced. If a value is specified and the file
|
|
is not found, the glance-api service will fail to start.
|
|
More information on property protections can be found at:
|
|
https://docs.openstack.org/glance/latest/admin/property-protections.html
|
|
|
|
Possible values:
|
|
* Empty string
|
|
* Valid path to the property protection configuration file
|
|
|
|
Related options:
|
|
* property_protection_rule_format
|
|
|
|
""")),
|
|
cfg.StrOpt('property_protection_rule_format',
|
|
default='roles',
|
|
choices=('roles', 'policies'),
|
|
help=_("""
|
|
Rule format for property protection.
|
|
|
|
Provide the desired way to set property protection on Glance
|
|
image properties. The two permissible values are ``roles``
|
|
and ``policies``. The default value is ``roles``.
|
|
|
|
If the value is ``roles``, the property protection file must
|
|
contain a comma separated list of user roles indicating
|
|
permissions for each of the CRUD operations on each property
|
|
being protected. If set to ``policies``, a policy defined in
|
|
policy.json is used to express property protections for each
|
|
of the CRUD operations. Examples of how property protections
|
|
are enforced based on ``roles`` or ``policies`` can be found at:
|
|
https://docs.openstack.org/glance/latest/admin/property-protections.html#examples
|
|
|
|
Possible values:
|
|
* roles
|
|
* policies
|
|
|
|
Related options:
|
|
* property_protection_file
|
|
|
|
""")),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(property_opts)
|
|
|
|
# NOTE (spredzy): Due to the particularly lengthy name of the exception
|
|
# and the number of occurrence it is raise in this file, a variable is
|
|
# created
|
|
InvalidPropProtectConf = exception.InvalidPropertyProtectionConfiguration
|
|
|
|
|
|
def is_property_protection_enabled():
|
|
if CONF.property_protection_file:
|
|
return True
|
|
return False
|
|
|
|
|
|
class PropertyRules(object):
|
|
|
|
def __init__(self, policy_enforcer=None):
|
|
self.rules = []
|
|
self.prop_exp_mapping = {}
|
|
self.policies = []
|
|
self.policy_enforcer = policy_enforcer or glance.api.policy.Enforcer()
|
|
self.prop_prot_rule_format = CONF.property_protection_rule_format
|
|
self.prop_prot_rule_format = self.prop_prot_rule_format.lower()
|
|
self._load_rules()
|
|
|
|
def _load_rules(self):
|
|
try:
|
|
conf_file = CONF.find_file(CONF.property_protection_file)
|
|
CONFIG.read(conf_file)
|
|
except Exception as e:
|
|
msg = (_LE("Couldn't find property protection file %(file)s: "
|
|
"%(error)s.") % {'file': CONF.property_protection_file,
|
|
'error': e})
|
|
LOG.error(msg)
|
|
raise InvalidPropProtectConf()
|
|
|
|
if self.prop_prot_rule_format not in ['policies', 'roles']:
|
|
msg = _LE("Invalid value '%s' for "
|
|
"'property_protection_rule_format'. "
|
|
"The permitted values are "
|
|
"'roles' and 'policies'") % self.prop_prot_rule_format
|
|
LOG.error(msg)
|
|
raise InvalidPropProtectConf()
|
|
|
|
operations = ['create', 'read', 'update', 'delete']
|
|
properties = CONFIG.sections()
|
|
for property_exp in properties:
|
|
property_dict = {}
|
|
compiled_rule = self._compile_rule(property_exp)
|
|
|
|
for operation in operations:
|
|
permissions = CONFIG.get(property_exp, operation)
|
|
if permissions:
|
|
if self.prop_prot_rule_format == 'policies':
|
|
if ',' in permissions:
|
|
LOG.error(
|
|
_LE("Multiple policies '%s' not allowed "
|
|
"for a given operation. Policies can be "
|
|
"combined in the policy file"),
|
|
permissions)
|
|
raise InvalidPropProtectConf()
|
|
self.prop_exp_mapping[compiled_rule] = property_exp
|
|
self._add_policy_rules(property_exp, operation,
|
|
permissions)
|
|
permissions = [permissions]
|
|
else:
|
|
permissions = [permission.strip() for permission in
|
|
permissions.split(',')]
|
|
if '@' in permissions and '!' in permissions:
|
|
msg = (_LE(
|
|
"Malformed property protection rule in "
|
|
"[%(prop)s] %(op)s=%(perm)s: '@' and '!' "
|
|
"are mutually exclusive") %
|
|
dict(prop=property_exp,
|
|
op=operation,
|
|
perm=permissions))
|
|
LOG.error(msg)
|
|
raise InvalidPropProtectConf()
|
|
property_dict[operation] = permissions
|
|
else:
|
|
property_dict[operation] = []
|
|
LOG.warn(
|
|
_LW('Property protection on operation %(operation)s'
|
|
' for rule %(rule)s is not found. No role will be'
|
|
' allowed to perform this operation.') %
|
|
{'operation': operation,
|
|
'rule': property_exp})
|
|
|
|
self.rules.append((compiled_rule, property_dict))
|
|
|
|
def _compile_rule(self, rule):
|
|
try:
|
|
return re.compile(rule)
|
|
except Exception as e:
|
|
msg = (_LE("Encountered a malformed property protection rule"
|
|
" %(rule)s: %(error)s.") % {'rule': rule,
|
|
'error': e})
|
|
LOG.error(msg)
|
|
raise InvalidPropProtectConf()
|
|
|
|
def _add_policy_rules(self, property_exp, action, rule):
|
|
"""Add policy rules to the policy enforcer.
|
|
|
|
For example, if the file listed as property_protection_file has:
|
|
[prop_a]
|
|
create = glance_creator
|
|
then the corresponding policy rule would be:
|
|
"prop_a:create": "rule:glance_creator"
|
|
where glance_creator is defined in policy.json. For example:
|
|
"glance_creator": "role:admin or role:glance_create_user"
|
|
"""
|
|
rule = "rule:%s" % rule
|
|
rule_name = "%s:%s" % (property_exp, action)
|
|
rule_dict = policy.Rules.from_dict({
|
|
rule_name: rule
|
|
})
|
|
self.policy_enforcer.add_rules(rule_dict)
|
|
|
|
def _check_policy(self, property_exp, action, context):
|
|
try:
|
|
action = ":".join([property_exp, action])
|
|
self.policy_enforcer.enforce(context, action, {}, registered=False)
|
|
except exception.Forbidden:
|
|
return False
|
|
return True
|
|
|
|
def check_property_rules(self, property_name, action, context):
|
|
roles = context.roles
|
|
|
|
# Include service roles to check if an action can be
|
|
# performed on the property or not
|
|
if context.service_roles:
|
|
roles.extend(context.service_roles)
|
|
if not self.rules:
|
|
return True
|
|
|
|
if action not in ['create', 'read', 'update', 'delete']:
|
|
return False
|
|
|
|
for rule_exp, rule in self.rules:
|
|
if rule_exp.search(str(property_name)):
|
|
break
|
|
else: # no matching rules
|
|
return False
|
|
|
|
rule_roles = rule.get(action)
|
|
if rule_roles:
|
|
if '!' in rule_roles:
|
|
return False
|
|
elif '@' in rule_roles:
|
|
return True
|
|
if self.prop_prot_rule_format == 'policies':
|
|
prop_exp_key = self.prop_exp_mapping[rule_exp]
|
|
return self._check_policy(prop_exp_key, action,
|
|
context)
|
|
if set(roles).intersection(set([role.lower() for role
|
|
in rule_roles])):
|
|
return True
|
|
return False
|