Skip to content

Commit

Permalink
Merge branch 'master' into security-patch
Browse files Browse the repository at this point in the history
  • Loading branch information
devbtech committed Feb 3, 2020
2 parents 6aa1e33 + 66c752c commit 9e036bb
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 15 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.rst
Expand Up @@ -6,12 +6,15 @@ Girder for MindLogger

Changes
-------
Unreleased
==========

2019-01-17: v0.7.6
^^^^^^^^^^^^^^^^^^
* :lock: Stop exposing sensitive data in deprecated `PUT /user/{:id}`

2019-12-20: v0.8.1
^^^^^^^^^^^^^^^^^^
* :sparkles: Data access for reviewers

2019-12-18: v0.7.5
^^^^^^^^^^^^^^^^^^
* :racehorse: Check if user requesting user list is a coordinator just once
Expand Down
86 changes: 81 additions & 5 deletions girderformindlogger/api/v1/applet.py
Expand Up @@ -23,7 +23,7 @@
import uuid
import requests
from ..describe import Description, autoDescribeRoute
from ..rest import Resource
from ..rest import Resource, rawResponse
from bson.objectid import ObjectId
from girderformindlogger.constants import AccessType, SortDir, TokenScope, \
DEFINED_INFORMANTS, REPROLIB_CANONICAL, SPECIAL_SUBJECTS, USER_ROLES
Expand All @@ -50,11 +50,13 @@ def __init__(self):
self.resourceName = 'applet'
self._model = AppletModel()
self.route('GET', (':id',), self.getApplet)
self.route('GET', (':id', 'data'), self.getAppletData)
self.route('GET', (':id', 'groups'), self.getAppletGroups)
self.route('POST', (), self.createApplet)
self.route('PUT', (':id', 'informant'), self.updateInformant)
self.route('PUT', (':id', 'assign'), self.assignGroup)
self.route('PUT', (':id', 'constraints'), self.setConstraints)
self.route('PUT', (':id', 'schedule'), self.setSchedule)
self.route('POST', (':id', 'invite'), self.invite)
self.route('GET', (':id', 'roles'), self.getAppletRoles)
self.route('GET', (':id', 'users'), self.getAppletUsers)
Expand Down Expand Up @@ -187,6 +189,44 @@ def createApplet(self, protocolUrl=None, name=None, informant=None):
"an email when your applet is ready."
})

@access.user(scope=TokenScope.DATA_WRITE)
@autoDescribeRoute(
Description('Get all data you are authorized to see for an applet.')
.param(
'id',
'ID of the applet for which to fetch data',
required=True
)
.param(
'format',
'JSON or CSV',
required=False
)
.errorResponse('Write access was denied for this applet.', 403)
)
def getAppletData(self, id, format='json'):
import pandas as pd
from datetime import datetime
from ..rest import setContentDisposition, setRawResponse, setResponseHeader

format = ('json' if format is None else format).lower()
thisUser = self.getCurrentUser()
data = AppletModel().getResponseData(id, thisUser)

setContentDisposition("{}-{}.{}".format(
str(id),
datetime.now().isoformat(),
format
))
if format=='csv':
setRawResponse()
setResponseHeader('Content-Type', 'text/{}'.format(format))
csv = pd.DataFrame(data).to_csv(index=False)
return(csv)
setResponseHeader('Content-Type', 'application/{}'.format(format))
return(data)


@access.user(scope=TokenScope.DATA_WRITE)
@autoDescribeRoute(
Description('(managers only) Update the informant of an applet.')
Expand Down Expand Up @@ -356,7 +396,7 @@ def getAppletRoles(self, folder):
)
.param(
'role',
'Role to invite this user to. One of ' + str(USER_ROLE_KEYS),
'Role to invite this user to. One of ' + str(set(USER_ROLE_KEYS)),
default='user',
required=False,
strip=True
Expand Down Expand Up @@ -405,22 +445,23 @@ def invite(self, applet, role="user", idCode=None, profile=None):

@access.user(scope=TokenScope.DATA_WRITE)
@autoDescribeRoute(
Description('Set or update schedule information for an activity.')
Description('Deprecated. Do not use')
.modelParam('id', model=AppletModel, level=AccessType.READ)
.param(
'activity',
'Girder ID (or Array thereof) of the activity/activities to '
'Deprecated. Do not use.'
'schedule.',
required=False
)
.jsonParam(
'schedule',
'A JSON object containing schedule information for an activity',
'Deprecated. Do not use.',
paramType='form',
required=False
)
.errorResponse('Invalid applet ID.')
.errorResponse('Read access was denied for this applet.', 403)
.deprecated()
)
def setConstraints(self, folder, activity, schedule, **kwargs):
thisUser = self.getCurrentUser()
Expand All @@ -437,6 +478,41 @@ def setConstraints(self, folder, activity, schedule, **kwargs):
thread.start()
return(applet)

@access.user(scope=TokenScope.DATA_WRITE)
@autoDescribeRoute(
Description('Set or update schedule information for an applet.')
.modelParam(
'id',
model=AppletModel,
level=AccessType.READ,
destName='applet'
)
.jsonParam(
'schedule',
'A JSON object containing schedule information for an applet',
paramType='form',
required=False
)
.errorResponse('Invalid applet ID.')
.errorResponse('Read access was denied for this applet.', 403)
)
def setSchedule(self, applet, schedule, **kwargs):
thisUser = self.getCurrentUser()
if not AppletModel().isCoordinator(applet['_id'], thisUser):
raise AccessException(
"Only coordinators and managers can update applet schedules."
)
appletMeta = applet['meta'] if 'meta' in applet else {'applet': {}}
if 'applet' not in appletMeta:
appletMeta['applet'] = {}
appletMeta['applet']['schedule'] = schedule
AppletModel().setMetadata(applet, appletMeta)
thread = threading.Thread(
target=AppletModel().updateUserCacheAllUsersAllRoles,
args=(applet, thisUser)
)
thread.start()
return(appletMeta)


def authorizeReviewer(applet, reviewer, user):
Expand Down
107 changes: 107 additions & 0 deletions girderformindlogger/api/v1/user.py
Expand Up @@ -37,6 +37,12 @@ def __init__(self):
self.route('GET', (), self.find)
self.route('GET', ('me',), self.getMe)
self.route('GET', ('authentication',), self.login)
self.route('PUT', ('applet', ':id', 'schedule'), self.setSchedule)
self.route(
'PUT',
(':uid', 'applet', ':aid', 'schedule'),
self.setOtherSchedule
)
self.route('GET', (':id',), self.getUserByID)
self.route('GET', (':id', 'access'), self.getUserAccess)
self.route('PUT', (':id', 'access'), self.updateUserAccess)
Expand Down Expand Up @@ -163,6 +169,107 @@ def getUserByID(self, id):
user = self.getCurrentUser()
return(ProfileModel().getProfile(id, user))

@access.user(scope=TokenScope.DATA_WRITE)
@autoDescribeRoute(
Description('Set or update your own custom schedule information for an applet.')
.modelParam(
'id',
model=AppletModel,
level=AccessType.READ,
destName='applet'
)
.jsonParam(
'schedule',
'A JSON object containing schedule information for an activity',
paramType='form',
required=False
)
.errorResponse('Invalid applet ID.')
.errorResponse('Read access was denied for this applet.', 403)
)
def setSchedule(self, applet, schedule, **kwargs):
import threading

thisUser = self.getCurrentUser()
if not AppletModel()._hasRole(applet['_id'], thisUser, 'user'):
raise AccessException(
"You aren't a user of this applet."
)
profile = ProfileModel().findOne(
{
'appletId': applet['_id'],
'userId': thisUser['_id'],
'profile': True
}
)
if not profile:
raise AccessException(
"You aren't a user of this applet."
)
ud = profile["userDefined"] if "userDefined" in profile else {}
ud["schedule"] = schedule
profile["userDefined"] = ud
ProfileModel().save(profile, validate=False)

thread = threading.Thread(
target=AppletModel().updateUserCacheAllUsersAllRoles,
args=(applet, thisUser)
)
thread.start()
return(profile["userDefined"])

@access.user(scope=TokenScope.DATA_WRITE)
@autoDescribeRoute(
Description('Set or update custom schedule information for a user of an applet you manage or coordinate.')
.modelParam(
'uid',
model=ProfileModel,
force=True,
destName='profile',
description='The ID of the user\'s profile for this applet.'
)
.modelParam(
'aid',
model=AppletModel,
level=AccessType.READ,
destName='applet',
description="The ID of the applet."
)
.jsonParam(
'schedule',
'A JSON object containing schedule information for an activity',
paramType='form',
required=False
)
.errorResponse('Invalid ID.')
.errorResponse('Read access was denied.', 403)
)
def setOtherSchedule(self, profile, applet, schedule, **kwargs):
import threading

thisUser = self.getCurrentUser()
if not AppletModel().isCoordinator(applet['_id'], thisUser):
raise AccessException(
"You aren't a coordinator or manager of this applet."
)
if profile["appletId"] not in [applet['_id'], str(applet['_id'])]:
raise AccessException(
"That profile is not a user of this applet."
)
ud = profile[
"coordinatorDefined"
] if "coordinatorDefined" in profile else {}
ud["schedule"] = schedule
profile["coordinatorDefined"] = ud
ProfileModel().save(profile, validate=False)

thread = threading.Thread(
target=AppletModel().updateUserCacheAllUsersAllRoles,
args=(applet, thisUser)
)
thread.start()
return(profile["coordinatorDefined"])

@access.public(scope=TokenScope.USER_INFO_READ)
@autoDescribeRoute(
Description('Add a relationship between users.')
Expand Down
13 changes: 10 additions & 3 deletions girderformindlogger/models/ID_code.py
Expand Up @@ -104,12 +104,20 @@ def load(self, id, level=AccessType.ADMIN, user=None, objectId=True,
return doc

def findIdCodes(self, profileId):
return([
from .profile import Profile

idCodes = [
i['code'] for i in list(self.find({'profileId': {'$in': [
str(profileId),
ObjectId(profileId)
]}})) if isinstance(i, dict) and 'code' in i
])
]

if not len(idCodes):
self.createIdCode(Profile().load(profileId, force=True))
return(self.findIdCodes(profileId))

return(idCodes)

def removeCode(self, profileId, code):
from .profile import Profile
Expand Down Expand Up @@ -165,7 +173,6 @@ def createIdCode(self, profile, idCode=None):
raise e
print(sys.exc_info())


def findProfile(self, idCode):
"""
Find a list of profiles for a given ID code.
Expand Down
60 changes: 56 additions & 4 deletions girderformindlogger/models/applet.py
Expand Up @@ -219,7 +219,40 @@ def getResponseData(self, appletId, reviewer, filter={}):
:type filter: dict
:reutrns: TBD
"""
pass
from .ID_code import IDCode
from .profile import Profile
from .response_folder import ResponseItem
from .user import User
from pymongo import DESCENDING

if not self._hasRole(appletId, reviewer, 'reviewer'):
raise AccessException("You are not a reviewer for this applet.")
query = {
"baseParentType": "user",
"meta.applet.@id": ObjectId(appletId)
}
responses = list(ResponseItem().find(
query=query,
user=reviewer,
sort=[("created", DESCENDING)]
))
respondents = {
str(response['baseParentId']): IDCode().findIdCodes(
Profile().createProfile(
appletId,
User().load(response['baseParentId'], force=True),
'user'
)['_id']
) for response in responses if 'baseParentId' in response
}
return([
{
"respondent": code,
**response.get('meta', {})
} for response in responses for code in respondents[
str(response['baseParentId'])
]
])

def updateRelationship(self, applet, relationship):
"""
Expand Down Expand Up @@ -341,10 +374,20 @@ def getAppletsForGroup(self, role, groupId, active=True):
return(applets if isinstance(applets, list) else [applets])

def updateUserCacheAllUsersAllRoles(self, applet, coordinator):
[self.updateUserCacheAllRoles(user) for user in self.getAppletUsers(
from .profile import Profile as ProfileModel

[self.updateUserCacheAllRoles(
UserModel().load(
id=ProfileModel().load(
user['_id'],
force=True
).get('userId'),
force=True
)
) for user in self.getAppletUsers(
applet,
coordinator
)]
).get('active', [])]

def updateUserCacheAllRoles(self, user):
[self.updateUserCache(role, user) for role in list(USER_ROLES.keys())]
Expand Down Expand Up @@ -477,7 +520,16 @@ def getAppletsForUser(self, role, user, active=True):
'roles.' + role + '.groups.id': {'$in': user.get('groups', [])}
}
))
return(applets if isinstance(applets, list) else [applets])

# filter out duplicates for coordinators
temp = set()
applets = [
k for k in applets if '_id' in k and k[
'_id'
] not in temp and not temp.add(k['_id'])
] if isinstance(applets, list) else [applets]

return(applets)

def listUsers(self, applet, role, user=None, force=False):
from .profile import Profile
Expand Down

0 comments on commit 9e036bb

Please sign in to comment.