Exemplo n.º 1
0
    def _authenticate(self, callback_success=None, callback_failure=None, token=None,
                      _request_fallback=None, **callback_success_kwargs):
        ''' Authenticate user with the user-provided token '''
        callback_success = callback_success or \
            (lambda: bottle.HTTPResponse(status=200, body='OK'))
        callback_failure = callback_failure or \
            (lambda code, err: bottle.HTTPResponse(status=code, body=err))
        callback_success_kwargs = callback_success_kwargs or dict()

        # Try to retrieve the token from the cookies first
        token_cookie = bottle.request.get_cookie('Token')

        # If no token was found, retrieve it from the request data
        if not token_cookie:
            request_dict = PandasDatabase._request(bottle.request, request_fallback=_request_fallback)
            token = token or request_dict.get('Token')
        else:
            token = str(token_cookie)

        # Verify that the token is provided in the request
        if token is None:
            msg = 'Auth error: "Token" field must be present as part of the request.'
            self._print(msg)
            return callback_failure(400, msg)

        # Validate the provided token against the token store
        token_record = self.pddb.find_one(
            'bottleship_tokens', where={'Token': token}, astype='dict')
        if not token_record or time.time() > float(token_record.get('Expiry', '0')):
            msg = 'Auth error: Provided token does not exist or has expired.'
            self._print(msg)
            return callback_failure(403, msg)

        # Retrieve the user record that the token belongs to
        user_record = self.pddb.find_one(
            'bottleship_users', where={'Username': token_record.get('Username')}, astype='dict')

        # If callback accepts it as an argument, add bottleship_user_record
        arg_spec = inspect.getargspec(callback_success)
        if 'bottleship_user_record' in arg_spec.args:
            callback_success_kwargs['bottleship_user_record'] = user_record

        # If token requires only plaintext security, we're done
        if 'plaintext' in token_record.get('SecurityLevel'):
            return callback_success(**callback_success_kwargs)

        # Depending on the security level, the data might need to be encrypted or signed
        elif 'hmac' in token_record.get('SecurityLevel'):
            key = token_record.get('Key')
            code, data = 200, callback_success(**callback_success_kwargs)
            if isinstance(data, bottle.HTTPResponse):
                code, data = data.status_code, data_encode(data.body, key)
            elif data:
                data = data_encode(data, key)
            return bottle.HTTPResponse(body=data, status=code)
Exemplo n.º 2
0
    def logout(self, token=None, cookie_only=True, _request_fallback=None):
        '''
        Expire a given token immediately.

        Parameters
        ----------
        token : str
            Token to immediately expire. This will be retrieved from the header cookies or from the
            request depending on the value of parameter `cookie_only`.
        cookie_only : bool
            If true, only retrieve Token from the header cookies. This is to prevent malicious
            users to log out other users; if this method is exposed in the application\'s API, this
            parameter should always be True (which is the default behavior).
        _request_fallback : dict
            Used for testing purposes.
            The parameter `Token` can also be passed to this method as items in the
            `_request_fallback` dictionary.
        '''

        # Try to retrieve the token from the cookies first
        token_cookie = bottle.request.get_cookie('Token')

        # If no token was found, retrieve it from the request data
        if not token_cookie and not cookie_only:
            request_dict = PandasDatabase._request(bottle.request, request_fallback=_request_fallback)
            token = token or request_dict.get('Token')
        else:
            token = str(token_cookie)

        # Verify that the token is provided in the request
        if token is None:
            msg = 'Auth error: "Token" field must be present as part of the request.'
            self._print(msg)
            return bottle.HTTPResponse(status=400, body=msg)

        # Validate the provided token against the token store
        token_record = self.pddb.find_one(
            'bottleship_tokens', where={'Token': token}, astype='dict')
        if not token_record or time.time() > float(token_record.get('Expiry', '0')):
            msg = 'Auth error: Provided token does not exist or has expired.'
            self._print(msg)
            return bottle.HTTPResponse(status=400, body=msg)

        # Expire token record in the database
        self.pddb.upsert('bottleship_tokens', where={'Token': token}, record={'Expiry': '0'})

        res = bottle.HTTPResponse(status=200, body='OK')
        res.set_cookie('Token', '', path='/', expires=0)
        return res
Exemplo n.º 3
0
    def login(self, username=None, password=None, _request_fallback=None):
        '''
        Log in an existing user.

        Parameters
        ----------
        username : str
            Username to login. It can also be passed as the value of key `Username` as part of the
            GET or POST request.
        password : str
            Plaintext password for this username. It can also be passed as the value of key
            `Password` as part of the GET or POST request. If the requested security level requires
            it, password must be signed/encrypted.
        _request_fallback : dict
            Used for testing purposes.
            The parameters `Username` and `Password` can also be passed to this method as items in
            the `_request_fallback` dictionary.

        Returns
        -------
        response : bottle.HTTPResponse
            Status code and body will determine if the login was successful. If successful, the
            body will contain the user record in JSON format. 

        Examples
        --------
        >>> app = BottleShip()
        >>> res = app.login("john", "1234")
        >>> print(res.status_code, res.body)
            403 Login error: Provided password does not match records for that username or username does not exist.
        '''
        request_dict = PandasDatabase._request(bottle.request, request_fallback=_request_fallback)

        # If data and token are provided, then this must be secure data transfer
        secure_data = False
        if 'Data' in request_dict and 'Token' in request_dict:
            secure_data = True
            request_dict, err_msg = self._read_secure_json(request_dict)
            if err_msg:
                self._print(err_msg)
                return bottle.HTTPResponse(status=400, body=err_msg)

        # Cleanup information from the request
        request_dict = {tos(req_k): tos(req_v) for req_k, req_v in request_dict.items()
                        if re.match(PandasDatabase._colname_rgx, tos(req_k))}

        # Verify username and password
        username = username or request_dict.get('Username')
        password = password or request_dict.get('Password', '')
        auth_header = bottle.request.get_header('Authorization')
        if auth_header: # If auth is available in the headers, take that
            username, password = bottle.parse_auth(auth_header)
        error_msg = self._error_username_password(username, password)
        if error_msg:
            self._print(error_msg)
            return bottle.HTTPResponse(status=400, body=error_msg)

        # Look for existing user record
        user_record = self.pddb.find_one(
            'bottleship_users', where={'Username': username}, astype='dict')
        if not user_record:
            msg = ('Login error: Provided password does not match records for that username or '
                   'username does not exist.')
            self._print(msg)
            return bottle.HTTPResponse(status=403, body=msg)

        # Make sure that the security level is supported
        security_level = request_dict.get(
            'SecurityLevel', user_record.get('SecurityLevel', self.allowed_security[0]))
        if 'ipaddr' in user_record.get('SecurityLevel') and 'ipaddr' not in security_level:
            security_level += '+ipaddr' # Force IP address verification if registration requests it
        user_record['SecurityLevel'] = security_level
        if security_level not in self.allowed_security:
            msg = 'Login error: Security level must be one of: %r' % list(self.allowed_security)
            self._print(msg)
            res = bottle.HTTPResponse(status=400, body=msg)
            return res
        elif not secure_data and ('hmac' in security_level or 'rsa' in security_level):
            msg = ('Login error: Security level requested requires secure data transfer but '
                   'plaintext was used instead')
            self._print(msg)
            res = bottle.HTTPResponse(status=400, body=msg)
            return res

        # Verify user password
        if 'Password' in user_record and user_record.get('Password') != str(hash(password)):
            msg = ('Login error: Provided password does not match records for that username or '
                   'username does not exist.')
            self._print(msg)
            return bottle.HTTPResponse(status=403, body=msg)

        # Get user's IP address from request
        ip_addr = bottle.request.environ.get('REMOTE_ADDR', '')
        if ip_addr != user_record.get('RemoteIpAddr'):
            if 'ipaddr' in security_level:
                msg = 'Login error: Registration IP address does not match login attempt.'
                self._print(msg)
                return bottle.HTTPResponse(status=403, body=msg)
            else:
                user_record['RemoteIpAddr'] = ip_addr

        # Provide user with a temporary token
        token_key = str(request_dict.get('Key') if secure_data else user_record.get('Key'))
        token_record = self._gen_token(username, security_level=security_level, key=token_key)
        user_record['Token'] = token_record['Token']

        # Update the user record
        user_cond = {'Username': username}
        user_record['Key'] = token_record.get('Key')
        user_record['LastLogin'] = str(time.time())
        user_record = self.pddb.upsert('bottleship_users', record=user_record, 
                                         where=user_cond, astype='dict')[0]

        # Depending on the security level, we may need to encrypt or sign the data
        user_record_json = self._dump_user_record(security_level, user_record)

        res = bottle.HTTPResponse(status=200, body=user_record_json)
        res.set_cookie('Token', token_record['Token'], path='/', expires=int(float(token_record.get('Expiry'))))
        return res
Exemplo n.º 4
0
    def register(self, username=None, password=None, user_info=None):
        '''
        Register a new user.

        Parameters
        ----------
        username : str
            Username to register. Must be unique in the application. It can also be passed as the
            value of key `Username` as part of the GET or POST request.
        password : str
            Plaintext password for this username. It can also be passed as the value of key
            `Password` as part of the GET or POST request.
        user_info : dict
            Dictionary containing any additional information about this user. The key
            `RemoteIpAddr` will be added to this dictionary with the value provided by
            `bottle.request.environ.get("REMOTE_ADDR")` prior to matching the user against the
            whitelist and blacklist parameters given to the constructor of this class.
            The parameters `username` and `password` can also be passed to this method as items in
            the `user_info` dictionary. Any key-value pairs not described above that are passed as
            part of the GET or POST request will be added to this dictionary. If the requested
            security level requires it, all user info including username and password must be
            serialized and signed/encrypted into a field named `Data` as a json string. In that
            case, the single-use `Token` provided by the key exchange must also be provided.

        Returns
        -------
        response : bottle.HTTPResponse
            Status code and body will determine if the login was successful. If successful, the
            body will contain the user record in JSON format. 

        Examples
        --------
        >>> app = BottleShip()
        >>> app.register("john", "1234").body
            '{"Username": "******", "Password": "******", "__id__": "2c849965-251f-4b5d-
            8a27-77f86fa9e0e3", "RemoteIpAddr": null}'
        '''
        request_dict = PandasDatabase._request(bottle.request, request_fallback=user_info)

        # If data and token are provided, then this must be secure data transfer
        secure_data = False
        if 'Data' in request_dict and 'Token' in request_dict:
            secure_data = True
            request_dict, err_msg = self._read_secure_json(request_dict)
            if err_msg:
                self._print(err_msg)
                return bottle.HTTPResponse(status=400, body=err_msg)

        # Cleanup information from the request
        request_dict = {tos(req_k): tos(req_v) for req_k, req_v in request_dict.items()
                        if re.match(PandasDatabase._colname_rgx, tos(req_k))}

        # Verify username and password
        username = username or request_dict.get('Username')
        password = password or request_dict.get('Password', '')
        auth_header = bottle.request.get_header('Authorization')
        if auth_header: # If auth is available in the headers, take that
            username, password = bottle.parse_auth(auth_header)
        error_msg = self._error_username_password(username, password)
        if error_msg:
            self._print(error_msg)
            return bottle.HTTPResponse(status=400, body=error_msg)

        # Look for existing user record and, if any, reject registration
        user_record = self.pddb.find_one(
            'bottleship_users', where={'Username': username}, astype='dict')
        if user_record:
            msg = 'Register error: Provided username already exists in the database.'
            self._print(msg)
            return bottle.HTTPResponse(status=400, body=msg)

        # Get the user requested security level or default
        security_level = request_dict.get('SecurityLevel', self.allowed_security[0])
        request_dict['SecurityLevel'] = security_level
        if security_level not in self.allowed_security:
            msg = 'Login error: Security level must be one of: %r' % list(self.allowed_security)
            self._print(msg)
            res = bottle.HTTPResponse(status=400, body=msg)
            return res
        elif not secure_data and ('hmac' in security_level or 'rsa' in security_level):
            msg = ('Login error: Security level requested requires secure data transfer but '
                   'plaintext was used instead')
            self._print(msg)
            res = bottle.HTTPResponse(status=400, body=msg)
            return res

        # Get user's IP address from request
        request_dict['RemoteIpAddr'] = bottle.request.environ.get('REMOTE_ADDR', '')

        # Insert the hashed password into user's record
        if password is not None:
            request_dict['Password'] = str(hash(password))

        # Validate the user against our rules
        if not self._check_user(request_dict):
            msg = 'User does not meet the requirements.'
            self._print(msg)
            return bottle.HTTPResponse(status=403, body=msg)

        # Insert or update the user record
        user_cond = {'Username': username}
        user_record = self.pddb.upsert('bottleship_users', record=request_dict, 
                                       where=user_cond, astype='dict')[0]

        # Depending on the security level, we may need to encrypt or sign the data
        user_record_json = self._dump_user_record(security_level, user_record)

        # Return the inserted user record
        return bottle.HTTPResponse(status=200, body=user_record_json)