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}')