Skip to content

Initial implementation of Pbench user model and associated APIs in server #1937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions lib/pbench/server/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

import os
import sys

from flask import Flask
from flask_restful import Api
Expand All @@ -17,41 +18,71 @@
from pbench.common.logger import get_pbench_logger
from pbench.server.api.resources.query_apis.elasticsearch_api import Elasticsearch
from pbench.server.api.resources.query_apis.query_controllers import QueryControllers
from pbench.server.database.database import Database
from pbench.server.api.resources.query_apis.query_month_indices import QueryMonthIndices
from pbench.server.api.auth import Auth

from pbench.server.api.resources.users_api import (
RegisterUser,
Login,
Logout,
UserAPI,
)


def register_endpoints(api, app, config):
"""Register flask endpoints with the corresponding resource classes
to make the APIs active."""

base_uri = config.rest_uri
app.logger.info("Registering service endpoints with base URI {}", base_uri)
logger = app.logger

# Init the the authentication module
token_auth = Auth()
Auth.set_logger(logger)

logger.info("Registering service endpoints with base URI {}", base_uri)

api.add_resource(
Upload,
f"{base_uri}/upload/ctrl/<string:controller>",
resource_class_args=(config, app.logger),
resource_class_args=(config, logger),
)
api.add_resource(
HostInfo, f"{base_uri}/host_info", resource_class_args=(config, app.logger),
HostInfo, f"{base_uri}/host_info", resource_class_args=(config, logger),
)
api.add_resource(
Elasticsearch,
f"{base_uri}/elasticsearch",
resource_class_args=(config, app.logger),
resource_class_args=(config, logger),
)
api.add_resource(
GraphQL, f"{base_uri}/graphql", resource_class_args=(config, app.logger),
GraphQL, f"{base_uri}/graphql", resource_class_args=(config, logger),
)
api.add_resource(
QueryControllers,
f"{base_uri}/controllers/list",
resource_class_args=(config, app.logger),
resource_class_args=(config, logger),
)
api.add_resource(
QueryMonthIndices,
f"{base_uri}/controllers/months",
resource_class_args=(config, app.logger),
resource_class_args=(config, logger),
)

api.add_resource(
RegisterUser, f"{base_uri}/register", resource_class_args=(config, logger),
)
api.add_resource(
Login, f"{base_uri}/login", resource_class_args=(config, logger, token_auth),
)
api.add_resource(
Logout, f"{base_uri}/logout", resource_class_args=(config, logger, token_auth),
)
api.add_resource(
UserAPI,
f"{base_uri}/user/<string:username>",
resource_class_args=(logger, token_auth),
)


Expand All @@ -74,14 +105,25 @@ def create_app(server_config):
"""Create Flask app with defined resource endpoints."""

app = Flask("api-server")
api = Api(app)
CORS(app, resources={r"/api/*": {"origins": "*"}})

app.logger = get_pbench_logger(__name__, server_config)

app.config["DEBUG"] = False
app.config["TESTING"] = False

api = Api(app)

register_endpoints(api, app, server_config)

try:
Database.init_db(server_config=server_config, logger=app.logger)
except Exception:
app.logger.exception("Exception while initializing sqlalchemy database")
sys.exit(1)

@app.teardown_appcontext
def shutdown_session(exception=None):
Database.db_session.remove()

return app
121 changes: 121 additions & 0 deletions lib/pbench/server/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import jwt
import os
import datetime
from flask import request, abort
from flask_httpauth import HTTPTokenAuth
from pbench.server.database.models.users import User
from pbench.server.database.models.active_tokens import ActiveTokens


class Auth:
token_auth = HTTPTokenAuth("Bearer")

@staticmethod
def set_logger(logger):
# Logger gets set at the time of auth module initialization
Auth.logger = logger

def encode_auth_token(self, token_expire_duration, user_id):
"""
Generates the Auth Token
:return: jwt token string
"""
current_utc = datetime.datetime.utcnow()
payload = {
"iat": current_utc,
"exp": current_utc + datetime.timedelta(minutes=int(token_expire_duration)),
"sub": user_id,
}

# Get jwt key
jwt_key = self.get_secret_key()
return jwt.encode(payload, jwt_key, algorithm="HS256")

def get_secret_key(self):
try:
return os.getenv("SECRET_KEY", "my_precious")
except Exception as e:
Auth.logger.exception(f"{__name__}: ERROR: {e.__traceback__}")

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 get_auth_token(self, logger):
# get auth token
auth_header = request.headers.get("Authorization")

if not auth_header:
logger.warning("Missing expected Authorization header")
abort(
403,
message="Please add 'Authorization' token as Authorization: Bearer <session_token>",
)

try:
auth_schema, auth_token = auth_header.split()
except ValueError:
logger.warning("Malformed Auth header")
abort(
401,
message="Malformed Authorization header, please add request header as Authorization: Bearer <session_token>",
)
else:
if auth_schema.lower() != "bearer":
logger.warning(
"Expected authorization schema to be 'bearer', not '{}'",
auth_schema,
)
abort(
401,
message="Malformed Authorization header, request auth needs bearer token: Bearer <session_token>",
)
return auth_token

@staticmethod
@token_auth.verify_token
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
Loading