Ejemplo n.º 1
0
def ensureTokenScopes(token, scope):
    """
    Call this to validate a token scope for endpoints that require tokens
    other than a user authentication token. Raises an AccessException if the
    required scopes are not allowed by the given token.

    :param token: The token object used in the request.
    :type token: dict
    :param scope: The required scope or set of scopes.
    :type scope: `str or list of str`
    """
    tokenModel = Token()
    if tokenModel.hasScope(token, TokenScope.USER_AUTH):
        return

    if not tokenModel.hasScope(token, scope):
        setCurrentUser(None)
        if isinstance(scope, six.string_types):
            scope = (scope,)
        raise AccessException(
            'Invalid token scope.\n'
            'Required: %s.\n'
            'Allowed: %s' % (
                ' '.join(scope),
                '' if token is None else ' '.join(tokenModel.getAllowedScopes(token))))
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    def verifyEmail(self, user, token):
        token = Token().load(token,
                             user=user,
                             level=AccessType.ADMIN,
                             objectId=False,
                             exc=True)
        delta = (token['expires'] - datetime.datetime.utcnow()).total_seconds()
        hasScope = Token().hasScope(token, TokenScope.EMAIL_VERIFICATION)

        if token.get('userId') != user['_id'] or delta <= 0 or not hasScope:
            raise AccessException('The token is invalid or expired.')

        user['emailVerified'] = True
        Token().remove(token)
        user = self._model.save(user)

        if self._model.canLogin(user):
            setCurrentUser(user)
            authToken = self.sendAuthTokenCookie(user)
            return {
                'user': self._model.filter(user, user),
                'authToken': {
                    'token': authToken['_id'],
                    'expires': authToken['expires'],
                    'scope': authToken['scope']
                },
                'message': 'Email verification succeeded.'
            }
        else:
            return {
                'user': self._model.filter(user, user),
                'message': 'Email verification succeeded.'
            }
Ejemplo n.º 4
0
    def checkTemporaryPassword(self, user, token):
        token = Token().load(token,
                             user=user,
                             level=AccessType.ADMIN,
                             objectId=False,
                             exc=True)
        delta = (token['expires'] - datetime.datetime.utcnow()).total_seconds()
        hasScope = Token().hasScope(token, TokenScope.TEMPORARY_USER_AUTH)

        if token.get('userId') != user['_id'] or delta <= 0 or not hasScope:
            raise AccessException(
                'The token does not grant temporary access to this user.')

        # Temp auth is verified, send an actual auth token now. We keep the
        # temp token around since it can still be used on a subsequent request
        # to change the password
        authToken = self.sendAuthTokenCookie(user)

        return {
            'user': self._model.filter(user, user),
            'authToken': {
                'token': authToken['_id'],
                'expires': authToken['expires'],
                'temporary': True
            },
            'message': 'Temporary access token is valid.'
        }
Ejemplo n.º 5
0
class Token(Resource):
    """API Endpoint for non-user tokens in the system."""
    def __init__(self):
        super(Token, self).__init__()
        self.resourceName = 'token'
        self._model = TokenModel()

        self.route('DELETE', ('session', ), self.deleteSession)
        self.route('GET', ('session', ), self.getSession)
        self.route('GET', ('current', ), self.currentSession)
        self.route('GET', ('scopes', ), self.listScopes)

    @access.public
    @autoDescribeRoute(
        Description('Retrieve the current session information.').responseClass(
            'Token'))
    def currentSession(self):
        return self.getCurrentToken()

    @access.public
    @autoDescribeRoute(
        Description('Get an anonymous session token for the system.').notes(
            'If you are logged in, this will return a token associated with that login.'
        ).responseClass('Token'))
    def getSession(self):
        token = self.getCurrentToken()

        # Only create and send new cookie if token isn't valid or will expire soon
        if not token:
            token = self.sendAuthTokenCookie(
                None, scope=TokenScope.ANONYMOUS_SESSION)

        return {'token': token['_id'], 'expires': token['expires']}

    @access.token
    @autoDescribeRoute(
        Description('Remove a session from the system.').responseClass(
            'Token').notes('Attempts to delete your authentication cookie.'))
    def deleteSession(self):
        token = self.getCurrentToken()
        if token:
            self._model.remove(token)
        self.deleteAuthTokenCookie()
        return {'message': 'Session deleted.'}

    @access.public
    @autoDescribeRoute(
        Description('List all token scopes available in the system.'))
    def listScopes(self):
        return TokenScope.listScopes()
Ejemplo n.º 6
0
def _authorizeInitUpload(event):
    """
    Called when initializing an upload, prior to the default handler. Checks if
    the user is passing an authorized upload token, and if so, sets the current
    request-thread user to be whoever created the token.
    """
    token = getCurrentToken()
    params = event.info['params']
    tokenModel = Token()
    parentType = params.get('parentType')
    parentId = params.get('parentId', '')
    requiredScopes = {TOKEN_SCOPE_AUTHORIZED_UPLOAD, 'authorized_upload_folder_%s' % parentId}

    if parentType == 'folder' and tokenModel.hasScope(token=token, scope=requiredScopes):
        user = User().load(token['userId'], force=True)
        setCurrentUser(user)
Ejemplo n.º 7
0
def getCurrentToken(allowCookie=None):
    """
    Returns the current valid token object that was passed via the token header
    or parameter, or None if no valid token was passed.

    :param allowCookie: Normally, authentication via cookie is disallowed to
        protect against CSRF attacks. If you want to expose an endpoint that can
        be authenticated with a token passed in the Cookie, set this to True.
        This should only be used on read-only operations that will not make any
        changes to data on the server, and only in cases where the user agent
        behavior makes passing custom headers infeasible, such as downloading
        data to disk in the browser. In the event that allowCookie is not explicitly
        passed, it will default to False unless the access.cookie decorator is used.
    :type allowCookie: bool
    """
    if allowCookie is None:
        allowCookie = getattr(cherrypy.request, 'girderAllowCookie', False)

    tokenStr = None
    if 'token' in cherrypy.request.params:  # Token as a parameter
        tokenStr = cherrypy.request.params.get('token')
    elif 'Girder-Token' in cherrypy.request.headers:
        tokenStr = cherrypy.request.headers['Girder-Token']
    elif allowCookie and 'girderToken' in cherrypy.request.cookie:
        tokenStr = cherrypy.request.cookie['girderToken'].value

    if not tokenStr:
        return None

    return Token().load(tokenStr, force=True, objectId=False)
Ejemplo n.º 8
0
 def wrapped(*args, **kwargs):
     if not rest.getCurrentToken():
         raise AccessException(
             'You must be logged in or have a valid auth token.')
     if required:
         Token().requireScope(rest.getCurrentToken(), scope)
     return fun(*args, **kwargs)
Ejemplo n.º 9
0
 def createJobToken(self, job, days=7):
     """
     Create a token that can be used just for the management of an individual
     job, e.g. updating job info, progress, logs, status.
     """
     return Token().createToken(days=days,
                                scope='jobs.job_' + str(job['_id']))
Ejemplo n.º 10
0
    def validate(self, doc):
        from girderformindlogger.models.token import Token
        from girderformindlogger.models.user import User

        if doc['tokenDuration']:
            doc['tokenDuration'] = float(doc['tokenDuration'])
        else:
            doc['tokenDuration'] = None

        doc['name'] = doc['name'].strip()
        doc['active'] = bool(doc.get('active', True))

        if doc['scope'] is not None:
            if not isinstance(doc['scope'], (list, tuple)):
                raise ValidationException('Scope must be a list, or None.')
            if not doc['scope']:
                raise ValidationException('Custom scope list must not be empty.')

            # Ensure only registered scopes are being set
            admin = User().load(doc['userId'], force=True)['admin']
            scopes = TokenScope.scopeIds(admin)
            unknownScopes = set(doc['scope']) - scopes
            if unknownScopes:
                raise ValidationException('Invalid scopes: %s.' % ','.join(unknownScopes))

        # Deactivating an already existing token
        if '_id' in doc and not doc['active']:
            Token().clearForApiKey(doc)

        return doc
Ejemplo n.º 11
0
    def createToken(self, key, days=None):
        """
        Create a token using an API key.

        :param key: The API key (the key itself, not the full document).
        :type key: str
        :param days: You may request a token duration up to the token duration
            of the API key itself, or pass None to use the API key duration.
        :type days: float or None
        """
        from girderformindlogger.models.setting import Setting
        from girderformindlogger.models.token import Token
        from girderformindlogger.models.user import User

        apiKey = self.findOne({
            'key': key
        })

        if apiKey is None or not apiKey['active']:
            raise ValidationException('Invalid API key.')

        cap = apiKey['tokenDuration'] or Setting().get(SettingKey.COOKIE_LIFETIME)
        days = min(float(days or cap), cap)

        user = User().load(apiKey['userId'], force=True)

        # Mark last used stamp
        apiKey['lastUse'] = datetime.datetime.utcnow()
        apiKey = self.save(apiKey)
        token = Token().createToken(user=user, days=days, scope=apiKey['scope'], apiKey=apiKey)
        return (user, token)
Ejemplo n.º 12
0
    def generateTemporaryPassword(self, email):
        user = self._model.findOne({'email': self._model.hash(email.lower()), 'email_encrypted': True})

        if not user:
            user = self._model.findOne({'email': email.lower(), 'email_encrypted': {'$ne': True}})

        if not user:
            raise RestException('That email is not registered.')

        token = Token().createToken(user, days=(15/1440.0), scope=TokenScope.TEMPORARY_USER_AUTH)

        url = '%s#useraccount/%s/token/%s' % (
            mail_utils.getEmailUrlPrefix(), str(user['_id']), str(token['_id']))

        html = mail_utils.renderTemplate('temporaryAccess.mako', {
            'url': url,
            'token': str(token['_id'])
        })

        mail_utils.sendMail(
            '%s: Temporary access' % Setting().get(SettingKey.BRAND_NAME),
            html,
            [email]
        )
        return {'message': 'Sent temporary access email.'}
Ejemplo n.º 13
0
    def acceptInvitationByToken(self, invitation, lang, email, token):
        """
        Accept an invitation.
        """
        currentUser = Token().load(
            token,
            force=True,
            objectId=False,
            exc=False
        ).get('userId', None)
        if currentUser is not None:
            currentUser = UserModel().load(currentUser, force=True)
        if currentUser is None:
            raise AccessException(
                "Invalid token."
            )
        if invitation.get('role', 'user') == 'owner':
            AppletModel().receiveOwnerShip(AppletModel().load(invitation['appletId'], force=True), currentUser, email)
        else:
            profile = InvitationModel().acceptInvitation(invitation, currentUser, email)

            # editors should be able to access duplicated applets
            if invitation.get('role','user') == 'editor' or invitation.get('role', 'user') == 'manager':
                InvitationModel().accessToDuplicatedApplets(invitation, currentUser, email)

        InvitationModel().remove(invitation)
        return profile
Ejemplo n.º 14
0
def _uploadComplete(event):
    """
    Called after an upload finishes. We check if our current token is a special
    authorized upload token, and if so, delete it.

    TODO we could alternatively keep a reference count inside each token that authorized
    more than a single upload at a time, and just decrement it here.
    """
    token = getCurrentToken()
    if token and 'authorizedUploadId' in token:
        user = User().load(token['userId'], force=True)
        item = Item().load(event.info['file']['itemId'], force=True)

        # Save the metadata on the item
        item['description'] = token['authorizedUploadDescription']
        item['authorizedUploadEmail'] = token['authorizedUploadEmail']
        Item().save(item)

        text = mail_utils.renderTemplate('authorized_upload.uploadFinished.mako', {
            'itemId': item['_id'],
            'itemName': item['name'],
            'itemDescription': item.get('description', '')
        })
        mail_utils.sendMail('Authorized upload complete', text, [user['email']])
        Token().remove(token)
Ejemplo n.º 15
0
def _storeUploadId(event):
    """
    Called after an upload is first initialized successfully. Sets the authorized upload ID
    in the token, ensuring it can be used for only this upload.
    """
    returnVal = event.info['returnVal']
    token = getCurrentToken()
    tokenModel = Token()
    isAuthorizedUpload = tokenModel.hasScope(token, TOKEN_SCOPE_AUTHORIZED_UPLOAD)

    if isAuthorizedUpload and returnVal.get('_modelType', 'upload') == 'upload':
        params = event.info['params']
        token['scope'].remove(TOKEN_SCOPE_AUTHORIZED_UPLOAD)
        token['authorizedUploadId'] = returnVal['_id']
        token['authorizedUploadDescription'] = params.get('authorizedUploadDescription', '')
        token['authorizedUploadEmail'] = params.get('authorizedUploadEmail')
        tokenModel.save(token)
Ejemplo n.º 16
0
    def __init__(self):
        super(Token, self).__init__()
        self.resourceName = 'token'
        self._model = TokenModel()

        self.route('DELETE', ('session', ), self.deleteSession)
        self.route('GET', ('session', ), self.getSession)
        self.route('GET', ('current', ), self.currentSession)
        self.route('GET', ('scopes', ), self.listScopes)
Ejemplo n.º 17
0
    def _sendVerificationEmail(self, user, email):
        from girderformindlogger.models.token import Token

        token = Token().createToken(user,
                                    days=1,
                                    scope=TokenScope.EMAIL_VERIFICATION)
        url = '%s#useraccount/%s/verification/%s' % (
            mail_utils.getEmailUrlPrefix(), str(user['_id']), str(
                token['_id']))
        text = mail_utils.renderTemplate('emailVerification.mako',
                                         {'url': url})
        mail_utils.sendMail('Girder: Email verification', text, [email])
Ejemplo n.º 18
0
 def acceptInvitationByToken(self, invitation, token):
     """
     Accept an invitation.
     """
     currentUser = Token().load(token,
                                force=True,
                                objectId=False,
                                exc=False).get('userId')
     if currentUser is not None:
         currentUser = UserModel().load(currentUser, force=True)
     if currentUser is None:
         raise AccessException(
             "You must be logged in to accept an invitation.")
     return (InvitationModel().acceptInvitation(invitation, currentUser))
Ejemplo n.º 19
0
    def changePassword(self, old, new):
        user = self.getCurrentUser()
        token = None

        if not old:
            raise RestException('Old password must not be empty.')

        if (not self._model.hasPassword(user)
                or not self._model._cryptContext.verify(old, user['salt'])):
            # If not the user's actual password, check for temp access token
            token = Token().load(old, force=True, objectId=False, exc=False)
            if (not token or not token.get('userId')
                    or token['userId'] != user['_id'] or not Token().hasScope(
                        token, TokenScope.TEMPORARY_USER_AUTH)):
                raise AccessException('Old password is incorrect.')

        self._model.setPassword(user, new)

        if token:
            # Remove the temporary access token if one was used
            Token().remove(token)

        return {'message': 'Password changed.'}
Ejemplo n.º 20
0
 def acceptInvitationByToken(self, invitation, email, token):
     """
     Accept an invitation.
     """
     currentUser = Token().load(token,
                                force=True,
                                objectId=False,
                                exc=False).get('userId', None)
     if currentUser is not None:
         currentUser = UserModel().load(currentUser, force=True)
     if currentUser is None:
         raise AccessException("Invalid token.")
     return (InvitationModel().acceptInvitation(invitation, currentUser,
                                                email))
Ejemplo n.º 21
0
    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.º 22
0
def buildHeaders(headers, cookie, user, token, basicAuth, authHeader):
    from girderformindlogger.models.token import Token

    headers = headers[:]
    if cookie is not None:
        headers.append(('Cookie', cookie))

    if user is not None:
        token = Token().createToken(user)
        headers.append(('Girder-Token', str(token['_id'])))
    elif token is not None:
        if isinstance(token, dict):
            headers.append(('Girder-Token', token['_id']))
        else:
            headers.append(('Girder-Token', token))

    if basicAuth is not None:
        auth = base64.b64encode(basicAuth.encode('utf8'))
        headers.append((authHeader, 'Basic %s' % auth.decode()))

    return headers
Ejemplo n.º 23
0
    def sendAuthTokenCookie(self, user=None, scope=None, token=None, days=None):
        """
        Helper method to send the authentication cookie
        """
        if days is None:
            days = float(Setting().get(SettingKey.COOKIE_LIFETIME))

        if token is None:
            token = Token().createToken(user, days=days, scope=scope)

        cookie = cherrypy.response.cookie
        cookie['girderToken'] = str(token['_id'])
        cookie['girderToken']['path'] = '/'
        cookie['girderToken']['expires'] = int(days * 3600 * 24)

        # CherryPy proxy tools modify the request.base, but not request.scheme, when receiving
        # X-Forwarded-Proto headers from a reverse proxy
        if cherrypy.request.scheme == 'https' or cherrypy.request.base.startswith('https'):
            cookie['girderToken']['secure'] = True

        return token
Ejemplo n.º 24
0
    def remove(self, user, progress=None, **kwargs):
        """
        Delete a user, and all references to it in the database.

        :param user: The user document to delete.
        :type user: dict
        :param progress: A progress context to record progress on.
        :type progress: girderformindlogger.utility.progress.ProgressContext or None.
        """
        from girderformindlogger.models.folder import Folder
        from girderformindlogger.models.group import Group
        from girderformindlogger.models.token import Token

        # Delete all authentication tokens owned by this user
        Token().removeWithQuery({'userId': user['_id']})

        # Delete all pending group invites for this user
        Group().update({'requests': user['_id']},
                       {'$pull': {
                           'requests': user['_id']
                       }})

        # Delete all of the folders under this user
        folderModel = Folder()
        folders = folderModel.find({
            'parentId': user['_id'],
            'parentCollection': 'user'
        })
        for folder in folders:
            folderModel.remove(folder, progress=progress, **kwargs)

        # Finally, delete the user document itself
        AccessControlledModel.remove(self, user)
        if progress:
            progress.update(increment=1,
                            message='Deleted user ' + user['login'])
Ejemplo n.º 25
0
 def remove(self, doc):
     # Clear tokens corresponding to this API key.
     from girderformindlogger.models.token import Token
     Token().clearForApiKey(doc)
     super(ApiKey, self).remove(doc)
Ejemplo n.º 26
0
 def logout(self):
     token = self.getCurrentToken()
     if token:
         Token().remove(token)
     self.deleteAuthTokenCookie()
     return {'message': 'Logged out.'}
Ejemplo n.º 27
0
    def testAuthorizedUpload(self):
        Setting().set(SettingKey.UPLOAD_MINIMUM_CHUNK_SIZE, 1)

        # Anon access should not work
        resp = self.request('/authorized_upload',
                            method='POST',
                            params={'folderId': self.privateFolder['_id']})
        self.assertStatus(resp, 401)

        # Create our secure URL
        resp = self.request('/authorized_upload',
                            method='POST',
                            user=self.admin,
                            params={'folderId': self.privateFolder['_id']})
        self.assertStatusOk(resp)
        parts = resp.json['url'].rsplit('/', 3)
        tokenId, folderId = parts[-1], parts[-2]

        token = Token().load(tokenId, force=True, objectId=False)

        self.assertIsNotNone(token)
        self.assertEqual(folderId, str(self.privateFolder['_id']))
        self.assertEqual(
            set(token['scope']), {
                TOKEN_SCOPE_AUTHORIZED_UPLOAD,
                'authorized_upload_folder_%s' % self.privateFolder['_id']
            })

        # Make sure this token doesn't let us upload into a different folder
        params = {
            'parentType': 'folder',
            'parentId': self.publicFolder['_id'],
            'name': 'hello.txt',
            'size': 11,
            'mimeType': 'text/plain'
        }

        resp = self.request(path='/file',
                            method='POST',
                            params=params,
                            token=tokenId)
        self.assertStatus(resp, 401)

        # Initialize upload into correct folder
        params['parentId'] = self.privateFolder['_id']
        resp = self.request(path='/file',
                            method='POST',
                            params=params,
                            token=tokenId)
        self.assertStatusOk(resp)

        # We should remove the scope that allows further uploads
        upload = Upload().load(resp.json['_id'])
        token = Token().load(tokenId, force=True, objectId=False)
        self.assertEqual(
            token['scope'],
            ['authorized_upload_folder_%s' % self.privateFolder['_id']])

        # Authorized upload ID should be present in the token
        self.assertEqual(token['authorizedUploadId'], upload['_id'])

        # Attempting to initialize new uploads using the token should fail
        resp = self.request(path='/file',
                            method='POST',
                            params=params,
                            token=tokenId)
        self.assertStatus(resp, 401)

        # Uploading a chunk should work with the token
        resp = self.request(path='/file/chunk',
                            method='POST',
                            token=tokenId,
                            body='hello ',
                            params={'uploadId': str(upload['_id'])},
                            type='text/plain')
        self.assertStatusOk(resp)

        # Requesting our offset should work with the token
        # The offset should not have changed
        resp = self.request(path='/file/offset',
                            method='GET',
                            token=tokenId,
                            params={'uploadId': upload['_id']})
        self.assertStatusOk(resp)
        self.assertEqual(resp.json['offset'], 6)

        # Upload the second chunk
        resp = self.request(path='/file/chunk',
                            method='POST',
                            token=tokenId,
                            body='world',
                            params={
                                'offset': 6,
                                'uploadId': str(upload['_id'])
                            },
                            type='text/plain')
        self.assertStatusOk(resp)

        # Trying to upload more chunks should fail
        resp = self.request(path='/file/chunk',
                            method='POST',
                            token=tokenId,
                            body='extra',
                            params={
                                'offset': 11,
                                'uploadId': str(upload['_id'])
                            },
                            type='text/plain')
        self.assertStatus(resp, 401)

        # The token should be destroyed
        self.assertIsNone(Token().load(tokenId, force=True, objectId=False))
Ejemplo n.º 28
0
    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