Felipe Monteiro | 42f7e1c | 2018-06-10 20:09:26 -0400 | [diff] [blame] | 1 | .. _multi-policy-validation: |
| 2 | |
| 3 | ======================= |
| 4 | Multi-policy Validation |
| 5 | ======================= |
| 6 | |
| 7 | Introduction |
| 8 | ------------ |
| 9 | |
| 10 | Multi-policy validation exists in Patrole because if one policy were assumed, |
| 11 | then tests could fail because they would not consider all the policies actually |
| 12 | being enforced. The reasoning can be found in `this spec`_. Basically, |
| 13 | since Patrole derives the expected test result dynamically in order to test any |
| 14 | role, each policy enforced by the API under test must be considered to derive |
| 15 | an accurate expected test result, or else the expected and actual test |
| 16 | results will not always match, resulting in overall test failure. For more |
| 17 | information about Patrole's RBAC validation work flow, reference |
| 18 | :ref:`rbac-validation`. |
| 19 | |
| 20 | Multi-policy support allows Patrole to more accurately offer RBAC tests for API |
| 21 | endpoints that enforce multiple policy actions. |
| 22 | |
Luigi Toscano | 6da06ed | 2019-01-07 17:50:41 +0100 | [diff] [blame] | 23 | .. _this spec: http://specs.openstack.org/openstack/qa-specs/specs/patrole/rbac-testing-multiple-policies.html |
Felipe Monteiro | 42f7e1c | 2018-06-10 20:09:26 -0400 | [diff] [blame] | 24 | |
| 25 | Scope |
| 26 | ----- |
| 27 | |
| 28 | Multiple policies should be applied only to tests that require them. Not all |
| 29 | API endpoints enforce multiple policies. Some services consistently enforce |
| 30 | 1 policy per API, while on the other side of the spectrum, services like |
| 31 | Neutron have much more involved policy enforcement work flows. See |
| 32 | :ref:`neutron-multi-policy-validation` for more information. |
| 33 | |
| 34 | .. _neutron-multi-policy-validation: |
| 35 | |
| 36 | Neutron Multi-policy Validation |
| 37 | ------------------------------- |
| 38 | |
| 39 | Neutron can raise different :ref:`policy-error-codes` following failed policy |
| 40 | authorization. Many endpoints in Neutron enforce multiple policies, which |
| 41 | complicates matters when trying to determine whether the endpoint raises a |
| 42 | 403 or a 404 following unauthorized access. |
| 43 | |
| 44 | Multi-policy Examples |
| 45 | --------------------- |
| 46 | |
| 47 | General Examples |
| 48 | ^^^^^^^^^^^^^^^^ |
| 49 | |
| 50 | Below is an example of multi-policy validation for a carefully chosen Nova API: |
| 51 | |
| 52 | .. code-block:: python |
| 53 | |
| 54 | @rbac_rule_validation.action( |
| 55 | service="nova", |
| 56 | rules=["os_compute_api:os-lock-server:unlock", |
| 57 | "os_compute_api:os-lock-server:unlock:unlock_override"]) |
| 58 | @decorators.idempotent_id('40dfeef9-73ee-48a9-be19-a219875de457') |
| 59 | def test_unlock_server_override(self): |
| 60 | """Test force unlock server, part of os-lock-server. |
| 61 | |
| 62 | In order to trigger the unlock:unlock_override policy instead |
| 63 | of the unlock policy, the server must be locked by a different |
| 64 | user than the one who is attempting to unlock it. |
| 65 | """ |
| 66 | self.os_admin.servers_client.lock_server(self.server['id']) |
| 67 | self.addCleanup(self.servers_client.unlock_server, self.server['id']) |
| 68 | |
Sergey Vilgelm | 78e7f57 | 2019-02-03 10:35:01 -0600 | [diff] [blame] | 69 | with self.override_role(): |
Felipe Monteiro | 42f7e1c | 2018-06-10 20:09:26 -0400 | [diff] [blame] | 70 | self.servers_client.unlock_server(self.server['id']) |
| 71 | |
| 72 | While the ``expected_error_codes`` parameter is omitted in the example above, |
| 73 | Patrole automatically populates it with a 403 for each policy in ``rules``. |
| 74 | Therefore, in the example above, the following expected error codes/rules |
| 75 | relationship is observed: |
| 76 | |
| 77 | * "os_compute_api:os-lock-server:unlock" => 403 |
| 78 | * "os_compute_api:os-lock-server:unlock:unlock_override" => 403 |
| 79 | |
| 80 | Below is an example that uses ``expected_error_codes`` to account for the |
| 81 | fact that Neutron is expected to raise a ``404`` on the first policy that |
| 82 | is enforced server-side ("get_port"). Also, in this example, soft authorization |
| 83 | is performed, meaning that it is necessary to check the response body for an |
| 84 | attribute that is added only following successful policy authorization. |
| 85 | |
| 86 | .. code-block:: python |
| 87 | |
| 88 | @utils.requires_ext(extension='binding', service='network') |
| 89 | @rbac_rule_validation.action(service="neutron", |
| 90 | rules=["get_port", |
| 91 | "get_port:binding:vif_type"], |
| 92 | expected_error_codes=[404, 403]) |
| 93 | @decorators.idempotent_id('125aff0b-8fed-4f8e-8410-338616594b06') |
| 94 | def test_show_port_binding_vif_type(self): |
| 95 | |
| 96 | # Verify specific fields of a port |
| 97 | fields = ['binding:vif_type'] |
| 98 | |
Sergey Vilgelm | 78e7f57 | 2019-02-03 10:35:01 -0600 | [diff] [blame] | 99 | with self.override_role(): |
Felipe Monteiro | 42f7e1c | 2018-06-10 20:09:26 -0400 | [diff] [blame] | 100 | retrieved_port = self.ports_client.show_port( |
| 101 | self.port['id'], fields=fields)['port'] |
| 102 | |
| 103 | # Rather than throwing a 403, the field is not present, so raise exc. |
| 104 | if fields[0] not in retrieved_port: |
| 105 | raise rbac_exceptions.RbacMalformedResponse( |
| 106 | attribute='binding:vif_type') |
| 107 | |
| 108 | Note that in the example above, failure to authorize |
| 109 | "get_port:binding:vif_type" results in the response body getting successfully |
| 110 | returned by the server, but without additional dictionary keys. If Patrole |
| 111 | fails to find those expected keys, it *acts as though* a 403 was thrown (by |
| 112 | raising an exception itself, the ``rbac_rule_validation`` decorator handles |
| 113 | the rest). |
| 114 | |
| 115 | Neutron Examples |
| 116 | ^^^^^^^^^^^^^^^^ |
| 117 | |
| 118 | A basic Neutron example that only expects 403's to be raised: |
| 119 | |
| 120 | .. code-block:: python |
| 121 | |
| 122 | @utils.requires_ext(extension='external-net', service='network') |
| 123 | @rbac_rule_validation.action(service="neutron", |
| 124 | rules=["create_network", |
| 125 | "create_network:router:external"], |
| 126 | expected_error_codes=[403, 403]) |
| 127 | @decorators.idempotent_id('51adf2a7-739c-41e0-8857-3b4c460cbd24') |
| 128 | def test_create_network_router_external(self): |
| 129 | |
| 130 | """Create External Router Network Test |
| 131 | |
| 132 | RBAC test for the neutron create_network:router:external policy |
| 133 | """ |
Sergey Vilgelm | 78e7f57 | 2019-02-03 10:35:01 -0600 | [diff] [blame] | 134 | with self.override_role(): |
Felipe Monteiro | 42f7e1c | 2018-06-10 20:09:26 -0400 | [diff] [blame] | 135 | self._create_network(router_external=True) |
| 136 | |
| 137 | Note that above the following expected error codes/rules relationship is |
| 138 | observed: |
| 139 | |
| 140 | * "create_network" => 403 |
| 141 | * "create_network:router:external" => 403 |
| 142 | |
| 143 | A more involved example that expects a 404 to be raised, should the first |
| 144 | policy under ``rules`` fail authorization, and a 403 to be raised for any |
| 145 | subsequent policy authorization failure: |
| 146 | |
| 147 | .. code-block:: python |
| 148 | |
| 149 | @rbac_rule_validation.action(service="neutron", |
| 150 | rules=["get_network", |
| 151 | "update_network", |
| 152 | "update_network:shared"], |
| 153 | expected_error_codes=[404, 403, 403]) |
| 154 | @decorators.idempotent_id('37ea3e33-47d9-49fc-9bba-1af98fbd46d6') |
| 155 | def test_update_network_shared(self): |
| 156 | |
| 157 | """Update Shared Network Test |
| 158 | |
| 159 | RBAC test for the neutron update_network:shared policy |
| 160 | """ |
Sergey Vilgelm | 78e7f57 | 2019-02-03 10:35:01 -0600 | [diff] [blame] | 161 | with self.override_role(): |
Felipe Monteiro | 42f7e1c | 2018-06-10 20:09:26 -0400 | [diff] [blame] | 162 | self._update_network(shared_network=True) |
| 163 | self.addCleanup(self._update_network, shared_network=False) |
| 164 | |
| 165 | Note that above the following expected error codes/rules relationship is |
| 166 | observed: |
| 167 | |
| 168 | * "get_network" => 404 |
| 169 | * "update_network" => 403 |
| 170 | * "update_network:shared" => 403 |
| 171 | |
| 172 | Limitations |
| 173 | ----------- |
| 174 | |
| 175 | Multi-policy validation in RBAC tests comes with limitations, due to technical |
| 176 | and practical challenges. |
| 177 | |
| 178 | Technically, there are challenges associated with multiple policies across |
| 179 | cross-service API communication in OpenStack, such as between Nova and Cinder |
| 180 | or Nova and Neutron. The current framework does not account for these |
| 181 | cross-service policy enforcement workflows, and it is still up for debate |
| 182 | whether it should. |
| 183 | |
| 184 | Practically, it is not possible to enumerate every policy enforced by every API |
| 185 | in Patrole, as the maintenance overhead would be huge. |
| 186 | |
| 187 | .. _Neutron policy documentation: https://docs.openstack.org/neutron/pike/contributor/internals/policy.html |