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 = {}
+53 -17
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: if user:
user = find_in_db(ctx.db_name) return user
if user:
return user if _has_tenant_configs() and tenant_db is None:
except Exception: logger.warning(
pass "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)
+80 -46
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,54 +262,67 @@ 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()
hostname, port = _parse_port_from_host(host) primary_host = host_candidates[0] if host_candidates else _first_host_token(getattr(request, 'host', ''))
self.port = port logger.info(
logger.info(f"Tenant resolution start: request.host={host} request.headers={dict(request.headers)}") "Tenant resolution start: request.host=%s host_candidates=%s request.headers=%s",
if port: getattr(request, 'host', ''),
tenant_from_port = _tenant_id_for_port(port) host_candidates,
if tenant_from_port: dict(request.headers),
self.tenant_id = tenant_from_port )
self.config = get_tenant_config(tenant_from_port)
session['tenant_id'] = tenant_from_port for host in host_candidates:
logger.info( hostname, port = _parse_port_from_host(host)
f"Tenant resolution by port: host={host} port={port} tenant={tenant_from_port} config={self.config}" if port:
) self.port = port
return self._get_db_name(tenant_from_port) tenant_from_port = _tenant_id_for_port(port)
logger.info(f"Tenant port not mapped: host={host} port={port}") if tenant_from_port:
self.tenant_id = tenant_from_port
self.config = get_tenant_config(tenant_from_port)
session['tenant_id'] = tenant_from_port
logger.info(
f"Tenant resolution by port: host={host} port={port} tenant={tenant_from_port} config={self.config}"
)
return self._get_db_name(tenant_from_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:
direct_host_match = _find_registered_tenant_id(host_without_port) host_without_port, _ = _parse_port_from_host(host)
if direct_host_match: host_without_port = (host_without_port or '').strip().lower()
self.subdomain = host_without_port if not host_without_port:
self.tenant_id = direct_host_match continue
self.config = get_tenant_config(direct_host_match)
session['tenant_id'] = direct_host_match
logger.info(
f"Tenant resolution by direct host match: host={host} tenant={direct_host_match} config={self.config}"
)
return self._get_db_name(direct_host_match)
if host_without_port and not _is_ip_host(host_without_port): direct_host_match = _find_registered_tenant_id(host_without_port)
parts = host_without_port.split('.') if direct_host_match:
if len(parts) >= 2: self.subdomain = host_without_port
potential_subdomain = parts[0] self.tenant_id = direct_host_match
if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'): self.config = get_tenant_config(direct_host_match)
matched_tenant = _find_registered_tenant_id(potential_subdomain) session['tenant_id'] = direct_host_match
if matched_tenant: logger.info(
self.subdomain = potential_subdomain f"Tenant resolution by direct host match: host={host} tenant={direct_host_match} config={self.config}"
self.tenant_id = matched_tenant )
self.config = get_tenant_config(matched_tenant) return self._get_db_name(direct_host_match)
session['tenant_id'] = matched_tenant
logger.info( if host_without_port and not _is_ip_host(host_without_port):
f"Tenant resolution by subdomain: host={host} tenant={matched_tenant} config={self.config}" parts = host_without_port.split('.')
) if len(parts) >= 2:
return self._get_db_name(matched_tenant) potential_subdomain = parts[0]
logger.info(f"Tenant subdomain not registered: {potential_subdomain}") if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'):
else: matched_tenant = _find_registered_tenant_id(potential_subdomain)
logger.info(f"Tenant subdomain ignored: {potential_subdomain}") if matched_tenant:
self.subdomain = potential_subdomain
self.tenant_id = matched_tenant
self.config = get_tenant_config(matched_tenant)
session['tenant_id'] = matched_tenant
logger.info(
f"Tenant resolution by subdomain: host={host} tenant={matched_tenant} config={self.config}"
)
return self._get_db_name(matched_tenant)
logger.info(f"Tenant subdomain not registered: {potential_subdomain}")
else:
logger.info(f"Tenant subdomain ignored: {potential_subdomain}")
# Priority 4: sticky tenant from the authenticated session # Priority 4: sticky tenant from the authenticated session
session_tenant = session.get('tenant_id', '').strip() if session.get('tenant_id') else '' session_tenant = session.get('tenant_id', '').strip() if session.get('tenant_id') else ''
@@ -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