Add auth levels and logging
This commit is contained in:
parent
c0a33397fc
commit
3ec92573e1
112
api/app.py
112
api/app.py
@ -1,8 +1,10 @@
|
|||||||
|
import functools
|
||||||
import json, os, sys
|
import json, os, sys
|
||||||
from datetime import timedelta, datetime, timezone
|
from datetime import timedelta, datetime, timezone
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from flask import Flask, request, Response, jsonify, session
|
from flask import Flask, request, Response, jsonify, session
|
||||||
|
from pymongo import database
|
||||||
|
|
||||||
from database.models import Flight, User, AuthLevel
|
from database.models import Flight, User, AuthLevel
|
||||||
from mongoengine import connect, ValidationError, DoesNotExist
|
from mongoengine import connect, ValidationError, DoesNotExist
|
||||||
@ -16,7 +18,6 @@ except KeyError:
|
|||||||
api.logger.error("Please set 'TAILFIN_DB_KEY' environment variable")
|
api.logger.error("Please set 'TAILFIN_DB_KEY' environment variable")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
print(os.environ.get("TAILFIN_DB_KEY"))
|
|
||||||
api.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
|
api.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
|
||||||
jwt = JWTManager(api)
|
jwt = JWTManager(api)
|
||||||
|
|
||||||
@ -24,6 +25,20 @@ jwt = JWTManager(api)
|
|||||||
connect('tailfin')
|
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
|
@api.after_request
|
||||||
def refresh_expiring_jwts(response):
|
def refresh_expiring_jwts(response):
|
||||||
try:
|
try:
|
||||||
@ -31,6 +46,7 @@ def refresh_expiring_jwts(response):
|
|||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
target_timestamp = datetime.timestamp(now + timedelta(minutes=30))
|
target_timestamp = datetime.timestamp(now + timedelta(minutes=30))
|
||||||
if target_timestamp > exp_timestamp:
|
if target_timestamp > exp_timestamp:
|
||||||
|
api.logger.info("Refreshing expiring JWT")
|
||||||
access_token = create_access_token(identity=get_jwt_identity())
|
access_token = create_access_token(identity=get_jwt_identity())
|
||||||
data = response.get_json()
|
data = response.get_json()
|
||||||
if type(data) is dict:
|
if type(data) is dict:
|
||||||
@ -39,36 +55,49 @@ def refresh_expiring_jwts(response):
|
|||||||
return response
|
return response
|
||||||
except (RuntimeError, KeyError):
|
except (RuntimeError, KeyError):
|
||||||
# No valid JWT, return original response
|
# No valid JWT, return original response
|
||||||
|
api.logger.info("No valid JWT, cannot refresh expiry")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@api.route('/add_user', methods=["POST"])
|
@api.route('/users', methods=["POST"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
|
@auth_level_required(AuthLevel.ADMIN)
|
||||||
def add_user():
|
def add_user():
|
||||||
user = User.objects.get(username=get_jwt_identity())
|
|
||||||
if user.level != AuthLevel.ADMIN:
|
|
||||||
return '', 401
|
|
||||||
|
|
||||||
username = request.json.get("username", None)
|
username = request.json.get("username", None)
|
||||||
password = request.json.get("password", 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:
|
try:
|
||||||
existing_user = User.objects.get(username=username)
|
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"})
|
return jsonify({"msg": "Username already exists"})
|
||||||
except DoesNotExist:
|
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())
|
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
||||||
user = User(username=username, password=hashed_password, level=auth_level).save()
|
user = User(username=username, password=hashed_password, level=auth_level.value)
|
||||||
return jsonify({"id": user.id}), 200
|
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"])
|
@api.route('/users', methods=["GET"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
|
@auth_level_required(AuthLevel.ADMIN)
|
||||||
def get_users():
|
def get_users():
|
||||||
user = User.objects.get(username=get_jwt_identity())
|
|
||||||
if user.level != AuthLevel.ADMIN:
|
|
||||||
return '', 401
|
|
||||||
|
|
||||||
users = User.objects.to_json()
|
users = User.objects.to_json()
|
||||||
return users, 200
|
return users, 200
|
||||||
|
|
||||||
@ -85,8 +114,10 @@ def create_token():
|
|||||||
else:
|
else:
|
||||||
if bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):
|
if bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):
|
||||||
access_token = create_access_token(identity=username)
|
access_token = create_access_token(identity=username)
|
||||||
|
api.logger.info("%s successfully logged in", username)
|
||||||
response = {"access_token": access_token}
|
response = {"access_token": access_token}
|
||||||
return jsonify(response), 200
|
return jsonify(response), 200
|
||||||
|
api.logger.info("Failed login attempt from %s", request.remote_addr)
|
||||||
return jsonify({"msg": "Invalid username or password"}), 401
|
return jsonify({"msg": "Invalid username or password"}), 401
|
||||||
|
|
||||||
|
|
||||||
@ -100,11 +131,43 @@ def logout():
|
|||||||
@api.route('/profile', methods=["GET"])
|
@api.route('/profile', methods=["GET"])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def get_profile():
|
def get_profile():
|
||||||
|
try:
|
||||||
user = User.objects.get(username=get_jwt_identity())
|
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
|
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'])
|
@api.route('/flights', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def get_flights():
|
def get_flights():
|
||||||
@ -113,13 +176,22 @@ def get_flights():
|
|||||||
return flights, 200
|
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'])
|
@api.route('/flights/<flight_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def get_flight(flight_id):
|
def get_flight(flight_id):
|
||||||
user = User.objects.get(username=get_jwt_identity()).id
|
user = User.objects.get(username=get_jwt_identity()).id
|
||||||
flight = Flight.objects(id=flight_id).to_json()
|
flight = Flight.objects(id=flight_id).to_json()
|
||||||
if flight.user != user:
|
if flight.user != user and AuthLevel(user.level) != AuthLevel.ADMIN:
|
||||||
return '', 401
|
api.logger.warning("Attempted access to unauthorized flight by %s", user.username)
|
||||||
|
return {"msg": "Unauthorized access"}, 403
|
||||||
return flight, 200
|
return flight, 200
|
||||||
|
|
||||||
|
|
||||||
@ -144,14 +216,14 @@ def update_flight(flight_id):
|
|||||||
return '', 200
|
return '', 200
|
||||||
|
|
||||||
|
|
||||||
@api.route('/flights/<int:index>', methods=['DELETE'])
|
@api.route('/flights/<flight_id>', methods=['DELETE'])
|
||||||
def delete_flight(index):
|
def delete_flight(flight_id):
|
||||||
Flight.objects(id=id).delete()
|
Flight.objects(id=flight_id).delete()
|
||||||
return '', 200
|
return '', 200
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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...")
|
api.logger.info("No admin users exist. Creating default admin user...")
|
||||||
try:
|
try:
|
||||||
admin_username = os.environ["TAILFIN_ADMIN_USERNAME"]
|
admin_username = os.environ["TAILFIN_ADMIN_USERNAME"]
|
||||||
|
@ -8,11 +8,29 @@ class AuthLevel(Enum):
|
|||||||
USER = 1
|
USER = 1
|
||||||
ADMIN = 2
|
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):
|
class User(Document):
|
||||||
username = StringField(required=True, unique=True)
|
username = StringField(required=True, unique=True)
|
||||||
password = StringField(required=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):
|
class Flight(Document):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user