Skip to content

Commit

Permalink
Merge pull request #43 from whole-tale/add_access_control_tale
Browse files Browse the repository at this point in the history
Add access control lists for Tale resource
  • Loading branch information
Xarthisius authored Mar 19, 2018
2 parents 61c68b6 + b2fb43a commit 6d80e58
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 2 deletions.
180 changes: 179 additions & 1 deletion plugin_tests/tale_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
from tests import base
from .tests_helpers import \
GOOD_REPO, GOOD_COMMIT, \
GOOD_REPO, GOOD_COMMIT, XPRA_REPO, XPRA_COMMIT, \
mockOtherRequest, mockCommitRequest, mockReposRequest


Expand Down Expand Up @@ -40,6 +40,12 @@ def setUp(self):
self.recipe = self.model('recipe', 'wholetale').createRecipe(
GOOD_COMMIT, 'https://github.com/' + GOOD_REPO,
creator=self.user, public=True)
recipe_admin = self.model('recipe', 'wholetale').createRecipe(
XPRA_COMMIT, 'https://github.com/' + XPRA_REPO,
creator=self.admin, public=True)
self.image_admin = self.model('image', 'wholetale').createImage(
recipe_admin, XPRA_REPO, name="xpra name", creator=self.admin,
public=True)
self.image = self.model('image', 'wholetale').createImage(
self.recipe, GOOD_REPO, name="my name", creator=self.user,
public=True)
Expand Down Expand Up @@ -187,6 +193,178 @@ def testTaleFlow(self):
continue
self.assertEqual(resp.json[key], tale[key])

def testTaleAccess(self):
with httmock.HTTMock(mockReposRequest, mockCommitRequest,
mockOtherRequest):
# Grab the default user folders
resp = self.request(
path='/folder', method='GET', user=self.user, params={
'parentType': 'user',
'parentId': self.user['_id'],
'sort': 'title',
'sortdir': 1
})
folder = resp.json[1]
# Create a new tale from a user image
resp = self.request(
path='/tale', method='POST', user=self.user,
type='application/json',
body=json.dumps(
{
'imageId': str(self.image['_id']),
'folderId': folder['_id'],
'public': True
})
)
self.assertStatusOk(resp)
tale_user_image = resp.json
# Create a new tale from an admin image
resp = self.request(
path='/tale', method='POST', user=self.user,
type='application/json',
body=json.dumps(
{
'imageId': str(self.image_admin['_id']),
'folderId': folder['_id']
})
)
self.assertStatusOk(resp)
tale_admin_image = resp.json

from girder.constants import AccessType

# Retrieve access control list for the newly created tale
resp = self.request(
path='/tale/%s/access' % tale_user_image['_id'], method='GET',
user=self.user)
self.assertStatusOk(resp)
result_tale_access = resp.json
expected_tale_access = {
'users': [{
'login': self.user['login'],
'level': AccessType.ADMIN,
'id': str(self.user['_id']),
'flags': [],
'name': '%s %s' % (
self.user['firstName'], self.user['lastName'])}],
'groups': []
}
self.assertEqual(result_tale_access, expected_tale_access)

# Update the access control list for the tale by adding the admin
# as a second user
input_tale_access = {
"users": [
{
"login": self.user['login'],
"level": AccessType.ADMIN,
"id": str(self.user['_id']),
"flags": [],
"name": "%s %s" % (self.user['firstName'], self.user['lastName'])
},
{
'login': self.admin['login'],
'level': AccessType.ADMIN,
'id': str(self.admin['_id']),
'flags': [],
'name': '%s %s' % (self.admin['firstName'], self.admin['lastName'])
}],
"groups": []}
resp = self.request(
path='/tale/%s/access' % tale_user_image['_id'], method='PUT',
user=self.user, params={'access': json.dumps(input_tale_access)})
self.assertStatusOk(resp)
# Check that the returned access control list for the tale is as expected
result_tale_access = resp.json['access']
result_image_id = resp.json['imageId']
result_folder_id = resp.json['folderId']
expected_tale_access = {
"groups": [],
"users": [
{
"flags": [],
"id": str(self.user['_id']),
"level": AccessType.ADMIN
},
{
"flags": [],
"id": str(self.admin['_id']),
"level": AccessType.ADMIN
},
]
}
self.assertEqual(result_tale_access, expected_tale_access)
# Check that the access control list propagated to the image that the tale
# was built from
resp = self.request(
path='/image/%s/access' % result_image_id, method='GET',
user=self.user)
self.assertStatusOk(resp)
result_image_access = resp.json
expected_image_access = input_tale_access
self.assertEqual(result_image_access, expected_image_access)

# Check that the access control list propagated to the folder that the tale
# is associated with
resp = self.request(
path='/folder/%s/access' % result_folder_id, method='GET',
user=self.user)
self.assertStatusOk(resp)
result_folder_access = resp.json
expected_folder_access = input_tale_access
self.assertEqual(result_folder_access, expected_folder_access)

# Update the access control list of a tale that was generated from an image that the user
# does not have admin access to
input_tale_access = {
"users": [
{
"login": self.user['login'],
"level": AccessType.ADMIN,
"id": str(self.user['_id']),
"flags": [],
"name": "%s %s" % (self.user['firstName'], self.user['lastName'])
}],
"groups": []
}
resp = self.request(
path='/tale/%s/access' % tale_admin_image['_id'], method='PUT',
user=self.user, params={'access': json.dumps(input_tale_access)})
self.assertStatus(resp, 403)

# Check that the access control list was correctly set for the tale
resp = self.request(
path='/tale/%s/access' % tale_admin_image['_id'], method='GET',
user=self.user)
self.assertStatusOk(resp)
result_tale_access = resp.json
expected_tale_access = input_tale_access
self.assertEqual(result_tale_access, expected_tale_access)

# Check that the access control list did not propagate to the image
resp = self.request(
path='/image/%s/access' % tale_admin_image['imageId'], method='GET',
user=self.user)
self.assertStatus(resp, 403)

# Setting the access list with bad json should throw an error
resp = self.request(
path='/tale/%s/access' % tale_user_image['_id'], method='PUT',
user=self.user, params={'access': 'badJSON'})
self.assertStatus(resp, 400)

# Change the access to private
resp = self.request(
path='/tale/%s/access' % tale_user_image['_id'], method='PUT',
user=self.user,
params={'access': json.dumps(input_tale_access), 'public': False})
self.assertStatusOk(resp)
resp = self.request(
path='/tale/%s' % tale_user_image['_id'], method='GET',
user=self.user)
self.assertStatusOk(resp)
self.assertEqual(resp.json['public'], False)

def tearDown(self):
self.model('user').remove(self.user)
self.model('user').remove(self.admin)
Expand Down
51 changes: 51 additions & 0 deletions server/models/tale.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,54 @@ def updateTale(self, tale):
"""
tale['updated'] = datetime.datetime.utcnow()
return self.save(tale)

def setAccessList(self, doc, access, save=False, user=None, force=False,
setPublic=None, publicFlags=None):
"""
Overrides AccessControlledModel.setAccessList to encapsulate ACL
functionality for a tale.
:param doc: the tale to set access settings on
:type doc: girder.models.tale
:param access: The access control list
:type access: dict
:param save: Whether the changes should be saved to the database
:type save: bool
:param user: The current user
:param force: Set this to True to set the flags regardless of the passed in
user's permissions.
:type force: bool
:param setPublic: Pass this if you wish to set the public flag on the
resources being updated.
:type setPublic: bool or None
:param publicFlags: Pass this if you wish to set the public flag list on
resources being updated.
:type publicFlags: flag identifier str, or list/set/tuple of them,
or None
"""
if setPublic is not None:
self.setPublic(doc, setPublic, save=False)

if publicFlags is not None:
doc = self.setPublicFlags(doc, publicFlags, user=user, save=False,
force=force)

doc = AccessControlledModel.setAccessList(self, doc, access,
user=user, save=save, force=force)

folder = AccessControlledModel.model('folder').load(
doc['folderId'], user=user, level=AccessType.ADMIN)

AccessControlledModel.model('folder').setAccessList(
folder, access, user=user, save=save, force=force,
setPublic=setPublic, publicFlags=publicFlags)

image = AccessControlledModel.model('image', 'wholetale').load(
doc['imageId'], user=user, level=AccessType.ADMIN)

if image:
AccessControlledModel.model('image', 'wholetale').setAccessList(
image, access, user=user, save=save, force=force,
setPublic=setPublic, publicFlags=publicFlags)

return doc
31 changes: 30 additions & 1 deletion server/rest/tale.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from girder.api.docs import addModel
from girder.api.describe import Description, autoDescribeRoute
from girder.api.rest import Resource, filtermodel, RestException
from girder.constants import AccessType, SortDir
from girder.constants import AccessType, SortDir, TokenScope
from ..schema.tale import taleModel


Expand All @@ -22,6 +22,8 @@ def __init__(self):
self.route('PUT', (':id',), self.updateTale)
self.route('POST', (), self.createTale)
self.route('DELETE', (':id',), self.deleteTale)
self.route('GET', (':id', 'access'), self.getTaleAccess)
self.route('PUT', (':id', 'access'), self.updateTaleAccess)

@access.public
@filtermodel(model='tale', plugin='wholetale')
Expand Down Expand Up @@ -148,3 +150,30 @@ def createTale(self, tale, params):
category=tale.get('category', 'science'),
published=False
)

@access.user(scope=TokenScope.DATA_OWN)
@autoDescribeRoute(
Description('Get the access control list for a tale')
.modelParam('id', model='tale', plugin='wholetale', level=AccessType.ADMIN)
.errorResponse('ID was invalid.')
.errorResponse('Admin access was denied for the tale.', 403)
)
def getTaleAccess(self, tale):
return self.model('tale', 'wholetale').getFullAccessList(tale)

@access.user(scope=TokenScope.DATA_OWN)
@autoDescribeRoute(
Description('Update the access control list for a tale.')
.modelParam('id', model='tale', plugin='wholetale', level=AccessType.ADMIN)
.jsonParam('access', 'The JSON-encoded access control list.', requireObject=True)
.jsonParam('publicFlags', 'JSON list of public access flags.', requireArray=True,
required=False)
.param('public', 'Whether the tale should be publicly visible.', dataType='boolean',
required=False)
.errorResponse('ID was invalid.')
.errorResponse('Admin access was denied for the tale.', 403)
)
def updateTaleAccess(self, tale, access, publicFlags, public):
user = self.getCurrentUser()
return self.model('tale', 'wholetale').setAccessList(
tale, access, save=True, user=user, setPublic=public, publicFlags=publicFlags)

0 comments on commit 6d80e58

Please sign in to comment.