diff --git a/horizon/static/horizon/js/angular/services/hz.api.policy.js b/horizon/static/horizon/js/angular/services/hz.api.policy.js new file mode 100644 index 0000000000..d01f695698 --- /dev/null +++ b/horizon/static/horizon/js/angular/services/hz.api.policy.js @@ -0,0 +1,72 @@ +/* +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. +*/ +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name hz.api.policyAPI + * @description Provides a direct pass through to the policy engine in + * Horizon. + */ + function PolicyService(apiService) { + + /** + * @name hz.api.policyAPI.check + * @description + * Check the passed in policy rule list to determine if the user has + * permission to perform the actions specified by the rules. The service + * APIs will ultimately reject actions that are not permitted. This is used + * for Role Based Access Control in the UI only. The required parameter + * should have the following structure: + * + * { + * "rules": [ + * [ "compute", "compute:get_all" ], + * ], + * "target": { + * "project_id": "1" + * } + * } + * + * where "rules" is a list of rules (1 or greater in length) policy rules + * which are composed of a + * * service name -- maps the policy rule to a service + * * rule -- the policy rule to check + * and "target" key and value is optional. In some cases, policy rules + * require specific details about the object that is to be acted on. + * If added, it is merely a dictionary of keys and values. + * + * + * The following is the response if the check passes: + * { + * "allowed": true + * } + * + * The following is the response if the check fails: + * { + * "allowed": false + * } + */ + this.check = function (policy_rules) { + return apiService.post('/api/policy/', policy_rules) + .error(function() { + horizon.alert('warning', gettext('Policy check failed.')); + }); + }; + } + + angular.module('hz.api') + .service('policyAPI', ['apiService', PolicyService]); +}()); diff --git a/horizon/templates/horizon/_scripts.html b/horizon/templates/horizon/_scripts.html index b50d7ce1e0..232188b443 100644 --- a/horizon/templates/horizon/_scripts.html +++ b/horizon/templates/horizon/_scripts.html @@ -28,6 +28,7 @@ + diff --git a/openstack_dashboard/api/rest/__init__.py b/openstack_dashboard/api/rest/__init__.py index d2b5be7bef..91b0a4c181 100644 --- a/openstack_dashboard/api/rest/__init__.py +++ b/openstack_dashboard/api/rest/__init__.py @@ -28,3 +28,4 @@ import glance #flake8: noqa import keystone #flake8: noqa import network #flake8: noqa import nova #flake8: noqa +import policy #flake8: noqa diff --git a/openstack_dashboard/api/rest/policy.py b/openstack_dashboard/api/rest/policy.py new file mode 100644 index 0000000000..d67de74023 --- /dev/null +++ b/openstack_dashboard/api/rest/policy.py @@ -0,0 +1,51 @@ +# +# 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. + +from django.views import generic + +from openstack_dashboard import policy + +from openstack_dashboard.api.rest import urls +from openstack_dashboard.api.rest import utils as rest_utils + + +@urls.register +class Policy(generic.View): + '''API for interacting with the policy engine.''' + + url_regex = r'policy/$' + + @rest_utils.ajax(data_required=True) + def post(self, request): + '''Check policy rules. + + Check the group of policy rules supplied in the POST + application/json object. The policy target, if specified will also be + passed in to the policy check method as well. + + The action returns an object with one key: "allowed" and the value + is the result of the policy check, True or False. + ''' + + rules = [] + try: + rules_in = request.DATA['rules'] + rules = tuple([tuple(rule) for rule in rules_in]) + except Exception: + raise rest_utils.AjaxError(400, 'unexpected parameter format') + + policy_target = request.DATA.get('target') or {} + + result = policy.check(rules, request, policy_target) + + return {"allowed": result} diff --git a/openstack_dashboard/test/api_tests/policy_rest_tests.py b/openstack_dashboard/test/api_tests/policy_rest_tests.py new file mode 100644 index 0000000000..cce68516eb --- /dev/null +++ b/openstack_dashboard/test/api_tests/policy_rest_tests.py @@ -0,0 +1,78 @@ +# 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. + +from django.test.utils import override_settings # noqa + +from openstack_dashboard.api.rest import policy +from openstack_dashboard import policy_backend +from openstack_dashboard.test import helpers as test + + +class PolicyRestTestCase(test.TestCase): + @override_settings(POLICY_CHECK_FUNCTION=policy_backend.check) + def test_policy(self, body='{"rules": []}'): + request = self.mock_rest_request(body=body) + response = policy.Policy().post(request) + self.assertStatusCode(response, 200) + self.assertEqual(response.content, '{"allowed": true}') + + @override_settings(POLICY_CHECK_FUNCTION=policy_backend.check) + def test_rule_alone(self): + body = '{"rules": [["compute", "compute:get_all" ]]}' + self.test_policy(body) + + @override_settings(POLICY_CHECK_FUNCTION=policy_backend.check) + def test_multiple_rule(self): + body = '{"rules": [["compute", "compute:get_all"],' \ + ' ["compute", "compute:start"]]}' + self.test_policy(body) + + @override_settings(POLICY_CHECK_FUNCTION=policy_backend.check) + def test_rule_with_empty_target(self): + body = '{"rules": [["compute", "compute:get_all"],' \ + ' ["compute", "compute:start"]],' \ + ' "target": {}}' + self.test_policy(body) + + @override_settings(POLICY_CHECK_FUNCTION=policy_backend.check) + def test_rule_with_target(self): + body = '{"rules": [["compute", "compute:get_all"],' \ + ' ["compute", "compute:start"]],' \ + ' "target": {"project_id": "1"}}' + self.test_policy(body) + + @override_settings(POLICY_CHECK_FUNCTION=policy_backend.check) + def test_policy_fail(self): + # admin only rule, default test case user should fail + request = self.mock_rest_request( + body='''{"rules": [["compute", "compute:unlock_override"]]}''') + response = policy.Policy().post(request) + self.assertStatusCode(response, 200) + self.assertEqual(response.content, '{"allowed": false}') + + @override_settings(POLICY_CHECK_FUNCTION=policy_backend.check) + def test_policy_error(self): + # admin only rule, default test case user should fail + request = self.mock_rest_request( + body='''{"bad": "compute"}''') + response = policy.Policy().post(request) + self.assertStatusCode(response, 400) + + +class AdminPolicyRestTestCase(test.BaseAdminViewTests): + @override_settings(POLICY_CHECK_FUNCTION=policy_backend.check) + def test_rule_with_target(self): + body = '{"rules": [["compute", "compute:unlock_override"]]}' + request = self.mock_rest_request(body=body) + response = policy.Policy().post(request) + self.assertStatusCode(response, 200) + self.assertEqual(response.content, '{"allowed": true}')