Skip to content

Commit

Permalink
Initial pbench user authentication model implementation
Browse files Browse the repository at this point in the history
This implements 5 basic user APIs
1. Register User
    Handles Pbench User registration via JSON request
    POST /v1/register
2. Login User:
    POST /v1/login
    Returns a valid pbench auth token
    User is allowed to issue multiple login requests and thus generating multiple auth tokens,
    Each token is stored in a active_tokens table and has its own expiry
    User is authenticated for subsequest API calls if token not expired and present in the active_tokens table
3. Logout user:
    POST /v1/logout
    Deletes the auth_token from the active_tokens table.
    Once logged out user can not use the same auth token for other API access.
4. Get User:
    GET /v1/user/<string:username>
    Returns the user's self information that was registered, the username must be provided in the url
    If the Auth header does not belong to the username, reject the request unless auth token belongs to the admin
5. Delete User:
    DELETE /v1/user/<string:username>"
    An API for a user to delete himself from the pbench database.
    A user can only perform delete action on himself unless the auth token belongs to the admin user.
6. Updare User:
    PUT /v1/user/<string:username>
    An API for updating the User registration fields, the username must be provided in the url
    Update is not allowed on registerd_on field
  • Loading branch information
npalaska committed Mar 5, 2021
1 parent 4c96c42 commit 60aa002
Show file tree
Hide file tree
Showing 16 changed files with 1,575 additions and 8 deletions.
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

0 comments on commit 60aa002

Please sign in to comment.