Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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}
Ejemplo n.º 3
0
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}
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
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}
Ejemplo n.º 7
0
    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'])
Ejemplo n.º 8
0
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')
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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)
Ejemplo n.º 11
0
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)
Ejemplo n.º 12
0
                    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 {
Ejemplo n.º 13
0
    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.
Ejemplo n.º 14
0
# -*- 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
Ejemplo n.º 15
0
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.'