Skip to content

Commit

Permalink
Qr check auth (#76)
Browse files Browse the repository at this point in the history
## Изменения
Resolves #57

## Детали реализации
- При подключении по WebSocket необходимо передать Header
"Authorization" с значением токена AuthAPI
- Если токен неверный – будет выдана ошибка `Not authenticated`
- Если у токена недостаточно прав – будет выдана ошибка `Unauthorized`
- Если токен уже использовался в течение последней минуты – будет выдана
ошибка `Already in use`
- Ошибка возвращается в теле первого сообщения от сокета в виде JSON
`{"error": "reason"}`, после чего сокет закрывается нормально
  • Loading branch information
dyakovri committed Dec 24, 2023
1 parent 532bdaf commit 2738308
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 5 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
run:
source ./venv/bin/activate && uvicorn --reload --log-level debug print_service.routes:app
source ./venv/bin/activate && uvicorn --reload --log-config logging_dev.conf print_service.routes:app

configure: venv
source ./venv/bin/activate && pip install -r requirements.dev.txt -r requirements.txt
Expand Down
21 changes: 21 additions & 0 deletions logging_dev.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[loggers]
keys=root

[handlers]
keys=all

[formatters]
keys=main

[logger_root]
level=DEBUG
handlers=all

[handler_all]
class=StreamHandler
formatter=main
level=DEBUG
args=(sys.stdout,)

[formatter_main]
format=%(asctime)s %(levelname)-8s %(name)-15s %(message)s
45 changes: 41 additions & 4 deletions print_service/routes/qrprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from datetime import datetime, timedelta
from typing import Set

from fastapi import APIRouter, Header, HTTPException, WebSocket
from auth_lib.aiomethods import AsyncAuthLib
from fastapi import APIRouter, Header, WebSocket, WebSocketException
from fastapi_sqlalchemy import db
from pydantic import Field
from redis import Redis
from starlette.status import WS_1000_NORMAL_CLOSURE
from typing_extensions import Annotated

from print_service.exceptions import FileNotFound, InvalidPageRequest, IsNotUploaded, TerminalQRNotFound
from print_service.exceptions import TerminalQRNotFound
from print_service.schema import BaseModel
from print_service.settings import Settings, get_settings
from print_service.utils import get_file
Expand All @@ -20,6 +22,7 @@
logger = logging.getLogger(__name__)
settings: Settings = get_settings()
router = APIRouter()
auth = AsyncAuthLib(auth_url=settings.AUTH_URL, userdata_url=settings.USERDATA_URL)


class InstantPrintCreate(BaseModel):
Expand Down Expand Up @@ -49,13 +52,14 @@ class InstantPrintFetcher:
def __init__(self, terminal_token: str, settings: Settings = None) -> None:
self.terminal_token = terminal_token
settings = settings or get_settings()
self.redis = Redis.from_url(str(settings.REDIS_DSN))
self.redis: Redis = Redis.from_url(str(settings.REDIS_DSN))
self.ttl = settings.QR_TOKEN_TTL
self.delay = settings.QR_TOKEN_DELAY
self.symbols = settings.QR_TOKEN_SYMBOLS
self.length = settings.QR_TOKEN_LENGTH

def new_qr(self):
logger.debug("Generating new QR token")
for _ in range(5):
qr_token = ''.join(random.choice(self.symbols) for _ in range(self.length))
if not self.redis.get(qr_token): # If this qr already exists, generate new
Expand All @@ -77,6 +81,31 @@ async def get_tasks(self) -> dict[str, list[str]]:
return {}
return json.loads(raw_value)

async def check_token(self):
"""Check if token valid and not used"""
logger.info("Checking token")

# Token should be valid
me = await auth.check_token(self.terminal_token)
if me is None:
logger.error("Not authenticated")
raise Exception("Not authenticated")

for scope in me['session_scopes']:
if scope['name'] == "print.qr_task.get":
break
else:
logger.error("Unauthorized")
logger.debug(me)
raise Exception("Unauthorized")

# Token shouldn't be used yet
for key in self.redis.keys():
value = self.redis.get(key)
if self.redis.get(key) == self.terminal_token.encode():
logger.error("Token already used")
raise Exception("Token already used")

def __aiter__(self):
return self

Expand All @@ -93,7 +122,7 @@ async def __anext__(self):
@router.post("")
async def instant_print(options: InstantPrintCreate):
options.qr_token = options.qr_token.removeprefix(str(settings.QR_TOKEN_PREFIX))
if redis_conn.send(**options.dict()):
if redis_conn.send(**options.model_dump()):
return {'status': 'ok'}
raise TerminalQRNotFound()

Expand All @@ -104,7 +133,15 @@ async def instant_print_terminal_connection(
authorization: str = Header(),
):
await websocket.accept()
logger.debug("Websocket connection started")
manager = InstantPrintFetcher(authorization.removeprefix("token "))
try:
await manager.check_token()
except Exception as e:
await websocket.send_text(json.dumps({"error": e.args[0]}))
raise WebSocketException(WS_1000_NORMAL_CLOSURE, "Auth error")
logger.debug("Websocket token checked")

await websocket.send_text(json.dumps({"qr_token": str(settings.QR_TOKEN_PREFIX) + manager.new_qr()}))
async for task in manager:
task['qr_token'] = str(settings.QR_TOKEN_PREFIX) + task['qr_token']
Expand Down

0 comments on commit 2738308

Please sign in to comment.