blob: 4697c3bc4ebab72bcc7d4e35d6b9406ef3023ac1 [file] [log] [blame]
Rick Bartraed950052017-06-29 17:20:33 -04001# Copyright 2017 AT&T Corporation.
2# 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.
Mykola Yakovlievd02a8d82018-10-30 21:35:20 -050015import copy
Rick Bartraed950052017-06-29 17:20:33 -040016import yaml
17
18from oslo_log import log as logging
19
Felipe Monteiro778b7802018-05-31 19:52:58 -040020from tempest import config
Rick Bartraed950052017-06-29 17:20:33 -040021from tempest.lib import exceptions
22
Felipe Monteiro31e308e2018-05-22 12:05:10 -070023from patrole_tempest_plugin.rbac_authority import RbacAuthority
Rick Bartraed950052017-06-29 17:20:33 -040024
Felipe Monteiro778b7802018-05-31 19:52:58 -040025CONF = config.CONF
Rick Bartraed950052017-06-29 17:20:33 -040026LOG = logging.getLogger(__name__)
27
28
29class RequirementsParser(object):
Felipe Monteiro778b7802018-05-31 19:52:58 -040030 """A class that parses a custom requirements file."""
Rick Bartraed950052017-06-29 17:20:33 -040031 _inner = None
32
33 class Inner(object):
34 _rbac_map = None
35
36 def __init__(self, filepath):
37 with open(filepath) as f:
38 RequirementsParser.Inner._rbac_map = \
39 list(yaml.safe_load_all(f))
40
41 def __init__(self, filepath):
42 if RequirementsParser._inner is None:
43 RequirementsParser._inner = RequirementsParser.Inner(filepath)
44
45 @staticmethod
46 def parse(component):
Felipe Monteiro778b7802018-05-31 19:52:58 -040047 """Parses a requirements file with the following format:
48
49 .. code-block:: yaml
50
51 <service_foo>:
52 <api_action_a>:
53 - <allowed_role_1>
Mykola Yakovlievd02a8d82018-10-30 21:35:20 -050054 - <allowed_role_2>,<allowed_role_3>
Felipe Monteiro778b7802018-05-31 19:52:58 -040055 - <allowed_role_3>
56 <api_action_b>:
57 - <allowed_role_2>
58 - <allowed_role_4>
59 <service_bar>:
60 <api_action_c>:
61 - <allowed_role_3>
62
63 :param str component: Name of the OpenStack service to be validated.
64 :returns: The dictionary that maps each policy action to the list
65 of allowed roles, for the given ``component``.
66 :rtype: dict
67 """
Rick Bartraed950052017-06-29 17:20:33 -040068 try:
69 for section in RequirementsParser.Inner._rbac_map:
70 if component in section:
Mykola Yakovlievd02a8d82018-10-30 21:35:20 -050071 rules = copy.copy(section[component])
72
73 for rule in rules:
74 rules[rule] = [
75 roles.split(',') for roles in rules[rule]]
76
77 for i, role_pack in enumerate(rules[rule]):
78 rules[rule][i] = [r.strip() for r in role_pack]
79
80 return rules
Rick Bartraed950052017-06-29 17:20:33 -040081 except yaml.parser.ParserError:
82 LOG.error("Error while parsing the requirements YAML file. Did "
83 "you pass a valid component name from the test case?")
84 return None
85
86
87class RequirementsAuthority(RbacAuthority):
Felipe Monteiro778b7802018-05-31 19:52:58 -040088 """A class that uses a custom requirements file to validate RBAC."""
89
Rick Bartraed950052017-06-29 17:20:33 -040090 def __init__(self, filepath=None, component=None):
Felipe Monteiro778b7802018-05-31 19:52:58 -040091 """This class can be used to achieve a requirements-driven approach to
92 validating an OpenStack cloud's RBAC implementation. Using this
93 approach, Patrole computes expected test results by performing lookups
94 against a custom requirements file which precisely defines the cloud's
95 RBAC requirements.
96
97 :param str filepath: Path where the custom requirements file lives.
98 Defaults to ``[patrole].custom_requirements_file``.
99 :param str component: Name of the OpenStack service to be validated.
100 """
101 filepath = filepath or CONF.patrole.custom_requirements_file
102
103 if component is not None:
Rick Bartraed950052017-06-29 17:20:33 -0400104 self.roles_dict = RequirementsParser(filepath).parse(component)
105 else:
106 self.roles_dict = None
107
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500108 def allowed(self, rule_name, roles):
Felipe Monteiro778b7802018-05-31 19:52:58 -0400109 """Checks if a given rule in a policy is allowed with given role.
110
111 :param string rule_name: Rule to be checked using provided requirements
112 file specified by ``[patrole].custom_requirements_file``. Must be
113 a key present in this file, under the appropriate component.
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500114 :param List[string] roles: Roles to validate against custom
115 requirements file.
Felipe Monteiro778b7802018-05-31 19:52:58 -0400116 :returns: True if ``role`` is allowed to perform ``rule_name``, else
117 False.
118 :rtype: bool
119 :raises KeyError: If ``rule_name`` does not exist among the keyed
120 policy names in the custom requirements file.
121 """
Rick Bartraed950052017-06-29 17:20:33 -0400122 if self.roles_dict is None:
123 raise exceptions.InvalidConfiguration(
124 "Roles dictionary parsed from requirements YAML file is "
125 "empty. Ensure the requirements YAML file is correctly "
126 "formatted.")
127 try:
Mykola Yakovlievd02a8d82018-10-30 21:35:20 -0500128 requirement_roles = self.roles_dict[rule_name]
129
130 for role_reqs in requirement_roles:
131 required_roles = [
132 role for role in role_reqs if not role.startswith("!")]
133 forbidden_roles = [
134 role[1:] for role in role_reqs if role.startswith("!")]
135
136 # User must have all required roles
137 required_passed = all([r in roles for r in required_roles])
138 # User must not have any forbidden roles
139 forbidden_passed = all([r not in forbidden_roles
140 for r in roles])
141
142 if required_passed and forbidden_passed:
143 return True
144
145 return False
Rick Bartraed950052017-06-29 17:20:33 -0400146 except KeyError:
147 raise KeyError("'%s' API is not defined in the requirements YAML "
148 "file" % rule_name)