def validate_user(name: str) -> str: """ Encapsulate a query to reject "username" values that don't correspond to a registered user. A valid username is translated to the internal representation for Elasticsearch indexing, which is the stringified user ID number. Args: name: The username field of a registered user Raises: ValueError: The username doesn't correspond to a registered user TypeError: Some other error occurred looking for the user Returns: The user's ID value (as a string) """ try: user = User.query(username=name) except Exception: User.logger.exception( "Unexpected exception from query for user {}", name) raise if not user: raise UnknownUser(name) return str(user.id)
def get(self, username): """ Get request for getting user data. This requires a Pbench auth token in the header field Required headers include Content-Type: application/json Accept: application/json Authorization: Bearer Pbench_auth_token (user received upon login) :return: JSON Payload Success: 200, response_object = { "username": <username>, "first_name": <firstName>, "last_name": <lastName>, "registered_on": <registered_on>, } Failure: <status_Code>, response_object = { "message": "failure message" } """ try: user, verified = self.auth.verify_user(username) except Exception: self.logger.exception( "Exception occurred during verifying the user") abort(500, message="INTERNAL ERROR") # TODO: Check if the user has the right privileges if verified: response_object = user.get_json() return make_response(jsonify(response_object), 200) elif user.is_admin(): try: target_user = User.query(username=username) response_object = target_user.get_json() return make_response(jsonify(response_object), 200) except Exception: self.logger.exception( "Exception occurred while querying the user. Username: {}", username) abort(500, message="INTERNAL ERROR") else: self.logger.warning( "User {} is not authorized to get user {}.", user.username, username, ) abort( 403, message= f"Not authorized to access information about user {username}", )
def delete(self, username): """ Delete request for deleting a user from database. This requires a Pbench auth token in the header field Required headers include Content-Type: application/json Accept: application/json Authorization: Bearer Pbench_auth_token (user received upon login) :return: Success: 200 with empty payload Failure: <status_Code>, response_object = { "message": "failure message" } """ try: user, verified = self.auth.verify_user(username) except Exception: self.logger.exception("Exception occurred during the getUser {}", username) abort(500, message="INTERNAL ERROR") # TODO: Check if the user has the right privileges if not verified and not user.is_admin(): self.logger.warning( "User {} is not authorized to delete user {}.", user.username, username, ) abort(403, message="Not authorized to delete user") try: user = User.query(username=username) # Do not delete if the user is admin if not user.is_admin(): User.delete(username) except Exception: self.logger.exception( "Exception occurred during deleting the user entry for user '{}'", username, ) abort(500, message="INTERNAL ERROR") else: if user.is_admin(): self.logger.warning("Admin attempted to delete admin user") abort(403, message="Admin user can not be deleted") self.logger.info("User entry deleted for user with username {}", username) return "", 200
def attach_dataset(monkeypatch, pbench_token, create_user): """ Mock a Dataset attach call to return an object. We mock the Dataset.attach method to avoid DB access here, however the user authentication mechanism is not yet mocked so we have to look up User data. Args: monkeypatch: patching fixture pbench_token: create a "drb" user for testing create_user: create a "test" user """ datasets = {} drb = User.query(username="******") # Created by pbench_token fixture test = User.query(username="******") # Created by create_user fixture datasets["drb"] = Dataset( owner=drb, owner_id=drb.id, controller="node", name="drb", access="private", id=1, ) datasets["test"] = Dataset( owner=test, owner_id=test.id, controller="node", name="test", access="private", id=2, ) def attach_dataset(controller: str, name: str) -> Dataset: return datasets[name] with monkeypatch.context() as m: m.setattr(Dataset, "attach", attach_dataset) yield
def verify_user(self, username): """ Check if the provided username belongs to the current user by querying the Usermodel with the current user :param username: :param logger :return: User (UserModel instance), verified status (boolean) """ user = User.query(id=self.token_auth.current_user().id) # check if the current username matches with the one provided verified = user is not None and user.username == username Auth.logger.warning("verified status of user '{}' is '{}'", username, verified) return user, verified
def user_update( context: object, updateuser: str, username: str, first_name: str, last_name: str, email: str, role: str, ) -> None: try: config_setup(context) # Query the user user = User.query(username=updateuser) if user is None: click.echo(f"User {updateuser} doesn't exist") rv = 1 else: dict_to_update = {} if username: dict_to_update["username"] = username if first_name: dict_to_update["first_name"] = first_name if last_name: dict_to_update["last_name"] = last_name if email: dict_to_update["email"] = email if role: dict_to_update["role"] = role # Update the user user.update(**dict_to_update) click.echo(f"User {updateuser} updated") rv = 0 except Exception as exc: click.echo(exc, err=True) rv = 2 if isinstance(exc, BadConfig) else 1 click.get_current_context().exit(rv)
def get_valid_target_user(self, target_username: str, request_type: str) -> "UserAPI.TargetUser": """ Helper function to determine whether the API call is permitted for the target username Right now it is only permitted for an admin user and the target user itself. This returns a target User on success or None on failure; in the case of failure, also returns the corresponding HTTP status code and message string """ current_user = self.auth.token_auth.current_user() if current_user.username == target_username: return UserAPI.TargetUser(target_user=current_user, http_status=HTTPStatus.OK, http_message="") if current_user.is_admin(): target_user = User.query(username=target_username) if target_user: return UserAPI.TargetUser(target_user=target_user, http_status=HTTPStatus.OK, http_message="") self.logger.warning( "User {} requested {} operation but user {} is not found.", current_user.username, request_type, target_username, ) return UserAPI.TargetUser( target_user=None, http_status=HTTPStatus.NOT_FOUND, http_message=f"User {target_username} not found", ) self.logger.warning( "User {} is not authorized to {} user {}.", current_user.username, request_type, target_username, ) return UserAPI.TargetUser( target_user=None, http_status=HTTPStatus.FORBIDDEN, http_message=f"Not authorized to access user {target_username}", )
def verify_user(self, target_username: str) -> User: """ Check whether the requested target user is the owner of the authorization token provided to the API. We're returning a User instance corresponding to the target username provided. If the request is not authenticated (current user is None) we return None as we won't be able to verify the target user. However, if the current user is an admin, we return the instance of the target user by querying the target username provided. """ current_user = Auth.token_auth.current_user() if not current_user: return None if current_user.username == target_username: return current_user if current_user.is_admin(): target_user = User.query(username=target_username) return target_user return None
def validate_owner(self, key: str, value: Any) -> User: """ Validate and translate owner name to User object Args: key: owner value: username Raises: DatasetBadParameter: the owner value given doesn't resolve to a Pbench username. Returns: User object """ if type(value) is User: return value elif type(value) is str: user = User.query(username=value) if user: return user raise DatasetBadParameterType(value, "username")
def preprocess(self, client_json: JSON) -> CONTEXT: """ Query the Dataset associated with this name, and determine whether the authenticated user has UPDATE access to this dataset. (Currently, this means the authenticated user is the owner of the dataset, or has ADMIN role.) If the user has authorization to update the dataset, return the dataset object as CONTEXT so that the postprocess operation can mark it as published. Args: json_data: JSON dictionary of type-normalized key-value pairs controller: the controller that generated the dataset name: name of the dataset to publish access: The desired access level of the dataset (currently either "private" or "public") Returns: CONTEXT referring to the dataset object if the operation should continue, or None """ dataset = Dataset.attach(controller=client_json["controller"], name=client_json["name"]) owner = User.query(id=dataset.owner_id) if not owner: self.logger.error("Dataset owner ID {} cannot be found in Users", dataset.owner_id) abort(HTTPStatus.INTERNAL_SERVER_ERROR, message="Dataset owner not found") # For publish, we check authorization against the ownership of the # dataset that was selected rather than having an explicit "user" # JSON parameter. This will raise UnauthorizedAccess on failure. self._check_authorization(owner.username, client_json["access"]) # The dataset exists, so continue the operation with the appropriate # CONTEXT. return {"dataset": dataset}
def validate_user(name: str) -> str: """ Encapsulate a query to reject "username" values that don't correspond to a registered user. TODO: We need to decide exactly how we're representing our users, and what's mutable. For example, if we allow changing "username" we need to have a stable "userid" field that we use for indexing... in which case this should translate "username" into "userid" for internal use. For now, just return the original username if it was found. FIXME: I thought this would be an ArgumentParser "type" so we could do validation during parsing. That's awkward because we need to finish parsing the --config parameter in order to access the config and logger objects required to initialize database access, which needs to be done before we can make a query. Can we work around this without too much mess? Args: :name: The username field of a registered user Raises: ValueError: The username doesn't correspond to a registered user TypeError: Some other error occurred looking for the user Returns: The specified username if it's valid; does not return on failure """ try: user = User.query(username=name) except Exception: User.logger.exception( "Unexpected exception from query for user {}", name) raise if not user: raise UnknownUser(name) return name
def verify_auth(auth_token): """ Validates the auth token :param auth_token: :return: User object/None """ try: payload = jwt.decode( auth_token, os.getenv("SECRET_KEY", "my_precious"), algorithms="HS256", ) user_id = payload["sub"] if ActiveTokens.valid(auth_token): user = User.query(id=user_id) return user except jwt.ExpiredSignatureError: try: ActiveTokens.delete(auth_token) except Exception: Auth.logger.error( "User attempted Pbench expired token but we could not delete the expired auth token from the database. token: '{}'", auth_token, ) return None Auth.logger.warning( "User attempted Pbench expired token '{}', Token deleted from the database and no longer tracked", auth_token, ) except jwt.InvalidTokenError: Auth.logger.warning("User attempted invalid Pbench token '{}'", auth_token) except Exception: Auth.logger.exception( "Exception occurred while verifying the auth token '{}'", auth_token) return None
def post(self): """ Post request for registering a new user. This requires a JSON data with required user fields { "username": "******", "password": "******", "first_name": first_name, "last_name": "last_name", "email": "*****@*****.**" } Required headers include Content-Type: application/json Accept: application/json :return: Success: 201 with empty payload Failure: <status_Code>, response_object = { "message": "failure message" } To get the auth token user has to perform the login action """ # get the post data user_data = request.get_json() if not user_data: self.logger.warning("Invalid json object: {}", request.url) abort(HTTPStatus.BAD_REQUEST, message="Invalid json object in request") username = user_data.get("username") if not username: self.logger.warning("Missing username field") abort(HTTPStatus.BAD_REQUEST, message="Missing username field") username = username.lower() if User.is_admin_username(username): self.logger.warning("User tried to register with admin username") abort( HTTPStatus.BAD_REQUEST, message="Please choose another username", ) # check if provided username already exists try: user = User.query(username=user_data.get("username")) except Exception: self.logger.exception("Exception while querying username") abort(HTTPStatus.INTERNAL_SERVER_ERROR, message="INTERNAL ERROR") if user: self.logger.warning("A user tried to re-register. Username: {}", user.username) abort(HTTPStatus.FORBIDDEN, message="Provided username is already in use.") password = user_data.get("password") if not password: self.logger.warning("Missing password field") abort(HTTPStatus.BAD_REQUEST, message="Missing password field") email_id = user_data.get("email") if not email_id: self.logger.warning("Missing email field") abort(HTTPStatus.BAD_REQUEST, message="Missing email field") # check if provided email already exists try: user = User.query(email=email_id) except Exception: self.logger.exception("Exception while querying user email") abort(HTTPStatus.INTERNAL_SERVER_ERROR, message="INTERNAL ERROR") if user: self.logger.warning("A user tried to re-register. Email: {}", user.email) abort(HTTPStatus.FORBIDDEN, message="Provided email is already in use") first_name = user_data.get("first_name") if not first_name: self.logger.warning("Missing first_name field") abort(HTTPStatus.BAD_REQUEST, message="Missing first_name field") last_name = user_data.get("last_name") if not last_name: self.logger.warning("Missing last_name field") abort(HTTPStatus.BAD_REQUEST, message="Missing last_name field") try: user = User( username=username, password=password, first_name=first_name, last_name=last_name, email=email_id, ) # insert the user user.add() self.logger.info("New user registered, username: {}, email: {}", username, email_id) return "", HTTPStatus.CREATED except EmailNotValidError: self.logger.warning("Invalid email {}", email_id) abort(HTTPStatus.BAD_REQUEST, message=f"Invalid email: {email_id}") except Exception: self.logger.exception("Exception while registering a user") abort(HTTPStatus.INTERNAL_SERVER_ERROR, message="INTERNAL ERROR")
def post(self): """ Post request for logging in user. The user is allowed to re-login multiple times and each time a new valid auth token will be provided This requires a JSON data with required user metadata fields { "username": "******", "password": "******", } Required headers include Content-Type: application/json Accept: application/json :return: JSON Payload Success: 200, response_object = { "auth_token": "<authorization_token>" "username": <username> } Failure: <status_Code>, response_object = { "message": "failure message" } """ # get the post data post_data = request.get_json() if not post_data: self.logger.warning("Invalid json object: {}", request.url) abort(HTTPStatus.BAD_REQUEST, message="Invalid json object in request") username = post_data.get("username") if not username: self.logger.warning( "Username not provided during the login process") abort(HTTPStatus.BAD_REQUEST, message="Please provide a valid username") password = post_data.get("password") if not password: self.logger.warning( "Password not provided during the login process") abort(HTTPStatus.BAD_REQUEST, message="Please provide a valid password") try: # fetch the user data user = User.query(username=username) except Exception: self.logger.exception("Exception occurred during user login") abort(HTTPStatus.INTERNAL_SERVER_ERROR, message="INTERNAL ERROR") if not user: self.logger.warning( "No user found in the db for Username: {} while login", username) abort(HTTPStatus.UNAUTHORIZED, message="Bad login") # Validate the password if not check_password_hash(user.password, password): self.logger.warning("Wrong password for user: {} during login", username) abort(HTTPStatus.UNAUTHORIZED, message="Bad login") try: auth_token = self.auth.encode_auth_token( self.token_expire_duration, user.id) except ( jwt.InvalidIssuer, jwt.InvalidIssuedAtError, jwt.InvalidAlgorithmError, jwt.PyJWTError, ): self.logger.exception( "Could not encode the JWT auth token for user: {} while login", username) abort( HTTPStatus.INTERNAL_SERVER_ERROR, message="INTERNAL ERROR", ) # Add the new auth token to the database for later access try: token = ActiveTokens(token=auth_token) # TODO: Decide on the auth token limit per user user.update(auth_tokens=token) self.logger.info("New auth token registered for user {}", user.email) except IntegrityError: self.logger.warning( "Duplicate auth token got created, user might have tried to re-login immediately" ) abort(HTTPStatus.CONFLICT, message="Login collision; please wait and retry") except SQLAlchemyError as e: self.logger.error( "SQLAlchemy Exception while logging in a user {}", type(e)) abort(HTTPStatus.INTERNAL_SERVER_ERROR, message="INTERNAL ERROR") except Exception: self.logger.exception("Exception while logging in a user") abort(HTTPStatus.INTERNAL_SERVER_ERROR, message="INTERNAL ERROR") response_object = { "auth_token": auth_token, "username": username, } return make_response(jsonify(response_object), HTTPStatus.OK)