From 53beb84c0187f3a5b80fce2e5288b1dd6e5540fe Mon Sep 17 00:00:00 2001 From: Ilya Kreymer Date: Sun, 5 Dec 2021 14:12:42 -0800 Subject: [PATCH] Config superuser (#59) * backend: automatically create super user, fixes #57 - if SUPERUSER_EMAIL is set, superuser is created with `is_superuser` and `is_verified` settings, if user doesn't already exist. - if SUPERUSER_PASSWORD if set, the password for superuser is set, otherwise a random password is generated update sample SUPERUSER_EMAIL and SUPERUSER_PASSWORD in config file and chart. - ensure verification email is not sent if user already verified --- backend/emailsender.py | 10 +++---- backend/users.py | 56 +++++++++++++++++++++++++++++++----- chart/templates/secrets.yaml | 3 ++ chart/values.yaml | 7 +++++ configs/config.sample.env | 5 ++++ 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/backend/emailsender.py b/backend/emailsender.py index fc9a7be2..b353d87d 100644 --- a/backend/emailsender.py +++ b/backend/emailsender.py @@ -34,13 +34,13 @@ class EmailSender: def get_origin(self, headers): """ Return origin of the received request""" - scheme = headers.get("X-Forwarded-Proto") - if not scheme: - scheme = "http" + if not headers: + return self.default_origin + scheme = headers.get("X-Forwarded-Proto") host = headers.get("Host") - if not host: - host = self.default_origin + if not scheme or not host: + return self.default_origin return scheme + "://" + host diff --git a/backend/users.py b/backend/users.py index 854629f7..de6e2e80 100644 --- a/backend/users.py +++ b/backend/users.py @@ -9,10 +9,12 @@ import asyncio from typing import Dict, Optional from pydantic import EmailStr, UUID4 +import passlib.pwd from fastapi import Request, Response, HTTPException, Depends from fastapi_users import FastAPIUsers, models, BaseUserManager +from fastapi_users.manager import UserAlreadyExists from fastapi_users.authentication import JWTAuthentication from fastapi_users.db import MongoDBUserDatabase @@ -35,10 +37,10 @@ class User(models.BaseUser): # ============================================================================ -# use custom model as model.BaseeserCreate includes is_* fields which should not be set -class UserCreate(models.CreateUpdateDictModel): +# use custom model as model.BaseUserCreate includes is_* field +class UserCreateIn(models.CreateUpdateDictModel): """ - User Creation Model + User Creation Model exposed to API """ email: EmailStr @@ -52,6 +54,20 @@ class UserCreate(models.CreateUpdateDictModel): newArchiveName: Optional[str] = "" +# ============================================================================ +class UserCreate(models.BaseUserCreate): + """ + User Creation Model + """ + + name: Optional[str] = "" + + inviteToken: Optional[str] + + newArchive: bool + newArchiveName: Optional[str] = "" + + # ============================================================================ class UserUpdate(User, models.CreateUpdateDictModel): """ @@ -124,6 +140,27 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): ) return await cursor.to_list(length=1000) + async def create_super_user(self): + """ Initialize a super user from env vars """ + email = os.environ.get("SUPERUSER_EMAIL") + password = os.environ.get("SUPERUSER_PASSWORD") + if not email: + print("No superuser defined", flush=True) + return + + if not password: + password = passlib.pwd.genword() + + try: + res = await self.create( + UserCreate(email=email, password=password, is_superuser=True, newArchive=False, is_verified=True) + ) + print(f"Super user {email} created", flush=True) + print(res, flush=True) + + except UserAlreadyExists: + print(f"User {email} already exists", flush=True) + async def on_after_register_custom( self, user: UserDB, user_create: UserCreate, request: Optional[Request] ): @@ -144,6 +181,8 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): user=user, ) + is_verified = hasattr(user_create, "is_verified") and user_create.is_verified + if user_create.inviteToken: try: await self.archive_ops.handle_new_user_invite( @@ -152,10 +191,11 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): except HTTPException as exc: print(exc) - # if user has been invited, mark as verified immediately - await self._update(user, {"is_verified": True}) + if not is_verified: + # if user has been invited, mark as verified immediately + await self._update(user, {"is_verified": True}) - else: + elif not is_verified: asyncio.create_task(self.request_verify(user, request)) async def on_after_forgot_password( @@ -202,7 +242,7 @@ def init_users_api(app, user_manager): lambda: user_manager, [jwt_authentication], User, - UserCreate, + UserCreateIn, UserUpdate, UserDB, ) @@ -261,4 +301,6 @@ def init_users_api(app, user_manager): app.include_router(users_router, prefix="/users", tags=["users"]) + asyncio.create_task(user_manager.create_super_user()) + return fastapi_users diff --git a/chart/templates/secrets.yaml b/chart/templates/secrets.yaml index 29cd0314..91e8a572 100644 --- a/chart/templates/secrets.yaml +++ b/chart/templates/secrets.yaml @@ -23,6 +23,9 @@ stringData: EMAIL_SENDER: "{{ .Values.email.sender_email }}" EMAIL_PASSWORD: "{{ .Values.email.password }}" + SUPERUSER_EMAIL: "{{ .Values.superuser.email }}" + SUPERUSER_PASSWORD: "{{ .Values.superuser.password }}" + {{- range $storage := .Values.storages }} --- apiVersion: v1 diff --git a/chart/values.yaml b/chart/values.yaml index 71b5d687..0128c9b6 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -5,6 +5,13 @@ name: browsertrix-cloud registration_enabled: 1 jwt_token_lifetime_minutes: 60 +superuser: + # set this to enable a superuser admin + email: admin@example.com + + # optional: if not set, automatically generated + password: + # API Image # ========================================= diff --git a/configs/config.sample.env b/configs/config.sample.env index 5b550610..ab343005 100644 --- a/configs/config.sample.env +++ b/configs/config.sample.env @@ -9,6 +9,11 @@ MONGO_INITDB_ROOT_PASSWORD=example MINIO_ROOT_USER=ADMIN MINIO_ROOT_PASSWORD=PASSW0RD +SUPERUSER_EMAIL=admin@example.com + +# if blank, a password is generated automatically +SUPERUSER_PASSWORD= + STORE_ENDPOINT_URL=http://minio:9000/test-bucket/ STORE_ACCESS_ENDPOINT_URL=http://localhost:9000/test-bucket/ STORE_ACCESS_KEY=ADMIN