API Security, Logging, & Restructure
- Added a .gitignore - Implemented security.py (apikeys.py from server-api) - Implemented wrapper.py (wrapper from radicalbot - Added proper logging using log.conf - Split tracker API endpoints into a router - Renamed Target classes to Tracker
This commit is contained in:
parent
13075f7fa3
commit
f5e0ac8cac
11 changed files with 278 additions and 111 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*/__pycache__/*
|
||||
*.log
|
75
api.py
75
api.py
|
@ -1,70 +1,27 @@
|
|||
import sys
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from target import Target
|
||||
from fastapi import FastAPI, status
|
||||
from response import Response
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
api = FastAPI()
|
||||
|
||||
api_data = {"version": "2021.8.4.0", "author": "Riley Housden"}
|
||||
api_data = {"version": "2021.8.23.0", "author": "Riley Housden"}
|
||||
|
||||
|
||||
@api.post("/update")
|
||||
def update():
|
||||
sys.exit(36)
|
||||
def load_module(name: str):
|
||||
module = importlib.import_module(name)
|
||||
api.include_router(module.router)
|
||||
logger.info(f"Loaded module: {module.__name__}")
|
||||
|
||||
|
||||
@api.post("/restart")
|
||||
def stop():
|
||||
sys.exit(26)
|
||||
for m_path in Path("routers").glob("*"):
|
||||
if "__pycache__" in m_path.parts or "__init__" == m_path.stem:
|
||||
continue
|
||||
load_module(str(m_path).replace("/", ".").replace(".py", ""))
|
||||
|
||||
|
||||
@api.get("/")
|
||||
def quack():
|
||||
return "quack!"
|
||||
|
||||
|
||||
@api.get("/target/points")
|
||||
def list_points(name: str):
|
||||
try:
|
||||
target = Target.from_data(name)
|
||||
return Response(status.HTTP_200_OK, target.points)
|
||||
except FileNotFoundError:
|
||||
return Response(status.HTTP_204_NO_CONTENT, {})
|
||||
|
||||
|
||||
@api.get("/target/points/add")
|
||||
def add_point(name: str, datestr: str, value: int):
|
||||
try:
|
||||
target = Target.from_data(name)
|
||||
except FileNotFoundError:
|
||||
target = Target(name, [])
|
||||
target.add_point(datestr, value)
|
||||
return Response(status.HTTP_200_OK, target).to_json()
|
||||
|
||||
|
||||
@api.put("/target/points/modify")
|
||||
def modify_point(name: str, datestr: str, value: int):
|
||||
target = Target.from_data(name)
|
||||
target.modify_point(datestr, value)
|
||||
return 200
|
||||
|
||||
|
||||
@api.delete("/target/points/delete")
|
||||
def delete_point(name: str, datestr: str):
|
||||
target = Target.from_data(name)
|
||||
target.delete_point(datestr)
|
||||
return 200
|
||||
|
||||
|
||||
@api.get("/target/rename")
|
||||
def rename_target(name: str, new_name: str):
|
||||
target = Target.from_data(name)
|
||||
target.rename(new_name)
|
||||
return 200
|
||||
|
||||
|
||||
@api.delete("/target/delete")
|
||||
def delete_target(name: str):
|
||||
target = Target.from_data(name)
|
||||
target.delete()
|
||||
return api_data
|
||||
|
|
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
|
@ -3,43 +3,52 @@ from datetime import datetime
|
|||
from json.encoder import JSONEncoder
|
||||
from pathlib import Path
|
||||
|
||||
DATA_DIR = Path.home().joinpath(".targetdata")
|
||||
DATA_DIR = Path.home().joinpath(".tracker_api/trackers")
|
||||
|
||||
|
||||
class TargetEncoder(JSONEncoder):
|
||||
def object_hook(dct):
|
||||
if "date_str" in dct:
|
||||
return TrackerPoint(dct["date_str"], dct["value"])
|
||||
if "name" in dct:
|
||||
return Tracker(dct["name"], dct["points"])
|
||||
|
||||
|
||||
class TrackerEncoder(JSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, datetime):
|
||||
return o.strftime("%m/%d/%Y - %H:%M")
|
||||
if isinstance(o, Tracker):
|
||||
return dict(name=o.name, points=o.points)
|
||||
elif isinstance(o, TrackerPoint):
|
||||
return dict(date_str=o.datetime.isoformat(), value=o.value)
|
||||
else:
|
||||
return o.__dict__
|
||||
return json.JSONEncoder.default(self, o)
|
||||
|
||||
|
||||
class TargetPoint:
|
||||
def __init__(self, datestr: str, value: int) -> None:
|
||||
self.datetime = datetime.strptime(datestr, "%m/%d/%Y - %H:%M")
|
||||
class TrackerPoint:
|
||||
def __init__(self, date_str: str, value: int) -> None:
|
||||
self.datetime = datetime.fromisoformat(date_str)
|
||||
self.value = value
|
||||
|
||||
|
||||
class Target:
|
||||
def __init__(self, name: str, points: list[TargetPoint]):
|
||||
class Tracker:
|
||||
def __init__(self, name: str, points: list[TrackerPoint]):
|
||||
self.name = name
|
||||
self.points = points
|
||||
|
||||
def rename(self, name):
|
||||
"""Rename the Target."""
|
||||
"""Rename the Tracker."""
|
||||
DATA_DIR.joinpath(f"{self.name}.json").rename(
|
||||
DATA_DIR.joinpath(f"{name}.json"))
|
||||
self.name = name
|
||||
self.save()
|
||||
|
||||
def delete(self):
|
||||
"""Delete the Target."""
|
||||
"""Delete the Tracker."""
|
||||
filepath = DATA_DIR.joinpath(f"{self.name}.json")
|
||||
filepath.unlink()
|
||||
|
||||
def to_json(self):
|
||||
"""Convert Target object to JSON."""
|
||||
return json.dumps(self, indent=4, cls=TargetEncoder)
|
||||
"""Convert Tracker object to JSON."""
|
||||
return json.dumps(self, indent=4, cls=TrackerEncoder)
|
||||
|
||||
def save(self):
|
||||
"""Save the tracker to JSON."""
|
||||
|
@ -48,24 +57,24 @@ class Target:
|
|||
filepath.touch(exist_ok=True)
|
||||
filepath.write_text(self.to_json())
|
||||
|
||||
def modify_point(self, datestr: str, value: int):
|
||||
def modify_point(self, date_str: str, value: int):
|
||||
"""Modify a point. Change its assigned value to the one given."""
|
||||
date_time = datetime.strptime(datestr, "%m/%d/%Y - %H:%M")
|
||||
date_time = datetime.fromisoformat(date_str)
|
||||
for point in self.points:
|
||||
if point.datetime == date_time:
|
||||
point.value = value
|
||||
break
|
||||
self.save()
|
||||
|
||||
def add_point(self, datestr: str, value: int):
|
||||
def add_point(self, date_str: str, value: int):
|
||||
"""Add a point to the tracker."""
|
||||
point = TargetPoint(datestr, value)
|
||||
point = TrackerPoint(date_str, value)
|
||||
self.points.append(point)
|
||||
self.save()
|
||||
|
||||
def delete_point(self, datestr: str):
|
||||
def delete_point(self, date_str: str):
|
||||
"""Remove a point from the tracker."""
|
||||
date_time = datetime.strptime(datestr, "%m/%d/%Y - %H:%M")
|
||||
date_time = datetime.fromisoformat(date_str)
|
||||
for point in self.points:
|
||||
if point.datetime == date_time:
|
||||
self.points.remove(point)
|
||||
|
@ -74,12 +83,11 @@ class Target:
|
|||
|
||||
@classmethod
|
||||
def from_data(cls, name):
|
||||
"""Load a target from the DATA_DIR."""
|
||||
"""Load a tracker from the DATA_DIR."""
|
||||
filepath = DATA_DIR.joinpath(f"{name}.json")
|
||||
return cls.from_json(filepath.read_text())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str):
|
||||
"""Load a target from a JSON string."""
|
||||
obj = json.loads(json_str)
|
||||
return cls(obj['name'], obj['points'])
|
||||
@staticmethod
|
||||
def from_json(json_str):
|
||||
"""Load a tracker from a JSON string."""
|
||||
return json.loads(json_str, object_hook=object_hook)
|
29
core/wrapper.py
Normal file
29
core/wrapper.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
"""Wrapper Class to handle reboots"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
class Wrapper:
|
||||
def __init__(self, args, stdout):
|
||||
"""
|
||||
Wraps another program and helps with restarting should the program request.
|
||||
:param args: Arguments used to run program
|
||||
:param stdout: The stdout stream to send output to
|
||||
"""
|
||||
self._args = args
|
||||
self.process = None
|
||||
self._stdout = stdout
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the wrapper.
|
||||
:return: None
|
||||
"""
|
||||
self.process = subprocess.Popen(self._args, stdout=self._stdout)
|
||||
while True:
|
||||
while self.process.poll() is None:
|
||||
pass
|
||||
if self.process.returncode == 26:
|
||||
self.process = subprocess.Popen(self._args, stdout=self._stdout)
|
||||
else:
|
||||
sys.exit()
|
46
log.conf
Normal file
46
log.conf
Normal file
|
@ -0,0 +1,46 @@
|
|||
[loggers]
|
||||
keys=root,uvicorn,api,security
|
||||
|
||||
[handlers]
|
||||
keys=console,file
|
||||
|
||||
[formatters]
|
||||
keys=simple
|
||||
|
||||
[logger_root]
|
||||
level=DEBUG
|
||||
handlers=console,file
|
||||
|
||||
[logger_uvicorn]
|
||||
level=INFO
|
||||
handlers=console,file
|
||||
qualname=uvicorn
|
||||
propagate=0
|
||||
|
||||
[logger_api]
|
||||
level=DEBUG
|
||||
handlers=console,file
|
||||
qualname=api
|
||||
propagate=0
|
||||
|
||||
[logger_security]
|
||||
level=INFO
|
||||
handlers=console,file
|
||||
qualname=security
|
||||
propagate=0
|
||||
|
||||
[handler_console]
|
||||
class=StreamHandler
|
||||
level=DEBUG
|
||||
formatter=simple
|
||||
args=(sys.stdout,)
|
||||
|
||||
[handler_file]
|
||||
class=FileHandler
|
||||
level=DEBUG
|
||||
formatter=simple
|
||||
args=('api.log', 'a+')
|
||||
|
||||
[formatter_simple]
|
||||
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
datefmt=
|
15
main.py
15
main.py
|
@ -1,13 +1,6 @@
|
|||
import subprocess
|
||||
import sys
|
||||
|
||||
script = "tracker_api/api.py"
|
||||
from core.wrapper import Wrapper
|
||||
|
||||
while True:
|
||||
try:
|
||||
subprocess.run(f"python {script}", check=True)
|
||||
except subprocess.CalledProcessError as err:
|
||||
if err.returncode == -1:
|
||||
print("Stopping.")
|
||||
break
|
||||
if err.returncode == -2:
|
||||
print("Run update check!")
|
||||
api = Wrapper(['gunicorn', "-k", "uvicorn.workers.UvicornWorker", "--log-config", "log.conf", 'api:api'], sys.stdout)
|
||||
api.start()
|
||||
|
|
15
response.py
15
response.py
|
@ -1,15 +0,0 @@
|
|||
import json
|
||||
|
||||
|
||||
class ResponseEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
return o.__dict__
|
||||
|
||||
|
||||
class Response:
|
||||
def __init__(self, status_code, obj):
|
||||
self.status_code = status_code
|
||||
self.response = obj
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self, indent=4, cls=ResponseEncoder)
|
0
routers/__init__.py
Normal file
0
routers/__init__.py
Normal file
91
routers/security.py
Normal file
91
routers/security.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
from fastapi import Security, HTTPException, APIRouter, Depends
|
||||
from fastapi.security.api_key import APIKeyQuery, APIKeyCookie, APIKeyHeader, APIKey
|
||||
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import uuid
|
||||
import json
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger("security")
|
||||
ROOT_PATH = Path.home().joinpath(".tracker_api/keys")
|
||||
|
||||
API_KEY_NAME = "access_token"
|
||||
|
||||
api_key_query = APIKeyQuery(name=API_KEY_NAME, auto_error=False)
|
||||
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
||||
api_key_cookie = APIKeyCookie(name=API_KEY_NAME, auto_error=False)
|
||||
|
||||
system_key_path = ROOT_PATH.joinpath("system_key")
|
||||
if not system_key_path.exists():
|
||||
logger.info("System Key not found, generating.")
|
||||
system_key_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
system_key_path.touch()
|
||||
system_uuid = str(uuid.uuid4())
|
||||
system_key_path.write_text(system_uuid)
|
||||
logger.info(f"System UUID: {system_uuid}")
|
||||
|
||||
|
||||
def load_keys():
|
||||
path = ROOT_PATH.joinpath("api_keys.json")
|
||||
if not path.exists():
|
||||
path.touch()
|
||||
path.write_text("{}")
|
||||
with path.open("r") as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def get_system_key(
|
||||
api_key_query: str = Security(api_key_query),
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_cookie: str = Security(api_key_cookie)
|
||||
):
|
||||
path = ROOT_PATH.joinpath("system_key")
|
||||
if api_key_query == path.read_text():
|
||||
return api_key_query
|
||||
elif api_key_header == path.read_text():
|
||||
return api_key_header
|
||||
elif api_key_cookie == path.read_text():
|
||||
return api_key_cookie
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
||||
)
|
||||
|
||||
|
||||
def get_api_key(
|
||||
api_key_query: str = Security(api_key_query),
|
||||
api_key_header: str = Security(api_key_header),
|
||||
api_key_cookie: str = Security(api_key_cookie)
|
||||
):
|
||||
if api_key_query in load_keys():
|
||||
return api_key_query
|
||||
elif api_key_header in load_keys():
|
||||
return api_key_header
|
||||
elif api_key_cookie in load_keys():
|
||||
return api_key_header
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/key/create")
|
||||
def create_key(name, system_key: APIKey = Depends(get_system_key)):
|
||||
keys: dict = load_keys()
|
||||
new_key = str(uuid.uuid4())
|
||||
keys[new_key] = {}
|
||||
keys[new_key]["name"] = name
|
||||
path = ROOT_PATH.joinpath("api_keys.json")
|
||||
with path.open("w") as f:
|
||||
json.dump(keys, f, indent=4, sort_keys=True)
|
||||
return new_key
|
||||
|
||||
|
||||
@router.get("/key/me")
|
||||
def key_info(api_key: APIKey = Depends(get_api_key)):
|
||||
data = load_keys()
|
||||
return data[api_key]
|
56
routers/tracker.py
Normal file
56
routers/tracker.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.security.api_key import APIKey
|
||||
|
||||
from .security import get_api_key
|
||||
from core.tracker import Tracker
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/tracker/points")
|
||||
def list_points(name: str, access_token: APIKey = Depends(get_api_key)):
|
||||
try:
|
||||
tracker = Tracker.from_data(name)
|
||||
return tracker.points
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
|
||||
@router.get("/tracker/points/add")
|
||||
def add_point(name: str, value: int, date_str: str = datetime.now().isoformat(),
|
||||
access_token: APIKey = Depends(get_api_key)):
|
||||
try:
|
||||
tracker = Tracker.from_data(name)
|
||||
except FileNotFoundError:
|
||||
tracker = Tracker(name, [])
|
||||
tracker.add_point(date_str, value)
|
||||
return tracker.to_json()
|
||||
|
||||
|
||||
@router.put("/tracker/points/modify")
|
||||
def modify_point(name: str, date_str: str, value: int, access_token: APIKey = Depends(get_api_key)):
|
||||
tracker = Tracker.from_data(name)
|
||||
tracker.modify_point(date_str, value)
|
||||
return 200
|
||||
|
||||
|
||||
@router.delete("/tracker/points/delete")
|
||||
def delete_point(name: str, date_str: str, access_token: APIKey = Depends(get_api_key)):
|
||||
tracker = Tracker.from_data(name)
|
||||
tracker.delete_point(date_str)
|
||||
return 200
|
||||
|
||||
|
||||
@router.get("/tracker/rename")
|
||||
def rename_tracker(name: str, new_name: str, access_token: APIKey = Depends(get_api_key)):
|
||||
tracker = Tracker.from_data(name)
|
||||
tracker.rename(new_name)
|
||||
return 200
|
||||
|
||||
|
||||
@router.delete("/tracker/delete")
|
||||
def delete_tracker(name: str, access_token: APIKey = Depends(get_api_key)):
|
||||
tracker = Tracker.from_data(name)
|
||||
tracker.delete()
|
Loading…
Reference in a new issue