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
This commit is contained in:
Idan Narotzki 2018-02-08 12:10:05 +00:00
parent 9036a06839
commit 074f8f474b
6 changed files with 206 additions and 29 deletions

View File

@ -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"),

View File

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

View File

@ -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.

View File

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

View File

@ -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

View File

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