blob: 4d6f25be5136df86469e2d4c8121108dceffafb2 [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
Felipe Monteiro0464e812018-11-08 01:08:07 -050021from tempest.lib import exceptions as lib_exc
Rick Bartraed950052017-06-29 17:20:33 -040022
Felipe Monteiro31e308e2018-05-22 12:05:10 -070023from patrole_tempest_plugin.rbac_authority import RbacAuthority
Felipe Monteiro0464e812018-11-08 01:08:07 -050024from patrole_tempest_plugin import rbac_exceptions
Rick Bartraed950052017-06-29 17:20:33 -040025
Felipe Monteiro778b7802018-05-31 19:52:58 -040026CONF = config.CONF
Rick Bartraed950052017-06-29 17:20:33 -040027LOG = logging.getLogger(__name__)
28
29
30class RequirementsParser(object):
Felipe Monteiro778b7802018-05-31 19:52:58 -040031 """A class that parses a custom requirements file."""
Rick Bartraed950052017-06-29 17:20:33 -040032 _inner = None
33
34 class Inner(object):
35 _rbac_map = None
36
37 def __init__(self, filepath):
38 with open(filepath) as f:
39 RequirementsParser.Inner._rbac_map = \
40 list(yaml.safe_load_all(f))
41
42 def __init__(self, filepath):
43 if RequirementsParser._inner is None:
44 RequirementsParser._inner = RequirementsParser.Inner(filepath)
45
46 @staticmethod
47 def parse(component):
Felipe Monteiro778b7802018-05-31 19:52:58 -040048 """Parses a requirements file with the following format:
49
50 .. code-block:: yaml
51
52 <service_foo>:
53 <api_action_a>:
54 - <allowed_role_1>
Mykola Yakovlievd02a8d82018-10-30 21:35:20 -050055 - <allowed_role_2>,<allowed_role_3>
Felipe Monteiro778b7802018-05-31 19:52:58 -040056 - <allowed_role_3>
57 <api_action_b>:
58 - <allowed_role_2>
59 - <allowed_role_4>
60 <service_bar>:
61 <api_action_c>:
62 - <allowed_role_3>
63
64 :param str component: Name of the OpenStack service to be validated.
65 :returns: The dictionary that maps each policy action to the list
66 of allowed roles, for the given ``component``.
67 :rtype: dict
68 """
Rick Bartraed950052017-06-29 17:20:33 -040069 try:
70 for section in RequirementsParser.Inner._rbac_map:
71 if component in section:
Mykola Yakovlievd02a8d82018-10-30 21:35:20 -050072 rules = copy.copy(section[component])
73
74 for rule in rules:
75 rules[rule] = [
76 roles.split(',') for roles in rules[rule]]
77
78 for i, role_pack in enumerate(rules[rule]):
79 rules[rule][i] = [r.strip() for r in role_pack]
80
81 return rules
Rick Bartraed950052017-06-29 17:20:33 -040082 except yaml.parser.ParserError:
83 LOG.error("Error while parsing the requirements YAML file. Did "
84 "you pass a valid component name from the test case?")
Felipe Monteiro0464e812018-11-08 01:08:07 -050085 return {}
Rick Bartraed950052017-06-29 17:20:33 -040086
87
88class RequirementsAuthority(RbacAuthority):
Felipe Monteiro778b7802018-05-31 19:52:58 -040089 """A class that uses a custom requirements file to validate RBAC."""
90
Rick Bartraed950052017-06-29 17:20:33 -040091 def __init__(self, filepath=None, component=None):
Felipe Monteiro778b7802018-05-31 19:52:58 -040092 """This class can be used to achieve a requirements-driven approach to
93 validating an OpenStack cloud's RBAC implementation. Using this
94 approach, Patrole computes expected test results by performing lookups
95 against a custom requirements file which precisely defines the cloud's
96 RBAC requirements.
97
98 :param str filepath: Path where the custom requirements file lives.
99 Defaults to ``[patrole].custom_requirements_file``.
100 :param str component: Name of the OpenStack service to be validated.
101 """
Felipe Monteiro0464e812018-11-08 01:08:07 -0500102 self.filepath = filepath or CONF.patrole.custom_requirements_file
Felipe Monteiro778b7802018-05-31 19:52:58 -0400103 if component is not None:
Felipe Monteiro0464e812018-11-08 01:08:07 -0500104 self.roles_dict = RequirementsParser(self.filepath).parse(
105 component)
Rick Bartraed950052017-06-29 17:20:33 -0400106 else:
107 self.roles_dict = None
108
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500109 def allowed(self, rule_name, roles):
Felipe Monteiro778b7802018-05-31 19:52:58 -0400110 """Checks if a given rule in a policy is allowed with given role.
111
112 :param string rule_name: Rule to be checked using provided requirements
113 file specified by ``[patrole].custom_requirements_file``. Must be
114 a key present in this file, under the appropriate component.
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500115 :param List[string] roles: Roles to validate against custom
116 requirements file.
Felipe Monteiro778b7802018-05-31 19:52:58 -0400117 :returns: True if ``role`` is allowed to perform ``rule_name``, else
118 False.
119 :rtype: bool
Felipe Monteiro0464e812018-11-08 01:08:07 -0500120 :raises RbacParsingException: If ``rule_name`` does not exist among the
121 keyed policy names in the custom requirements file.
Felipe Monteiro778b7802018-05-31 19:52:58 -0400122 """
Felipe Monteiro0464e812018-11-08 01:08:07 -0500123 if not self.roles_dict:
124 raise lib_exc.InvalidConfiguration(
Rick Bartraed950052017-06-29 17:20:33 -0400125 "Roles dictionary parsed from requirements YAML file is "
126 "empty. Ensure the requirements YAML file is correctly "
127 "formatted.")
128 try:
Mykola Yakovlievd02a8d82018-10-30 21:35:20 -0500129 requirement_roles = self.roles_dict[rule_name]
Rick Bartraed950052017-06-29 17:20:33 -0400130 except KeyError:
Felipe Monteiro0464e812018-11-08 01:08:07 -0500131 raise rbac_exceptions.RbacParsingException(
132 "'%s' rule name is not defined in the requirements YAML file: "
133 "%s" % (rule_name, self.filepath))
134
135 for role_reqs in requirement_roles:
136 required_roles = [
137 role for role in role_reqs if not role.startswith("!")]
138 forbidden_roles = [
139 role[1:] for role in role_reqs if role.startswith("!")]
140
141 # User must have all required roles
142 required_passed = all([r in roles for r in required_roles])
143 # User must not have any forbidden roles
144 forbidden_passed = all([r not in forbidden_roles
145 for r in roles])
146
147 if required_passed and forbidden_passed:
148 return True
149
150 return False