The ever elusive bug 968696 is still out there, due, in no small part, to the distributed nature of the policy mechanism. One Question I asked myself as I chased this beastie is “how many distinct policy rules do we actually have to implement?” This is an interesting question because, if we can an automated way to answer that question, it can lead to an automated way to transforming the policy rules themselves, and thus getting to a more unified approach to policy.
The set of policy files used in a Tripleo overcloud have around 1400 rules:
$ find /tmp/policy -name \*.json | xargs wc -l 73 /tmp/policy/etc/sahara/policy.json 61 /tmp/policy/etc/glance/policy.json 138 /tmp/policy/etc/cinder/policy.json 42 /tmp/policy/etc/gnocchi/policy.json 20 /tmp/policy/etc/aodh/policy.json 74 /tmp/policy/etc/ironic/policy.json 214 /tmp/policy/etc/neutron/policy.json 257 /tmp/policy/etc/nova/policy.json 198 /tmp/policy/etc/keystone/policy.json 18 /tmp/policy/etc/ceilometer/policy.json 135 /tmp/policy/etc/manila/policy.json 3 /tmp/policy/etc/heat/policy.json 88 /tmp/policy/auth_token_scoped.json 140 /tmp/policy/auth_v3_token_scoped.json 1461 total
Granted, that might not be distinct rule lines, as some are multi-line, but most rules seem to be on a single line. There is some whitespace, too.
Many of the rules, while written differently, can map to the same implementation. For example:
“rule: False”
can reduce to
“False”
which is the same as
“!”
All are instances of oslo_policy.policy._check.FalseCheck.
With that in mind, I gathered up the set of policy files deployed on a Tripleo overcloud and hacked together some analysis.
Note: Nova embeds its policy rules in code now. In order to convert them to an old-style policy file, you need to run a command line tool:
oslopolicy-policy-generator --namespace nova --output-file /tmp/policy/etc/nova/policy.json
Ironic does something similar, but uses
oslopolicy-sample-generator --namespace=ironic.api --output-file=/tmp/policy/etc/ironic/policy.json
I’ve attached my source code at the bottom of this article. Running the code provides the following summary:
55 unique rules found
The longest rule belongs to Ironic:
OR(OR(OR((ROLE:admin)(ROLE:administrator))AND(OR((tenant == demo)(tenant == baremetal))(ROLE:baremetal_admin)))AND(OR((tenant == demo)(tenant == baremetal))OR((ROLE:observer)(ROLE:baremetal_observer))))
Some look somewhat repetitive, such as
OR((ROLE:admin)(is_admin == 1))
And some downright dangerous:
NOT( (ROLE:heat_stack_user)
A there are ways to work around having an explicit role in your token.
Many are indications of places where we want to use implied roles, such as:
- OR((ROLE:admin)(ROLE:administrator))
- OR((ROLE:admin)(ROLE:advsvc)
- (ROLE:admin)
- (ROLE:advsvc)
- (ROLE:service)
This is the set of keys that appear more than one time:
9 context_is_admin 4 admin_api 2 owner 6 admin_or_owner 2 service:index 2 segregation 7 default
Doing a grep for context_is_admin shows all of them with the following rule:
"context_is_admin": "role:admin",
admin_api is roughly the same:
cinder/policy.json: "admin_api": "is_admin:True", ironic/policy.json: "admin_api": "role:admin or role:administrator" nova/policy.json: "admin_api": "is_admin:True" manila/policy.json: "admin_api": "is_admin:True",
I think these here are supposed to include the new check for is_admin_project as well.
Owner is defined two different ways in two files:
neutron/policy.json: "owner": "tenant_id:%(tenant_id)s", keystone/policy.json: "owner": "user_id:%(user_id)s",
Keystone’s meaning is that the user matches, where as neutron is a project scope check. Both rules should change.
Admin or owner has the same variety
cinder/policy.json: "admin_or_owner": "is_admin:True or project_id:%(project_id)s", aodh/policy.json: "admin_or_owner": "rule:context_is_admin or project_id:%(project_id)s", neutron/policy.json: "admin_or_owner": "rule:context_is_admin or rule:owner", nova/policy.json: "admin_or_owner": "is_admin:True or project_id:%(project_id)s" keystone/policy.json: "admin_or_owner": "rule:admin_required or rule:owner", manila/policy.json: "admin_or_owner": "is_admin:True or project_id:%(project_id)s",
Keystone is the odd one out here, with owner again meaning “user matches.”
Segregation is another rules that means admin:
aodh/policy.json: "segregation": "rule:context_is_admin", ceilometer/policy.json: "segregation": "rule:context_is_admin",
Probably the trickiest one to deal with is default, as that is a magic term that is used when a rule is not defined:
sahara/policy.json: "default": "", glance/policy.json: "default": "role:admin", cinder/policy.json: "default": "rule:admin_or_owner", aodh/policy.json: "default": "rule:admin_or_owner", neutron/policy.json: "default": "rule:admin_or_owner", keystone/policy.json: "default": "rule:admin_required", manila/policy.json: "default": "rule:admin_or_owner",
There seem to be three catch all approaches:
- require admin,
- look for a project match but let admin override
- let anyone execute the API.
This is the only rule that cannot be made globally unique across all the files.
Here is the complete list of suffixes. The format is not strict policy format; I munged it to look for duplicates.
(ROLE:admin) (ROLE:advsvc) (ROLE:service) (field == address_scopes:shared=True) (field == networks:router:external=True) (field == networks:shared=True) (field == port:device_owner=~^network:) (field == subnetpools:shared=True) (group == nobody) (is_admin == False) (is_admin == True) (is_public_api == True) (project_id == %(project_id)s) (project_id == %(resource.project_id)s) (tenant_id == %(tenant_id)s) (user_id == %(target.token.user_id)s) (user_id == %(trust.trustor_user_id)s) (user_id == %(user_id)s) AND(OR((tenant == demo)(tenant == baremetal))OR((ROLE:observer)(ROLE:baremetal_observer))) AND(OR(NOT( (field == rbac_policy:target_tenant=*) (ROLE:admin))OR((ROLE:admin)(tenant_id == %(tenant_id)s))) FALSE NOT( (ROLE:heat_stack_user) OR((ROLE:admin)(ROLE:administrator)) OR((ROLE:admin)(ROLE:advsvc)) OR((ROLE:admin)(is_admin == 1)) OR((ROLE:admin)(project_id == %(created_by_project_id)s)) OR((ROLE:admin)(project_id == %(project_id)s)) OR((ROLE:admin)(tenant_id == %(network:tenant_id)s)) OR((ROLE:admin)(tenant_id == %(tenant_id)s)) OR((ROLE:advsvc)OR((ROLE:admin)(tenant_id == %(network:tenant_id)s))) OR((ROLE:advsvc)OR((tenant_id == %(tenant_id)s)OR((ROLE:admin)(tenant_id == %(network:tenant_id)s)))) OR((is_admin == True)(project_id == %(project_id)s)) OR((is_admin == True)(quota_class == %(quota_class)s)) OR((is_admin == True)(user_id == %(user_id)s)) OR((tenant == demo)(tenant == baremetal)) OR((tenant_id == %(tenant_id)s)OR((ROLE:admin)(tenant_id == %(network:tenant_id)s))) OR(NOT( (field == port:device_owner=~^network:) (ROLE:advsvc)OR((ROLE:admin)(tenant_id == %(network:tenant_id)s))) OR(NOT( (field == rbac_policy:target_tenant=*) (ROLE:admin)) OR(OR((ROLE:admin)(ROLE:administrator))AND(OR((tenant == demo)(tenant == baremetal))(ROLE:baremetal_admin))) OR(OR((ROLE:admin)(is_admin == 1))(ROLE:service)) OR(OR((ROLE:admin)(is_admin == 1))(project_id == %(target.project.id)s)) OR(OR((ROLE:admin)(is_admin == 1))(token.project.domain.id == %(target.domain.id)s)) OR(OR((ROLE:admin)(is_admin == 1))(user_id == %(target.token.user_id)s)) OR(OR((ROLE:admin)(is_admin == 1))(user_id == %(user_id)s)) OR(OR((ROLE:admin)(is_admin == 1))AND((user_id == %(user_id)s)(user_id == %(target.credential.user_id)s))) OR(OR((ROLE:admin)(project_id == %(created_by_project_id)s))(project_id == %(project_id)s)) OR(OR((ROLE:admin)(project_id == %(created_by_project_id)s))(project_id == %(resource.project_id)s)) OR(OR((ROLE:admin)(tenant_id == %(tenant_id)s))(ROLE:advsvc)) OR(OR((ROLE:admin)(tenant_id == %(tenant_id)s))(field == address_scopes:shared=True)) OR(OR((ROLE:admin)(tenant_id == %(tenant_id)s))(field == networks:shared=True)(field == networks:router:external=True)(ROLE:advsvc)) OR(OR((ROLE:admin)(tenant_id == %(tenant_id)s))(field == networks:shared=True)) OR(OR((ROLE:admin)(tenant_id == %(tenant_id)s))(field == subnetpools:shared=True)) OR(OR(OR((ROLE:admin)(ROLE:administrator))AND(OR((tenant == demo)(tenant == baremetal))(ROLE:baremetal_admin)))AND(OR((tenant == demo)(tenant == baremetal))OR((ROLE:observer)(ROLE:baremetal_observer)))) OR(OR(OR((ROLE:admin)(is_admin == 1))(ROLE:service))(user_id == %(target.token.user_id)s))
Here is the source code I used to analyze the policy files:
#!/usr/bin/env python # 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 os import sys from oslo_serialization import jsonutils from oslo_policy import policy import oslo_policy._checks as _checks def display_suffix(rules, rule): if isinstance (rule, _checks.RuleCheck): return display_suffix(rules, rules[rule.match.__str__()]) if isinstance (rule, _checks.OrCheck): answer = 'OR(' for subrule in rule.rules: answer += display_suffix(rules, subrule) answer += ')' elif isinstance (rule, _checks.AndCheck): answer = 'AND(' for subrule in rule.rules: answer += display_suffix(rules, subrule) answer += ')' elif isinstance (rule, _checks.TrueCheck): answer = "TRUE" elif isinstance (rule, _checks.FalseCheck): answer = "FALSE" elif isinstance (rule, _checks.RoleCheck): answer = ("(ROLE:%s)" % rule.match) elif isinstance (rule, _checks.GenericCheck): answer = ("(%s == %s)" % (rule.kind, rule.match)) elif isinstance (rule, _checks.NotCheck): answer = 'NOT( %s ' % display_suffix(rules, rule.rule) else: answer = (rule) return answer class Tool(): def __init__(self): self.prefixes = dict() self.suffixes = dict() def add(self, policy_file): policy_data = policy_file.read() rules = policy.Rules.load(policy_data, "default") suffixes = [] for key, rule in rules.items(): suffix = display_suffix(rules, rule) self.prefixes[key] = self.prefixes.get(key, 0) + 1 self.suffixes[suffix] = self.suffixes.get(suffix, 0) + 1 def report(self): suffixes = sorted(self.suffixes.keys()) for suffix in suffixes: print (suffix) print ("%d unique rules found" % len(suffixes)) for prefix, count in self.prefixes.items(): if count > 1: print ("%d %s" % (count, prefix)) def main(argv=sys.argv[1:]): tool = Tool() policy_dir = "/tmp/policy" name = 'policy.json' suffixes = [] for root, dirs, files in os.walk(policy_dir): if name in files: policy_file_path = os.path.join(root, name) print (policy_file_path) policy_file = open(policy_file_path, 'r') tool.add(policy_file) tool.report() if __name__ == "__main__": sys.exit(main(sys.argv[1:]))