Skip to content

Commit 8213843

Browse files
npalaskariya-17
authored andcommitted
Initial pbench user authentication model implementation (distributed-system-analysis#1937)
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
1 parent ad09077 commit 8213843

File tree

16 files changed

+1575
-8
lines changed

16 files changed

+1575
-8
lines changed

lib/pbench/server/api/__init__.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import os
8+
import sys
89

910
from flask import Flask
1011
from flask_restful import Api
@@ -17,41 +18,71 @@
1718
from pbench.common.logger import get_pbench_logger
1819
from pbench.server.api.resources.query_apis.elasticsearch_api import Elasticsearch
1920
from pbench.server.api.resources.query_apis.query_controllers import QueryControllers
21+
from pbench.server.database.database import Database
2022
from pbench.server.api.resources.query_apis.query_month_indices import QueryMonthIndices
23+
from pbench.server.api.auth import Auth
24+
25+
from pbench.server.api.resources.users_api import (
26+
RegisterUser,
27+
Login,
28+
Logout,
29+
UserAPI,
30+
)
2131

2232

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

2737
base_uri = config.rest_uri
28-
app.logger.info("Registering service endpoints with base URI {}", base_uri)
38+
logger = app.logger
39+
40+
# Init the the authentication module
41+
token_auth = Auth()
42+
Auth.set_logger(logger)
43+
44+
logger.info("Registering service endpoints with base URI {}", base_uri)
2945

3046
api.add_resource(
3147
Upload,
3248
f"{base_uri}/upload/ctrl/<string:controller>",
33-
resource_class_args=(config, app.logger),
49+
resource_class_args=(config, logger),
3450
)
3551
api.add_resource(
36-
HostInfo, f"{base_uri}/host_info", resource_class_args=(config, app.logger),
52+
HostInfo, f"{base_uri}/host_info", resource_class_args=(config, logger),
3753
)
3854
api.add_resource(
3955
Elasticsearch,
4056
f"{base_uri}/elasticsearch",
41-
resource_class_args=(config, app.logger),
57+
resource_class_args=(config, logger),
4258
)
4359
api.add_resource(
44-
GraphQL, f"{base_uri}/graphql", resource_class_args=(config, app.logger),
60+
GraphQL, f"{base_uri}/graphql", resource_class_args=(config, logger),
4561
)
4662
api.add_resource(
4763
QueryControllers,
4864
f"{base_uri}/controllers/list",
49-
resource_class_args=(config, app.logger),
65+
resource_class_args=(config, logger),
5066
)
5167
api.add_resource(
5268
QueryMonthIndices,
5369
f"{base_uri}/controllers/months",
54-
resource_class_args=(config, app.logger),
70+
resource_class_args=(config, logger),
71+
)
72+
73+
api.add_resource(
74+
RegisterUser, f"{base_uri}/register", resource_class_args=(config, logger),
75+
)
76+
api.add_resource(
77+
Login, f"{base_uri}/login", resource_class_args=(config, logger, token_auth),
78+
)
79+
api.add_resource(
80+
Logout, f"{base_uri}/logout", resource_class_args=(config, logger, token_auth),
81+
)
82+
api.add_resource(
83+
UserAPI,
84+
f"{base_uri}/user/<string:username>",
85+
resource_class_args=(logger, token_auth),
5586
)
5687

5788

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

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

80110
app.logger = get_pbench_logger(__name__, server_config)
81111

82112
app.config["DEBUG"] = False
83113
app.config["TESTING"] = False
84114

115+
api = Api(app)
116+
85117
register_endpoints(api, app, server_config)
86118

119+
try:
120+
Database.init_db(server_config=server_config, logger=app.logger)
121+
except Exception:
122+
app.logger.exception("Exception while initializing sqlalchemy database")
123+
sys.exit(1)
124+
125+
@app.teardown_appcontext
126+
def shutdown_session(exception=None):
127+
Database.db_session.remove()
128+
87129
return app

lib/pbench/server/api/auth.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import jwt
2+
import os
3+
import datetime
4+
from flask import request, abort
5+
from flask_httpauth import HTTPTokenAuth
6+
from pbench.server.database.models.users import User
7+
from pbench.server.database.models.active_tokens import ActiveTokens
8+
9+
10+
class Auth:
11+
token_auth = HTTPTokenAuth("Bearer")
12+
13+
@staticmethod
14+
def set_logger(logger):
15+
# Logger gets set at the time of auth module initialization
16+
Auth.logger = logger
17+
18+
def encode_auth_token(self, token_expire_duration, user_id):
19+
"""
20+
Generates the Auth Token
21+
:return: jwt token string
22+
"""
23+
current_utc = datetime.datetime.utcnow()
24+
payload = {
25+
"iat": current_utc,
26+
"exp": current_utc + datetime.timedelta(minutes=int(token_expire_duration)),
27+
"sub": user_id,
28+
}
29+
30+
# Get jwt key
31+
jwt_key = self.get_secret_key()
32+
return jwt.encode(payload, jwt_key, algorithm="HS256")
33+
34+
def get_secret_key(self):
35+
try:
36+
return os.getenv("SECRET_KEY", "my_precious")
37+
except Exception as e:
38+
Auth.logger.exception(f"{__name__}: ERROR: {e.__traceback__}")
39+
40+
def verify_user(self, username):
41+
"""
42+
Check if the provided username belongs to the current user by
43+
querying the Usermodel with the current user
44+
:param username:
45+
:param logger
46+
:return: User (UserModel instance), verified status (boolean)
47+
"""
48+
user = User.query(id=self.token_auth.current_user().id)
49+
# check if the current username matches with the one provided
50+
verified = user is not None and user.username == username
51+
Auth.logger.warning("verified status of user '{}' is '{}'", username, verified)
52+
53+
return user, verified
54+
55+
def get_auth_token(self, logger):
56+
# get auth token
57+
auth_header = request.headers.get("Authorization")
58+
59+
if not auth_header:
60+
logger.warning("Missing expected Authorization header")
61+
abort(
62+
403,
63+
message="Please add 'Authorization' token as Authorization: Bearer <session_token>",
64+
)
65+
66+
try:
67+
auth_schema, auth_token = auth_header.split()
68+
except ValueError:
69+
logger.warning("Malformed Auth header")
70+
abort(
71+
401,
72+
message="Malformed Authorization header, please add request header as Authorization: Bearer <session_token>",
73+
)
74+
else:
75+
if auth_schema.lower() != "bearer":
76+
logger.warning(
77+
"Expected authorization schema to be 'bearer', not '{}'",
78+
auth_schema,
79+
)
80+
abort(
81+
401,
82+
message="Malformed Authorization header, request auth needs bearer token: Bearer <session_token>",
83+
)
84+
return auth_token
85+
86+
@staticmethod
87+
@token_auth.verify_token
88+
def verify_auth(auth_token):
89+
"""
90+
Validates the auth token
91+
:param auth_token:
92+
:return: User object/None
93+
"""
94+
try:
95+
payload = jwt.decode(
96+
auth_token, os.getenv("SECRET_KEY", "my_precious"), algorithms="HS256",
97+
)
98+
user_id = payload["sub"]
99+
if ActiveTokens.valid(auth_token):
100+
user = User.query(id=user_id)
101+
return user
102+
except jwt.ExpiredSignatureError:
103+
try:
104+
ActiveTokens.delete(auth_token)
105+
except Exception:
106+
Auth.logger.error(
107+
"User attempted Pbench expired token but we could not delete the expired auth token from the database. token: '{}'",
108+
auth_token,
109+
)
110+
return None
111+
Auth.logger.warning(
112+
"User attempted Pbench expired token '{}', Token deleted from the database and no longer tracked",
113+
auth_token,
114+
)
115+
except jwt.InvalidTokenError:
116+
Auth.logger.warning("User attempted invalid Pbench token '{}'", auth_token)
117+
except Exception:
118+
Auth.logger.exception(
119+
"Exception occurred while verifying the auth token '{}'", auth_token
120+
)
121+
return None

0 commit comments

Comments
 (0)