diff --git a/UPGRADING.md b/UPGRADING.md index 4b35cf5..42aa9e2 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -38,6 +38,11 @@ explicitly. Once we implement them, their usage will be described in the python tools/import_from_mongodb.py mongodb://mongo/obs \ --keycloak-users-file /export/users.json ``` + There is an option `--keep-api-keys` which means the users won't have to + reconfigure the devices they used their API key in. **However**, please try + to avoid this option if at all possible, as the old keys are *very* insecure. + The default without this option to generate a new, secure API key for each + user. * Shut down the `mongo` service, you can now remove it from docker-compose.yaml * Start `keycloak` and configure it, similarly to how it was configured in the development setup (but choose more secure options). Update the API config diff --git a/api/obs/api/db.py b/api/obs/api/db.py index 00dffb2..09eb3af 100644 --- a/api/obs/api/db.py +++ b/api/obs/api/db.py @@ -10,6 +10,7 @@ import math import aiofiles import random import string +import secrets from slugify import slugify from sqlalchemy.ext.declarative import declarative_base @@ -337,6 +338,13 @@ class User(Base): # migrating *to* the external authentication scheme. match_by_username_email = Column(Boolean, server_default=false()) + def generate_api_key(self): + """ + Generates a new :py:obj:`api_key` into this instance. The new key is + sourced from a secure random source and is urlsafe. + """ + self.api_key = secrets.token_urlsafe(24) + def to_dict(self, for_user_id=None): return { "username": self.username, diff --git a/api/obs/api/routes/users.py b/api/obs/api/routes/users.py index 715c213..2c86e3d 100644 --- a/api/obs/api/routes/users.py +++ b/api/obs/api/routes/users.py @@ -1,6 +1,4 @@ import logging -import os -import binascii from sanic.response import json from sanic.exceptions import InvalidUsage @@ -42,7 +40,7 @@ async def put_user(req): user.are_tracks_visible_for_all = bool(data["areTracksVisibleForAll"]) if data.get("updateApiKey"): - user.api_key = binascii.b2a_hex(os.urandom(16)).decode("ascii") + user.generate_api_key() await req.ctx.db.commit() return json(user_to_json(req.ctx.user)) diff --git a/api/tools/import_from_mongodb.py b/api/tools/import_from_mongodb.py index 926bca6..de25d8a 100644 --- a/api/tools/import_from_mongodb.py +++ b/api/tools/import_from_mongodb.py @@ -38,21 +38,37 @@ async def main(): default=None, ) + parser.add_argument( + "--keep-api-keys", + action="store_true", + help="keep the old API keys (very insecure!) instead of generating new ones", + default=False, + ) + args = parser.parse_args() + if args.keep_api_keys: + log.warning( + "Importing users with their old API keys. These keys are very insecure and " + "could provide access to user data to third parties. Consider to notify " + "your users about the need to generate a new API key through their profile pages." + ) + async with connect_db(app.config.POSTGRES_URL): async with make_session() as session: mongo = AsyncIOMotorClient(args.mongodb_url).get_default_database() log.debug("Connected to mongodb and postgres.") - user_id_map = await import_users(mongo, session, args.keycloak_users_file) + user_id_map = await import_users( + mongo, session, args.keycloak_users_file, args.keep_api_keys + ) await import_tracks(mongo, session, user_id_map) await session.commit() -async def import_users(mongo, session, keycloak_users_file): +async def import_users(mongo, session, keycloak_users_file, keep_api_keys): keycloak_users = [] old_id_by_email = {} @@ -66,12 +82,16 @@ async def import_users(mongo, session, keycloak_users_file): bio=user.get("bio"), image=user.get("image"), are_tracks_visible_for_all=user.get("areTracksVisibleForAll") or False, - api_key=str(user["_id"]), created_at=user.get("createdAt") or datetime.utcnow(), updated_at=user.get("updatedAt") or datetime.utcnow(), match_by_username_email=True, ) + if keep_api_keys: + new_user.api_key = str(user["_id"]) + else: + new_user.generate_api_key() + if keycloak_users_file: needs_email_verification = user.get("needsEmailValidation", True) required_actions = ["UPDATE_PASSWORD"]