Compare commits

..

7 Commits

5 changed files with 157 additions and 66 deletions
+1 -1
View File
@@ -1 +1 @@
v0.7.74 v0.7.77
+22 -2
View File
@@ -100,9 +100,29 @@ DEFAULTS = {
} }
# Load configuration file # Load configuration file
CONFIG_PATH = os.path.join(BASE_DIR, '..', 'config.json') def _resolve_config_path():
"""Resolve runtime config path with robust fallbacks for Docker deployments."""
env_path = os.getenv('INVENTAR_CONFIG_PATH', '').strip()
if env_path:
return env_path
candidates = [
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(BASE_DIR))), 'config.json'),
os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'config.json'),
os.path.join(os.path.dirname(BASE_DIR), 'config.json'),
os.path.join(BASE_DIR, '..', 'config.json'),
]
for candidate in candidates:
if os.path.isfile(candidate):
return os.path.abspath(candidate)
return os.path.abspath(candidates[0])
CONFIG_PATH = _resolve_config_path()
try: try:
with open(CONFIG_PATH, 'r') as f: with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
_conf = json.load(f) _conf = json.load(f)
except Exception: except Exception:
_conf = {} _conf = {}
+51 -15
View File
@@ -12,6 +12,7 @@ Provides methods for creating, validating, and retrieving user information.
''' '''
import hashlib import hashlib
import copy import copy
import importlib
import logging import logging
import re import re
import secrets import secrets
@@ -59,6 +60,35 @@ def _get_tenant_db(client):
return client[cfg.MONGODB_DB] return client[cfg.MONGODB_DB]
def _has_tenant_configs():
for module_name in ('tenant', 'Web.tenant'):
try:
tenant_module = importlib.import_module(module_name)
tenant_registry = getattr(tenant_module, 'TENANT_REGISTRY', None)
if isinstance(tenant_registry, dict) and tenant_registry:
return True
except Exception:
continue
return isinstance(getattr(cfg, 'TENANT_CONFIGS', None), dict) and bool(cfg.TENANT_CONFIGS)
def _resolve_request_tenant_db():
for module_name in ('tenant', 'Web.tenant'):
try:
tenant_module = importlib.import_module(module_name)
get_tenant_context = getattr(tenant_module, 'get_tenant_context', None)
if not callable(get_tenant_context):
continue
ctx = get_tenant_context()
if ctx and ctx.tenant_id:
return ctx.db_name or ctx.resolve_tenant(), ctx.tenant_id
if ctx and ctx.db_name and not _has_tenant_configs():
return ctx.db_name, None
except Exception as exc:
logger.debug("Tenant context import %s failed: %s", module_name, exc)
return None, None
def build_name_synonym(first_name, last_name=''): def build_name_synonym(first_name, last_name=''):
"""Build a deterministic, non-personalized short alias from 2 letters each.""" """Build a deterministic, non-personalized short alias from 2 letters each."""
first = _clean_name_fragment(first_name) first = _clean_name_fragment(first_name)
@@ -424,22 +454,26 @@ def check_nm_pwd(username, password):
Returns: Returns:
dict: User document if credentials are valid, None otherwise dict: User document if credentials are valid, None otherwise
""" """
db_name = cfg.MONGODB_DB db_name, tenant_id = _resolve_request_tenant_db()
tenant_db = None
ctx = None ctx = None
try: try:
from tenant import get_tenant_context from tenant import get_tenant_context
ctx = get_tenant_context() ctx = get_tenant_context()
if ctx and ctx.tenant_id: except Exception:
tenant_db = ctx.db_name or ctx.resolve_tenant() ctx = None
db_name = tenant_db
except Exception as exc: if not db_name:
logger.exception(f"Failed to resolve tenant context in check_nm_pwd: {exc}") if _has_tenant_configs():
logger.warning(
"Refusing default DB fallback for login because tenant configs exist and no tenant was resolved."
)
return None
db_name = cfg.MONGODB_DB
logger.info( logger.info(
"check_nm_pwd start: username=%r tenant=%r db=%r host=%r port=%r uri=%r", "check_nm_pwd start: username=%r tenant=%r db=%r host=%r port=%r uri=%r",
username, username,
ctx.tenant_id if ctx else None, tenant_id,
db_name, db_name,
cfg.MONGODB_HOST, cfg.MONGODB_HOST,
cfg.MONGODB_PORT, cfg.MONGODB_PORT,
@@ -644,15 +678,17 @@ def get_user(username):
return users.find_one({'Username': username}) or users.find_one({'username': username}) return users.find_one({'Username': username}) or users.find_one({'username': username})
# Try current tenant first when available # Try current tenant first when available
try: tenant_db, tenant_id = _resolve_request_tenant_db()
from tenant import get_tenant_context if tenant_db:
ctx = get_tenant_context() user = find_in_db(tenant_db)
if ctx and ctx.db_name:
user = find_in_db(ctx.db_name)
if user: if user:
return user return user
except Exception:
pass if _has_tenant_configs() and tenant_db is None:
logger.warning(
"Refusing default DB fallback for user lookup because tenant configs exist and no tenant was resolved."
)
return None
# Fallback to default configured database # Fallback to default configured database
user = find_in_db(cfg.MONGODB_DB) user = find_in_db(cfg.MONGODB_DB)
+40 -6
View File
@@ -66,6 +66,27 @@ def _parse_port_from_host(host):
return host, None return host, None
def _first_host_token(value):
raw = str(value or '').strip()
if not raw:
return ''
return raw.split(',', 1)[0].strip().lower()
def _request_host_candidates():
candidates = []
for header_name in ('X-Forwarded-Host', 'X-Original-Host', 'Host'):
token = _first_host_token(request.headers.get(header_name, ''))
if token and token not in candidates:
candidates.append(token)
direct_host = _first_host_token(getattr(request, 'host', ''))
if direct_host and direct_host not in candidates:
candidates.append(direct_host)
return candidates
def _tenant_id_for_port(port): def _tenant_id_for_port(port):
"""Map a host port to a registered tenant ID via tenant configs or env overrides.""" """Map a host port to a registered tenant ID via tenant configs or env overrides."""
for tenant_id, config in TENANT_REGISTRY.items(): for tenant_id, config in TENANT_REGISTRY.items():
@@ -241,12 +262,20 @@ class TenantContext:
session['tenant_id'] = matched_tenant session['tenant_id'] = matched_tenant
return self._get_db_name(matched_tenant) return self._get_db_name(matched_tenant)
# Priority 2: Port-based tenant mapping # Priority 2: Port/host based tenant mapping
host = request.host.lower() host_candidates = _request_host_candidates()
primary_host = host_candidates[0] if host_candidates else _first_host_token(getattr(request, 'host', ''))
logger.info(
"Tenant resolution start: request.host=%s host_candidates=%s request.headers=%s",
getattr(request, 'host', ''),
host_candidates,
dict(request.headers),
)
for host in host_candidates:
hostname, port = _parse_port_from_host(host) hostname, port = _parse_port_from_host(host)
self.port = port
logger.info(f"Tenant resolution start: request.host={host} request.headers={dict(request.headers)}")
if port: if port:
self.port = port
tenant_from_port = _tenant_id_for_port(port) tenant_from_port = _tenant_id_for_port(port)
if tenant_from_port: if tenant_from_port:
self.tenant_id = tenant_from_port self.tenant_id = tenant_from_port
@@ -259,7 +288,12 @@ class TenantContext:
logger.info(f"Tenant port not mapped: host={host} port={port}") logger.info(f"Tenant port not mapped: host={host} port={port}")
# Priority 3: Subdomain extraction # Priority 3: Subdomain extraction
host_without_port = (hostname or '').strip().lower() for host in host_candidates:
host_without_port, _ = _parse_port_from_host(host)
host_without_port = (host_without_port or '').strip().lower()
if not host_without_port:
continue
direct_host_match = _find_registered_tenant_id(host_without_port) direct_host_match = _find_registered_tenant_id(host_without_port)
if direct_host_match: if direct_host_match:
self.subdomain = host_without_port self.subdomain = host_without_port
@@ -296,7 +330,7 @@ class TenantContext:
self.tenant_id = session_tenant self.tenant_id = session_tenant
self.config = get_tenant_config(session_tenant) self.config = get_tenant_config(session_tenant)
logger.info( logger.info(
f"Tenant resolution by session: host={host} tenant={session_tenant} config={self.config}" f"Tenant resolution by session: host={primary_host} tenant={session_tenant} config={self.config}"
) )
return self._get_db_name(session_tenant) return self._get_db_name(session_tenant)
+1
View File
@@ -95,6 +95,7 @@ services:
INVENTAR_MONGODB_HOST: mongodb INVENTAR_MONGODB_HOST: mongodb
INVENTAR_MONGODB_PORT: "27017" INVENTAR_MONGODB_PORT: "27017"
INVENTAR_MONGODB_DB: inventar_default INVENTAR_MONGODB_DB: inventar_default
INVENTAR_CONFIG_PATH: /app/config.json
# Redis Configuration (for sessions and caching) # Redis Configuration (for sessions and caching)
INVENTAR_REDIS_HOST: redis INVENTAR_REDIS_HOST: redis