diff --git a/etc/surveil/policy.json b/etc/surveil/policy.json new file mode 100644 index 0000000..0e746fa --- /dev/null +++ b/etc/surveil/policy.json @@ -0,0 +1,4 @@ +{ + "surveil:break":"!", + "surveil:pass":"@" +} diff --git a/requirements.txt b/requirements.txt index f66cd0b..f492fd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,6 @@ requests watchdog oslo.config oslo.middleware +oslo.policy>=0.3.0 keystonemiddleware PasteDeploy diff --git a/surveil/api/controllers/v2/hello.py b/surveil/api/controllers/v2/hello.py index a96c99e..7e3c968 100644 --- a/surveil/api/controllers/v2/hello.py +++ b/surveil/api/controllers/v2/hello.py @@ -15,10 +15,19 @@ import pecan from pecan import rest +from surveil.common import util + class HelloController(rest.RestController): @pecan.expose() + @util.policy_enforce(['pass']) def get(self): """Says hello.""" return "Hello World!" + + @pecan.expose() + @util.policy_enforce(['break']) + def post(self): + """What are you trying to post dude?""" + return "Looks like policies are not working." diff --git a/surveil/api/controllers/v2/v2.py b/surveil/api/controllers/v2/v2.py index e59f493..e66db5a 100644 --- a/surveil/api/controllers/v2/v2.py +++ b/surveil/api/controllers/v2/v2.py @@ -16,6 +16,7 @@ from surveil.api.controllers.v2 import actions as v2_actions from surveil.api.controllers.v2 import admin as v2_admin from surveil.api.controllers.v2 import auth as v2_auth from surveil.api.controllers.v2 import config as v2_config +from surveil.api.controllers.v2 import hello as v2_hello from surveil.api.controllers.v2 import logs as v2_logs from surveil.api.controllers.v2 import status as v2_status @@ -24,6 +25,7 @@ class V2Controller(object): """Version 2 API controller root.""" actions = v2_actions.ActionsController() config = v2_config.ConfigController() + hello = v2_hello.HelloController() status = v2_status.StatusController() surveil = v2_admin.AdminController() auth = v2_auth.AuthController() diff --git a/surveil/api/rbac.py b/surveil/api/rbac.py new file mode 100644 index 0000000..0b8af56 --- /dev/null +++ b/surveil/api/rbac.py @@ -0,0 +1,112 @@ +# +# Copyright 2012 New Dream Network, LLC (DreamHost) +# Copyright 2014 Hewlett-Packard Company +# Copyright 2014 - Savoir-Faire Linux inc. +# +# 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. + +"""Access Control Lists (ACL's) control access the API server.""" + +from oslo_config import cfg +from oslo_policy import policy + +_ENFORCER = None + + +policy_opts = [ + cfg.StrOpt('config_dir', default='/etc/surveil/'), + cfg.StrOpt('config_file', default='policy.json'), + cfg.StrOpt('project', default='surveil') +] + + +CONF = cfg.CONF + +# We are not trying to override anything +try: + CONF.register_opts(policy_opts) +except cfg.DuplicateOptError: + pass + + +def _has_rule(name): + return name in _ENFORCER.rules.keys() + + +def enforce(policy_name, request): + """Return the user and project the request should be limited to. + + :param request: HTTP request + :param policy_name: the policy name to validate authz against. + """ + + global _ENFORCER + if not _ENFORCER: + _ENFORCER = policy.Enforcer(CONF) + _ENFORCER.load_rules() + + rule_method = "surveil:" + policy_name + headers = request.headers + + policy_dict = dict() + policy_dict['roles'] = headers.get('X-Roles', "").split(",") + policy_dict['target.user_id'] = (headers.get('X-User-Id')) + policy_dict['target.project_id'] = (headers.get('X-Project-Id')) + + # maintain backward compat with Juno and previous by allowing the action if + # there is no rule defined for it + if _has_rule('default') or _has_rule(rule_method): + return _ENFORCER.enforce(rule_method, {}, policy_dict) + else: + return False + +# TODO(fabiog): these methods are still used because the scoping part is really +# convoluted and difficult to separate out. + + +def get_limited_to(headers): + """Return the user and project the request should be limited to. + + :param headers: HTTP headers dictionary + :return: A tuple of (user, project), set to None if there's no limit on + one of these. + """ + global _ENFORCER + if not _ENFORCER: + _ENFORCER = policy.Enforcer(CONF) + _ENFORCER.load_rules() + + policy_dict = dict() + policy_dict['roles'] = headers.get('X-Roles', "").split(",") + policy_dict['target.user_id'] = (headers.get('X-User-Id')) + policy_dict['target.project_id'] = (headers.get('X-Project-Id')) + + # maintain backward compat with Juno and previous by using context_is_admin + # rule if the segregation rule (added in Kilo) is not defined + rule_name = 'segregation' if _has_rule( + 'segregation') else 'context_is_admin' + if not _ENFORCER.enforce(rule_name, + {}, + policy_dict): + return headers.get('X-User-Id'), headers.get('X-Project-Id') + + return None, None + + +def get_limited_to_project(headers): + """Return the project the request should be limited to. + + :param headers: HTTP headers dictionary + :return: A project, or None if there's no limit on it. + """ + return get_limited_to(headers)[1] diff --git a/surveil/common/__init__.py b/surveil/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/surveil/common/util.py b/surveil/common/util.py new file mode 100644 index 0000000..d85269c --- /dev/null +++ b/surveil/common/util.py @@ -0,0 +1,36 @@ +# Copyright 2014 - Savoir-Faire Linux inc. +# +# 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. + + +import pecan +from webob import exc + +from surveil.api import rbac + + +# TODO(aviau && maybe Freddrickk): Properly document this decorator dudeasdasfd +def policy_enforce(actions): + def policy_enforce_inner(handler): + def handle_stack_method(controller, **kwargs): + request = pecan.request + print(request) + for action in actions: + allowed = rbac.enforce(action, request) + + if not allowed: + raise exc.HTTPForbidden() + + return handler(controller, **kwargs) + return handle_stack_method + return policy_enforce_inner diff --git a/surveil/tests/api/controllers/v2/test_hello.py b/surveil/tests/api/controllers/v2/test_hello.py new file mode 100644 index 0000000..af6cecb --- /dev/null +++ b/surveil/tests/api/controllers/v2/test_hello.py @@ -0,0 +1,27 @@ +# Copyright 2014 - Savoir-Faire Linux inc. +# +# 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 surveil.tests.api import functionalTest + + +class TestHelloController(functionalTest.FunctionalTest): + + def test_get(self): + response = self.app.get('/v2/hello') + self.assertEqual(response.body, b"Hello World!") + assert response.status_int == 200 + + def test_post_policy_forbidden(self): + with self.assertRaisesRegexp(Exception, '403 Forbidden'): + self.app.post('/v2/hello') diff --git a/surveil/tests/api/functionalTest.py b/surveil/tests/api/functionalTest.py index a054db2..af626a1 100644 --- a/surveil/tests/api/functionalTest.py +++ b/surveil/tests/api/functionalTest.py @@ -12,7 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import os + import mongomock +from oslo_config import cfg import pecan import pecan.testing @@ -42,6 +45,16 @@ class FunctionalTest(base.BaseTestCase): ) ] + policy_path = os.path.dirname(os.path.realpath(__file__)) + + opts = [ + cfg.StrOpt('config_dir', default=policy_path), + cfg.StrOpt('config_file', default='policy.json'), + cfg.StrOpt('project', default='surveil'), + ] + + cfg.CONF.register_opts(opts) + self.app = pecan.testing.load_test_app({ 'app': { 'root': 'surveil.api.controllers.root.RootController', diff --git a/surveil/tests/api/policy.json b/surveil/tests/api/policy.json new file mode 100644 index 0000000..0e746fa --- /dev/null +++ b/surveil/tests/api/policy.json @@ -0,0 +1,4 @@ +{ + "surveil:break":"!", + "surveil:pass":"@" +}