diff --git a/etc/policy.json.sample b/etc/policy.json.sample index 9c2deb74..f8f92416 100644 --- a/etc/policy.json.sample +++ b/etc/policy.json.sample @@ -7,4 +7,6 @@ "runtime:create": "rule:context_is_admin", "runtime:update": "rule:context_is_admin", "runtime:delete": "rule:context_is_admin", + + "function:get_all:all_projects": "rule:context_is_admin", } diff --git a/qinling/api/controllers/v1/function.py b/qinling/api/controllers/v1/function.py index 0170c024..4ab7a857 100644 --- a/qinling/api/controllers/v1/function.py +++ b/qinling/api/controllers/v1/function.py @@ -24,6 +24,7 @@ from pecan import rest from webob.static import FileIter import wsmeext.pecan as wsme_pecan +from qinling.api import access_control as acl from qinling.api.controllers.v1 import resources from qinling.api.controllers.v1 import types from qinling import context @@ -175,12 +176,32 @@ class FunctionsController(rest.RestController): return resources.Function.from_dict(func_db.to_dict()).to_dict() @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.Functions) - def get_all(self): + @wsme_pecan.wsexpose(resources.Functions, bool, types.uuid) + def get_all(self, all_projects=False, project_id=None): + """Return a list of functions. + + :param project_id: Optional. Admin user can query other projects + resources, the param is ignored for normal user. + :param all_projects: Optional. Get resources of all projects. + """ + ctx = context.get_ctx() + if project_id and not ctx.is_admin: + project_id = context.ctx().projectid + if project_id and ctx.is_admin: + all_projects = True + + if all_projects: + acl.enforce('function:get_all:all_projects', ctx) + LOG.info("Get all functions.") + filters = rest_utils.get_filters( + project_id=project_id, + ) + + db_functions = db_api.get_functions(insecure=all_projects, **filters) functions = [resources.Function.from_dict(db_model.to_dict()) - for db_model in db_api.get_functions()] + for db_model in db_functions] return resources.Functions(functions=functions) diff --git a/qinling/db/base.py b/qinling/db/base.py index 82d29808..10e77fc9 100644 --- a/qinling/db/base.py +++ b/qinling/db/base.py @@ -18,6 +18,7 @@ from oslo_config import cfg from oslo_db import options as db_options from oslo_db.sqlalchemy import session as db_session +from qinling import context from qinling.db.sqlalchemy import sqlite_lock from qinling import exceptions as exc from qinling.utils import thread_local @@ -159,6 +160,22 @@ def session_aware(): return _decorator +def insecure_aware(): + """Decorator for methods working within insecure db query or not.""" + + def _decorator(func): + @functools.wraps(func) + def _with_insecure(*args, **kw): + if kw.get('insecure') is None: + insecure = context.get_ctx().is_admin + kw['insecure'] = insecure + return func(*args, **kw) + + return _with_insecure + + return _decorator + + @session_aware() def get_driver_name(session=None): return session.bind.url.drivername diff --git a/qinling/db/sqlalchemy/api.py b/qinling/db/sqlalchemy/api.py index 0cf3816a..159eb695 100644 --- a/qinling/db/sqlalchemy/api.py +++ b/qinling/db/sqlalchemy/api.py @@ -186,13 +186,15 @@ def _get_collection_sorted_by_time(model, insecure=False, fields=None, ) -def _get_db_object_by_id(model, id, insecure=False): +@db_base.insecure_aware() +def _get_db_object_by_id(model, id, insecure=None): query = db_base.model_query(model) if insecure else _secure_query(model) return query.filter_by(id=id).first() -def _delete_all(model, insecure=False, **kwargs): +@db_base.insecure_aware() +def _delete_all(model, insecure=None, **kwargs): # NOTE(kong): Because we use 'in_' operator in _secure_query(), delete() # method will raise error with default parameter. Please refer to # http://docs.sqlalchemy.org/en/rel_1_0/orm/query.html#sqlalchemy.orm.query.Query.delete @@ -216,6 +218,7 @@ def conditional_update(model, values, expected_values, insecure=False, return 0 != result +@db_base.insecure_aware() @db_base.session_aware() def get_function(id, insecure=False, session=None): function = _get_db_object_by_id(models.Function, id, insecure=insecure) @@ -262,8 +265,8 @@ def delete_function(id, session=None): @db_base.session_aware() -def delete_functions(session=None, insecure=False, **kwargs): - return _delete_all(models.Function, insecure=insecure, **kwargs) +def delete_functions(session=None, **kwargs): + return _delete_all(models.Function, **kwargs) @db_base.session_aware() @@ -304,21 +307,25 @@ def get_runtimes(session=None, **kwargs): @db_base.session_aware() def delete_runtime(id, session=None): + # Because we don't allow normal user to delete runtime in api layer, so it + # is safe to get runtime here runtime = get_runtime(id) - session.delete(runtime) @db_base.session_aware() def update_runtime(id, values, session=None): + # Because we don't allow normal user to update runtime in api layer, so it + # is safe to get runtime here runtime = get_runtime(id) runtime.update(values.copy()) return runtime +@db_base.insecure_aware() @db_base.session_aware() -def delete_runtimes(session=None, insecure=False, **kwargs): +def delete_runtimes(session=None, insecure=None, **kwargs): return _delete_all(models.Runtime, insecure=insecure, **kwargs) @@ -337,9 +344,10 @@ def create_execution(values, session=None): return execution +@db_base.insecure_aware() @db_base.session_aware() -def get_execution(id, session=None): - execution = _get_db_object_by_id(models.Execution, id) +def get_execution(id, insecure=None, session=None): + execution = _get_db_object_by_id(models.Execution, id, insecure=insecure) if not execution: raise exc.DBEntityNotFoundError("Execution not found [id=%s]" % id) @@ -359,8 +367,9 @@ def delete_execution(id, session=None): session.delete(execution) +@db_base.insecure_aware() @db_base.session_aware() -def delete_executions(session=None, insecure=False, **kwargs): +def delete_executions(session=None, insecure=None, **kwargs): return _delete_all(models.Execution, insecure=insecure, **kwargs) @@ -522,6 +531,7 @@ def get_jobs(session=None, **kwargs): return _get_collection_sorted_by_time(models.Job, **kwargs) +@db_base.insecure_aware() @db_base.session_aware() -def delete_jobs(session=None, insecure=False, **kwargs): +def delete_jobs(session=None, insecure=None, **kwargs): return _delete_all(models.Job, insecure=insecure, **kwargs) diff --git a/qinling_tempest_plugin/post_test_hook.sh b/qinling_tempest_plugin/post_test_hook.sh index f3d3ae12..37c2718f 100755 --- a/qinling_tempest_plugin/post_test_hook.sh +++ b/qinling_tempest_plugin/post_test_hook.sh @@ -31,4 +31,4 @@ sudo cp $BASE/new/tempest/etc/logging.conf.sample $BASE/new/tempest/etc/logging. export TOX_TESTENV_PASSENV=ZUUL_PROJECT (cd $BASE/new/tempest/; sudo -E testr init) -(cd $BASE/new/tempest/; sudo -E tox -eall-plugin qinling) +(cd $BASE/new/tempest/; sudo -E tox -eall-plugin -- qinling --serial) diff --git a/qinling_tempest_plugin/tests/api/test_functions.py b/qinling_tempest_plugin/tests/api/test_functions.py index f66ac3e9..3d1af6fd 100644 --- a/qinling_tempest_plugin/tests/api/test_functions.py +++ b/qinling_tempest_plugin/tests/api/test_functions.py @@ -17,6 +17,7 @@ import zipfile from tempest.lib.common.utils import data_utils from tempest.lib import decorators +from tempest.lib import exceptions from qinling_tempest_plugin.tests import base @@ -115,3 +116,56 @@ class FunctionsTest(base.BaseQinlingTest): resp = self.client.delete_resource('functions', function_id) self.assertEqual(204, resp.status) + + @decorators.idempotent_id('051f3106-df01-4fcd-a0a3-c81c99653163') + def test_get_all_admin(self): + # Create function by normal user + function_name = data_utils.rand_name('function', + prefix=self.name_prefix) + with open(self.python_zip_file, 'rb') as package_data: + resp, body = self.client.create_function( + {"source": "package"}, + self.runtime_id, + name=function_name, + package_data=package_data, + entry='%s.main' % self.base_name + ) + + self.assertEqual(201, resp.status_code) + + function_id = body['id'] + self.addCleanup(self.client.delete_resource, 'functions', + function_id, ignore_notfound=True) + + # Get functions by admin + resp, body = self.admin_client.get_resources('functions') + + self.assertEqual(200, resp.status) + self.assertNotIn( + function_id, + [function['id'] for function in body['functions']] + ) + + # Get other projects functions by admin + resp, body = self.admin_client.get_resources( + 'functions?all_projects=true' + ) + + self.assertEqual(200, resp.status) + self.assertIn( + function_id, + [function['id'] for function in body['functions']] + ) + + @decorators.idempotent_id('cd396bda-2174-4335-9f7f-2457aab61a4a') + def test_get_all_not_allowed(self): + # Get other projects functions by normal user + context = self.assertRaises( + exceptions.Forbidden, + self.client.get_resources, + 'functions?all_projects=true' + ) + self.assertIn( + 'Operation not allowed', + context.resp_body.get('faultstring') + )