From 773922e2630ad355340f5497ac5a86f899817985 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Thu, 4 Jul 2024 22:00:54 -0400 Subject: [PATCH] remove tokens table and fix auth handling --- relay/application.py | 18 ++++++---- relay/data/statements.sql | 41 ++++----------------- relay/database/connection.py | 70 +++++++++++++----------------------- relay/database/schema.py | 30 +++------------- relay/misc.py | 7 ++-- relay/views/api.py | 39 ++------------------ relay/views/frontend.py | 4 +-- 7 files changed, 55 insertions(+), 154 deletions(-) diff --git a/relay/application.py b/relay/application.py index 6ab481b..5ee1a73 100644 --- a/relay/application.py +++ b/relay/application.py @@ -310,15 +310,21 @@ async def handle_response_headers( if request.path == "/" or request.path.startswith(TOKEN_PATHS): with app.database.session() as conn: - if (token := request.headers.get('Authorization')) is not None: - token = token.replace('Bearer', '').strip() + tokens = ( + request.headers.get('Authorization', '').replace('Bearer', '').strip(), + request.cookies.get('user-token') + ) + + for token in tokens: + if not token: + continue request['token'] = conn.get_app_by_token(token) - request['user'] = conn.get_user_by_app_token(token) - elif (token := request.cookies.get('user-token')) is not None: - request['token'] = conn.get_token(token) - request['user'] = conn.get_user_by_token(token) + if request['token'] is not None: + request['user'] = conn.get_user(request['token'].user) + + break try: resp = await handler(request) diff --git a/relay/data/statements.sql b/relay/data/statements.sql index e8694ae..0097252 100644 --- a/relay/data/statements.sql +++ b/relay/data/statements.sql @@ -50,17 +50,9 @@ WHERE username = :value or handle = :value; -- name: get-user-by-token SELECT * FROM users -WHERE username = ( - SELECT user FROM tokens - WHERE code = :code -); - - --- name: get-user-by-app-token -SELECT * FROM users WHERE username = ( SELECT user FROM app - WHERE code = :code + WHERE token = :token ); @@ -80,46 +72,25 @@ SELECT * FROM app WHERE client_id = :id and client_secret = :secret; --- name: get-app-token +-- name: get-app-with-token SELECT * FROM app WHERE client_id = :id and client_secret = :secret and token = :token; -- name: get-app-by-token -SELECT * FROM app +SELECT * FROM apps WHERE token = :token; -- name: del-app -DELETE FROM users +DELETE FROM apps WHERE client_id = :id and client_secret = :secret; --- name: del-app-token -DELETE FROM users +-- name: del-app-with-token +DELETE FROM apps WHERE client_id = :id and client_secret = :secret and token = :token; --- name: get-token -SELECT * FROM tokens -WHERE code = :code; - - --- name: put-token -INSERT INTO tokens (code, user, created) -VALUES (:code, :user, :created) -RETURNING *; - - --- name: del-token -DELETE FROM tokens -WHERE code = :code; - - --- name: del-token-user -DELETE FROM tokens -WHERE user = :username; - - -- name: get-software-ban SELECT * FROM software_bans WHERE name = :name; diff --git a/relay/database/connection.py b/relay/database/connection.py index 3c973b8..603e63a 100644 --- a/relay/database/connection.py +++ b/relay/database/connection.py @@ -9,7 +9,6 @@ from collections.abc import Iterator from datetime import datetime, timezone from typing import TYPE_CHECKING, Any from urllib.parse import urlparse -from uuid import uuid4 from . import schema from .config import ( @@ -73,10 +72,6 @@ class Connection(SqlConnection): data = {'created': sban.created.timestamp()} self.update('software_bans', data, name = sban.name) - for token in self.select('tokens').all(schema.Token): - data = {'created': token.created.timestamp(), 'accessed': token.accessed.timestamp()} - self.update('tokens', data, code = token.code) - for user in self.select('users').all(schema.User): data = {'created': user.created.timestamp()} self.update('users', data, username = user.username) @@ -230,13 +225,8 @@ class Connection(SqlConnection): return cur.one(schema.User) - def get_user_by_token(self, code: str) -> schema.User | None: - with self.run('get-user-by-token', {'code': code}) as cur: - return cur.one(schema.User) - - - def get_user_by_app_token(self, code: str) -> schema.User | None: - with self.run('get-user-by-app-token', {'code': code}) as cur: + def get_user_by_token(self, token: str) -> schema.User | None: + with self.run('get-user-by-token', {'token': token}) as cur: return cur.one(schema.User) @@ -328,13 +318,34 @@ class Connection(SqlConnection): 'accessed': Date.new_utc().timestamp() } - with self.insert('app', params) as cur: + with self.insert('apps', params) as cur: if (row := cur.one(schema.App)) is None: raise RuntimeError(f'Failed to insert app: {name}') return row + def put_app_login(self, user: schema.User) -> schema.App: + params = { + 'name': 'Web', + 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', + 'website': None, + 'user': user.username, + 'client_id': secrets.token_hex(20), + 'client_secret': secrets.token_hex(20), + 'auth_code': None, + 'token': secrets.token_hex(20), + 'created': Date.new_utc().timestamp(), + 'accessed': Date.new_utc().timestamp() + } + + with self.insert('apps', params) as cur: + if (row := cur.one(schema.App)) is None: + raise RuntimeError(f'Failed to create app for "{user.username}"') + + return row + + def update_app(self, app: schema.App, user: schema.User | None, set_auth: bool) -> schema.App: data: dict[str, str | None] = {} @@ -367,7 +378,7 @@ class Connection(SqlConnection): } if token is not None: - command = 'del-app-token' + command = 'del-app-with-token' params['token'] = token else: @@ -380,37 +391,6 @@ class Connection(SqlConnection): return cur.row_count == 0 - def get_token(self, code: str) -> schema.Token | None: - with self.run('get-token', {'code': code}) as cur: - return cur.one(schema.Token) - - - def get_tokens(self, username: str | None = None) -> Iterator[schema.Token]: - if username is None: - return self.select('tokens').all(schema.Token) - - return self.select('tokens', username = username).all(schema.Token) - - - def put_token(self, username: str) -> schema.Token: - data = { - 'code': uuid4().hex, - 'user': username, - 'created': datetime.now(tz = timezone.utc) - } - - with self.run('put-token', data) as cur: - if (row := cur.one(schema.Token)) is None: - raise RuntimeError(f"Failed to insert token for user: {username}") - - return row - - - def del_token(self, code: str) -> None: - with self.run('del-token', {'code': code}): - pass - - def get_domain_ban(self, domain: str) -> schema.DomainBan | None: if domain.startswith('http'): domain = urlparse(domain).netloc diff --git a/relay/database/schema.py b/relay/database/schema.py index 660e527..a6016bb 100644 --- a/relay/database/schema.py +++ b/relay/database/schema.py @@ -105,23 +105,6 @@ class User(Row): ) -@TABLES.add_row -class Token(Row): - table_name: str = 'tokens' - - - code: Column[str] = Column('code', 'text', primary_key = True, unique = True, nullable = False) - user: Column[str] = Column('user', 'text', nullable = False) - created: Column[Date] = Column( - 'created', 'timestamp', nullable = False, - deserializer = deserialize_timestamp, serializer = Date.timestamp - ) - accessed: Column[Date] = Column( - 'accessed', 'timestamp', nullable = False, - deserializer = deserialize_timestamp, serializer = Date.timestamp - ) - - @TABLES.add_row class App(Row): table_name: str = 'apps' @@ -148,9 +131,8 @@ class App(Row): def get_api_data(self, include_token: bool = False) -> dict[str, Any]: data = deepcopy(self) + data.pop('user') data.pop('auth_code') - data.pop('created') - data.pop('accessed') if not include_token: data.pop('token') @@ -176,15 +158,11 @@ def migrate_20240206(conn: Connection) -> None: @migration def migrate_20240310(conn: Connection) -> None: - conn.execute('ALTER TABLE "inboxes" ADD COLUMN "accepted" BOOLEAN') - conn.execute('UPDATE "inboxes" SET accepted = 1') + conn.execute('ALTER TABLE "inboxes" ADD COLUMN "accepted" BOOLEAN').close() + conn.execute('UPDATE "inboxes" SET accepted = 1').close() @migration def migrate_20240625(conn: Connection) -> None: - conn.execute('ALTER TABLE "tokens" ADD "accessed" timestamp') - - for token in conn.get_tokens(): - conn.update('tokens', {'accessed': token.created}, code = token.code).one() - conn.create_tables() + conn.execute('DROP TABLE tokens').close() diff --git a/relay/misc.py b/relay/misc.py index b27c89a..cb35339 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -76,12 +76,11 @@ JSON_PATHS: tuple[str, ...] = ( ) TOKEN_PATHS: tuple[str, ...] = ( - '/api', - '/login', '/logout', + '/admin', + '/api', '/oauth/authorize', - '/oauth/revoke', - '/admin' + '/oauth/revoke' ) diff --git a/relay/views/api.py b/relay/views/api.py index 76cd1e5..f8fe828 100644 --- a/relay/views/api.py +++ b/relay/views/api.py @@ -1,4 +1,3 @@ -import secrets import traceback from aiohttp.web import Request, middleware @@ -209,12 +208,12 @@ class Login(View): except VerifyMismatchError: raise HttpError(401, 'Invalid password') - token = conn.put_token(data['username']) + app = conn.put_app_login(user) - resp = Response.new({'token': token.code}, ctype = 'json') + resp = Response.new({'token': app.token}, ctype = 'json') resp.set_cookie( 'user-token', - token.code, + app.token, # type: ignore[arg-type] max_age = 60 * 60 * 24 * 365, domain = self.config.domain, path = '/', @@ -226,38 +225,6 @@ class Login(View): return resp - - async def post2(self, request: Request) -> Response: - data = await self.get_api_data(['username', 'password'], []) - - with self.database.session(True) as conn: - if not (user := conn.get_user(data['username'])): - raise HttpError(401, 'User not found') - - try: - conn.hasher.verify(user['hash'], data['password']) - - except VerifyMismatchError: - raise HttpError(401, 'Invalid password') - - app = conn.put_app( - data['app_name'], - DEFAULT_REDIRECT, - data.get('website') - ) - - params = { - 'code': secrets.token_hex(20), - 'user': user.username - } - - with conn.update('app', params, client_id = app.client_id) as cur: - if (row := cur.one(schema.App)) is None: - raise HttpError(500, 'Failed to create app') - - return Response.new(row.get_api_data(True), ctype = 'json') - - @register_route('/api/v1/relay') class RelayInfo(View): async def get(self, request: Request) -> Response: diff --git a/relay/views/frontend.py b/relay/views/frontend.py index a383d20..b6dba7b 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -18,7 +18,7 @@ async def handle_frontend_path( if request['user'] is not None and request.path == '/login': return Response.new_redir('/') - if request.path.startswith(TOKEN_PATHS) and request['user'] is None: + if request.path.startswith(TOKEN_PATHS[:2]) and request['user'] is None: if request.path == '/logout': return Response.new_redir('/') @@ -62,7 +62,7 @@ class Login(View): class Logout(View): async def get(self, request: web.Request) -> Response: with self.database.session(True) as conn: - conn.del_token(request['token'].code) + conn.del_app(request['token'].client_id, request['token'].client_secret) resp = Response.new_redir('/') resp.del_cookie('user-token', domain = self.config.domain, path = '/')