blob: 9926613e8b414717a4b2a65c4758ddc87d4bfa7d [file] [log] [blame]
DavidPurcellb25f93d2017-01-27 12:46:27 -05001# Copyright 2017 AT&T Corporation.
DavidPurcell029d8c32017-01-06 15:27:41 -05002# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
Felipe Monteirob0595652017-01-23 16:51:58 -050016import copy
DavidPurcell029d8c32017-01-06 15:27:41 -050017import os
18
DavidPurcell029d8c32017-01-06 15:27:41 -050019from oslo_log import log as logging
Felipe Monteiro9c978502017-01-27 17:07:54 -050020from oslo_policy import generator
DavidPurcell029d8c32017-01-06 15:27:41 -050021from oslo_policy import policy
DavidPurcell029d8c32017-01-06 15:27:41 -050022
Rick Bartra503c5572017-03-09 13:49:58 -050023from tempest.common import credentials_factory as credentials
24
Felipe Monteirob0595652017-01-23 16:51:58 -050025from patrole_tempest_plugin import rbac_exceptions
DavidPurcell029d8c32017-01-06 15:27:41 -050026
DavidPurcell029d8c32017-01-06 15:27:41 -050027LOG = logging.getLogger(__name__)
28
DavidPurcell029d8c32017-01-06 15:27:41 -050029
Felipe Monteiro322c5b62017-02-26 02:44:21 +000030class RbacPolicyParser(object):
DavidPurcell029d8c32017-01-06 15:27:41 -050031 """A class for parsing policy rules into lists of allowed roles.
32
33 RBAC testing requires that each rule in a policy file be broken up into
34 the roles that constitute it. This class automates that process.
Felipe Monteirob0595652017-01-23 16:51:58 -050035
36 The list of roles per rule can be reverse-engineered by checking, for
37 each role, whether a given rule is allowed using oslo policy.
DavidPurcell029d8c32017-01-06 15:27:41 -050038 """
39
Felipe Monteiro889264e2017-03-01 17:19:35 -050040 def __init__(self, tenant_id, user_id, service=None, path=None):
Felipe Monteiro322c5b62017-02-26 02:44:21 +000041 """Initialization of Rbac Policy Parser.
DavidPurcell029d8c32017-01-06 15:27:41 -050042
Felipe Monteiro9c978502017-01-27 17:07:54 -050043 Parses a policy file to create a dictionary, mapping policy actions to
44 roles. If a policy file does not exist, checks whether the policy file
45 is registered as a namespace under oslo.policy.policies. Nova, for
46 example, doesn't use a policy.json file by default; its policy is
47 implemented in code and registered as 'nova' under
48 oslo.policy.policies.
49
50 If the policy file is not found in either place, raises an exception.
51
52 Additionally, if the policy file exists in both code and as a
53 policy.json (for example, by creating a custom nova policy.json file),
54 the custom policy file over the default policy implementation is
55 prioritized.
Felipe Monteirob0595652017-01-23 16:51:58 -050056
57 :param tenant_id: type uuid
Felipe Monteiro889264e2017-03-01 17:19:35 -050058 :param user_id: type uuid
DavidPurcell029d8c32017-01-06 15:27:41 -050059 :param service: type string
60 :param path: type string
61 """
Rick Bartra503c5572017-03-09 13:49:58 -050062 # First check if the service is valid
63 service = service.lower().strip() if service else None
64 self.admin_mgr = credentials.AdminManager()
65 services = self.admin_mgr.identity_services_v3_client.\
66 list_services()['services']
67 service_names = [s['name'] for s in services]
68 if not service or not any(service in name for name in service_names):
69 LOG.debug(str(service) + " is NOT a valid service.")
70 raise rbac_exceptions.RbacInvalidService
71
72 # Use default path if no path provided
DavidPurcell029d8c32017-01-06 15:27:41 -050073 if path is None:
Felipe Monteiro889264e2017-03-01 17:19:35 -050074 self.path = os.path.join('/etc', service, 'policy.json')
Felipe Monteirob0595652017-01-23 16:51:58 -050075 else:
76 self.path = path
DavidPurcell029d8c32017-01-06 15:27:41 -050077
Felipe Monteiro9c978502017-01-27 17:07:54 -050078 policy_data = "{}"
DavidPurcell029d8c32017-01-06 15:27:41 -050079
Rick Bartra503c5572017-03-09 13:49:58 -050080 # Check whether policy file exists.
Felipe Monteiro9c978502017-01-27 17:07:54 -050081 if os.path.isfile(self.path):
82 policy_data = open(self.path, 'r').read()
83 # Otherwise use oslo_policy to fetch the rules for provided service.
84 else:
85 policy_generator = generator._get_policies_dict([service])
86 if policy_generator and service in policy_generator:
87 policy_data = "{\n"
88 for r in policy_generator[service]:
89 policy_data = policy_data + r.__str__() + ",\n"
90 policy_data = policy_data[:-2] + "\n}"
91 # Otherwise raise an exception.
92 else:
93 raise rbac_exceptions.RbacResourceSetupFailed(
94 'Policy file for service: {0}, {1} not found.'
95 .format(service, self.path))
96
97 self.rules = policy.Rules.load(policy_data, "default")
Felipe Monteirob0595652017-01-23 16:51:58 -050098 self.tenant_id = tenant_id
Felipe Monteiro889264e2017-03-01 17:19:35 -050099 self.user_id = user_id
DavidPurcell029d8c32017-01-06 15:27:41 -0500100
Felipe Monteirob0595652017-01-23 16:51:58 -0500101 def allowed(self, rule_name, role):
Felipe Monteiro9c978502017-01-27 17:07:54 -0500102 is_admin_context = self._is_admin_context(role)
Felipe Monteirob0595652017-01-23 16:51:58 -0500103 is_allowed = self._allowed(
Felipe Monteiro9c978502017-01-27 17:07:54 -0500104 access=self._get_access_token(role),
Felipe Monteirob0595652017-01-23 16:51:58 -0500105 apply_rule=rule_name,
Felipe Monteiro9c978502017-01-27 17:07:54 -0500106 is_admin=is_admin_context)
DavidPurcell029d8c32017-01-06 15:27:41 -0500107
Felipe Monteiro9c978502017-01-27 17:07:54 -0500108 return is_allowed
DavidPurcell029d8c32017-01-06 15:27:41 -0500109
Felipe Monteiro9c978502017-01-27 17:07:54 -0500110 def _is_admin_context(self, role):
111 """Checks whether a role has admin context.
112
113 If context_is_admin is contained in the policy file, then checks
114 whether the given role is contained in context_is_admin. If it is not
115 in the policy file, then default to context_is_admin: admin.
116 """
117 if 'context_is_admin' in self.rules.keys():
118 return self._allowed(
119 access=self._get_access_token(role),
120 apply_rule='context_is_admin')
121 return role == 'admin'
DavidPurcell029d8c32017-01-06 15:27:41 -0500122
Felipe Monteirob0595652017-01-23 16:51:58 -0500123 def _get_access_token(self, role):
124 access_token = {
125 "token": {
126 "roles": [
127 {
128 "name": role
129 }
130 ],
Felipe Monteiro9fc782e2017-02-01 15:38:46 -0500131 "project_id": self.tenant_id,
Felipe Monteiro889264e2017-03-01 17:19:35 -0500132 "tenant_id": self.tenant_id,
133 "user_id": self.user_id
Felipe Monteirob0595652017-01-23 16:51:58 -0500134 }
135 }
136 return access_token
DavidPurcell029d8c32017-01-06 15:27:41 -0500137
Felipe Monteiro9c978502017-01-27 17:07:54 -0500138 def _allowed(self, access, apply_rule, is_admin=False):
Felipe Monteirob0595652017-01-23 16:51:58 -0500139 """Checks if a given rule in a policy is allowed with given access.
DavidPurcell029d8c32017-01-06 15:27:41 -0500140
Felipe Monteirob0595652017-01-23 16:51:58 -0500141 Adapted from oslo_policy.shell.
DavidPurcell029d8c32017-01-06 15:27:41 -0500142
Felipe Monteirob0595652017-01-23 16:51:58 -0500143 :param access: type dict: dictionary from ``_get_access_token``
144 :param apply_rule: type string: rule to be checked
145 :param is_admin: type bool: whether admin context is used
DavidPurcell029d8c32017-01-06 15:27:41 -0500146 """
Felipe Monteirob0595652017-01-23 16:51:58 -0500147 access_data = copy.copy(access['token'])
148 access_data['roles'] = [role['name'] for role in access_data['roles']]
Felipe Monteirob0595652017-01-23 16:51:58 -0500149 access_data['is_admin'] = is_admin
Felipe Monteiro9c978502017-01-27 17:07:54 -0500150 # TODO(felipemonteiro): Dynamically calculate is_admin_project rather
151 # than hard-coding it to True. is_admin_project cannot be determined
152 # from the role, but rather from project and domain names. See
153 # _populate_is_admin_project in keystone.token.providers.common
154 # for more information.
155 access_data['is_admin_project'] = True
DavidPurcell029d8c32017-01-06 15:27:41 -0500156
Felipe Monteirob0595652017-01-23 16:51:58 -0500157 class Object(object):
158 pass
159 o = Object()
Felipe Monteiro9c978502017-01-27 17:07:54 -0500160 o.rules = self.rules
DavidPurcell029d8c32017-01-06 15:27:41 -0500161
Felipe Monteiro9fc782e2017-02-01 15:38:46 -0500162 target = {"project_id": access_data['project_id'],
163 "tenant_id": access_data['project_id'],
Felipe Monteiro889264e2017-03-01 17:19:35 -0500164 "network:tenant_id": access_data['project_id'],
165 "user_id": access_data['user_id']}
Felipe Monteirob0595652017-01-23 16:51:58 -0500166
Felipe Monteiro9c978502017-01-27 17:07:54 -0500167 result = self._try_rule(apply_rule, target, access_data, o)
Felipe Monteirob0595652017-01-23 16:51:58 -0500168 return result
169
Felipe Monteiro9c978502017-01-27 17:07:54 -0500170 def _try_rule(self, apply_rule, target, access_data, o):
Felipe Monteirob0595652017-01-23 16:51:58 -0500171 try:
Felipe Monteiro9c978502017-01-27 17:07:54 -0500172 rule = self.rules[apply_rule]
Felipe Monteirob0595652017-01-23 16:51:58 -0500173 return rule(target, access_data, o)
Felipe Monteiro9c978502017-01-27 17:07:54 -0500174 except KeyError as e:
175 LOG.debug("{0} not found in policy file.".format(apply_rule))
176 return False
Felipe Monteirob0595652017-01-23 16:51:58 -0500177 except Exception as e:
Felipe Monteiro322c5b62017-02-26 02:44:21 +0000178 LOG.debug("Exception: {0} for rule: {1}.".format(e, apply_rule))
Felipe Monteirob0595652017-01-23 16:51:58 -0500179 return False