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