def dicomSubstringSearchHandler(query, types, user=None, level=None, limit=0, offset=0): """ Provide a substring search on both keys and values. """ if types != ['item']: raise RestException('The dicom search is only able to search in Item.') if not isinstance(query, six.string_types): raise RestException('The search query must be a string.') jsQuery = """ function() { var queryKey = %(query)s.toLowerCase(); var queryValue = queryKey; var dicomMeta = obj.dicom.meta; return Object.keys(dicomMeta).some( function(key) { return (key.toLowerCase().indexOf(queryKey) !== -1) || dicomMeta[key].toString().toLowerCase().indexOf(queryValue) !== -1; }) } """ % { # This could eventually be a separately-defined key and value 'query': json.dumps(query) } # Sort the documents inside MongoDB cursor = Item().find({'dicom': {'$exists': True}, '$where': jsQuery}) # Filter the result result = { 'item': [ Item().filter(doc, user) for doc in Item().filterResultsByPermission( cursor, user, level, limit, offset) ] } return result
def getUser(self, token): headers = { 'Authorization': ' '.join( (token['token_type'], token['access_token'])) } # For privacy and efficiency, fetch only the specific needed fields # https://developers.google.com/+/web/api/rest/#partial-response query = urllib.parse.urlencode( {'fields': ','.join(self._API_USER_FIELDS)}) resp = self._getJson(method='GET', url='%s?%s' % (self._API_USER_URL, query), headers=headers) # Get user's OAuth2 ID oauthId = resp.get('id') if not oauthId: raise RestException('Google Plus did not return a user ID.', code=502) # Get user's email address # Prefer email address with 'account' type emails = [ email.get('value') for email in resp.get('emails', []) if email.get('type') == 'account' ] if not emails: # If an 'account' email can't be found, consider them all emails = [email.get('value') for email in resp.get('emails', [])] if emails: # Even if there are multiple emails, just use the first one email = emails[0] else: raise RestException( 'This Google Plus user has no available email address.', code=502) # Get user's name firstName = resp.get('name', {}).get('givenName', '') lastName = resp.get('name', {}).get('familyName', '') user = self._createOrReuseUser(oauthId, email, firstName, lastName) return user
def _validate_fileSizeQuota(self, value): """Validate the fileSizeQuota parameter. :param value: the proposed value. :returns: the validated value :rtype: None or int """ (value, err) = ValidateSizeQuota(value) if err: raise RestException(err, extra='fileSizeQuota') return value
def createColormapFromGradient(self, name, public, gradient, labels, labelmap): try: return Colormap().createColormapFromGradient( self.getCurrentUser(), gradient, name, labels, public, labelmap) except ValidationException as exc: logger.exception('Failed to validate colormap') raise RestException( "Validation Error: JSON doesn\'t follow schema (%r)." % (exc.args, ))
def _getGeometry(self, geometry): try: GeoJSON.to_instance(geometry, strict=True) if geometry['type'] != 'Point': raise ValueError return geometry except (TypeError, ValueError): raise RestException( "Invalid GeoJSON passed as 'geometry' parameter.")
def verify(self): headers = { "X-Dataverse-key": "{}".format(self.key), } try: r = requests.get(self.token_url, headers=headers) r.raise_for_status() except requests.exceptions.HTTPError: raise RestException( "Key '{}' is not valid for '{}'".format(self.key, self.resource_server) )
def acquireLock(self, params): user = self.getCurrentUser() sessionId = params['sessionId'] itemId = params['itemId'] ownerId = None if 'ownerId' in params: ownerId = params['ownerId'] if not Session().containsItem(sessionId, itemId, user): raise RestException('Item not in the session', 404) return self.model('lock', 'wt_data_manager').acquireLock( user, sessionId, itemId, ownerId)
def importData(self, assetstore, params): self.requireParams(('parentId', 'path'), params) user = self.getCurrentUser() parentType = params.get('parentType', 'folder') if parentType not in ('user', 'collection', 'folder'): raise RestException('Invalid parentType.') parent = self.model(parentType).load(params['parentId'], force=True, exc=True) progress = self.boolParam('progress', params, default=False) client = HdfsClient( host=assetstore['hdfs']['host'], port=assetstore['hdfs']['port'], use_trash=False) path = params['path'] with ProgressContext(progress, user=user, title='Importing data from HDFS') as ctx: try: self._importData(parentType, parent, assetstore, client, path, ctx, user) except FileNotFoundException: raise RestException('File not found: %s.' % path)
def getUser(self, token): headers = { 'Authorization': 'Bearer {}'.format(token['access_token']), 'Accept': 'application/json' } # Get user's email address # In the unlikely case that a user has more than 30 email addresses, # this HTTP request might have to be made multiple times with # pagination resp = self._getJson(method='GET', url=self._API_EMAILS_URL, headers=headers) emails = [ email.get('email') for email in resp['values'] if email.get('is_primary') and email.get('is_confirmed') ] if not emails: raise RestException( 'This Bitbucket user has no registered email address.', code=502) # There should never be more than one primary email email = emails[0] # Get user's OAuth2 ID, login, and name resp = self._getJson(method='GET', url=self._API_USER_URL, headers=headers) oauthId = resp.get('uuid') if not oauthId: raise RestException('Bitbucket did not return a user ID.', code=502) login = resp.get('username', None) names = (resp.get('display_name') or login).split() firstName, lastName = names[0], names[-1] user = self._createOrReuseUser(oauthId, email, firstName, lastName, login) return user
def callback(self, provider, state, code, error): if error is not None: raise RestException("Provider returned error: '%s'." % error, code=502) self.requireParams({'state': state, 'code': code}) providerName = provider provider = providers.idMap.get(providerName) if not provider: raise RestException('Unknown provider "%s".' % providerName) redirect = self._validateCsrfToken(state) providerObj = provider(cherrypy.url()) token = providerObj.getToken(code) event = events.trigger('oauth.auth_callback.before', { 'provider': provider, 'token': token }) if event.defaultPrevented: raise cherrypy.HTTPRedirect(redirect) user = providerObj.getUser(token) event = events.trigger('oauth.auth_callback.after', { 'provider': provider, 'token': token, 'user': user }) if event.defaultPrevented: raise cherrypy.HTTPRedirect(redirect) girderToken = self.sendAuthTokenCookie(user) try: redirect = redirect.format(girderToken=str(girderToken['_id'])) except KeyError: pass # in case there's another {} that's not handled by format raise cherrypy.HTTPRedirect(redirect)
def readChunk(self, upload, offset, params): """ After the temporary upload record has been created (see initUpload), the bytes themselves should be passed up in ordered chunks. The user must remain logged in when passing each chunk, to authenticate that the writer of the chunk is the same as the person who initiated the upload. The passed offset is a verification mechanism for ensuring the server and client agree on the number of bytes sent/received. """ if cherrypy.request.headers.get('Content-Type', '').startswith('multipart/form-data'): raise RestException( 'Multipart encoding is no longer supported. Send the chunk in ' 'the request body, and other parameters in the query string.') if 'chunk' in params: # If we see the undocumented "chunk" query string parameter, then we abort trying to # read the body, use the query string value as chunk, and pass it through to # Upload().handleChunk. This case is used by the direct S3 upload process. chunk = params['chunk'] else: chunk = RequestBodyStream(cherrypy.request.body) logging.error(chunk) logging.error(params) logging.error(offset) user = self.getCurrentUser() if upload['userId'] != user['_id']: raise AccessException('You did not initiate this upload.') if upload['received'] != offset: raise RestException( 'Server has received %s bytes, but client sent offset %s.' % (upload['received'], offset)) try: logging.error('S3 upload----') return Upload().handleChunk(upload, chunk, filter=True, user=user) except IOError as exc: if exc.errno == errno.EACCES: raise Exception('Failed to store upload.') raise
def revokeAccount(self, provider, resource_server): """Revoke account authorization. In case of OAuth use the proper flow (usually calling /revoke with refreshToken). In case of API Key, just drop it from the user model. In case of DataONE, hahaha you thought that's possible via API?! You silly goose! (User would have to clear cookies from CN or login to ORCID and deauthorize there) """ try: provider_obj = self.supported_providers()[provider] except KeyError: raise RestException( "Invalid account provider (provider={})".format(provider)) if provider_obj["type"] == "apikey" and not resource_server: raise RestException( "Missing resource_server for apikey provider (provider={})". format(provider)) user = self.getCurrentUser() user_tokens = user.get("otherTokens", []) # In case of APIKey, it's resource_server that's unique, not provider id if resource_server: key = "resource_server" target_value = resource_server else: key = "provider" target_value = provider token = next((_ for _ in user_tokens if _.get(key) == target_value), None) if token: user_tokens.remove(token) user["otherTokens"] = user_tokens User().save(user) if provider_obj["type"] == "bearer": oauth_provider = providers.idMap[provider] # NOTE: only ORCID has that implemented oauth_provider("").revokeToken(token)
def setContentDisposition(filename, disposition='attachment', setHeader=True): """ Set the content disposition header to either inline or attachment, and specify a filename that is properly escaped. See developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition, tools.ietf.org/html/rfc2183, tools.ietf.org/html/rfc6266, and tools.ietf.org/html/rfc5987 for specifications and details. :param filename: the filename to add to the content disposition header. :param disposition: either 'inline' or 'attachment'. None is the same as 'attachment'. Any other value skips setting the content disposition header. :param setHeader: if False, return the value that would be set to the Content-Disposition header, but do not set it. :returns: the content-disposition header value. """ if (not disposition or (disposition not in ('inline', 'attachment') and not disposition.startswith('form-data'))): raise RestException( 'Error: Content-Disposition (%r) is not a recognized value.' % disposition) if not filename: raise RestException('Error: Content-Disposition filename is empty.') if not isinstance(disposition, six.binary_type): disposition = disposition.encode('iso8859-1', 'ignore') if not isinstance(filename, six.text_type): filename = filename.decode('utf8', 'ignore') # Decompose the name before trying to encode it. This will de-accent # characters rather than remove them in some instances. safeFilename = unicodedata.normalize('NFKD', filename).encode('iso8859-1', 'ignore') utf8Filename = filename.encode('utf8', 'ignore') value = disposition + b'; filename="' + safeFilename.replace( b'\\', b'\\\\').replace(b'"', b'\\"') + b'"' if safeFilename != utf8Filename: quotedFilename = six.moves.urllib.parse.quote(utf8Filename) if not isinstance(quotedFilename, six.binary_type): quotedFilename = quotedFilename.encode('iso8859-1', 'ignore') value += b'; filename*=UTF-8\'\'' + quotedFilename value = value.decode('utf8') if setHeader: setResponseHeader('Content-Disposition', value) return value
def wrapped(*args, **kwargs): """ Transform any passed params according to the spec, or fill in default values for any params not passed. """ # Combine path params with form/query params into a single lookup table params = {k: v for k, v in six.viewitems(kwargs) if k != 'params'} params.update(kwargs.get('params', {})) for descParam in self.description.params: # We need either a type or a schema ( for message body ) if 'type' not in descParam and 'schema' not in descParam: continue name = descParam['name'] model = self._getModel(name, self.description.modelParams) if name in params: if name in self.description.jsonParams: info = self.description.jsonParams[name] val = self._loadJson(name, info, params[name]) self._passArg(fun, kwargs, name, val) elif name in self.description.modelParams: info = self.description.modelParams[name] kwargs.pop(name, None) # Remove from path params val = self._loadModel(name, info, params[name], model) self._passArg(fun, kwargs, self._destName(info, model), val) else: val = self._validateParam(name, descParam, params[name]) self._passArg(fun, kwargs, name, val) elif descParam['in'] == 'body': if name in self.description.jsonParams: info = self.description.jsonParams[name].copy() info['required'] = descParam['required'] val = self._loadJsonBody(name, info) self._passArg(fun, kwargs, name, val) else: self._passArg(fun, kwargs, name, cherrypy.request.body) elif 'default' in descParam: self._passArg(fun, kwargs, name, descParam['default']) elif descParam['required']: raise RestException('Parameter "%s" is required.' % name) else: # If required=False but no default is specified, use None if name in self.description.modelParams: info = self.description.modelParams[name] kwargs.pop(name, None) # Remove from path params self._passArg(fun, kwargs, info['destName'] or model.name, None) else: self._passArg(fun, kwargs, name, None) self._mungeKwargs(kwargs, fun) return fun(*args, **kwargs)
def convertImage(self, item, params): if 'concurrent' in params: params['_concurrency'] = params.pop('concurrent') largeImageFileId = params.get('fileId') if largeImageFileId is None: files = list(Item().childFiles(item=item, limit=2)) if len(files) == 1: largeImageFileId = str(files[0]['_id']) if not largeImageFileId: raise RestException('Missing "fileId" parameter.') largeImageFile = File().load(largeImageFileId, force=True, exc=True) user = self.getCurrentUser() token = self.getCurrentToken() params.pop('notify', None) localJob = self.boolParam('localJob', params, default=True) params.pop('localJob', None) try: return self.imageItemModel.convertImage( item, largeImageFile, user, token, localJob=localJob, **params) except TileGeneralException as e: raise RestException(e.args[0])
def getUser(self, token): headers = { 'Authorization': 'Bearer %s' % token['access_token'], 'Accept': 'application/json' } # Get user's email address resp = self._getJson(method='GET', url=self._API_USER_URL, headers=headers) email = resp.get('login') if not email: raise RestException( 'Box did not return user information.', code=502) # Get user's OAuth2 ID, login, and name oauthId = resp.get('id') if not oauthId: raise RestException('Box did not return a user ID.', code=502) names = resp.get('name').split() firstName, lastName = names[0], names[-1] return self._createOrReuseUser(oauthId, email, firstName, lastName)
def _getTilesInfo(self, item, imageArgs): """ Get metadata for an item's large image. :param item: the item to query. :param imageArgs: additional arguments to use when fetching image data. :return: the tile metadata. """ try: return self.imageItemModel.getMetadata(item, **imageArgs) except TileGeneralException as e: raise RestException(e.args[0], code=400)
def login(self): if not Setting().get(SettingKey.ENABLE_PASSWORD_LOGIN): raise RestException('Password login is disabled on this instance.') user, token = self.getCurrentUser(returnToken=True) # Only create and send new cookie if user isn't already sending a valid one. if not user: authHeader = cherrypy.request.headers.get('Authorization') if not authHeader: authHeader = cherrypy.request.headers.get( 'Girder-Authorization') if not authHeader or not authHeader[0:6] == 'Basic ': raise RestException('Use HTTP Basic Authentication', 401) try: credentials = base64.b64decode(authHeader[6:]).decode('utf8') if ':' not in credentials: raise TypeError except Exception: raise RestException('Invalid HTTP Authorization header', 401) login, password = credentials.split(':', 1) otpToken = cherrypy.request.headers.get('Girder-OTP') user = self._model.authenticate(login, password, otpToken) setCurrentUser(user) token = self.sendAuthTokenCookie(user) return { 'user': self._model.filter(user, user), 'authToken': { 'token': token['_id'], 'expires': token['expires'], 'scope': token['scope'] }, 'message': 'Login succeeded.' }
def getTilesRegion(self, item, params): _adjustParams(params) params = self._parseParams(params, True, [ ('left', float, 'region', 'left'), ('top', float, 'region', 'top'), ('right', float, 'region', 'right'), ('bottom', float, 'region', 'bottom'), ('regionWidth', float, 'region', 'width'), ('regionHeight', float, 'region', 'height'), ('units', str, 'region', 'units'), ('unitsWH', str, 'region', 'unitsWH'), ('width', int, 'output', 'maxWidth'), ('height', int, 'output', 'maxHeight'), ('fill', str), ('magnification', float, 'scale', 'magnification'), ('mm_x', float, 'scale', 'mm_x'), ('mm_y', float, 'scale', 'mm_y'), ('exact', bool, 'scale', 'exact'), ('frame', int), ('encoding', str), ('jpegQuality', int), ('jpegSubsampling', int), ('tiffCompression', str), ('style', str), ('resample', 'boolOrInt'), ('contentDisposition', str), ]) _handleETag('getTilesRegion', item, params) try: regionData, regionMime = self.imageItemModel.getRegion( item, **params) except TileGeneralException as e: raise RestException(e.args[0]) except ValueError as e: raise RestException('Value Error: %s' % e.args[0]) self._setContentDisposition( item, params.get('contentDisposition'), regionMime, 'region') setResponseHeader('Content-Type', regionMime) setRawResponse() return regionData
def callbackAccount(self, provider, state, code, error): """Classical OAuth callback endpoint that parses incoming token. The main difference between this callback and /oauth/:provider/callback is that we store the incoming bearer token into User model, instead of using it to actually login to Girder. Note: We conveniently hid the userId in the OAuth state. """ if error is not None: raise RestException("Provider returned error: '%s'." % error, code=502) providerName = provider provider = providers.idMap.get(providerName) if not provider: raise RestException('Unknown provider "%s".' % providerName) self.requireParams({"state": state, "code": code}) user, redirect = self._validateCsrfToken(state) providerObj = provider(cherrypy.url()) new_token = providerObj.getToken(code) if "resource_server" not in new_token: new_token["resource_server"] = providerObj.getProviderName( external=False) new_token["provider"] = providerObj.getProviderName(external=False) user_tokens = user.get("otherTokens", []) for i, user_token in enumerate(user_tokens): if user_token["resource_server"] == new_token["resource_server"]: user_tokens[i] = new_token # update token if found. break else: user_tokens.append(new_token) # not found, append user["otherTokens"] = user_tokens user = User().save(user) self.sendAuthTokenCookie(user) raise cherrypy.HTTPRedirect(redirect)
def run_pipeline_task(self, folder, pipeline: PipelineDescription): """ Run a pipeline on a dataset. :param folder: The girder folder containing the dataset to run on. :param pipeline: The pipeline to run the dataset on. """ folder_id_str = str(folder["_id"]) # First, verify that no other outstanding jobs are running on this dataset existing_jobs = Job().findOne({ JOBCONST_DATASET_ID: folder_id_str, 'status': { # Find jobs that are inactive, queued, or running # https://github.com/girder/girder/blob/master/plugins/jobs/girder_jobs/constants.py '$in': [0, 1, 2] }, }) if existing_jobs is not None: raise RestException( (f"A pipeline for {folder_id_str} is already running. " "Only one outstanding job may be run at a time for " "a dataset.")) user = self.getCurrentUser() token = Token().createToken(user=user, days=14) move_existing_result_to_auxiliary_folder(folder, user) params: PipelineJob = { "input_folder": folder_id_str, "input_type": folder["meta"]["type"], "output_folder": folder_id_str, "pipeline": pipeline, } newjob = run_pipeline.apply_async( queue="pipelines", kwargs=dict( params=params, girder_job_title= f"Running {pipeline['name']} on {str(folder['name'])}", girder_client_token=str(token["_id"]), girder_job_type="pipelines", ), ) newjob.job[JOBCONST_DATASET_ID] = folder_id_str newjob.job[JOBCONST_RESULTS_FOLDER_ID] = folder_id_str newjob.job[JOBCONST_PIPELINE_NAME] = pipeline['name'] # Allow any users with accecss to the input data to also # see and possibly manage the job Job().copyAccessPolicies(folder, newjob.job) Job().save(newjob.job) return newjob.job
def deleteStudy(self, study, params): user = self.getCurrentUser() # For now, study admins will be the ones that can delete studies User().requireAdminStudy(user) if Study().childAnnotations(study=study, state=Study().State.COMPLETE).count(): raise RestException('Study has completed annotations.', 409) Study().remove(study) # No Content cherrypy.response.status = 204
def acceptCollectionTerms(self, collection, termsHash): if not collection.get('terms'): raise RestException('This collection currently has no terms.') # termsHash should be encoded to a bytes object, but storing bytes into MongoDB behaves # differently in Python 2 vs 3. Additionally, serializing a bytes to JSON behaves differently # in Python 2 vs 3. So, just keep it as a unicode (or ordinary Python 2 str). realTermsHash = hashlib.sha256(collection['terms'].encode('utf-8')).hexdigest() if termsHash != realTermsHash: # This "proves" that the client has at least accessed the terms raise RestException( 'The submitted "termsHash" does not correspond to the collection\'s current terms.') User().update( {'_id': self.getCurrentUser()['_id']}, {'$set': { 'terms.collection.%s' % collection['_id']: { 'hash': termsHash, 'accepted': datetime.datetime.now() } }} )
def intersects(self, field, geometry, limit, offset, sort): try: GeoJSON.to_instance(geometry, strict=True) except (TypeError, ValueError): raise RestException( "Invalid GeoJSON passed as 'geometry' parameter.") if field[:3] != '%s.' % GEOSPATIAL_FIELD: field = '%s.%s' % (GEOSPATIAL_FIELD, field) query = {field: {'$geoIntersects': {'$geometry': geometry}}} return self._find(query, limit, offset, sort)
def getPagingParameters(self, params, defaultSortField=None, defaultSortDir=SortDir.ASCENDING): """ Pass the URL parameters into this function if the request is for a list of resources that should be paginated. It will return a tuple of the form (limit, offset, sort) whose values should be passed directly into the model methods that are finding the resources. If the client did not pass the parameters, this always uses the same defaults of limit=50, offset=0, sort='name', sortdir=SortDir.ASCENDING=1. :param params: The URL query parameters. :type params: dict :param defaultSortField: If the client did not pass a 'sort' parameter, set this to choose a default sort field. If None, the results will be returned unsorted. :type defaultSortField: str or None :param defaultSortDir: Sort direction. :type defaultSortDir: girder.constants.SortDir """ try: offset = int(params.get('offset', 0)) limit = int(params.get('limit', 50)) sortdir = int(params.get('sortdir', defaultSortDir)) except ValueError: raise RestException( 'Invalid value for offset, limit, or sortdir parameter.') if sortdir not in [SortDir.ASCENDING, SortDir.DESCENDING]: raise RestException('Invalid value for sortdir parameter.') if 'sort' in params: sort = [(params['sort'].strip(), sortdir)] elif isinstance(defaultSortField, six.string_types): sort = [(defaultSortField, sortdir)] else: sort = None return limit, offset, sort
def updateColormap(self, model, name, colormap): user = self.getCurrentUser() if name is not None: model['name'] = name if colormap is not None: model['colormap'] = colormap try: return Colormap().updateColormap(model, updateUser=user) except ValidationException as exc: logger.exception('Failed to validate colormap') raise RestException( "Validation Error: JSON doesn\'t follow schema (%r)." % (exc.args, ))
def _validateCsrfToken(self, state): """ Tests the CSRF token value in the cookie to authenticate the user as the originator of the OAuth2 login. Raises a RestException if the token is invalid. """ csrfTokenId, _, redirect = state.partition('.') token = Token().load(csrfTokenId, objectId=False, level=AccessType.READ) if token is None: raise RestException('Invalid CSRF token (state="%s").' % state, code=403) Token().remove(token) if token['expires'] < datetime.datetime.utcnow(): raise RestException('Expired CSRF token (state="%s").' % state, code=403) if not redirect: raise RestException('No redirect location (state="%s").' % state) return redirect
def createTiles(self, item, params): largeImageFileId = params.get('fileId') if largeImageFileId is None: files = list(Item().childFiles(item=item, limit=2)) if len(files) == 1: largeImageFileId = str(files[0]['_id']) if not largeImageFileId: raise RestException('Missing "fileId" parameter.') largeImageFile = File().load(largeImageFileId, force=True, exc=True) user = self.getCurrentUser() token = self.getCurrentToken() try: return self.imageItemModel.createImageItem(item, largeImageFile, user, token, notify=self.boolParam( 'notify', params, default=True)) except TileGeneralException as e: raise RestException(e.args[0])
def getUser(self, token): headers = {'Authorization': 'Bearer {}'.format(token['access_token'])} resp = self._getJson(method='GET', url=self._API_USER_URL, headers=headers) oauthId = resp.get('sub') if not oauthId: raise RestException('Globus identity did not return a valid ID.', code=502) email = resp.get('email') if not email: raise RestException( 'Globus identity did not return a valid email.', code=502) name = resp['name'].split() firstName = name[0] lastName = name[-1] return self._createOrReuseUser(oauthId, email, firstName, lastName)
def createThumbnail(self, file, width, height, crop, attachToId, attachToType): user = self.getCurrentUser() ModelImporter.model(attachToType).load( attachToId, user=user, level=AccessType.WRITE, exc=True) width = max(width, 0) height = max(height, 0) if not width and not height: raise RestException('You must specify a valid width, height, or both.') return utils.scheduleThumbnailJob(file, attachToType, attachToId, user, width, height, crop)