|
# retoor <retoor@molodetz.nl>
|
|
|
|
import logging
|
|
|
|
from devplacepy.database import CONSENT_KINDS, bump_cache_version
|
|
from devplacepy.services.acceptance.agreements import (
|
|
AGREEMENTS,
|
|
label_for,
|
|
setting_key,
|
|
)
|
|
from devplacepy.services.acceptance.grant import converge_user
|
|
from devplacepy.services.acceptance.pending import current_version, pending
|
|
from devplacepy.services.base import BaseService, ConfigField
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DRY_RUN_KEY = "acceptance_dry_run"
|
|
BATCH_SIZE_KEY = "acceptance_batch_size"
|
|
DEFAULT_BATCH_SIZE = 200
|
|
MAX_BATCH_SIZE = 5000
|
|
SAMPLE_NAMES = 20
|
|
|
|
|
|
def sample_names(candidates: list[dict]) -> str:
|
|
names = [row.get("username") or row.get("uid") or "" for row in candidates]
|
|
shown = ", ".join(names[:SAMPLE_NAMES])
|
|
remaining = len(names) - SAMPLE_NAMES
|
|
if remaining > 0:
|
|
return f"{shown} and {remaining} more"
|
|
return shown
|
|
|
|
|
|
class AcceptanceService(BaseService):
|
|
title = "Acceptance convergence"
|
|
description = (
|
|
"Grants every policy agreement to every account that has not declined it, "
|
|
"so a production-identical test instance never interrupts manual testing "
|
|
"with an acceptance dialog. Off by default and never appropriate on a real "
|
|
"production host."
|
|
)
|
|
details = (
|
|
"An account that withdrew a consent is never granted it again, with no "
|
|
"further action: the latest row in the consent ledger is the decline "
|
|
"register. The service only ever grants, so any withdrawal in the ledger "
|
|
"was written by a human. Withdraw a consent from the profile privacy tab "
|
|
"to keep an account permanently outside the convergence."
|
|
)
|
|
default_enabled = False
|
|
min_interval = 60
|
|
METRICS_SECONDS = 60
|
|
config_fields = [
|
|
ConfigField(
|
|
DRY_RUN_KEY,
|
|
"Dry run",
|
|
type="bool",
|
|
default=True,
|
|
help=(
|
|
"Log which accounts would be converged and write nothing. Switch "
|
|
"off only after the log lists what you expect."
|
|
),
|
|
group="Safety",
|
|
),
|
|
ConfigField(
|
|
BATCH_SIZE_KEY,
|
|
"Accounts per agreement per run",
|
|
type="int",
|
|
default=DEFAULT_BATCH_SIZE,
|
|
minimum=1,
|
|
maximum=MAX_BATCH_SIZE,
|
|
help="Upper bound on one sweep. The remainder converges on the next run.",
|
|
group="Safety",
|
|
),
|
|
*[
|
|
ConfigField(
|
|
setting_key(agreement.kind),
|
|
label_for(agreement.kind),
|
|
type="bool",
|
|
default=False,
|
|
help=f"Grant the {agreement.kind} agreement to every account that has not declined it.",
|
|
group="Agreements",
|
|
)
|
|
for agreement in AGREEMENTS
|
|
],
|
|
]
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__("acceptance", interval_seconds=300)
|
|
self._converged = {agreement.kind: 0 for agreement in AGREEMENTS}
|
|
|
|
async def run_once(self) -> None:
|
|
config = self.get_config()
|
|
dry = config[DRY_RUN_KEY]
|
|
limit = config[BATCH_SIZE_KEY]
|
|
gate_changed = False
|
|
for agreement in AGREEMENTS:
|
|
if not config[setting_key(agreement.kind)]:
|
|
continue
|
|
version = current_version(agreement)
|
|
candidates = pending(agreement, limit)
|
|
if not candidates:
|
|
continue
|
|
if dry:
|
|
self.log(
|
|
f"[dry run] {agreement.kind}: {len(candidates)} account(s) would be "
|
|
f"converged to version {version}: {sample_names(candidates)}"
|
|
)
|
|
continue
|
|
granted = 0
|
|
for user in candidates:
|
|
if converge_user(agreement, user, version):
|
|
granted += 1
|
|
gate_changed = gate_changed or bool(agreement.gate_column)
|
|
self._converged[agreement.kind] += granted
|
|
self.log(
|
|
f"{agreement.kind}: converged {granted} of {len(candidates)} pending "
|
|
f"at version {version}"
|
|
)
|
|
if len(candidates) == limit:
|
|
self.log(
|
|
f"{agreement.kind}: batch cap of {limit} reached, more remain "
|
|
"for the next run"
|
|
)
|
|
if gate_changed:
|
|
bump_cache_version("auth")
|
|
|
|
def collect_metrics(self) -> dict:
|
|
config = self.get_config()
|
|
limit = config[BATCH_SIZE_KEY]
|
|
enabled = [
|
|
agreement
|
|
for agreement in AGREEMENTS
|
|
if config[setting_key(agreement.kind)]
|
|
]
|
|
stats = [
|
|
{"label": "Dry run", "value": 1 if config[DRY_RUN_KEY] else 0},
|
|
{"label": "Agreements enabled", "value": len(enabled)},
|
|
]
|
|
rows = []
|
|
for agreement in AGREEMENTS:
|
|
is_enabled = config[setting_key(agreement.kind)]
|
|
count = len(pending(agreement, limit)) if is_enabled else 0
|
|
display = f"{count}+" if is_enabled and count == limit else str(count)
|
|
if is_enabled:
|
|
stats.append(
|
|
{"label": f"{agreement.kind} pending", "value": display}
|
|
)
|
|
rows.append(
|
|
[
|
|
agreement.kind,
|
|
CONSENT_KINDS.get(agreement.kind, agreement.kind)[:48],
|
|
1 if is_enabled else 0,
|
|
agreement.version_setting or "",
|
|
display if is_enabled else "",
|
|
self._converged[agreement.kind],
|
|
]
|
|
)
|
|
table = {
|
|
"columns": [
|
|
"Agreement",
|
|
"Policy",
|
|
"Enabled",
|
|
"Version setting",
|
|
"Pending",
|
|
"Converged",
|
|
],
|
|
"rows": rows,
|
|
}
|
|
return {"stats": stats, "table": table}
|