Add auth levels and logging
This commit is contained in:
parent
c0a33397fc
commit
3ec92573e1
114
api/app.py
114
api/app.py
@ -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():
|
||||
user = User.objects.get(username=get_jwt_identity())
|
||||
print(user.to_json())
|
||||
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
|
||||
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"]
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user