mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-13 06:06:38 +00:00
WordPress put objstore.public_url, the clearnet api.seaweedfs.s3 host, into S3_UPLOADS_BUCKET_URL, so the onion pass of WordPress#0 (CI run 40979252521) loaded the site logo from clearnet and the leak spec failed; the /objstore rebase only fired on onion-only consumers and never on a multi-network node. lookup('objstore', app, 'relative_url') now returns /_infinito/objstore/<bucket> for a shared consumer with a public bucket and an API domain, and locations.conf.j2 serves that path on every vhost of the consumer, clearnet and onion alike, by proxying over the loopback to the provider's own vhost.
The route passes GET and HEAD only, answers 404 for a path ending in / (no ListObjects), drops the query string (no S3 subresources or presigned queries) and the Authorization and Cookie headers, so only the public bucket's anonymous read applies, and adds nosniff plus a sandbox CSP; private and embedded buckets get no route. Only web-app-wordpress switches to relative_url: mastodon, pixelfed, baserow, bookwyrm, snipe-it, listmonk, mediawiki, shopware and peertube need absolute URLs for federation, mail or server-side rendering, and decidim and funkwhale sign URLs, which the route deliberately refuses. The prefix moves from /objstore to /_infinito/objstore.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
325 lines
10 KiB
Python
325 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from urllib.parse import quote
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.plugins.loader import lookup_loader
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
from utils.manager.credential_key import OVERRIDE_SECTION
|
|
from utils.roles.applications.config import get
|
|
from utils.roles.entity.name import entity_name
|
|
from utils.tls_common import (
|
|
collect_domains_for_app,
|
|
is_onion_domain,
|
|
resolve_enabled,
|
|
)
|
|
|
|
OBJSTORE_ENGINES = ("seaweedfs", "minio")
|
|
OBJSTORE_PROVIDER_ROLE = {
|
|
"seaweedfs": "web-svc-seaweedfs",
|
|
"minio": "web-app-minio",
|
|
}
|
|
OBJSTORE_DEFAULT_PORT = {"seaweedfs": 8333, "minio": 9000}
|
|
OBJSTORE_DEFAULT_IMAGE = {
|
|
"seaweedfs": "chrislusf/seaweedfs",
|
|
"minio": "quay.io/minio/minio",
|
|
}
|
|
OBJSTORE_DEFAULT_REGION = "us-east-1"
|
|
OBJSTORE_PROXY_PREFIX = "/_infinito/objstore"
|
|
|
|
|
|
def _is_enabled(applications: dict[str, Any], consumer_id: str, engine: str) -> bool:
|
|
return bool(
|
|
get(
|
|
applications,
|
|
consumer_id,
|
|
f"services.{engine}.enabled",
|
|
strict=False,
|
|
default=False,
|
|
)
|
|
)
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
"""
|
|
Resolve engine-agnostic S3 object-store values for a consumer.
|
|
|
|
Mirrors plugins/lookup/database.py (postgres|mariadb): branches on which
|
|
object-store engine (seaweedfs|minio) the consumer has enabled and yields a
|
|
uniform S3 connection payload, so consumer templates stay engine-agnostic.
|
|
|
|
API (STRICT):
|
|
- {{ lookup('objstore', consumer_id) }}
|
|
- {{ lookup('objstore', consumer_id, 'url') }}
|
|
"""
|
|
|
|
def run(
|
|
self,
|
|
terms: list[Any],
|
|
variables: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> list[Any]:
|
|
terms = terms or []
|
|
if len(terms) not in (1, 2):
|
|
raise AnsibleError("objstore: requires consumer_id [, want_path]")
|
|
|
|
consumer_id = str(terms[0]).strip()
|
|
if not consumer_id:
|
|
raise AnsibleError("objstore: consumer_id must not be empty")
|
|
|
|
want = str(terms[1]).strip() if len(terms) == 2 else ""
|
|
if not want:
|
|
want = "all"
|
|
|
|
vars_ = variables or self._templar.available_variables
|
|
applications = lookup_loader.get(
|
|
"applications",
|
|
loader=self._loader,
|
|
templar=getattr(self, "_templar", None),
|
|
).run([], variables=vars_)[0]
|
|
path_instances = self._require_var(vars_, "DIR_COMPOSITIONS")
|
|
consumer_entity = entity_name(consumer_id)
|
|
|
|
active = [
|
|
e for e in OBJSTORE_ENGINES if _is_enabled(applications, consumer_id, e)
|
|
]
|
|
if len(active) > 1:
|
|
raise AnsibleError(
|
|
f"objstore: multiple object-store engines enabled for "
|
|
f"'{consumer_id}': {active}; enable only one."
|
|
)
|
|
engine = active[0] if active else ""
|
|
|
|
if not engine:
|
|
resolved = {
|
|
"id": "",
|
|
"engine": "",
|
|
"enabled": False,
|
|
"shared": False,
|
|
"local": False,
|
|
"host": "",
|
|
"port": "",
|
|
"network": "",
|
|
"container": "",
|
|
"bucket": consumer_entity,
|
|
"public": False,
|
|
"access_key": consumer_entity,
|
|
"secret_key": "",
|
|
"image": "",
|
|
"version": "",
|
|
"region": OBJSTORE_DEFAULT_REGION,
|
|
"env": "",
|
|
"volume": "",
|
|
"endpoint": "",
|
|
"url": "",
|
|
"credentialed_url": "",
|
|
"public_domain": "",
|
|
"public_bucket": "",
|
|
"public_endpoint": "",
|
|
"public_url": "",
|
|
"relative_url": "",
|
|
"proxy_required": False,
|
|
"proxy_prefix": "",
|
|
"upstream_domain": "",
|
|
"upstream_scheme": "",
|
|
"reach_host": "127.0.0.1",
|
|
}
|
|
return [resolved if want == "all" else resolved.get(want, "")]
|
|
|
|
provider_role = OBJSTORE_PROVIDER_ROLE[engine]
|
|
shared = bool(
|
|
get(
|
|
applications,
|
|
consumer_id,
|
|
f"services.{engine}.shared",
|
|
strict=False,
|
|
default=False,
|
|
)
|
|
)
|
|
central_enabled = shared
|
|
|
|
central_name = get(
|
|
applications,
|
|
provider_role,
|
|
f"services.{engine}.name",
|
|
strict=False,
|
|
default=engine,
|
|
skip_missing_app=True,
|
|
)
|
|
central_name = (
|
|
str(central_name) if central_name is not None else ""
|
|
).strip() or engine
|
|
|
|
host = central_name if central_enabled else engine
|
|
network = entity_name(provider_role) if central_enabled else consumer_entity
|
|
container = central_name if central_enabled else f"{consumer_entity}-{engine}"
|
|
port = get(
|
|
applications,
|
|
provider_role,
|
|
f"services.{engine}.api_port",
|
|
strict=False,
|
|
default=OBJSTORE_DEFAULT_PORT[engine],
|
|
skip_missing_app=True,
|
|
)
|
|
region = get(
|
|
applications,
|
|
provider_role,
|
|
f"services.{engine}.region",
|
|
strict=False,
|
|
default=OBJSTORE_DEFAULT_REGION,
|
|
skip_missing_app=True,
|
|
)
|
|
|
|
access_key = consumer_entity
|
|
secret_key = get(
|
|
applications,
|
|
consumer_id,
|
|
f"{OVERRIDE_SECTION}.objstore_secret_key",
|
|
strict=False,
|
|
default="",
|
|
)
|
|
bucket = consumer_entity
|
|
public = bool(
|
|
get(
|
|
applications,
|
|
consumer_id,
|
|
f"services.{engine}.public",
|
|
strict=False,
|
|
default=False,
|
|
)
|
|
)
|
|
|
|
image = get(
|
|
applications,
|
|
provider_role,
|
|
f"services.{engine}.image",
|
|
strict=False,
|
|
default=OBJSTORE_DEFAULT_IMAGE[engine],
|
|
skip_missing_app=True,
|
|
)
|
|
default_version = get(
|
|
applications,
|
|
provider_role,
|
|
f"services.{engine}.version",
|
|
strict=False,
|
|
default="latest",
|
|
skip_missing_app=True,
|
|
)
|
|
version = get(
|
|
applications,
|
|
consumer_id,
|
|
f"services.{engine}.version",
|
|
strict=False,
|
|
default=default_version,
|
|
)
|
|
|
|
env = f"{path_instances}{consumer_entity}/.env/objstore.env"
|
|
volume_prefix = "" if central_enabled else f"{consumer_entity}_"
|
|
volume = (
|
|
f"{volume_prefix}{engine}"
|
|
if bool(_is_enabled(applications, consumer_id, engine) and not shared)
|
|
else ""
|
|
)
|
|
endpoint = f"{host}:{port}"
|
|
url = f"http://{endpoint}"
|
|
userinfo = f"{quote(access_key, safe='')}:{quote(secret_key, safe='')}"
|
|
credentialed_url = f"http://{userinfo}@{endpoint}"
|
|
|
|
public_domain = get(
|
|
applications,
|
|
provider_role,
|
|
"domains.canonical.api",
|
|
strict=False,
|
|
default="",
|
|
skip_missing_app=True,
|
|
)
|
|
public_domain = (
|
|
str(public_domain) if public_domain is not None else ""
|
|
).strip()
|
|
provider_app = applications.get(provider_role) or {}
|
|
if not isinstance(provider_app, dict):
|
|
provider_app = {}
|
|
scheme = (
|
|
"https"
|
|
if resolve_enabled(provider_app, bool(vars_.get("TLS_ENABLED", True)))
|
|
else "http"
|
|
)
|
|
|
|
def _lookup(name: str, args: list[Any], **kwargs: Any) -> Any:
|
|
return lookup_loader.get(
|
|
name, loader=self._loader, templar=getattr(self, "_templar", None)
|
|
).run(args, variables=vars_, **kwargs)[0]
|
|
|
|
def _consumer_is_onion_only() -> bool:
|
|
domains_map = _lookup("domains", [])
|
|
if consumer_id not in domains_map:
|
|
return False
|
|
served_on = collect_domains_for_app(
|
|
domains_map, consumer_id, err_prefix="objstore"
|
|
)
|
|
return bool(served_on) and all(is_onion_domain(d) for d in served_on)
|
|
|
|
relative_url = (
|
|
f"{OBJSTORE_PROXY_PREFIX}/{bucket}"
|
|
if public and shared and public_domain
|
|
else ""
|
|
)
|
|
proxy_required = bool(
|
|
relative_url
|
|
and not is_onion_domain(public_domain)
|
|
and _consumer_is_onion_only()
|
|
)
|
|
browser_domain = public_domain
|
|
browser_scheme = scheme
|
|
if proxy_required:
|
|
consumer_tls = _lookup("tls", [consumer_id], consumer=consumer_id)
|
|
browser_domain = f"{consumer_tls['domain']}{OBJSTORE_PROXY_PREFIX}"
|
|
browser_scheme = consumer_tls["protocols"]["web"]
|
|
|
|
public_bucket = f"{browser_domain}/{bucket}" if browser_domain else ""
|
|
public_endpoint = f"{scheme}://{public_domain}" if public_domain else ""
|
|
public_url = f"{browser_scheme}://{public_bucket}" if public_bucket else ""
|
|
|
|
resolved = {
|
|
"id": provider_role,
|
|
"engine": engine,
|
|
"enabled": True,
|
|
"shared": shared,
|
|
"local": bool(not shared),
|
|
"host": host,
|
|
"port": port,
|
|
"network": network,
|
|
"container": container,
|
|
"bucket": bucket,
|
|
"public": public,
|
|
"access_key": access_key,
|
|
"secret_key": secret_key,
|
|
"image": image,
|
|
"version": version,
|
|
"region": region,
|
|
"env": env,
|
|
"volume": volume,
|
|
"endpoint": endpoint,
|
|
"url": url,
|
|
"credentialed_url": credentialed_url,
|
|
"public_domain": public_domain,
|
|
"public_bucket": public_bucket,
|
|
"public_endpoint": public_endpoint,
|
|
"public_url": public_url,
|
|
"relative_url": relative_url,
|
|
"proxy_required": proxy_required,
|
|
"proxy_prefix": OBJSTORE_PROXY_PREFIX,
|
|
"upstream_domain": public_domain,
|
|
"upstream_scheme": scheme,
|
|
"reach_host": "127.0.0.1",
|
|
}
|
|
return [resolved if want == "all" else resolved.get(want, "")]
|
|
|
|
@staticmethod
|
|
def _require_var(vars_: dict[str, Any], key: str) -> Any:
|
|
if key not in vars_:
|
|
raise AnsibleError(f"objstore: required variable '{key}' is not set")
|
|
return vars_[key]
|