class Homepage(Resource): def __init__(self): super(Homepage, self).__init__() self.resourceName = 'homepage' self.route('GET', (), self.getSettings) self.route('GET', ('assets', ), self.getAssets) @access.public @autoDescribeRoute( Description('Public url for getting the homepage properties.')) def getSettings(self): settings = Setting() return { PluginSettings.MARKDOWN: settings.get(PluginSettings.MARKDOWN), PluginSettings.HEADER: settings.get(PluginSettings.HEADER), PluginSettings.SUBHEADER: settings.get(PluginSettings.SUBHEADER), PluginSettings.WELCOME_TEXT: settings.get(PluginSettings.WELCOME_TEXT), PluginSettings.LOGO: settings.get(PluginSettings.LOGO), } @access.admin @autoDescribeRoute( Description('Return the folder IDs for uploaded asset content.')) def getAssets(self): return { # Keep MARKDOWN folder as 'Homepage Assets', for compatibility PluginSettings.MARKDOWN: self._getAssetsFolder('Homepage Assets')['_id'], PluginSettings.WELCOME_TEXT: self._getAssetsFolder('Welcome Text')['_id'], PluginSettings.LOGO: self._getAssetsFolder('Logo')['_id'], } def _getAssetsFolder(self, folderName): """ Get or create a public folder, in the private "Homepage Assets" collection. This makes the folder effectively "unlisted" as it can't be browsed to by normal users, but its content can still be downloaded directly. :param folderName: The name of the folder to get or create. :return: The new folder document. """ collection = Collection().createCollection(constants.COLLECTION_NAME, public=False, reuseExisting=True) folder = Folder().createFolder(collection, folderName, parentType='collection', public=True, reuseExisting=True) return folder
class Sentry(Resource): def __init__(self): super(Sentry, self).__init__() self.resourceName = 'sentry' self.route('GET', ('dsn', ), self._getDsn) @access.public @describeRoute(Description('Public URL for getting the Sentry DSN.')) def _getDsn(self, params): dsn = Setting().get(PluginSettings.FRONTEND_DSN) return {'sentry_dsn': dsn}
class GoogleAnalytics(Resource): def __init__(self): super(GoogleAnalytics, self).__init__() self.resourceName = 'google_analytics' self.route('GET', ('id',), self.getId) @access.public @describeRoute( Description('Public url for getting the Google Analytics tracking id.') ) def getId(self, params): trackingId = Setting().get(PluginSettings.TRACKING_ID) return {'google_analytics_id': trackingId}
class Thumbnail(Resource): def __init__(self): super(Thumbnail, self).__init__() self.resourceName = 'thumbnail' self.route('POST', (), self.createThumbnail) @access.user @filtermodel(model=Job) @autoDescribeRoute( Description('Create a new thumbnail from an existing image file.') .notes('Setting a width or height parameter of 0 will preserve the ' 'original aspect ratio.') .modelParam('fileId', 'The ID of the source file.', model=File, paramType='formData', level=AccessType.READ) .param('width', 'The desired width.', required=False, dataType='integer', default=0) .param('height', 'The desired height.', required=False, dataType='integer', default=0) .param('crop', 'Whether to crop the image to preserve aspect ratio. ' 'Only used if both width and height parameters are nonzero.', dataType='boolean', required=False, default=True) .param('attachToId', 'The lifecycle of this thumbnail is bound to the ' 'resource specified by this ID.') .param('attachToType', 'The type of resource to which this thumbnail is attached.', enum=['folder', 'user', 'collection', 'item']) .errorResponse() .errorResponse(('Write access was denied on the attach destination.', 'Read access was denied on the file.'), 403) ) 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)
class DicomItem(Resource): @access.user(scope=TokenScope.DATA_READ) @autoDescribeRoute( Description( 'Get and store common DICOM metadata, if any, for all files in the item.' ).modelParam( 'id', 'The item ID', model='item', level=AccessType.WRITE, paramType='path').errorResponse('ID was invalid.').errorResponse( 'Read permission denied on the item.', 403)) def makeDicomItem(self, item): """ Try to convert an existing item into a "DICOM item", which contains a "dicomMeta" field with DICOM metadata that is common to all DICOM files. """ metadataReference = None dicomFiles = [] for file in Item().childFiles(item): dicomMeta = _parseFile(file) if dicomMeta is None: continue dicomFiles.append(_extractFileData(file, dicomMeta)) metadataReference = (dicomMeta if metadataReference is None else _removeUniqueMetadata( metadataReference, dicomMeta)) setResponseTimeLimit() if dicomFiles: # Sort the dicom files dicomFiles.sort(key=_getDicomFileSortKey) # Store in the item item['dicom'] = {'meta': metadataReference, 'files': dicomFiles} # Save the item Item().save(item)
class AuthorizedUpload(Resource): def __init__(self): super(AuthorizedUpload, self).__init__() self.resourceName = 'authorized_upload' self.route('POST', (), self.createAuthorizedUpload) @access.user(scope=TokenScope.DATA_WRITE) @loadmodel(map={'folderId': 'folder'}, model='folder', level=AccessType.WRITE) @describeRoute( Description('Create an authorized upload URL.').param( 'folderId', 'Destination folder ID for the upload.').param( 'duration', 'How many days the token should last.', required=False, dataType='int')) def createAuthorizedUpload(self, folder, params): try: if params.get('duration'): days = int(params.get('duration')) else: days = Setting().get(SettingKey.COOKIE_LIFETIME) except ValueError: raise ValidationException( 'Token duration must be an integer, or leave it empty.') token = Token().createToken( days=days, user=self.getCurrentUser(), scope=(TOKEN_SCOPE_AUTHORIZED_UPLOAD, 'authorized_upload_folder_%s' % folder['_id'])) url = '%s#authorized_upload/%s/%s' % (mail_utils.getEmailUrlPrefix(), folder['_id'], token['_id']) return {'url': url}
class _WebClientTestEndpoints(Resource): def __init__(self): super(_WebClientTestEndpoints, self).__init__() self.route('GET', ('progress', ), self.testProgress) self.route('PUT', ('progress', 'stop'), self.testProgressStop) self.route('POST', ('file', ), self.uploadFile) self.route('POST', ('access_flag', ), self.registerAccessFlags) self.stop = False @access.token @describeRoute( Description('Test progress contexts from the web').param( 'test', 'Name of test to run. These include "success" and "failure".', required=False).param('duration', 'Duration of the test in seconds', required=False, dataType='int'). param('resourceId', 'Resource ID associated with the progress notification.', required=False). param( 'resourceName', 'Type of resource associated with the progress notification.', required=False)) def testProgress(self, params): test = params.get('test', 'success') duration = int(params.get('duration', 10)) resourceId = params.get('resourceId', None) resourceName = params.get('resourceName', None) startTime = time.time() with ProgressContext(True, user=self.getCurrentUser(), title='Progress Test', message='Progress Message', total=duration, resource={'_id': resourceId}, resourceName=resourceName) as ctx: for current in range(duration): if self.stop: break ctx.update(current=current) wait = startTime + current + 1 - time.time() if wait > 0: time.sleep(wait) if test == 'error': raise RestException('Progress error test.') @access.token @describeRoute(Description('Halt all progress tests')) def testProgressStop(self, params): self.stop = True @access.user @describeRoute(None) def uploadFile(self, params): """ Providing this works around a limitation in phantom that makes us unable to upload binary files, or at least ones that contain certain byte values. The path parameter should be provided relative to the root directory of the repository. """ self.requireParams(('folderId', 'path'), params) if params['path'].startswith( '${'): # relative to plugin e.g. ${my_plugin}/path end = params['path'].find('}') plugin = params['path'][2:end] plugin = getPlugin(plugin) if plugin is None: raise Exception('Invalid plugin %s.' % plugin) root = os.path.dirname(inspect.getfile(plugin.__class__)) path = root + params['path'][end + 1:] else: # assume relative to core package path = os.path.join(ROOT_DIR, params['path']) name = os.path.basename(path) folder = Folder().load(params['folderId'], force=True) upload = Upload().createUpload(user=self.getCurrentUser(), name=name, parentType='folder', parent=folder, size=os.path.getsize(path)) with open(path, 'rb') as fd: file = Upload().handleChunk(upload, fd) return file @access.public @describeRoute(None) def registerAccessFlags(self, params): """ Helper that can be used to register access flags in the system. This is used to test the access flags UI since the core does not expose any flags. """ flags = self.getBodyJson() for key, info in six.viewitems(flags): registerAccessFlag(key, info['name'], info['description'], info['admin'])
class QuotaPolicy(Resource): def _filter(self, model, resource): """ Filter a resource to include only the ordinary data and the quota field. :param model: the type of resource (e.g., user or collection) :param resource: the resource document. :returns: filtered field of the resource with the quota data, if any. """ filtered = ModelImporter.model(model).filter(resource, self.getCurrentUser()) filtered[QUOTA_FIELD] = resource.get(QUOTA_FIELD, {}) return filtered def _setResourceQuota(self, model, resource, policy): """ Handle setting quota policies for any resource that supports them. :param model: the type of resource (e.g., user or collection) :param resource: the resource document. :param params: the query parameters. 'policy' is required and used. :returns: the updated resource document. """ policy = self._validatePolicy(policy) if QUOTA_FIELD not in resource: resource[QUOTA_FIELD] = {} resource[QUOTA_FIELD].update(policy) ModelImporter.model(model).save(resource, validate=False) return self._filter(model, resource) def _validate_fallbackAssetstore(self, value): """Validate the fallbackAssetstore parameter. :param value: the proposed value. :returns: the validated value: either None or 'current' to use the current assetstore, 'none' to disable a fallback assetstore, or an assetstore ID. """ if not value or value == 'current': return None if value == 'none': return value try: value = ObjectId(value) except InvalidId: raise RestException( 'Invalid fallbackAssetstore. Must either be an assetstore ' 'ID, be blank or "current" to use the current assetstore, or ' 'be "none" to disable fallback usage.', extra='fallbackAssetstore') return value 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 _validate_preferredAssetstore(self, value): """Validate the preferredAssetstore parameter. :param value: the proposed value. :returns: the validated value: either None or 'current' to use the current assetstore or an assetstore ID. """ if not value or value == 'current': return None try: value = ObjectId(value) except InvalidId: raise RestException( 'Invalid preferredAssetstore. Must either be an assetstore ' 'ID, or be blank or "current" to use the current assetstore.', extra='preferredAssetstore') return value def _validate_useQuotaDefault(self, value): """Validate the useQuotaDefault parameter. :param value: the proposed value. :returns: the validated value :rtype: None or bool """ if str(value).lower() in ('none', 'true', 'yes', '1'): return True if str(value).lower() in ('false', 'no', '0'): return False raise RestException( 'Invalid useQuotaDefault. Must either be true or false.', extra='useQuotaDefault') def _validatePolicy(self, policy): """ Validate a policy JSON object. Only a limited set of keys is supported, and each of them has a restricted data type. :param policy: JSON object to validate. This may also be a Python dictionary as if the JSON was already decoded. :returns: a validate policy dictionary. """ validKeys = [k[10:] for k in dir(self) if k.startswith('_validate_')] policy = { k: v for k, v in six.iteritems(policy) if not k.startswith('_') } for key in policy: if key not in validKeys: raise RestException( '%s is not a valid quota policy key. Valid keys are %s.' % (key, ', '.join(sorted(validKeys)))) policy[key] = getattr(self, '_validate_' + key)(policy[key]) return policy @access.user @autoDescribeRoute( Description('Get quota and assetstore policies for the collection.'). modelParam('id', 'The collection ID', model=Collection, level=AccessType.ADMIN).errorResponse('ID was invalid.')) def getCollectionQuota(self, collection): if QUOTA_FIELD not in collection: collection[QUOTA_FIELD] = {} collection[QUOTA_FIELD][ '_currentFileSizeQuota'] = self._getFileSizeQuota( 'collection', collection) return self._filter('collection', collection) @access.admin @autoDescribeRoute( Description('Set quota and assetstore policies for the collection.'). modelParam( 'id', 'The collection ID', model=Collection, level=AccessType.ADMIN).jsonParam( 'policy', 'A JSON object containing the policies. This is a ' 'dictionary of keys and values. Any key that is not specified ' 'does not change.', requireObject=True).errorResponse('ID was invalid.')) def setCollectionQuota(self, collection, policy): return self._setResourceQuota('collection', collection, policy) @access.user @autoDescribeRoute( Description( 'Get quota and assetstore policies for the user.').modelParam( 'id', 'The user ID', model=User, level=AccessType.ADMIN).errorResponse('ID was invalid.')) def getUserQuota(self, user): if QUOTA_FIELD not in user: user[QUOTA_FIELD] = {} user[QUOTA_FIELD]['_currentFileSizeQuota'] = self._getFileSizeQuota( 'user', user) return self._filter('user', user) @access.admin @autoDescribeRoute( Description( 'Set quota and assetstore policies for the user.').modelParam( 'id', 'The user ID', model=User, level=AccessType.ADMIN). jsonParam( 'policy', 'A JSON object containing the policies. This is a ' 'dictionary of keys and values. Any key that is not specified ' 'does not change.', requireObject=True).errorResponse('ID was invalid.').errorResponse( 'Read permission denied on the user.', 403)) def setUserQuota(self, user, policy): return self._setResourceQuota('user', user, policy) def _checkAssetstore(self, assetstoreSpec): """ Check is a specified assetstore is available. :param assetstoreSpec: None for use current assetstore, 'none' to disallow the assetstore, or an assetstore ID to check if that assetstore exists and is nominally available. :returns: None to use the current assetstore, False to indicate no assetstore is allowed, or an assetstore document of an allowed assetstore. """ if assetstoreSpec is None: return None if assetstoreSpec == 'none': return False assetstore = Assetstore().load(id=assetstoreSpec) if not assetstore: return False adapter = assetstore_utilities.getAssetstoreAdapter(assetstore) if getattr(adapter, 'unavailable', False): return False return assetstore def _getBaseResource(self, model, resource): """ Get the base resource for something pertaining to quota policies. If the base resource has no quota policy, return (None, None). :param model: the initial model type. Could be file, item, folder, user, or collection. :param resource: the initial resource document. :returns: A pair ('model', 'resource'), where 'model' is the base model type, either 'user' or 'collection'., and 'resource' is the base resource document or the id of that document. """ if isinstance(resource, six.string_types + (ObjectId, )): try: resource = ModelImporter.model(model).load(id=resource, force=True) except ImportError: return None, None if model == 'file': model = 'item' resource = Item().load(id=resource['itemId'], force=True) if model in ('folder', 'item'): if ('baseParentType' not in resource or 'baseParentId' not in resource): resource = ModelImporter.model(model).load(id=resource['_id'], force=True) if ('baseParentType' not in resource or 'baseParentId' not in resource): return None, None model = resource['baseParentType'] resourceId = resource['baseParentId'] resource = ModelImporter.model(model).load(id=resourceId, force=True) if model in ('user', 'collection') and resource: # Ensure the base resource has a quota field so we can use the # default quota if appropriate if QUOTA_FIELD not in resource: resource[QUOTA_FIELD] = {} if not resource or QUOTA_FIELD not in resource: return None, None return model, resource def getUploadAssetstore(self, event): """ Handle the model.upload.assetstore event. This event passes a dictionary consisting of a model type and resource document. If the base document has an assetstore policy, then set an assetstore key of this dictionary to an assetstore document that should be used or prevent the default action if no appropriate assetstores are allowed. :param event: event record. """ model, resource = self._getBaseResource(event.info['model'], event.info['resource']) if resource is None: return policy = resource[QUOTA_FIELD] assetstore = self._checkAssetstore( policy.get('preferredAssetstore', None)) if assetstore is False: assetstore = self._checkAssetstore( policy.get('fallbackAssetstore', None)) if assetstore is not False: logger.info( 'preferredAssetstore not available for %s %s, ' 'using fallbackAssetstore', model, resource['_id']) if assetstore is False: raise GirderException('Required assetstore is unavailable') if assetstore: event.addResponse(assetstore) def _getFileSizeQuota(self, model, resource): """ Get the current fileSizeQuota for a resource. This takes the default quota into account if necessary. :param model: the type of resource (e.g., user or collection) :param resource: the resource document. :returns: the fileSizeQuota. None for no quota (unlimited), otherwise a positive integer. """ useDefault = resource[QUOTA_FIELD].get('useQuotaDefault', True) quota = resource[QUOTA_FIELD].get('fileSizeQuota', None) if useDefault: if model == 'user': key = PluginSettings.DEFAULT_USER_QUOTA elif model == 'collection': key = PluginSettings.DEFAULT_COLLECTION_QUOTA else: key = None if key: quota = Setting().get(key) if not quota or quota < 0 or not isinstance(quota, six.integer_types): return None return quota def _checkUploadSize(self, upload): """ Check if an upload will fit within a quota restriction. :param upload: an upload document. :returns: None if the upload is allowed, otherwise a dictionary of information about the quota restriction. """ origSize = 0 if 'fileId' in upload: file = File().load(id=upload['fileId'], force=True) origSize = int(file.get('size', 0)) model, resource = self._getBaseResource('file', file) else: model, resource = self._getBaseResource(upload['parentType'], upload['parentId']) if resource is None: return None fileSizeQuota = self._getFileSizeQuota(model, resource) if not fileSizeQuota: return None newSize = resource['size'] + upload['size'] - origSize # always allow replacement with a smaller object if newSize <= fileSizeQuota or upload['size'] < origSize: return None left = fileSizeQuota - resource['size'] if left < 0: left = 0 return { 'fileSizeQuota': fileSizeQuota, 'sizeNeeded': upload['size'] - origSize, 'quotaLeft': left, 'quotaUsed': resource['size'] } def checkUploadStart(self, event): """ Check if an upload will fit within a quota restriction. This is before the upload occurs, but since multiple uploads can be started concurrently, we also have to check when the upload is being completed. :param event: event record. """ if '_id' in event.info: return quotaInfo = self._checkUploadSize(event.info) if quotaInfo: raise ValidationException( 'Upload would exceed file storage quota (need %s, only %s ' 'available - used %s out of %s)' % (formatSize(quotaInfo['sizeNeeded']), formatSize(quotaInfo['quotaLeft']), formatSize(quotaInfo['quotaUsed']), formatSize(quotaInfo['fileSizeQuota'])), field='size') def checkUploadFinalize(self, event): """ Check if an upload will fit within a quota restriction before finalizing it. If it doesn't, discard it. :param event: event record. """ upload = event.info quotaInfo = self._checkUploadSize(upload) if quotaInfo: # Delete the upload Upload().cancelUpload(upload) raise ValidationException( 'Upload exceeded file storage quota (need %s, only %s ' 'available - used %s out of %s)' % (formatSize(quotaInfo['sizeNeeded']), formatSize(quotaInfo['quotaLeft']), formatSize(quotaInfo['quotaUsed']), formatSize(quotaInfo['fileSizeQuota'])), field='size')
class HashedFile(File): @property def supportedAlgorithms(self): girderformindlogger.logger.warning( 'HashedFile.supportedAlgorithms is deprecated, use the module-level ' 'SUPPORTED_ALGORITHMS instead.') return SUPPORTED_ALGORITHMS def __init__(self, node): super(HashedFile, self).__init__() node.route('GET', ('hashsum', ':algo', ':hash'), self.getByHash) node.route('GET', ('hashsum', ':algo', ':hash', 'download'), self.downloadWithHash) node.route('GET', (':id', 'hashsum_file', ':algo'), self.downloadKeyFile) node.route('POST', (':id', 'hashsum'), self.computeHashes) @access.public(scope=TokenScope.DATA_READ, cookie=True) @autoDescribeRoute( Description( 'Download the hashsum key file for a given file.').modelParam( 'id', 'The ID of the file.', model=FileModel, level=AccessType.READ).param('algo', 'The hashsum algorithm.', paramType='path', lower=True, enum=SUPPORTED_ALGORITHMS). notes( "This is meant to be used in conjunction with CMake's ExternalData module." ).produces('text/plain').errorResponse().errorResponse( 'Read access was denied on the file.', 403)) def downloadKeyFile(self, file, algo): self._validateAlgo(algo) if algo not in file: raise RestException( 'This file does not have the %s hash computed.' % algo) keyFileBody = '%s\n' % file[algo] name = '.'.join((file['name'], algo)) setResponseHeader('Content-Length', len(keyFileBody)) setResponseHeader('Content-Type', 'text/plain') setContentDisposition(name) setRawResponse() return keyFileBody @access.public(scope=TokenScope.DATA_READ, cookie=True) @autoDescribeRoute( Description('Download a file by its hashsum.').param( 'algo', 'The type of the given hashsum (case insensitive).', paramType='path', lower=True, enum=SUPPORTED_ALGORITHMS). param( 'hash', 'The hexadecimal hashsum of the file to download (case insensitive).', paramType='path', lower=True).errorResponse('No file with the given hash exists.')) def downloadWithHash(self, algo, hash, params): file = self._getFirstFileByHash(algo, hash) if not file: raise RestException('File not found.', code=404) return self.download(id=file['_id'], params=params) @access.public(scope=TokenScope.DATA_READ, cookie=True) @filtermodel(FileModel) @autoDescribeRoute( Description('Return a list of files matching a hashsum.').param( 'algo', 'The type of the given hashsum (case insensitive).', paramType='path', lower=True, enum=SUPPORTED_ALGORITHMS). param( 'hash', 'The hexadecimal hashsum of the file to download (case insensitive).', paramType='path', lower=True)) def getByHash(self, algo, hash): self._validateAlgo(algo) model = FileModel() user = self.getCurrentUser() cursor = model.find({algo: hash}) return [ file for file in cursor if model.hasAccess(file, user, AccessType.READ) ] @access.user(scope=TokenScope.DATA_WRITE) @autoDescribeRoute( Description('Manually compute the checksum values for a given file.'). modelParam('id', 'The ID of the file.', model=FileModel, level=AccessType.WRITE).param( 'progress', 'Whether to track progress of the operation', dataType='boolean', default=False, required=False).errorResponse().errorResponse( 'Write access was denied on the file.', 403)) def computeHashes(self, file, progress): with ProgressContext(progress, title='Computing hash: %s' % file['name'], total=file['size'], user=self.getCurrentUser()) as pc: return _computeHash(file, progress=pc) def _validateAlgo(self, algo): """ Print an exception if a user requests an invalid checksum algorithm. """ if algo not in SUPPORTED_ALGORITHMS: msg = 'Invalid algorithm "%s". Supported algorithms: %s.' % ( algo, ', '.join(SUPPORTED_ALGORITHMS)) raise RestException(msg, code=400) def _getFirstFileByHash(self, algo, hash, user=None): """ Return the first file that the user has access to given its hash and its associated hashsum algorithm name. :param algo: Algorithm the given hash is encoded with. :param hash: Hash of the file to find. :param user: User to test access against. Default (none) is the current user. :return: A file document. """ self._validateAlgo(algo) query = {algo: hash} fileModel = FileModel() cursor = fileModel.find(query) if not user: user = self.getCurrentUser() for file in cursor: if fileModel.hasAccess(file, user, AccessType.READ): return file return None
class OAuth(Resource): def __init__(self): super(OAuth, self).__init__() self.resourceName = 'oauth' self.route('GET', ('provider', ), self.listProviders) self.route('GET', (':provider', 'callback'), self.callback) def _createStateToken(self, redirect): csrfToken = Token().createToken(days=0.25) # The delimiter is arbitrary, but a dot doesn't need to be URL-encoded state = '%s.%s' % (csrfToken['_id'], redirect) return state 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 @access.public @autoDescribeRoute( Description( 'Get the list of enabled OAuth2 providers and their URLs.').notes( 'By default, returns an object mapping names of providers to ' 'the appropriate URL.').param( 'redirect', 'Where the user should be redirected upon completion' ' of the OAuth2 flow.').param( 'list', 'Whether to return the providers as an ordered list.', required=False, dataType='boolean', default=False)) def listProviders(self, redirect, list): enabledNames = Setting().get(PluginSettings.PROVIDERS_ENABLED) enabledProviders = [ provider for providerName, provider in six.viewitems(providers.idMap) if providerName in enabledNames ] if enabledProviders: state = self._createStateToken(redirect) else: state = None if list: return [{ 'id': provider.getProviderName(external=False), 'name': provider.getProviderName(external=True), 'url': provider.getUrl(state) } for provider in enabledProviders] else: return { provider.getProviderName(external=True): provider.getUrl(state) for provider in enabledProviders } @access.public @autoDescribeRoute( Description('Callback called by OAuth providers.').param( 'provider', 'The provider name.', paramType='path').param( 'state', 'Opaque state string.', required=False).param( 'code', 'Authorization code from provider.', required=False).param('error', 'Error message from provider.', required=False), hide=True) 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)
class Job(Resource): def __init__(self): super(Job, self).__init__() self.resourceName = 'job' self._model = JobModel() self.route('GET', (), self.listJobs) self.route('POST', (), self.createJob) self.route('GET', ('all', ), self.listAllJobs) self.route('GET', (':id', ), self.getJob) self.route('PUT', (':id', ), self.updateJob) self.route('PUT', (':id', 'cancel'), self.cancelJob) self.route('DELETE', (':id', ), self.deleteJob) self.route('GET', ( 'typeandstatus', 'all', ), self.allJobsTypesAndStatuses) self.route('GET', ('typeandstatus', ), self.jobsTypesAndStatuses) @access.public @filtermodel(model=JobModel) @autoDescribeRoute( Description('List jobs for a given user.').param( 'userId', 'The ID of the user whose jobs will be listed. If ' 'not passed or empty, will use the currently logged in user. If ' 'set to "None", will list all jobs that do not have an owning ' 'user.', required=False).modelParam( 'parentId', 'Id of the parent job.', model=JobModel, level=AccessType.ADMIN, destName='parentJob', paramType='query', required=False).jsonParam( 'types', 'Filter for type', requireArray=True, required=False).jsonParam( 'statuses', 'Filter for status', requireArray=True, required=False).pagingParams( defaultSort='created', defaultSortDir=SortDir.DESCENDING)) def listJobs(self, userId, parentJob, types, statuses, limit, offset, sort): currentUser = self.getCurrentUser() if not userId: user = currentUser elif userId.lower() == 'none': user = '******' else: user = User().load(userId, user=currentUser, level=AccessType.READ) parent = None if parentJob: parent = parentJob return list( self._model.list(user=user, offset=offset, limit=limit, types=types, statuses=statuses, sort=sort, currentUser=currentUser, parentJob=parent)) @filtermodel(model=JobModel) @access.token(scope=constants.REST_CREATE_JOB_TOKEN_SCOPE, required=True) @autoDescribeRoute( Description('Create a job model').param( 'title', 'Title of the job.', required=True).param( 'type', 'Type of the job.', required=True).modelParam( 'parentId', 'ID of the parent job.', model=JobModel, destName='parentJob', paramType='query', required=False).param( 'public', 'Whether the job is publicly visible.', required=False, dataType='boolean', default=False).param('handler', 'Job handler string.', required=False).jsonParam( 'args', 'Job arguments', required=False, requireArray=True). jsonParam('kwargs', 'Job keyword arguments', required=False, requireObject=True).jsonParam( 'otherFields', 'Other fields specific to the job handler', requireObject=True, required=False)) def createJob(self, title, type, parentJob, public, handler, args, kwargs, otherFields): params = { 'title': title, 'type': type, 'public': public, 'handler': handler, 'user': self.getCurrentUser(), 'args': args, 'kwargs': kwargs, 'parentJob': parentJob, 'otherFields': otherFields } return self._model.createJob(**params) @access.admin @filtermodel(model=JobModel) @autoDescribeRoute( Description('List all jobs.').jsonParam( 'types', 'Filter for type', requireArray=True, required=False).jsonParam('statuses', 'Filter for status', requireArray=True, required=False).pagingParams( defaultSort='created', defaultSortDir=SortDir.DESCENDING)) def listAllJobs(self, types, statuses, limit, offset, sort): currentUser = self.getCurrentUser() return list( self._model.list(user='******', offset=offset, limit=limit, types=types, statuses=statuses, sort=sort, currentUser=currentUser)) @access.public @filtermodel(JobModel) @autoDescribeRoute( Description('Get a job by ID.').modelParam( 'id', 'The ID of the job.', model=JobModel, force=True, includeLog=True).errorResponse('ID was invalid.').errorResponse( 'Read access was denied for the job.', 403)) def getJob(self, job): user = self.getCurrentUser() # If the job is not public check access if not job.get('public', False): if user: self._model.requireAccess(job, user, level=AccessType.READ) else: self.ensureTokenScopes('jobs.job_' + str(job['_id'])) return job @access.token @filtermodel(JobModel) @autoDescribeRoute( Description('Update an existing job.').notes( 'In most cases, regular users should not call this endpoint. It ' 'will typically be used by a batch processing system to send ' 'updates regarding the execution of the job. If using a non-' 'user-associated token for authorization, the token must be ' 'granted the "jobs.job_<id>" scope, where <id> is the ID of ' 'the job being updated.').modelParam('id', 'The ID of the job.', model=JobModel, force=True). param('log', "A message to add to the job's log field. If you want " 'to overwrite any existing log content, pass another parameter ' '"overwrite=true".', required=False). param('overwrite', 'If passing a log parameter, you may set this to ' '"true" if you wish to overwrite the log field rather than ' 'append to it.', dataType='boolean', required=False, default=False).param( 'status', 'Update the status of the job. See the JobStatus ' 'enumeration in the constants module in this plugin for the ' 'numerical values of each status.', required=False).param( 'progressTotal', 'Maximum progress value, set <= 0 to indicate ' 'indeterminate progress for this job.', required=False, dataType='float').param('progressCurrent', 'Current progress value.', required=False, dataType='float').param( 'progressMessage', 'Current progress message.', required=False). param('notify', 'If this update should trigger a notification, set ' 'this field to true.', dataType='boolean', required=False, default=True).errorResponse('ID was invalid.').errorResponse( 'Write access was denied for the job.', 403)) def updateJob(self, job, log, overwrite, notify, status, progressTotal, progressCurrent, progressMessage): user = self.getCurrentUser() if user: self._model.requireAccess(job, user, level=AccessType.WRITE) else: self.ensureTokenScopes('jobs.job_' + str(job['_id'])) return self._model.updateJob(job, log=log, status=status, overwrite=overwrite, notify=notify, progressCurrent=progressCurrent, progressTotal=progressTotal, progressMessage=progressMessage) @access.user @autoDescribeRoute( Description('Delete an existing job.').modelParam( 'id', 'The ID of the job.', model=JobModel, level=AccessType.ADMIN).errorResponse( 'ID was invalid.').errorResponse( 'Admin access was denied for the job.', 403)) def deleteJob(self, job): self._model.remove(job) @access.admin @autoDescribeRoute( Description('Get types and statuses of all jobs').errorResponse( 'Admin access was denied for the job.', 403)) def allJobsTypesAndStatuses(self): return self._model.getAllTypesAndStatuses(user='******') @access.user @autoDescribeRoute( Description('Get types and statuses of jobs of current user')) def jobsTypesAndStatuses(self): currentUser = self.getCurrentUser() return self._model.getAllTypesAndStatuses(user=currentUser) @access.user @filtermodel(JobModel) @autoDescribeRoute( Description('Cancel a job by ID.').modelParam( 'id', 'The ID of the job.', model=JobModel, level=AccessType.WRITE, includeLog=False).errorResponse('ID was invalid.').errorResponse( 'Write access was denied for the job.', 403)) def cancelJob(self, job, params): return self._model.cancelJob(job)
conn.unbind_s() user = _getLdapUser(attrs, server) if user: event.stopPropagation().preventDefault().addResponse(user) except ldap.LDAPError: logger.exception('LDAP connection exception (%s).' % server['uri']) continue @access.admin @boundHandler @autoDescribeRoute( Description('Test connection status to a LDAP server.') .notes('You must be an administrator to call this.') .param('uri', 'The URI of the server.') .param('bindName', 'The LDAP identity to bind with.') .param('password', 'Password to bind with.') .errorResponse('You are not an administrator.', 403) ) def _ldapServerTest(self, uri, bindName, password, params): conn = ldap.initialize(uri) conn.set_option(ldap.OPT_TIMEOUT, _CONNECT_TIMEOUT) conn.set_option(ldap.OPT_NETWORK_TIMEOUT, _CONNECT_TIMEOUT) try: conn.bind_s(bindName, password, ldap.AUTH_SIMPLE) return { 'connected': True } except ldap.LDAPError as e: return {
Compute the base gravatar URL for a user and return it. For the moment, the current default image is cached in this URL. It is the caller's responsibility to save this value on the user document. """ defaultImage = Setting().get(PluginSettings.DEFAULT_IMAGE) md5 = hashlib.md5(user['email'].encode('utf8')).hexdigest() return 'https://www.gravatar.com/avatar/%s?d=%s' % (md5, defaultImage) @access.public @autoDescribeRoute( Description('Redirects to the gravatar image for a user.').modelParam( 'id', 'The ID of the user.', model=User, level=AccessType.READ).param( 'size', 'Size in pixels for the image (default=64).', required=False, dataType='int', default=64)) def getGravatar(user, size): if not user.get('gravatar_baseUrl'): # the save hook will cause the gravatar base URL to be computed user = User().save(user) raise cherrypy.HTTPRedirect(user['gravatar_baseUrl'] + '&s=%d' % size) def _userUpdate(event): """ Called when the user document is being changed. We update the cached gravatar URL.
# -*- coding: utf-8 -*- from girderformindlogger.api import access from girderformindlogger.api.describe import Description, autoDescribeRoute from girderformindlogger.api.rest import boundHandler from girderformindlogger.models.setting import Setting from .settings import PluginSettings @access.user @boundHandler @autoDescribeRoute( Description('Get list of item licenses.') .param('default', 'Whether to return the default list of item licenses.', required=False, dataType='boolean', default=False) ) def getLicenses(self, default): if default: licenses = Setting().getDefault(PluginSettings.LICENSES) else: licenses = Setting().get(PluginSettings.LICENSES) return licenses
from girderformindlogger.api import access from girderformindlogger.api.describe import Description, autoDescribeRoute from girderformindlogger.api.rest import boundHandler from girderformindlogger.api.v1.collection import Collection from girderformindlogger.constants import AccessType, TokenScope from girderformindlogger.exceptions import RestException from girderformindlogger.models.collection import Collection as CollectionModel from girderformindlogger.models.user import User from girderformindlogger.plugin import GirderPlugin @access.user(scope=TokenScope.DATA_READ) @boundHandler @autoDescribeRoute( Description("Accept a collection's Terms of Use for the current user."). modelParam('id', model=CollectionModel, level=AccessType.READ).param( 'termsHash', "The SHA-256 hash of this collection's terms, encoded in hexadecimal.") ) 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.'