fix: Don't load from secrets dir if nonexistent or inaccessible (#4002)

Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
Andrew Morgan 2024-08-12 15:55:32 +01:00 committed by GitHub
parent f11af52d30
commit 65ece35966
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 72 additions and 30 deletions

View File

@ -4,9 +4,12 @@ from pathlib import Path
import dotenv
from mealie.core.settings import app_settings_constructor
from .settings import AppDirectories, AppSettings
from mealie.core.settings import (
AppDirectories,
AppLoggingSettings,
AppSettings,
app_settings_constructor,
)
CWD = Path(__file__).parent
BASE_DIR = CWD.parent.parent
@ -38,3 +41,8 @@ def get_app_dirs() -> AppDirectories:
@lru_cache
def get_app_settings() -> AppSettings:
return app_settings_constructor(env_file=ENV, production=PRODUCTION, data_dir=determine_data_dir())
@lru_cache
def get_logging_settings() -> AppLoggingSettings:
return AppLoggingSettings(PRODUCTION=PRODUCTION)

View File

@ -1,6 +1,6 @@
import logging
from .config import get_app_dirs, get_app_settings
from .config import get_app_dirs, get_logging_settings
from .logger.config import configured_logger
__root_logger: None | logging.Logger = None
@ -18,25 +18,25 @@ def get_logger(module=None) -> logging.Logger:
global __root_logger
if __root_logger is None:
app_settings = get_app_settings()
app_logging_settings = get_logging_settings()
mode = "development"
if app_settings.TESTING:
if app_logging_settings.TESTING:
mode = "testing"
elif app_settings.PRODUCTION:
elif app_logging_settings.PRODUCTION:
mode = "production"
dirs = get_app_dirs()
substitutions = {
"DATA_DIR": dirs.DATA_DIR.as_posix(),
"LOG_LEVEL": app_settings.LOG_LEVEL.upper(),
"LOG_LEVEL": app_logging_settings.LOG_LEVEL.upper(),
}
__root_logger = configured_logger(
mode=mode,
config_override=app_settings.LOG_CONFIG_OVERRIDE,
config_override=app_logging_settings.LOG_CONFIG_OVERRIDE,
substitutions=substitutions,
)

View File

@ -1,4 +1,5 @@
import logging
import os
import secrets
from datetime import datetime, timezone
from pathlib import Path
@ -34,10 +35,54 @@ def determine_secrets(data_dir: Path, production: bool) -> str:
return new_secret
class AppSettings(BaseSettings):
def get_secrets_dir() -> str | None:
"""
Returns a directory to load secret settings from, or `None` if the secrets
directory does not exist or cannot be accessed.
"""
# Avoid a circular import by importing here instead of at the file's top-level.
# get_logger -> AppSettings -> get_logger
from mealie.core.root_logger import get_logger
logger = get_logger()
secrets_dir = "/run/secrets"
# Check that the secrets directory exists.
if not os.path.exists(secrets_dir):
logger.warning(f"Secrets directory '{secrets_dir}' does not exist")
return None
# Likewise, check we have permission to read from the secrets directory.
if not os.access(secrets_dir, os.R_OK):
logger.warning(f"Secrets directory '{secrets_dir}' cannot be read from. Check permissions")
return None
# The secrets directory exists and can be accessed.
return secrets_dir
class AppLoggingSettings(BaseSettings):
"""
Subset of AppSettings to only access logging-related settings.
This is separated out from AppSettings to allow logging during construction
of AppSettings.
"""
TESTING: bool = False
PRODUCTION: bool
LOG_CONFIG_OVERRIDE: Path | None = None
"""path to custom logging configuration file"""
LOG_LEVEL: str = "info"
"""corresponds to standard Python log levels"""
class AppSettings(AppLoggingSettings):
theme: Theme = Theme()
PRODUCTION: bool
BASE_URL: str = "http://localhost:8080"
"""trailing slashes are trimmed (ex. `http://localhost:8080/` becomes ``http://localhost:8080`)"""
@ -56,12 +101,6 @@ class AppSettings(BaseSettings):
SECRET: str
LOG_CONFIG_OVERRIDE: Path | None = None
"""path to custom logging configuration file"""
LOG_LEVEL: str = "info"
"""corresponds to standard Python log levels"""
GIT_COMMIT_HASH: str = "unknown"
ALLOW_SIGNUP: bool = False
@ -69,17 +108,13 @@ class AppSettings(BaseSettings):
DAILY_SCHEDULE_TIME: str = "23:45"
"""Local server time, in HH:MM format. See `DAILY_SCHEDULE_TIME_UTC` for the parsed UTC equivalent"""
_logger: logging.Logger | None = None
@property
def logger(self) -> logging.Logger:
if self._logger is None:
# lazy load the logger, since get_logger depends on the settings being loaded
from mealie.core.root_logger import get_logger
# Avoid a circular import by importing here instead of at the file's top-level.
# get_logger -> AppSettings -> get_logger
from mealie.core.root_logger import get_logger
self._logger = get_logger()
return self._logger
return get_logger()
@property
def DAILY_SCHEDULE_TIME_UTC(self) -> ScheduleTime:
@ -303,11 +338,7 @@ class AppSettings(BaseSettings):
"""Validates OpenAI settings are all set"""
return bool(self.OPENAI_API_KEY and self.OPENAI_MODEL)
# ===============================================
# Testing Config
TESTING: bool = False
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow", secrets_dir="/run/secrets")
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
@ -319,6 +350,9 @@ def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, e
app_settings = AppSettings(
_env_file=env_file, # type: ignore
_env_file_encoding=env_encoding, # type: ignore
# `get_secrets_dir` must be called here rather than within `AppSettings`
# to avoid a circular import.
_secrets_dir=get_secrets_dir(), # type: ignore
**{"SECRET": determine_secrets(data_dir, production)},
)