From 074f8f474bac4e2d8781e498b704a038e6963127 Mon Sep 17 00:00:00 2001 From: Idan Narotzki Date: Thu, 8 Feb 2018 12:10:05 +0000 Subject: [PATCH] Adding support to super user roles. Currently in order to list artifacts from all tenants, keyclock token must include 'admin' role. The same goes for getting artifact from different realms, or download blob of artifact from different realm. The following changes enable more flexbility: to list artifacts from all tenants, user can define artifact:list_all_artifacts in policy.yaml with his own choice for role. E.G. "artifact:list_all_artifacts": "role:su_role" ^ this will allow any user with role "su_role" to list artifacts from any realm. The same logic holds for getting artifact from other realm (get_any_artifact), or download blob from artifact in any realm (download_from_any_artifact) Change-Id: Iaaa7f4b366230e0c5e4bee136bcdf9d072d498d8 --- glare/common/policy.py | 10 +++ glare/db/artifact_api.py | 16 +++- glare/db/sqlalchemy/api.py | 32 +++++--- glare/engine.py | 37 +++++++-- glare/objects/base.py | 11 ++- glare/tests/unit/test_policies.py | 129 ++++++++++++++++++++++++++++++ 6 files changed, 206 insertions(+), 29 deletions(-) diff --git a/glare/common/policy.py b/glare/common/policy.py index a4a5b71..985c453 100644 --- a/glare/common/policy.py +++ b/glare/common/policy.py @@ -51,8 +51,13 @@ artifact_policy_rules = [ "Policy to publish artifact"), policy.RuleDefault("artifact:get", "", "Policy to get artifact definition"), + policy.RuleDefault("artifact:get_any_artifact", "rule:context_is_admin", + "Policy to get artifact from any project"), policy.RuleDefault("artifact:list", "", "Policy to list artifacts"), + policy.RuleDefault("artifact:list_all_artifacts", + "rule:context_is_admin", + "Policy to list artifacts from all projects"), policy.RuleDefault("artifact:delete_public", "'public':%(visibility)s and rule:context_is_admin " "or not 'public':%(visibility)s", @@ -80,6 +85,11 @@ artifact_policy_rules = [ "rule:admin_or_owner and " "rule:artifact:download_deactivated", "Policy to download blob from artifact"), + policy.RuleDefault("artifact:download_from_any_artifact", + "rule:context_is_admin", + "Policy to download blob from any artifact" + " in any project" + ), policy.RuleDefault("artifact:delete_blob", "rule:admin_or_owner", "Policy to delete blob with external location " "from artifact"), diff --git a/glare/db/artifact_api.py b/glare/db/artifact_api.py index 6fc71e8..b2e71f6 100644 --- a/glare/db/artifact_api.py +++ b/glare/db/artifact_api.py @@ -87,20 +87,24 @@ class ArtifactAPI(object): @retry(retry_on_exception=_retry_on_connection_error, wait_fixed=1000, stop_max_attempt_number=20) - def get(self, context, type_name, artifact_id): + def get(self, context, type_name, artifact_id, get_any_artifact=False): """Return artifact values from database :param context: user context :param type_name: artifact type name or None for metatypes :param artifact_id: id of the artifact + :param get_any_artifact: flag that indicate, if we want to enable + to get artifact from other realm as well. :return: dict of artifact values """ session = api.get_session() - return api.get(context, type_name, artifact_id, session) + return api.get(context, type_name, artifact_id, + session, get_any_artifact) @retry(retry_on_exception=_retry_on_connection_error, wait_fixed=1000, stop_max_attempt_number=20) - def list(self, context, filters, marker, limit, sort, latest): + def list(self, context, filters, marker, limit, sort, + latest, list_all_artifacts=False): """List artifacts from db :param context: user request context @@ -111,13 +115,17 @@ class ArtifactAPI(object): :param sort: sort conditions :param latest: flag that indicates, that only artifacts with highest versions should be returned in output + :param list_all_artifacts: flag that indicate, if the list should + return artifact from all realms (True), + or from the specific realm (False) :return: list of artifacts. Each artifact is represented as dict of values. """ session = api.get_session() return api.get_all(context=context, session=session, filters=filters, marker=marker, limit=limit, sort=sort, - latest=latest) + latest=latest, + list_all_artifacts=list_all_artifacts) @retry(retry_on_exception=_retry_on_connection_error, wait_fixed=1000, stop_max_attempt_number=20) diff --git a/glare/db/sqlalchemy/api.py b/glare/db/sqlalchemy/api.py index c151a89..454995c 100644 --- a/glare/db/sqlalchemy/api.py +++ b/glare/db/sqlalchemy/api.py @@ -164,10 +164,11 @@ def create_or_update(context, artifact_id, values, session): return artifact.to_dict() -def _get(context, type_name, artifact_id, session): +def _get(context, type_name, artifact_id, session, get_any_artifact=False): try: - query = _do_artifacts_query(context, session).filter_by( - id=artifact_id) + query = _do_artifacts_query( + context, session, list_all_artifacts=get_any_artifact).\ + filter_by(id=artifact_id) if type_name is not None: query = query.filter_by(type_name=type_name) artifact = query.one() @@ -178,12 +179,13 @@ def _get(context, type_name, artifact_id, session): return artifact -def get(context, type_name, artifact_id, session): - return _get(context, type_name, artifact_id, session).to_dict() +def get(context, type_name, artifact_id, session, get_any_artifact=False): + return _get(context, type_name, artifact_id, + session, get_any_artifact).to_dict() def get_all(context, session, filters=None, marker=None, limit=None, - sort=None, latest=False): + sort=None, latest=False, list_all_artifacts=False): """List all visible artifacts :param filters: dict of filter keys and values. @@ -193,11 +195,15 @@ def get_all(context, session, filters=None, marker=None, limit=None, which results should be sorted, dir is a direction: 'asc' or 'desc', and type is type of the attribute: 'bool', 'string', 'numeric' or 'int' or None if attribute is base. + :param list_all_artifacts: flag that indicate, if the list should + return artifact from all realms (True), + or from the specific realm (False) :param latest: flag that indicates, that only artifacts with highest versions should be returned in output """ artifacts = _get_all( - context, session, filters, marker, limit, sort, latest) + context, session, filters, marker, + limit, sort, latest, list_all_artifacts) total_artifacts_count = get_artifact_count(context, session, filters, latest) return { @@ -265,11 +271,11 @@ def _apply_user_filters(query, basic_conds, tag_conds, prop_conds): def _get_all(context, session, filters=None, marker=None, limit=None, - sort=None, latest=False): + sort=None, latest=False, list_all_artifacts=False): filters = filters or {} - query = _do_artifacts_query(context, session) + query = _do_artifacts_query(context, session, list_all_artifacts) basic_conds, tag_conds, prop_conds = _do_query_filters(filters) @@ -375,7 +381,7 @@ def _do_paginate_query(query, marker=None, limit=None, sort=None): return query -def _do_artifacts_query(context, session): +def _do_artifacts_query(context, session, list_all_artifacts=False): """Build the query to get all artifacts based on the context""" query = session.query(models.Artifact) @@ -384,12 +390,12 @@ def _do_artifacts_query(context, session): options(joinedload(models.Artifact.tags)). options(joinedload(models.Artifact.blobs))) - return _apply_query_base_filters(query, context) + return _apply_query_base_filters(query, context, list_all_artifacts) -def _apply_query_base_filters(query, context): +def _apply_query_base_filters(query, context, list_all_artifacts=False): # If admin, return everything. - if context.is_admin: + if context.is_admin or list_all_artifacts: return query # If anonymous user, return only public artifacts. diff --git a/glare/engine.py b/glare/engine.py index 7a5a1cc..1fa9b36 100644 --- a/glare/engine.py +++ b/glare/engine.py @@ -110,7 +110,8 @@ class Engine(object): return lock @staticmethod - def _show_artifact(ctx, type_name, artifact_id, read_only=False): + def _show_artifact(ctx, type_name, artifact_id, + read_only=False, get_any_artifact=False): """Return artifact requested by user. Check access permissions and policies. @@ -120,11 +121,13 @@ class Engine(object): :param artifact_id: id of the artifact to be updated :param read_only: flag, if set to True only read access is checked, if False then engine checks if artifact can be modified by the user + :param get_any_artifact: flag, if set to True will get artifact from + any realm """ artifact_type = registry.ArtifactRegistry.get_artifact_type(type_name) # only artifact is available for class users - af = artifact_type.show(ctx, artifact_id) - if not read_only: + af = artifact_type.show(ctx, artifact_id, get_any_artifact) + if not read_only and not get_any_artifact: if not ctx.is_admin and ctx.tenant != af.owner or ctx.read_only: raise exception.Forbidden() LOG.debug("Artifact %s acquired for read-write access", @@ -307,9 +310,14 @@ class Engine(object): :param artifact_id: id of artifact to show :return: definition of requested artifact """ + get_any_artifact = False + if policy.authorize("artifact:get_any_artifact", {}, + context, do_raise=False): + get_any_artifact = True policy.authorize("artifact:get", {}, context) af = self._show_artifact(context, type_name, artifact_id, - read_only=True) + read_only=True, + get_any_artifact=get_any_artifact) return af.to_dict() @staticmethod @@ -329,11 +337,16 @@ class Engine(object): versions should be returned in output :return: list of artifact definitions """ + list_all_artifacts = False + if policy.authorize("artifact:list_all_artifacts", + {}, context, do_raise=False): + list_all_artifacts = True policy.authorize("artifact:list", {}, context) artifact_type = registry.ArtifactRegistry.get_artifact_type(type_name) # return list to the user - artifacts_data = artifact_type.list(context, filters, marker, - limit, sort, latest) + + artifacts_data = artifact_type.list( + context, filters, marker, limit, sort, latest, list_all_artifacts) artifacts_data["artifacts"] = [af.to_dict() for af in artifacts_data["artifacts"]] return artifacts_data @@ -631,9 +644,17 @@ class Engine(object): in this dict :return: file iterator for requested file """ + download_from_any_artifact = False + if policy.authorize("artifact:download_from_any_artifact", {}, + context, do_raise=False): + download_from_any_artifact = True + af = self._show_artifact(context, type_name, artifact_id, - read_only=True) - policy.authorize("artifact:download", af.to_dict(), context) + read_only=True, + get_any_artifact=download_from_any_artifact) + + if not download_from_any_artifact: + policy.authorize("artifact:download", af.to_dict(), context) blob_name = self._generate_blob_name(field_name, blob_key) diff --git a/glare/objects/base.py b/glare/objects/base.py index 705023f..7d06f9f 100644 --- a/glare/objects/base.py +++ b/glare/objects/base.py @@ -277,7 +277,7 @@ Possible values: return self.init_artifact(context, updated_af) @classmethod - def show(cls, context, artifact_id): + def show(cls, context, artifact_id, get_any_artifact=False): """Return Artifact from Glare repo :param context: user context @@ -288,7 +288,7 @@ Possible values: type_name = cls.get_type_name() else: type_name = None - af = cls.db_api.get(context, type_name, artifact_id) + af = cls.db_api.get(context, type_name, artifact_id, get_any_artifact) return cls.init_artifact(context, af) @classmethod @@ -396,7 +396,7 @@ Possible values: @classmethod def list(cls, context, filters=None, marker=None, limit=None, - sort=None, latest=False): + sort=None, latest=False, list_all_artifacts=False): """Return list of artifacts requested by user. :param context: user context @@ -408,6 +408,9 @@ Possible values: :param sort: sorting options :param latest: flag that indicates, that only artifacts with highest versions should be returned in output + :param list_all_artifacts: flag that indicate, if the list should + return artifact from all tenants (True), + or from the specific tenant (False) :return: list of artifact objects """ @@ -435,7 +438,7 @@ Possible values: filters.append(default_filter) artifacts_data = cls.db_api.list(context, filters, marker, limit, - sort, latest) + sort, latest, list_all_artifacts) artifacts_data["artifacts"] = [cls.init_artifact(context, af) for af in artifacts_data["artifacts"]] return artifacts_data diff --git a/glare/tests/unit/test_policies.py b/glare/tests/unit/test_policies.py index dc15320..0fe0032 100644 --- a/glare/tests/unit/test_policies.py +++ b/glare/tests/unit/test_policies.py @@ -11,8 +11,11 @@ # 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 io import BytesIO from glare.common import exception as exc +from glare.common import store_api + from glare.tests.unit import base @@ -45,3 +48,129 @@ class TestPolicies(base.BaseTestArtifactAPI): self.controller.list_type_schemas, self.req) self.assertRaises(exc.PolicyException, self.controller.list_type_schemas, admin_req) + + def test_list_all_artifacts(self): + + # create artifacts with user1 and user 2 + user1_req = self.get_fake_request(user=self.users['user1']) + user2_req = self.get_fake_request(user=self.users['user2']) + + for i in range(5): + self.controller.create(user1_req, 'sample_artifact', + {'name': 'user1%s' % i, + 'version': '%d.0' % i}) + + self.controller.create(user2_req, 'sample_artifact', + {'name': 'user2%s' % i, + 'version': '%d.0' % i}) + + # user1 list only its realm artifacts + user1_art_list = self.controller.list(user1_req, 'sample_artifact') + self.assertEqual(5, len(user1_art_list["artifacts"])) + + # user2 list only it's realm artifact + user2_art_list = self.controller.list(user2_req, 'sample_artifact') + self.assertEqual(5, len(user2_art_list["artifacts"])) + + # enable to list all arts from all realms for a user with role su_role + rule = {"artifact:list_all_artifacts": "role:su_role"} + self.policy(rule) + + # Append su_role to 'user1' + self.users['user1']['roles'].append("su_role") + list_all_art = self.controller.list(user1_req, "sample_artifact") + + # now glare returns all the artifacts (from all the realms) + self.assertEqual(10, len(list_all_art["artifacts"])) + + def test_get_any_artifact(self): + + # create artifacts with user1 and user 2 + user1_req = self.get_fake_request(user=self.users['user1']) + user2_req = self.get_fake_request(user=self.users['user2']) + + art1 = self.controller.create(user1_req, 'sample_artifact', + {'name': 'user1%s' % 1, + 'version': '%d.0' % 1}) + + art2 = self.controller.create(user2_req, 'sample_artifact', + {'name': 'user2%s' % 2, + 'version': '%d.0' % 2}) + + # user1 can get artifacts from its realm + self.controller.show(user1_req, 'sample_artifact', art1['id']) + + # user1 cannot get artifacts from other realm + self.assertRaises(exc.NotFound, self.controller.show, user1_req, + 'sample_artifact', art2['id']) + + # get_any_artifact + rule = {"artifact:get_any_artifact": "role:su_role"} + self.policy(rule) + + # user2 can get artifact from his realm only + self.controller.show(user2_req, 'sample_artifact', art2['id']) + self.assertRaises(exc.NotFound, self.controller.show, user2_req, + 'sample_artifact', art1['id']) + + # Append su_role to 'user1' + self.users['user1']['roles'].append("su_role") + + # Now user1 can get artifact from other realm + self.controller.show(user1_req, 'sample_artifact', art2['id']) + + def test_download_from_any_artifact(self): + + # create artifacts with user1 and user 2 + user1_req = self.get_fake_request(user=self.users['user1']) + user2_req = self.get_fake_request(user=self.users['user2']) + + art1 = self.controller.create(user1_req, 'sample_artifact', + {'name': 'user1%s' % 1, + 'version': '%d.0' % 1}) + + art2 = self.controller.create(user2_req, 'sample_artifact', + {'name': 'user2%s' % 2, + 'version': '%d.0' % 2}) + + # Upload blobs + self.controller.upload_blob( + user1_req, 'sample_artifact', art1['id'], 'blob', + BytesIO(b'a' * 100), 'application/octet-stream') + + self.controller.upload_blob( + user2_req, 'sample_artifact', art2['id'], 'blob', + BytesIO(b'a' * 50), 'application/octet-stream') + + # Download blobs + flobj1 = self.controller.download_blob( + user1_req, 'sample_artifact', art1['id'], 'blob') + self.assertEqual(b'a' * 100, store_api.read_data(flobj1['data'])) + + flobj2 = self.controller.download_blob( + user2_req, 'sample_artifact', art2['id'], 'blob') + self.assertEqual(b'a' * 50, store_api.read_data(flobj2['data'])) + + # Make sure user2 cannot download blob from artifact in realm1 + self.assertRaises(exc.NotFound, self.controller.download_blob, + user2_req, 'sample_artifact', art1['id'], 'blob') + + # Add role def to policy + rule = {"artifact:download_from_any_artifact": "role:su_role"} + self.policy(rule) + + # Make sure user1 cannot download blob from artifact in realm2 + self.assertRaises(exc.NotFound, self.controller.download_blob, + user1_req, 'sample_artifact', art2['id'], 'blob') + + # Append su_role to 'user1' + self.users['user1']['roles'].append("su_role") + + # Now user1 can get download blob from other realm + flobj2 = self.controller.download_blob( + user1_req, 'sample_artifact', art2['id'], 'blob') + self.assertEqual(b'a' * 50, store_api.read_data(flobj2['data'])) + + # User2 still cannot download blob from artifact in realm1 + self.assertRaises(exc.NotFound, self.controller.download_blob, + user2_req, 'sample_artifact', art1['id'], 'blob')