Add API key authentication.

We don't have any sensitive data, but we do access the file system. On
the off chance that some phishing website decides to try to use
Liberation as an attack vector, prevent access to the API by
unauthorized applications. An API key is generated at each program start
and passed to the front end via the QWebChannel.
This commit is contained in:
Dan Albert 2022-02-19 14:41:39 -08:00
parent 09457d8aab
commit 77d29e314c
5 changed files with 36 additions and 5 deletions

View File

@ -1,7 +1,8 @@
from fastapi import FastAPI from fastapi import Depends, FastAPI
from . import debuggeometries, eventstream from . import debuggeometries, eventstream
from .security import ApiKeyManager
app = FastAPI() app = FastAPI(dependencies=[Depends(ApiKeyManager.verify)])
app.include_router(debuggeometries.router) app.include_router(debuggeometries.router)
app.include_router(eventstream.router) app.include_router(eventstream.router)

15
game/server/security.py Normal file
View File

@ -0,0 +1,15 @@
import secrets
from fastapi import HTTPException, Security, status
from fastapi.security import APIKeyHeader
API_KEY_HEADER = APIKeyHeader(name="X-API-Key")
class ApiKeyManager:
KEY = secrets.token_urlsafe()
@classmethod
def verify(cls, api_key_header: str = Security(API_KEY_HEADER)) -> None:
if api_key_header != cls.KEY:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)

View File

@ -10,6 +10,7 @@ from game import Game
from game.ato.airtaaskingorder import AirTaskingOrder from game.ato.airtaaskingorder import AirTaskingOrder
from game.profiling import logged_duration from game.profiling import logged_duration
from game.server.leaflet import LeafletLatLon from game.server.leaflet import LeafletLatLon
from game.server.security import ApiKeyManager
from game.theater import ( from game.theater import (
ConflictTheater, ConflictTheater,
) )
@ -46,6 +47,7 @@ from .unculledzonejs import UnculledZone
class MapModel(QObject): class MapModel(QObject):
cleared = Signal() cleared = Signal()
apiKeyChanged = Signal(str)
mapCenterChanged = Signal(list) mapCenterChanged = Signal(list)
controlPointsChanged = Signal() controlPointsChanged = Signal()
groundObjectsChanged = Signal() groundObjectsChanged = Signal()
@ -187,6 +189,10 @@ class MapModel(QObject):
self._map_center = [ll.latitude, ll.longitude] self._map_center = [ll.latitude, ll.longitude]
self.mapCenterChanged.emit(self._map_center) self.mapCenterChanged.emit(self._map_center)
@Property(str, notify=apiKeyChanged)
def apiKey(self) -> str:
return ApiKeyManager.KEY
@Property(list, notify=mapCenterChanged) @Property(list, notify=mapCenterChanged)
def mapCenter(self) -> LeafletLatLon: def mapCenter(self) -> LeafletLatLon:
return self._map_center return self._map_center

View File

@ -21,6 +21,7 @@ import qt_ui.uiconstants as CONST
from game import Game, VERSION, persistency from game import Game, VERSION, persistency
from game.debriefing import Debriefing from game.debriefing import Debriefing
from game.server import EventStream from game.server import EventStream
from game.server.security import ApiKeyManager
from qt_ui import liberation_install from qt_ui import liberation_install
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel from qt_ui.models import GameModel
@ -88,6 +89,8 @@ class QLiberationWindow(QMainWindow):
else: else:
self.onGameGenerated(self.game) self.onGameGenerated(self.game)
logging.debug(f"API Key: {ApiKeyManager.KEY}")
def initUi(self): def initUi(self):
hbox = QSplitter(Qt.Horizontal) hbox = QSplitter(Qt.Horizontal)
vbox = QSplitter(Qt.Vertical) vbox = QSplitter(Qt.Vertical)

View File

@ -3,10 +3,15 @@ const ENABLE_EXPENSIVE_DEBUG_TOOLS = false;
const HTTP_BACKEND = "http://[::1]:5000"; const HTTP_BACKEND = "http://[::1]:5000";
const WS_BACKEND = "ws://[::1]:5000/eventstream"; const WS_BACKEND = "ws://[::1]:5000/eventstream";
// Uniquely generated at startup and passed to use by the QWebChannel.
var API_KEY = null;
function getJson(endpoint) { function getJson(endpoint) {
return fetch(`${HTTP_BACKEND}${endpoint}`).then((response) => return fetch(`${HTTP_BACKEND}${endpoint}`, {
response.json() headers: {
); "X-API-Key": API_KEY,
},
}).then((response) => response.json());
} }
const Colors = Object.freeze({ const Colors = Object.freeze({
@ -356,6 +361,7 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
}); });
game = channel.objects.game; game = channel.objects.game;
API_KEY = game.apiKey;
drawInitialMap(); drawInitialMap();
game.cleared.connect(clearAllLayers); game.cleared.connect(clearAllLayers);
game.mapCenterChanged.connect(recenterMap); game.mapCenterChanged.connect(recenterMap);