def log(self, id, params): user = getCurrentUser() offset = 0 if 'offset' in params: offset = int(params['offset']) if not self._model.load(id, user=user, level=AccessType.READ): raise RestException('Volume not found.', code=404) log_records = self._model.log_records(user, id, offset) return {'log': log_records}
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 _getGeometry(self, params): try: geometry = bson.json_util.loads(params['geometry']) 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 setGeospatial(self, item, params): """ Set geospatial data on an item. :param item: item on which to set geospatial data. :type item: dict[str, unknown] :param params: parameters to the API call, unused. :type params: dict[str, unknown] :returns: filtered fields of the item with geospatial data appended to its 'geo' field. :rtype : dict[str, unknown] :raise RestException: on malformed, forbidden, or unauthorized API call. """ geospatial = self.getBodyJson() for k, v in six.viewitems(geospatial): if '.' in k or k[0] == '$': raise RestException('Geospatial key name %s must not contain a' ' period or begin with a dollar sign.' % k) if v: try: GeoJSON.to_instance(v, strict=True) except ValueError: raise RestException('Geospatial field with key %s does not' ' contain valid GeoJSON: %s' % (k, v)) if GEOSPATIAL_FIELD not in item: item[GEOSPATIAL_FIELD] = dict() item[GEOSPATIAL_FIELD].update(six.viewitems(geospatial)) keys = [ k for k, v in six.viewitems(item[GEOSPATIAL_FIELD]) if v is None ] for key in keys: del item[GEOSPATIAL_FIELD][key] item = self.model('item').updateItem(item) return self._filter(item)
def login(self, params): """ Login endpoint. Sends an auth cookie in the response on success. The caller is expected to use HTTP Basic Authentication when calling this endpoint. """ 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('Girder-Authorization') if not authHeader: authHeader = cherrypy.request.headers.get('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) login = login.lower().strip() loginField = 'email' if '@' in login else 'login' user = self.model('user').findOne({loginField: login}) if user is None: raise RestException('Login failed.', code=403) if not self.model('password').authenticate(user, password): raise RestException('Login failed.', code=403) if self.model('user').emailVerificationRequired(user): raise RestException( 'Email verification required.', code=403, extra='emailVerification') if self.model('user').adminApprovalRequired(user): raise RestException( 'Account approval required.', code=403, extra='accountApproval') setattr(cherrypy.request, 'girderUser', user) token = self.sendAuthTokenCookie(user) return { 'user': self.model('user').filter(user, user), 'authToken': { 'token': token['_id'], 'expires': token['expires'], 'scope': token['scope'] }, 'message': 'Login succeeded.' }
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'] 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]) self._passArg(fun, kwargs, info['destName'], 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'], None) else: self._passArg(fun, kwargs, name, None) self._mungeKwargs(kwargs, fun) return fun(*args, **kwargs)
def callback(self, provider, params): if 'error' in params: raise RestException("Provider returned error: '%s'." % params['error'], code=502) self.requireParams(('state', 'code'), params) providerName = provider provider = providers.idMap.get(providerName) if not provider: raise RestException("Unknown provider '%s'." % providerName) redirect = self._validateCsrfToken(params['state']) providerObj = provider(cherrypy.url()) token = providerObj.getToken(params['code']) user = providerObj.getUser(token) self.sendAuthTokenCookie(user) raise cherrypy.HTTPRedirect(redirect)
def _add_indexed_output_param(param, args, user, result_hooks): value = args[param.identifier()] folder = args[param.identifier() + FOLDER_SUFFIX] folderModel = ModelImporter.model('folder') instance = folderModel.load(folder, level=AccessType.WRITE, user=user) if not instance: raise RestException('Invalid Folder id (%s).' % (str(folder))) # Output Binding !! path = VolumePath(value) result_hooks.append(GirderUploadVolumePathToFolder(path, folder)) return path
def image_shape(self, id, user, name): path = '/stem/images' with self._open_h5py_file(id, user) as rf: dataset = rf[path] if self._str_is_int(name): index = int(name) if index >= len(dataset): raise RestException('Index is too large') else: index = self._get_image_index_from_name(dataset, name) return dataset[index].shape
def mongoSearch(self, params): self.requireParams(('type', 'q'), params) allowed = { 'collection': ['_id', 'name', 'description'], 'folder': ['_id', 'name', 'description'], 'item': ['_id', 'name', 'description', 'folderId'], 'user': ['_id', 'firstName', 'lastName', 'login'] } limit, offset, sort = self.getPagingParameters(params, 'name') coll = params['type'] events.trigger('mongo_search.allowed_collections', info=allowed) if coll not in allowed: raise RestException('Invalid resource type: {}'.format(coll)) try: query = bson.json_util.loads(params['q']) except ValueError: raise RestException('The query parameter must be a JSON object.') model = ModelImporter().model(coll) if hasattr(model, 'filterResultsByPermission'): cursor = model.find(query, fields=allowed[coll] + ['public', 'access']) return list( model.filterResultsByPermission(cursor, user=self.getCurrentUser(), level=AccessType.READ, limit=limit, offset=offset, removeKeys=('public', 'access'))) else: return list( model.find(query, fields=allowed[coll], limit=limit, offset=offset))
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 updateGraph(self, graphObj, graph, params): """Update graph.""" user = self.getCurrentUser() graphObj['name'] = graph['name'] graphObj['content'] = graph['content'] if 'public' in graph and graph['public'] and not user['admin']: raise RestException('Not authorized to create public graphs', 403) elif 'public' in graph: graphObj['public'] = graph['public'] return self.model('graph', 'cis').updateGraph(graphObj)
def resetPassword(self, email): user = self.model('user').findOne({'email': email.lower()}) if user is None: raise RestException('That email is not registered.') randomPass = genToken(length=12) html = mail_utils.renderTemplate('resetPassword.mako', { 'password': randomPass }) mail_utils.sendEmail(to=email, subject='Girder: Password reset', text=html) self.model('user').setPassword(user, randomPass) return {'message': 'Sent password reset email.'}
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.model('image_item', 'large_image').getMetadata( item, **imageArgs) except TileGeneralException as e: raise RestException(e.message, code=400)
def launch_taskflow(user, body): # Perform some validation taskFlowBody = body.get('taskFlowBody') if taskFlowBody is None: raise RestException('taskFlowBody is a required key') if 'taskFlowClass' not in taskFlowBody: raise RestException('taskFlowClass is required in taskFlowBody') taskflow_class = taskFlowBody['taskFlowClass'] # Check that we can load the taskflow class try: load_class(taskflow_class) except Exception as ex: msg = 'Unable to load taskflow class: %s (%s)' % \ (taskflow_class, ex) raise RestException(msg, 400) # Set up the taskflow input taskFlowInput = body.get('taskFlowInput', {}) if 'cluster' not in taskFlowInput: # Make a cluster taskFlowInput['cluster'] = create_cluster_object(user) if 'container' not in taskFlowInput: taskFlowInput['container'] = 'docker' # Load the queue queue = fetch_or_create_queue(user) # Create the taskflow taskflow = TaskflowModel().create(user, taskFlowBody) # Add it to the queue and start it QueueModel().add(queue, taskflow, taskFlowInput, user) QueueModel().pop(queue, limit=sys.maxsize, user=user) return taskflow['_id']
def searchCase(self, params): user = self.getCurrentUser() limit, offset, sort = self.getPagingParameters(params, 'name') self.requireParams('table', params) table = params.get('table') key = params.get('key') value = params.get('value') substring = params.get('substring') if value and substring: raise RestException('Cannot search by both value and substring') if (value or substring) and not key: raise RestException('A key must be provided to search by value') if key and invalid_key_re.search(key): raise RestException('Invalid key parameter') query = {} if not key: query = {'tcga.meta.' + table: {'$exists': True}} elif not value and not substring: query = {'tcga.meta.' + table + '.' + key: {'$exists': True}} elif value: query = {'tcga.meta.' + table + '.' + key: value} else: query = { 'tcga.meta.' + table + '.' + key: re.compile(re.escape(substring)) } cursor = self.model('case', 'digital_slide_archive').find(query, user=user, offset=offset, limit=limit, sort=sort) return pagedResponse(cursor, limit, offset, sort)
def updateAccess(self, challenge, params): self.requireParams('access', params) public = self.boolParam('public', params, default=False) self.model('challenge', 'covalic').setPublic(challenge, public) try: access = json.loads(params['access']) return self.model('challenge', 'covalic').setAccessList(challenge, access, save=True) except ValueError: raise RestException('The access parameter must be JSON.')
def _get_h5_dataset(self, id, user, path, offset=None, limit=None, format='bytes'): if format == 'bytes' or format is None: return self._get_h5_dataset_bytes(id, user, path, offset, limit) elif format == 'msgpack': return self._get_h5_dataset_msgpack(id, user, path, offset, limit) else: raise RestException('Unknown format: ' + format)
def cullNotebooks(self): # SC16: query all tmpnb_urls here and aggregate the results # may not be necessary if we disable 'heartbeat', but # I'm annotating it for completeness resp = requests.get( self.model('setting').get(PluginSettings.TMPNB_URL)) content = resp.content if isinstance(content, six.binary_type): content = content.decode('utf8') try: resp.raise_for_status() except requests.HTTPError: raise RestException('Got %s code from tmpnb, response="%s"/' % (resp.status_code, content), code=502) try: activity = json.loads(content) except ValueError: raise RestException('Non-JSON response: %s' % content, code=502) admin = next(_ for _ in self.model('user').getAdmins()) token = self.model('token').createToken(user=admin, days=1) # Iterate over all notebooks, not the prettiest way... cull_period = self.model('setting').get(PluginSettings.CULLING_PERIOD, '4') cull_time = datetime.datetime.utcnow() - \ datetime.timedelta(hours=float(cull_period)) for nb in self.find({}): try: last_activity = dateutil.parser.parse( activity[nb['containerId']], ignoretz=True) except KeyError: # proxy is not aware of such container, kill it... logger.info('Deleting nb %s' % nb['_id']) self.deleteNotebook(nb, token) if last_activity < cull_time: logger.info('Deleting nb %s' % nb['_id']) self.deleteNotebook(nb, token)
def import_script(self, id, params): user = self.getCurrentUser() lines = cherrypy.request.body.read().decode('utf8').splitlines() script = self._model.load(id, user=user, level=AccessType.ADMIN) if not script: raise RestException('Script doesn\'t exist', code=404) script['commands'] = lines self._model.save(script) return self._clean(script)
def createThumbnail(self, file, params): self.requireParams(('attachToId', 'attachToType'), params) user = self.getCurrentUser() width = params.get('width') height = params.get('height') if params['attachToType'] not in ( 'item', 'collection', 'user', 'folder'): raise RestException('You can only attach thumbnails to users, ' 'folders, collections, or items.') self.model(params['attachToType']).load( params['attachToId'], user=user, level=AccessType.WRITE, exc=True) width = max(int(params.get('width', 0)), 0) height = max(int(params.get('height', 0)), 0) if not width and not height: raise RestException( 'You must specify a valid width, height, or both.') kwargs = { 'width': width, 'height': height, 'fileId': str(file['_id']), 'crop': self.boolParam('crop', params, default=True), 'attachToType': params['attachToType'], 'attachToId': params['attachToId'] } job = self.model('job', 'jobs').createLocalJob( title='Generate thumbnail for %s' % file['name'], user=user, type='thumbnails.create', public=False, kwargs=kwargs, module='girder.plugins.thumbnails.worker') self.model('job', 'jobs').scheduleJob(job) return job
def max_instances(user, profile, params): client = get_ec2_client(profile) response = client.describe_account_attributes( AttributeNames=['max-instances']) jsonpath = 'AccountAttributes[0].AttributeValues[0].AttributeValue' max_instances = parse(jsonpath).find(response) if max_instances: max_instances = max_instances[0].value else: raise RestException('Unable to extract "max-instances" attribute.') return {'maxinstances': max_instances}
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.') ModelImporter.model('user').update( {'_id': self.getCurrentUser()['_id']}, {'$set': { 'terms.collection.%s' % collection['_id']: { 'hash': termsHash, 'accepted': datetime.datetime.now() } }} )
def delete_profile(user, profile, params): query = {'profileId': profile['_id']} if ModelImporter.model('volume', 'cumulus').findOne(query): raise RestException( 'Unable to delete profile as it is associated with' ' a volume', 400) if ModelImporter.model('cluster', 'cumulus').findOne(query): raise RestException( 'Unable to delete profile as it is associated with' ' a cluster', 400) # Clean up key associate with profile cumulus.aws.ec2.tasks.key.delete_key_pair.delay(_filter(profile), get_task_token()['_id']) client = get_ec2_client(profile) client.delete_key_pair(KeyName=str(profile['_id'])) ModelImporter.model('aws', 'cumulus').remove(profile)
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 deleteFeatureset(self, featureset, params): user = self.getCurrentUser() # For now, study admins will be the ones that can delete featuresets User().requireAdminStudy(user) if Study().find({'meta.featuresetId': featureset['_id']}).count(): raise RestException('Featureset is in use by one or more studies.', 409) Featureset().remove(featureset) # No Content cherrypy.response.status = 204
def createGeojsonDataset(self, item, params): user = self.getCurrentUser() folder = findDatasetFolder(user, user, create=True) if folder is None: raise RestException('User has no Minerva Dataset folder.') if folder['_id'] != item['folderId']: raise RestException("Items need to be in user's Minerva Dataset " + "folder.") minerva_metadata = { 'original_type': 'geojson', 'dataset_type': 'geojson', } # Use the first geojson or json file found as the dataset. for file in self.model('item').childFiles(item=item, limit=0): if ('geojson' in file['exts'] or 'json' in file['exts'] or file.get('mimeType') in ( 'application/json', 'application/vnd.geo+json', )): minerva_metadata['original_files'] = [{ 'name': file['name'], '_id': file['_id'] }] minerva_metadata['geojson_file'] = { 'name': file['name'], '_id': file['_id'] } minerva_metadata['geo_render'] = { 'type': 'geojson', 'file_id': file['_id'] } minerva_metadata['original_type'] = 'geojson' minerva_metadata['source'] = {'layer_source': 'GeoJSON'} minerva_metadata['source_type'] = 'item' break if 'geojson_file' not in minerva_metadata: raise RestException('Item contains no geojson file.') updateMinervaMetadata(item, minerva_metadata) return item
def getHistograms(self, item, params): if 'meta' not in item or 'rlab' not in item[ 'meta'] or 'schema' not in item['meta']['rlab']: raise RestException('Item ' + str(item['_id']) + ' has no schema information.') params, binSettings = self.fillInDefaultHistogramParams(item, params) # Stringify the params for cache hashing paramsCode = json.dumps(params, sort_keys=True) # Check if this query has already been run - if so, return the cached result if params['cache']: paramsMD5 = md5.md5(paramsCode).hexdigest() if 'histogramCaches' in item['meta']['rlab'] and paramsMD5 in item[ 'meta']['rlab']['histogramCaches']: return item['meta']['rlab']['histogramCaches'][paramsMD5] # Construct and run the histogram MapReduce code mapScript = 'function map () {\n' + \ self.foreignCode['binUtils.js'] + '\n' + \ self.foreignCode['histogram_map.js'] + '\n}' reduceScript = 'function reduce (attrName, allHistograms) {\n' + \ self.foreignCode['histogram_reduce.js'] + '\n' + \ 'return {histogram: histogram};\n}' histogram = self.mapReduce(item, mapScript, reduceScript, params) # We have to clean up the histogram wrappers (mongodb can't return # an array from reduce functions). for attrName, wrappedHistogram in histogram.iteritems(): histogram[attrName] = wrappedHistogram['histogram'] if '__passedFilters__' not in histogram: # This will only happen if there's a count of zero; # the __passedFilters__ bin will never have been emitted histogram['__passedFilters__'] = [{'count': 0, 'label': 'count'}] # Cache the results before returning them if params['cache']: if 'histogramCaches' not in item['meta']['rlab']: item['meta']['rlab']['histogramCaches'] = {} item['meta']['rlab']['histogramCaches'][paramsMD5] = histogram try: self.model('item').updateItem(item) except AccessException: # Meh, we couldn't cache the result. Not a big enough deal # to throw / display errors, so just fail silently pass return histogram
def create_calc(self, params): body = getBodyJson() if 'cjson' not in body and ('fileId' not in body or 'format' not in body): raise RestException('Either cjson or fileId is required.') user = getCurrentUser() cjson = body.get('cjson') props = body.get('properties', {}) molecule_id = body.get('moleculeId', None) geometry_id = body.get('geometryId', None) public = body.get('public', True) notebooks = body.get('notebooks', []) image = body.get('image') input_parameters = body.get('input', {}).get('parameters') if input_parameters is None: input_parameters = body.get('inputParameters', {}) file_id = None file_format = body.get('format', 'cjson') if 'fileId' in body: file = File().load(body['fileId'], user=getCurrentUser()) file_id = file['_id'] cjson = self._file_to_cjson(file, file_format) if molecule_id is None: mol = create_molecule(json.dumps(cjson), 'cjson', user, public, parameters=input_parameters) molecule_id = mol['_id'] calc = CalculationModel().create_cjson( user, cjson, props, molecule_id, geometry_id=geometry_id, image=image, input_parameters=input_parameters, file_id=file_id, notebooks=notebooks, public=public) cherrypy.response.status = 201 cherrypy.response.headers['Location'] \ = '/calculations/%s' % (str(calc['_id'])) return CalculationModel().filter(calc, user)
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