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 pathlib import Path
|
||||||
from fastapi import FastAPI, status
|
from fastapi import FastAPI
|
||||||
from response import Response
|
|
||||||
|
logger = logging.getLogger("api")
|
||||||
|
|
||||||
api = FastAPI()
|
api = FastAPI()
|
||||||
|
api_data = {"version": "2021.8.23.0", "author": "Riley Housden"}
|
||||||
api_data = {"version": "2021.8.4.0", "author": "Riley Housden"}
|
|
||||||
|
|
||||||
|
|
||||||
@api.post("/update")
|
def load_module(name: str):
|
||||||
def update():
|
module = importlib.import_module(name)
|
||||||
sys.exit(36)
|
api.include_router(module.router)
|
||||||
|
logger.info(f"Loaded module: {module.__name__}")
|
||||||
|
|
||||||
|
|
||||||
@api.post("/restart")
|
for m_path in Path("routers").glob("*"):
|
||||||
def stop():
|
if "__pycache__" in m_path.parts or "__init__" == m_path.stem:
|
||||||
sys.exit(26)
|
continue
|
||||||
|
load_module(str(m_path).replace("/", ".").replace(".py", ""))
|
||||||
|
|
||||||
|
|
||||||
@api.get("/")
|
@api.get("/")
|
||||||
def quack():
|
def quack():
|
||||||
return "quack!"
|
return api_data
|
||||||
|
|
||||||
|
|
||||||
@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()
|
|
||||||
|
|
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 json.encoder import JSONEncoder
|
||||||
from pathlib import Path
|
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):
|
def default(self, o):
|
||||||
if isinstance(o, datetime):
|
if isinstance(o, Tracker):
|
||||||
return o.strftime("%m/%d/%Y - %H:%M")
|
return dict(name=o.name, points=o.points)
|
||||||
|
elif isinstance(o, TrackerPoint):
|
||||||
|
return dict(date_str=o.datetime.isoformat(), value=o.value)
|
||||||
else:
|
else:
|
||||||
return o.__dict__
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
||||||
|
|
||||||
class TargetPoint:
|
class TrackerPoint:
|
||||||
def __init__(self, datestr: str, value: int) -> None:
|
def __init__(self, date_str: str, value: int) -> None:
|
||||||
self.datetime = datetime.strptime(datestr, "%m/%d/%Y - %H:%M")
|
self.datetime = datetime.fromisoformat(date_str)
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
|
|
||||||
class Target:
|
class Tracker:
|
||||||
def __init__(self, name: str, points: list[TargetPoint]):
|
def __init__(self, name: str, points: list[TrackerPoint]):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.points = points
|
self.points = points
|
||||||
|
|
||||||
def rename(self, name):
|
def rename(self, name):
|
||||||
"""Rename the Target."""
|
"""Rename the Tracker."""
|
||||||
DATA_DIR.joinpath(f"{self.name}.json").rename(
|
DATA_DIR.joinpath(f"{self.name}.json").rename(
|
||||||
DATA_DIR.joinpath(f"{name}.json"))
|
DATA_DIR.joinpath(f"{name}.json"))
|
||||||
self.name = name
|
self.name = name
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""Delete the Target."""
|
"""Delete the Tracker."""
|
||||||
filepath = DATA_DIR.joinpath(f"{self.name}.json")
|
filepath = DATA_DIR.joinpath(f"{self.name}.json")
|
||||||
filepath.unlink()
|
filepath.unlink()
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
"""Convert Target object to JSON."""
|
"""Convert Tracker object to JSON."""
|
||||||
return json.dumps(self, indent=4, cls=TargetEncoder)
|
return json.dumps(self, indent=4, cls=TrackerEncoder)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save the tracker to JSON."""
|
"""Save the tracker to JSON."""
|
||||||
|
@ -48,24 +57,24 @@ class Target:
|
||||||
filepath.touch(exist_ok=True)
|
filepath.touch(exist_ok=True)
|
||||||
filepath.write_text(self.to_json())
|
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."""
|
"""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:
|
for point in self.points:
|
||||||
if point.datetime == date_time:
|
if point.datetime == date_time:
|
||||||
point.value = value
|
point.value = value
|
||||||
break
|
break
|
||||||
self.save()
|
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."""
|
"""Add a point to the tracker."""
|
||||||
point = TargetPoint(datestr, value)
|
point = TrackerPoint(date_str, value)
|
||||||
self.points.append(point)
|
self.points.append(point)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def delete_point(self, datestr: str):
|
def delete_point(self, date_str: str):
|
||||||
"""Remove a point from the tracker."""
|
"""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:
|
for point in self.points:
|
||||||
if point.datetime == date_time:
|
if point.datetime == date_time:
|
||||||
self.points.remove(point)
|
self.points.remove(point)
|
||||||
|
@ -74,12 +83,11 @@ class Target:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_data(cls, name):
|
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")
|
filepath = DATA_DIR.joinpath(f"{name}.json")
|
||||||
return cls.from_json(filepath.read_text())
|
return cls.from_json(filepath.read_text())
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
def from_json(cls, json_str):
|
def from_json(json_str):
|
||||||
"""Load a target from a JSON string."""
|
"""Load a tracker from a JSON string."""
|
||||||
obj = json.loads(json_str)
|
return json.loads(json_str, object_hook=object_hook)
|
||||||
return cls(obj['name'], obj['points'])
|
|
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:
|
api = Wrapper(['gunicorn', "-k", "uvicorn.workers.UvicornWorker", "--log-config", "log.conf", 'api:api'], sys.stdout)
|
||||||
try:
|
api.start()
|
||||||
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!")
|
|
||||||
|
|
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