From fafc0aab2fea3ec3eab86fb3a2b02629ca0de497 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Thu, 9 May 2024 06:23:55 -0500 Subject: [PATCH] move users to database instead of config --- frigate/api/auth.py | 68 ++++++++++++++++++++++++----- frigate/app.py | 28 ++++++++++++ frigate/models.py | 5 +++ migrations/025_create_user_table.py | 36 +++++++++++++++ 4 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 migrations/025_create_user_table.py diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 7e493da22..69d09c610 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -10,12 +10,14 @@ import time from datetime import datetime from pathlib import Path -from flask import Blueprint, current_app, make_response, request +from flask import Blueprint, current_app, jsonify, make_response, request from flask_limiter import Limiter from flask_limiter.util import get_remote_address from joserfc import jwt +from peewee import DoesNotExist from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM +from frigate.models import User logger = logging.getLogger(__name__) @@ -204,17 +206,13 @@ def login(): content = request.get_json() user = content["user"] password = content["password"] - password_hash = next( - ( - u.password_hash - for u in current_app.frigate_config.auth.users - if u.user == user - ), - None, - ) - # if the user wasn't found in the config - if password_hash is None: - make_response({"message": "Login failed"}, 400) + + try: + db_user: User = User.get_by_id(user) + except DoesNotExist: + return make_response({"message": "Login failed"}, 400) + + password_hash = db_user.password_hash if verify_password(password, password_hash): expiration = int(time.time()) + JWT_SESSION_LENGTH encoded_jwt = create_encoded_jwt(user, expiration, current_app.jwt_token) @@ -222,3 +220,49 @@ def login(): set_jwt_cookie(response, JWT_COOKIE_NAME, encoded_jwt, expiration) return response return make_response({"message": "Login failed"}, 400) + + +@AuthBp.route("/users") +def get_users(): + exports = User.select(User.username).order_by(User.username).dicts().iterator() + return jsonify([e for e in exports]) + + +@AuthBp.route("/users", methods=["POST"]) +def create_user(): + HASH_ITERATIONS = current_app.frigate_config.auth.hash_iterations + + request_data = request.get_json() + + password_hash = hash_password(request_data["password"], iterations=HASH_ITERATIONS) + + User.insert( + { + User.username: request_data["username"], + User.password_hash: password_hash, + } + ).execute() + return jsonify({"username": request_data["username"]}) + + +@AuthBp.route("/users/", methods=["DELETE"]) +def delete_user(username: str): + User.delete_by_id(username).execute() + return jsonify({"success": True}) + + +@AuthBp.route("/users//password", methods=["PUT"]) +def update_password(username: str): + HASH_ITERATIONS = current_app.frigate_config.auth.hash_iterations + + request_data = request.get_json() + + password_hash = hash_password(request_data["password"], iterations=HASH_ITERATIONS) + + User.set_by_id( + username, + { + User.password_hash: password_hash, + }, + ) + return jsonify({"success": True}) diff --git a/frigate/app.py b/frigate/app.py index 1fce7c1ac..d2dea4fa1 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -3,6 +3,7 @@ import datetime import logging import multiprocessing as mp import os +import secrets import shutil import signal import sys @@ -19,6 +20,7 @@ from playhouse.sqliteq import SqliteQueueDatabase from pydantic import ValidationError from frigate.api.app import create_app +from frigate.api.auth import hash_password from frigate.comms.config_updater import ConfigPublisher from frigate.comms.detections_updater import DetectionProxy from frigate.comms.dispatcher import Communicator, Dispatcher @@ -49,6 +51,7 @@ from frigate.models import ( Regions, ReviewSegment, Timeline, + User, ) from frigate.object_detection import ObjectDetectProcess from frigate.object_processing import TrackedObjectProcessor @@ -338,6 +341,7 @@ class FrigateApp: Regions, ReviewSegment, Timeline, + User, ] self.db.bind(models) @@ -587,6 +591,29 @@ class FrigateApp: f"The current SHM size of {available_shm}MB is too small, recommend increasing it to at least {min_req_shm}MB." ) + def init_auth(self) -> None: + if self.config.auth.enabled: + if User.select().count() == 0: + password = secrets.token_hex(16) + password_hash = hash_password( + password, iterations=self.config.auth.hash_iterations + ) + User.insert( + { + User.username: "admin", + User.password_hash: password_hash, + } + ).execute() + + logger.info("********************************************************") + logger.info("********************************************************") + logger.info("*** Auth is enabled, but no users exist. ***") + logger.info("*** Created a default user: ***") + logger.info("*** User: admin ***") + logger.info(f"*** Password: {password} ***") + logger.info("********************************************************") + logger.info("********************************************************") + def start(self) -> None: parser = argparse.ArgumentParser( prog="Frigate", @@ -664,6 +691,7 @@ class FrigateApp: self.start_record_cleanup() self.start_watchdog() self.check_shm() + self.init_auth() def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: self.stop() diff --git a/frigate/models.py b/frigate/models.py index eb1cf0dc5..b6588ed3b 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -113,3 +113,8 @@ class RecordingsToDelete(Model): # type: ignore[misc] class Meta: temporary = True + + +class User(Model): # type: ignore[misc] + username = CharField(null=False, primary_key=True, max_length=30) + password_hash = CharField(null=False, max_length=120) diff --git a/migrations/025_create_user_table.py b/migrations/025_create_user_table.py new file mode 100644 index 000000000..6b971a6f1 --- /dev/null +++ b/migrations/025_create_user_table.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 025_create_user_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.python(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "user" ("username" VARCHAR(30) NOT NULL PRIMARY KEY, "password_hash" VARCHAR(120) NOT NULL)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass