Do not blindly import old API keys

This commit is contained in:
Paul Bienkowski 2021-12-01 09:42:27 +01:00
parent 40784ba51e
commit 4003d5e938
4 changed files with 37 additions and 6 deletions

View file

@ -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 \ python tools/import_from_mongodb.py mongodb://mongo/obs \
--keycloak-users-file /export/users.json --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 * 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 * Start `keycloak` and configure it, similarly to how it was configured in the
development setup (but choose more secure options). Update the API config development setup (but choose more secure options). Update the API config

View file

@ -10,6 +10,7 @@ import math
import aiofiles import aiofiles
import random import random
import string import string
import secrets
from slugify import slugify from slugify import slugify
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
@ -337,6 +338,13 @@ class User(Base):
# migrating *to* the external authentication scheme. # migrating *to* the external authentication scheme.
match_by_username_email = Column(Boolean, server_default=false()) 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): def to_dict(self, for_user_id=None):
return { return {
"username": self.username, "username": self.username,

View file

@ -1,6 +1,4 @@
import logging import logging
import os
import binascii
from sanic.response import json from sanic.response import json
from sanic.exceptions import InvalidUsage from sanic.exceptions import InvalidUsage
@ -42,7 +40,7 @@ async def put_user(req):
user.are_tracks_visible_for_all = bool(data["areTracksVisibleForAll"]) user.are_tracks_visible_for_all = bool(data["areTracksVisibleForAll"])
if data.get("updateApiKey"): if data.get("updateApiKey"):
user.api_key = binascii.b2a_hex(os.urandom(16)).decode("ascii") user.generate_api_key()
await req.ctx.db.commit() await req.ctx.db.commit()
return json(user_to_json(req.ctx.user)) return json(user_to_json(req.ctx.user))

View file

@ -38,21 +38,37 @@ async def main():
default=None, 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() 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 connect_db(app.config.POSTGRES_URL):
async with make_session() as session: async with make_session() as session:
mongo = AsyncIOMotorClient(args.mongodb_url).get_default_database() mongo = AsyncIOMotorClient(args.mongodb_url).get_default_database()
log.debug("Connected to mongodb and postgres.") 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 import_tracks(mongo, session, user_id_map)
await session.commit() 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 = [] keycloak_users = []
old_id_by_email = {} old_id_by_email = {}
@ -66,12 +82,16 @@ async def import_users(mongo, session, keycloak_users_file):
bio=user.get("bio"), bio=user.get("bio"),
image=user.get("image"), image=user.get("image"),
are_tracks_visible_for_all=user.get("areTracksVisibleForAll") or False, are_tracks_visible_for_all=user.get("areTracksVisibleForAll") or False,
api_key=str(user["_id"]),
created_at=user.get("createdAt") or datetime.utcnow(), created_at=user.get("createdAt") or datetime.utcnow(),
updated_at=user.get("updatedAt") or datetime.utcnow(), updated_at=user.get("updatedAt") or datetime.utcnow(),
match_by_username_email=True, 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: if keycloak_users_file:
needs_email_verification = user.get("needsEmailValidation", True) needs_email_verification = user.get("needsEmailValidation", True)
required_actions = ["UPDATE_PASSWORD"] required_actions = ["UPDATE_PASSWORD"]