move users to database instead of config

This commit is contained in:
Blake Blackshear 2024-05-09 06:23:55 -05:00
parent 7cf3abc850
commit fafc0aab2f
4 changed files with 125 additions and 12 deletions

View File

@ -10,12 +10,14 @@ import time
from datetime import datetime from datetime import datetime
from pathlib import Path 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 import Limiter
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
from joserfc import jwt from joserfc import jwt
from peewee import DoesNotExist
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
from frigate.models import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -204,17 +206,13 @@ def login():
content = request.get_json() content = request.get_json()
user = content["user"] user = content["user"]
password = content["password"] password = content["password"]
password_hash = next(
( try:
u.password_hash db_user: User = User.get_by_id(user)
for u in current_app.frigate_config.auth.users except DoesNotExist:
if u.user == user return make_response({"message": "Login failed"}, 400)
),
None, password_hash = db_user.password_hash
)
# if the user wasn't found in the config
if password_hash is None:
make_response({"message": "Login failed"}, 400)
if verify_password(password, password_hash): if verify_password(password, password_hash):
expiration = int(time.time()) + JWT_SESSION_LENGTH expiration = int(time.time()) + JWT_SESSION_LENGTH
encoded_jwt = create_encoded_jwt(user, expiration, current_app.jwt_token) 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) set_jwt_cookie(response, JWT_COOKIE_NAME, encoded_jwt, expiration)
return response return response
return make_response({"message": "Login failed"}, 400) 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/<username>", methods=["DELETE"])
def delete_user(username: str):
User.delete_by_id(username).execute()
return jsonify({"success": True})
@AuthBp.route("/users/<username>/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})

View File

@ -3,6 +3,7 @@ import datetime
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import os import os
import secrets
import shutil import shutil
import signal import signal
import sys import sys
@ -19,6 +20,7 @@ from playhouse.sqliteq import SqliteQueueDatabase
from pydantic import ValidationError from pydantic import ValidationError
from frigate.api.app import create_app from frigate.api.app import create_app
from frigate.api.auth import hash_password
from frigate.comms.config_updater import ConfigPublisher from frigate.comms.config_updater import ConfigPublisher
from frigate.comms.detections_updater import DetectionProxy from frigate.comms.detections_updater import DetectionProxy
from frigate.comms.dispatcher import Communicator, Dispatcher from frigate.comms.dispatcher import Communicator, Dispatcher
@ -49,6 +51,7 @@ from frigate.models import (
Regions, Regions,
ReviewSegment, ReviewSegment,
Timeline, Timeline,
User,
) )
from frigate.object_detection import ObjectDetectProcess from frigate.object_detection import ObjectDetectProcess
from frigate.object_processing import TrackedObjectProcessor from frigate.object_processing import TrackedObjectProcessor
@ -338,6 +341,7 @@ class FrigateApp:
Regions, Regions,
ReviewSegment, ReviewSegment,
Timeline, Timeline,
User,
] ]
self.db.bind(models) 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." 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: def start(self) -> None:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="Frigate", prog="Frigate",
@ -664,6 +691,7 @@ class FrigateApp:
self.start_record_cleanup() self.start_record_cleanup()
self.start_watchdog() self.start_watchdog()
self.check_shm() self.check_shm()
self.init_auth()
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
self.stop() self.stop()

View File

@ -113,3 +113,8 @@ class RecordingsToDelete(Model): # type: ignore[misc]
class Meta: class Meta:
temporary = True 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)

View File

@ -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