Add auth levels and logging

This commit is contained in:
april 2023-12-19 10:58:07 -06:00
parent c0a33397fc
commit 3ec92573e1
2 changed files with 112 additions and 22 deletions

View File

@ -1,8 +1,10 @@
import functools
import json, os, sys
from datetime import timedelta, datetime, timezone
import bcrypt
from flask import Flask, request, Response, jsonify, session
from pymongo import database
from database.models import Flight, User, AuthLevel
from mongoengine import connect, ValidationError, DoesNotExist
@ -16,7 +18,6 @@ except KeyError:
api.logger.error("Please set 'TAILFIN_DB_KEY' environment variable")
exit(1)
print(os.environ.get("TAILFIN_DB_KEY"))
api.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
jwt = JWTManager(api)
@ -24,6 +25,20 @@ jwt = JWTManager(api)
connect('tailfin')
def auth_level_required(level):
def auth_inner(func):
def auth_wrapper(*args, **kwargs):
user = User.objects.get(username=get_jwt_identity())
if AuthLevel(user.level) < level:
api.logger.warning("Attempted access to unauthorized resource by %s", user.username)
return '', 403
else:
return func(*args, **kwargs)
auth_wrapper.__name__ = func.__name__
return auth_wrapper
return auth_inner
@api.after_request
def refresh_expiring_jwts(response):
try:
@ -31,6 +46,7 @@ def refresh_expiring_jwts(response):
now = datetime.now(timezone.utc)
target_timestamp = datetime.timestamp(now + timedelta(minutes=30))
if target_timestamp > exp_timestamp:
api.logger.info("Refreshing expiring JWT")
access_token = create_access_token(identity=get_jwt_identity())
data = response.get_json()
if type(data) is dict:
@ -39,36 +55,49 @@ def refresh_expiring_jwts(response):
return response
except (RuntimeError, KeyError):
# No valid JWT, return original response
api.logger.info("No valid JWT, cannot refresh expiry")
return response
@api.route('/add_user', methods=["POST"])
@api.route('/users', methods=["POST"])
@jwt_required()
@auth_level_required(AuthLevel.ADMIN)
def add_user():
user = User.objects.get(username=get_jwt_identity())
if user.level != AuthLevel.ADMIN:
return '', 401
username = request.json.get("username", None)
password = request.json.get("password", None)
auth_level = request.json.get("auth_level", None)
auth_level = AuthLevel(request.json.get("auth_level", None))
try:
existing_user = User.objects.get(username=username)
api.logger.info("User %s already exists at auth level %s", existing_user.username, existing_user.level)
return jsonify({"msg": "Username already exists"})
except DoesNotExist:
api.logger.info("Creating user %s with auth level %s", username, auth_level)
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
user = User(username=username, password=hashed_password, level=auth_level).save()
return jsonify({"id": user.id}), 200
user = User(username=username, password=hashed_password, level=auth_level.value)
user.save()
return jsonify({"id": user.id}), 201
@api.route('/users/<user_id>', methods=['DELETE'])
@jwt_required()
@auth_level_required(AuthLevel.ADMIN)
def remove_user(user_id):
try:
User.objects.get(id=user_id).delete()
except DoesNotExist:
api.logger.info("Attempt to delete nonexistent user %s by %s", user_id, get_jwt_identity())
return {"msg": "User does not exist"}, 401
Flight.objects(user=user_id).delete()
return '', 200
@api.route('/users', methods=["GET"])
@jwt_required()
@auth_level_required(AuthLevel.ADMIN)
def get_users():
user = User.objects.get(username=get_jwt_identity())
if user.level != AuthLevel.ADMIN:
return '', 401
users = User.objects.to_json()
return users, 200
@ -85,8 +114,10 @@ def create_token():
else:
if bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):
access_token = create_access_token(identity=username)
api.logger.info("%s successfully logged in", username)
response = {"access_token": access_token}
return jsonify(response), 200
api.logger.info("Failed login attempt from %s", request.remote_addr)
return jsonify({"msg": "Invalid username or password"}), 401
@ -100,11 +131,43 @@ def logout():
@api.route('/profile', methods=["GET"])
@jwt_required()
def get_profile():
try:
user = User.objects.get(username=get_jwt_identity())
print(user.to_json())
except DoesNotExist:
api.logger.warning("User %s not found", get_jwt_identity())
return {"msg": "User not found"}, 401
return jsonify({"username": user.username, "auth_level:": str(user.level)}), 200
@api.route('/profile', methods=["PUT"])
@jwt_required()
def update_profile():
try:
user = User.objects.get(username=get_jwt_identity())
except DoesNotExist:
api.logger.warning("User %s not found", get_jwt_identity())
return {"msg": "user not found"}, 401
body = request.get_json()
username = request.json.get("username", None)
password = request.json.get("password", None)
auth_level = request.json.get("level", None)
if username:
existing_users = User.objects(username=username).count()
if existing_users != 0:
return jsonify({"msg": "Username not available"})
if password:
hashed_password = bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt())
if auth_level:
if AuthLevel(user.level) < AuthLevel.ADMIN:
api.logger.warning("Unauthorized attempt to change auth level of %s", user.username)
return jsonify({"msg": "Unauthorized attempt to change auth level"}), 403
user.update(**body)
return '', 200
@api.route('/flights', methods=['GET'])
@jwt_required()
def get_flights():
@ -113,13 +176,22 @@ def get_flights():
return flights, 200
@api.route('/flights/all', methods=['GET'])
@jwt_required()
@auth_level_required(AuthLevel.ADMIN)
def get_all_flights():
flights = Flight.objects.to_json()
return flights, 200
@api.route('/flights/<flight_id>', methods=['GET'])
@jwt_required()
def get_flight(flight_id):
user = User.objects.get(username=get_jwt_identity()).id
flight = Flight.objects(id=flight_id).to_json()
if flight.user != user:
return '', 401
if flight.user != user and AuthLevel(user.level) != AuthLevel.ADMIN:
api.logger.warning("Attempted access to unauthorized flight by %s", user.username)
return {"msg": "Unauthorized access"}, 403
return flight, 200
@ -144,14 +216,14 @@ def update_flight(flight_id):
return '', 200
@api.route('/flights/<int:index>', methods=['DELETE'])
def delete_flight(index):
Flight.objects(id=id).delete()
@api.route('/flights/<flight_id>', methods=['DELETE'])
def delete_flight(flight_id):
Flight.objects(id=flight_id).delete()
return '', 200
if __name__ == '__main__':
if User.objects(level=AuthLevel.ADMIN).count() == 0:
if User.objects(level=AuthLevel.ADMIN.value).count() == 0:
api.logger.info("No admin users exist. Creating default admin user...")
try:
admin_username = os.environ["TAILFIN_ADMIN_USERNAME"]

View File

@ -8,11 +8,29 @@ class AuthLevel(Enum):
USER = 1
ADMIN = 2
def __lt__(self, other):
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented
def __gt__(self, other):
if self.__class__ is other.__class__:
return self.value > other.value
return NotImplemented
def __eq__(self, other):
if self.__class__ is other.__class__:
return self.value == other.value
return NotImplemented
class User(Document):
username = StringField(required=True, unique=True)
password = StringField(required=True)
level = EnumField(AuthLevel, default=AuthLevel.USER)
# EnumField validation is currently broken, replace workaround if MongoEngine is updated to fix it
level = IntField(choices=[l.value for l in AuthLevel], default=1)
# level = EnumField(AuthLevel, default=AuthLevel.USER)
class Flight(Document):