# Copyright (C) 2017-2025 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from __future__ import annotations
import importlib
import os
from typing import TYPE_CHECKING, Any
from swh.core import config
from swh.web import settings
if TYPE_CHECKING:
from swh.counters.interface import CountersInterface
from swh.indexer.storage.interface import IndexerStorageInterface
from swh.scheduler.interface import SchedulerInterface
from swh.search.interface import SearchInterface
from swh.storage.interface import StorageInterface
from swh.vault.interface import VaultInterface
SETTINGS_DIR = os.path.dirname(settings.__file__)
[docs]
class ConfigurationError(Exception):
pass
[docs]
class SWHWebConfig(dict[str, Any]):
def __getitem__(self, key: str) -> Any:
try:
return super().__getitem__(key)
except KeyError:
raise ConfigurationError(f"Missing '{key}' configuration")
DEFAULT_CONFIG = {
"allowed_hosts": ("list", []),
"unauthenticated_api_hosts": ("list", []),
"search_config": (
"dict",
{
"metadata_backend": "swh-search",
}, # or "swh-search"
),
"log_dir": ("string", "/tmp/swh/log"),
"debug": ("bool", False),
"serve_assets": ("bool", False),
"host": ("string", "127.0.0.1"),
"port": ("int", 5004),
"secret_key": ("string", "development key"),
"secret_key_fallbacks": ("list[string]", []),
# do not display code highlighting for content > 1MB
"content_display_max_size": ("int", 5 * 1024 * 1024),
"snapshot_content_max_size": ("int", 1000),
"throttling": (
"dict",
{
"cache_uri": None, # production: memcached as cache (127.0.0.1:11211)
# development: in-memory cache so None
"scopes": {
"swh_api": {
"limiter_rate": {"default": "120/h"},
"exempted_networks": ["127.0.0.0/8"],
},
"swh_api_origin_search": {
"limiter_rate": {"default": "10/m"},
"exempted_networks": ["127.0.0.0/8"],
},
"swh_vault_cooking": {
"limiter_rate": {"default": "120/h", "GET": "60/m"},
"exempted_networks": ["127.0.0.0/8"],
},
"swh_save_origin": {
"limiter_rate": {"default": "120/h", "POST": "10/h"},
"exempted_networks": ["127.0.0.0/8"],
},
"swh_api_origin_visit_latest": {
"limiter_rate": {"default": "700/m"},
"exempted_networks": ["127.0.0.0/8"],
},
"swh_api_metadata_citation": {
"limiter_rate": {"default": "60/m"},
"exempted_networks": ["127.0.0.0/8"],
},
},
},
),
"development_db": ("string", os.path.join(SETTINGS_DIR, "db.sqlite3")),
"test_db": ("dict", {"name": "swh-web-test"}),
"production_db": ("dict", {"name": "swh-web"}),
"e2e_tests_mode": ("bool", False),
"client_config": ("dict", {}),
"keycloak": ("dict", {"server_url": "", "realm_name": ""}),
"counters_backend": ("string", "swh-storage"), # or "swh-counters"
"instance_name": ("str", "archive-test.softwareheritage.org"),
"give": ("dict", {"public_key": "", "token": ""}),
"features": ("dict", {"add_forge_now": True}),
"add_forge_now": (
"dict",
{
"email_address": "add-forge-now@example.com",
"gitlab_pipeline": {
"token": "sometoken",
"trigger_url": "someurl",
},
},
),
"swh_extra_django_apps": (
"list",
[
"swh.web.add_forge_now",
"swh.web.alter",
"swh.web.admin",
"swh.web.archive_coverage",
"swh.web.badges",
"swh.web.banners",
"swh.web.deposit",
"swh.web.inbound_email",
"swh.web.jslicenses",
"swh.web.mailmap",
"swh.web.metrics",
"swh.web.provenance",
"swh.web.save_bulk",
"swh.web.save_code_now",
"swh.web.save_origin_webhooks",
"swh.web.vault",
],
),
"mirror_config": ("dict", {}),
"top_bar": (
"dict",
{
"links": {
"Home": "https://www.softwareheritage.org",
"Development": "https://gitlab.softwareheritage.org",
"Documentation": "https://docs.softwareheritage.org",
},
"donate_link": "https://www.softwareheritage.org/donate",
},
),
"matomo": ("dict", {}),
"show_corner_ribbon": ("bool", False),
"corner_ribbon_text": ("str", ""),
"save_code_now_webhook_secret": ("str", ""),
"inbound_email": ("dict", {"shared_key": "shared_key"}),
"browse_content_rate_limit": ("dict", {"enabled": True, "rate": "60/m"}),
"activate_citations_ui": ("bool", False),
"datatables_max_page_size": ("int", 1000),
"email_setup": (
"dict",
{
"backend": "django.core.mail.backends.smtp.EmailBackend",
"host": "smtp",
"port": 1025,
"username": "username",
"password": "password",
"use_tls": False,
"use_ssl": False,
"default_from_email": "no-reply@localhost",
},
),
# when using keycloak as the user backend we use these email aliases to send
# notifications, this should be replaced by a proper way of querying emails linked
# to an alter role
"alter_settings": (
"dict",
{
"support_mail_alias": "alter-support@localhost",
"manager_mail_alias": "alter-manager@localhost",
"legal_mail_alias": "alter-legal@localhost",
"technical_mail_alias": "alter-technical@localhost",
"block_disposable_email_domains": False,
},
),
}
swhweb_config: SWHWebConfig = SWHWebConfig()
[docs]
def get_config(config_file: str = "web/web") -> SWHWebConfig:
"""Read the configuration file `config_file`.
If an environment variable SWH_CONFIG_FILENAME is defined, this
takes precedence over the config_file parameter.
In any case, update the app with parameters (secret_key, conf)
and return the parsed configuration as a dict.
If no configuration file is provided, return a default
configuration.
"""
if not swhweb_config:
config_filename = os.environ.get("SWH_CONFIG_FILENAME")
if config_filename:
config_file = config_filename
cfg = config.load_named_config(config_file, DEFAULT_CONFIG)
swhweb_config.update(cfg)
config.prepare_folders(swhweb_config, "log_dir")
for service, modname in (
("search", "search"),
("storage", "storage"),
("vault", "vault"),
("indexer_storage", "indexer.storage"),
("scheduler", "scheduler"),
("counters", "counters"),
):
if isinstance(swhweb_config.get(service), dict):
mod = importlib.import_module(f"swh.{modname}")
getter = getattr(mod, f"get_{service}")
swhweb_config[service] = getter(**swhweb_config[service])
return swhweb_config
[docs]
def oidc_enabled() -> bool:
try:
return bool(get_config()["keycloak"]["server_url"])
except: # noqa: E722
return False
[docs]
def search() -> SearchInterface:
"""Return the current application's search."""
return get_config()["search"]
[docs]
def storage() -> StorageInterface:
"""Return the current application's storage."""
return get_config()["storage"]
[docs]
def vault() -> VaultInterface:
"""Return the current application's vault."""
return get_config()["vault"]
[docs]
def indexer_storage() -> IndexerStorageInterface:
"""Return the current application's indexer storage."""
return get_config()["indexer_storage"]
[docs]
def scheduler() -> SchedulerInterface:
"""Return the current application's scheduler."""
return get_config()["scheduler"]
[docs]
def counters() -> CountersInterface:
"""Return the current application's counters."""
return get_config()["counters"]