Skip to content

Treetop support - #596

Open
terjekv wants to merge 35 commits into
masterfrom
treetop-support
Open

terjekv wants to merge 35 commits into
masterfrom
treetop-support

Conversation

@terjekv

@terjekv terjekv commented Nov 12, 2025

Copy link
Copy Markdown
Collaborator

Summary

This PR adds bundle-based TreeTop/Cedar authorization to MREG. The MREG_POLICY_MODE environment variable explicitly selects off, observational shadow, or authoritative enforce behavior.

Mode TreeTop behavior Returned decision
off no call legacy MREG permissions
shadow (default) one synchronous batched call for the endpoint stack; compare decisions legacy MREG decision
enforce the same synchronous endpoint decision TreeTop decision; integration errors deny

MREG_POLICY_MODE defaults to shadow. An empty MREG_POLICY_BASE_URL makes shadow behave like off. enforce requires a URL during Django configuration and never falls back to legacy authorization after a TreeTop error.

The integration uses treetop-client 0.0.12+, treetop-rest 0.0.14, and a deterministic unsigned bundle built with treetop-bundle 0.0.5. Bundle loading belongs to treetop-rest; the client needs no bundle-specific changes.

Request-path design

Authorization has to complete before endpoint processing can continue. An async HTTP client would only change how the application waits, and a queue would either allow work to proceed before authorization or still require the request to wait for a queued result. Consequently:

  • both shadow and enforce call TreeTop synchronously;
  • there is no authorization outbox, database migration, dispatcher, background worker, retry queue, or dead-letter state;
  • each protected endpoint constructs its complete permission tree before calling TreeTop.

Endpoint trees contain PolicyLeaf, PolicyAll, and PolicyAny nodes. All leaves are sent in one authorize batch, and MREG composes the ordered results locally. This models old-and-new rename targets, any eligible host IP, ownership constraints, and other dependent checks without serial HTTP calls.

The authorization state is owned by the underlying Django request, not middleware or a global context. An identical repeated stack reuses its cached decision. A second different stack is rejected, increments mreg_policy_stack_conflicts_total, returns legacy behavior in shadow, and fails closed in enforce. Large stacks remain visible through mreg_policy_stack_size; high-count endpoints can later be consolidated into coarser endpoint-level Cedar decisions.

Each application process owns a reusable synchronous client and a thread-safe closed/open/half-open circuit breaker. The client is PID-aware and is closed at process exit.

Authority boundary

MREG continues to own authentication, parsing and serializer validation, object lookup, database transactions, conflicts, and non-authorization business invariants. TreeTop owns authorization for protected endpoints only when mode is enforce. Token creation, health, metrics, schema, and Django admin routes are explicit exemptions.

Every leaf contains:

  • a qualified user principal and the user's current MREG groups;
  • one explicit Cedar action;
  • a registered typed resource with a stable ID;
  • contract-typed attributes required for that decision.

mreg/policy/contracts.py is the dependency-free source of truth for resource types, attributes, CRUD operations, custom actions, and schema generation. Registered adapters resolve model/view data; unknown resource types must be added explicitly. Boolean and IP wire attributes are typed from the contract rather than guessed from their string values.

disable_policy_parity() can suppress narrow shadow-mode comparisons in tests, but cannot bypass authoritative enforcement.

Permission mapping

MREG concept Cedar/TreeTop representation
Authenticated reads and introspection typed read action or an explicit action such as user_info_read
Super/admin/network/group/host-policy roles current principal group membership and explicit management actions
Host and DNS-record CRUD complete old/new hostname and IP target stack
NetGroup access principal group + raw hostname + raw IP/range
Network/community APIs typed CRUD action plus exact network data
Host contacts and BACnet one action against the complete attached-host target
Host groups ownership and mutation relationship attributes
Host-policy host membership exact HostPolicyRole resource plus raw candidate hostname/IP
Zones, labels, networks, policies, and excluded ranges typed actions with the appropriate administrative role

MREG does not calculate TreeTop name labels. For name-based rules it sends hostname and, when applicable, ip. treetop-rest applies the patterns in labels.json to the hostname field and supplies nameLabels to Cedar. DNS wildcard/underscore forbids and NetGroup permits consume those derived labels.

The static Cedar policy also covers restricted-address assignment, ownership constraints, and administrative permits. The committed restricted-address ranges are sanitized examples and must be replaced/reviewed for the deployment before enabling enforce.

Converting database permissions

TreeTop cannot be authoritative while mutable NetGroupRegexPermission rows independently decide the same access. This PR therefore generates the bundle policy from a deterministic, reviewable snapshot of the existing MREG JSON APIs. It does not require mreg-cli, and it deliberately does not add the future one-shot export endpoint yet.

The generator reads and paginates:

  • /api/v1/labels/?ordering=name
  • /api/v1/permissions/netgroupregex/?ordering=range,group
  • /api/v1/hostpolicy/roles/?ordering=name

It authenticates with Authorization: Token, resolves integer label IDs to their names, rejects malformed or cross-origin pagination responses, and writes only the source fields needed for conversion to treetop/fixtures/policy-source.json. The token is read from the environment and never persisted. The checked-in normalized snapshot keeps generation and CI offline and deterministic; a later export endpoint can produce the same snapshot schema without changing the Cedar generator.

Refresh the snapshot and generated files against the MREG instance whose policy is being migrated:

export MREG_API_BASE_URL=https://mreg.example
export MREG_API_TOKEN='replace-with-an-MREG-API-token'
python scripts/generate-treetop-policy.py
unset MREG_API_TOKEN

Use HTTPS outside a trusted local environment. The token needs authenticated read access to all three endpoints. MREG_API_TIMEOUT optionally changes the 20-second per-page timeout.

The converter validates every CIDR and regular expression, removes duplicate permission rows, collapses redundant ranges, and emits stable hashed policy/label IDs. It generates:

  • treetop/fixtures/policy-source.json: normalized source snapshot;
  • treetop/data/labels.json: hostname patterns used by treetop-rest;
  • treetop/data/netgroup.cedar: NetGroup and exact host-policy-role permits;
  • treetop/data/netgroup-conversion-report.json: counts, generated IDs, and unmatched/unused labels.

Database fields map as follows:

Database field Generated bundle representation
group Cedar principal group
range ip.isInRange(...) or exact network comparison
regex deterministic pattern in labels.json
labels conversion-only join key to exact HostPolicyRole names

Legacy permission and role labels are never runtime facts. They are used only while converting the endpoint responses to determine which exact role resources a permission previously covered.

In enforce, the NetGroup permission API remains readable but returns HTTP 409 for POST/PUT/PATCH/DELETE, preventing a database edit from appearing to change authoritative policy. Writes retain legacy behavior in off and shadow, which supports a staged comparison before bundle publication.

The checked-in API snapshot is a sanitized example, not production policy. A deployment must refresh it, run the converter, review the snapshot, report, and Cedar diff, publish the resulting bundle, and complete a shadow observation window before enforcing.

Bundle workflow and local setup

The MREG module loads the hand-written endpoint policy, generated NetGroup policy, generated schema, and one combined labels.json. A global module contains the global superuser policy.

python scripts/generate-treetop-schema.py --check
python scripts/generate-treetop-policy.py --check
TREETOP_BUNDLE_BIN=treetop-bundle scripts/build-treetop-bundle.sh
TREETOP_BUNDLE_BIN=treetop-bundle scripts/check-treetop-bundle.sh

The build script creates and validates the archive in a temporary directory before replacing the committed artifact. CI verifies both generators, validates the unsigned archive, rebuilds it, and compares it byte-for-byte. Signed bundles are deliberately out of scope; local treetop-rest uses TREETOP_BUNDLE_SIGNATURE_POLICY=allow-unsigned.

Start treetop-rest and its bundle file server with:

docker compose -f treetop/docker-compose.yml up -d

Then observe before enforcing:

export MREG_POLICY_MODE=shadow
export MREG_POLICY_BASE_URL=http://localhost:9999
export MREG_POLICY_NAMESPACE=MREG

# after bundle review, parity observation, and rollout-gate approval
export MREG_POLICY_MODE=enforce

Restart MREG processes after changing mode. If MREG runs in a container, the base URL must be reachable from that container rather than referring to its own localhost. Roll back by selecting shadow or off and restarting. There is no TreeTop database migration to apply.

Environment variables

Runtime authorization settings:

Variable Default Purpose
MREG_POLICY_MODE shadow off, observational shadow, or authoritative enforce
MREG_POLICY_PARITY_ENABLED True deprecated compatibility flag used only when mode is unset
MREG_POLICY_BASE_URL empty treetop-rest base URL; required in enforce
MREG_POLICY_NAMESPACE MREG Cedar namespace for principal/action/resource IDs
MREG_POLICY_TIMEOUT_SECONDS 5.0 synchronous authorization timeout
MREG_POLICY_CIRCUIT_FAILURES 5 consecutive failures before the process-local circuit opens
MREG_POLICY_CIRCUIT_RESET_SECONDS 30.0 cooldown before one half-open probe
MREG_POLICY_PARITY_LOG_LEVEL WARNING dedicated parity logger level
MREG_POLICY_PARITY_LOG_DETAILS False include sensitive principal/resource details in parity logs
MREG_POLICY_ROLLOUT_MIN_COMPARISONS 10000 minimum observations required by the rollout gate
MREG_POLICY_ROLLOUT_MAX_MISMATCH_RATE 0.001 maximum accepted mismatch ratio
MREG_POLICY_ROLLOUT_MAX_ERROR_RATE 0.001 maximum accepted integration-error ratio

Bundle-generation settings (used by the script, not Django at runtime):

Variable Default Purpose
MREG_API_BASE_URL unset MREG instance to read policy source data from; omitted means use the checked-in snapshot
MREG_API_TOKEN unset API token required with MREG_API_BASE_URL; never persisted
MREG_API_TIMEOUT 20 per-page MREG API timeout in seconds

Failure handling and observability

  • shadow: record comparison/error telemetry and return the complete legacy endpoint decision.
  • enforce: return the TreeTop decision; on timeout, transport error, invalid response count/ID/index, per-result error, circuit rejection, or stack conflict, log critically and deny.
  • The Grafana dashboard and Prometheus alerts cover mismatch/error rates, mode, latency, stack size, stack conflicts, circuit state, and fail-closed errors.
  • manage.py check_policy_rollout --prometheus-url URL --window 24h checks observation volume and configured mismatch/error thresholds before authority is enabled.

Scope cleanup and supporting changes

  • Removes the custom Gunicorn configuration, Prometheus multiprocess lifecycle, and split runtime/test Docker targets introduced during this PR; the container layout and process lifecycle are restored to master.
  • Retains the supported Python floor of 3.12, dependency refresh (treetop-client>=0.0.12), Python 3.12/3.13/3.14 CI, workflow concurrency, pinned mreg-cli compatibility-test revision, and general readiness/cleanup hardening.
  • Updates the metrics tests without carrying a new pytest dependency.

Validation

  • Full local Python 3.12/Django 5.2 matrix passes: 1,029 tests, 2 expected skips.
  • Ruff and the 97% aggregate coverage gate pass (98% locally).
  • The policy importer itself has 99% statement coverage, including API pagination, authentication, malformed data, transport failures, and stale snapshots.
  • makemigrations --check --dry-run reports no changes.
  • Generated Cedar schema and generated permission data match their sources.
  • treetop-bundle validates the manifests and unsigned archive; rebuilding is byte-for-byte reproducible.

Documentation

  • Concept, mapping, API snapshot workflow, bundle workflow, and endpoint checklist: docs/policies.md
  • Environment and rollout settings: docs/env.md
  • Shadow/enforcement testing and mismatch triage: docs/parity_testing.md
  • Metrics, dashboard, alerts, and rollout gate: docs/metrics.md

@terjekv
terjekv marked this pull request as draft November 12, 2025 10:41
@terjekv terjekv self-assigned this Nov 12, 2025
@coveralls

coveralls commented Nov 12, 2025

Copy link
Copy Markdown
Collaborator

Coverage Status

coverage: 97.558% (-0.1%) from 97.672% — treetop-support into master

terjekv and others added 14 commits February 15, 2026 02:26
* Add Prometheus metrics middleware and expose metrics endpoint

- Implement PrometheusRequestMiddleware to track a selection of HTTP, DB, and LDAP metrics.
- Create MetricsView to serve metrics at /api/meta/metrics.
- Update URL routing to include metrics endpoint.
- Add tests for metrics endpoint and metrics recording.
- Include prometheus-client dependency in project configuration.
…ons.

  - Also adds documentation for the policy framework and setup.
- Introduced new Prometheus metrics for policy decisions, legacy decisions, and parity results.
- Log file management ensures the parity log is truncated only once in the main process.
- Updated tests to validate the new metrics and log file behavior.
- Add detailed docstrings for clarity on function purposes and side effects.
- Deduplicate policy-engine authorize + metrics/error handling
@terjekv
terjekv marked this pull request as ready for review February 16, 2026 06:39
@terjekv
terjekv requested a review from pederhan February 16, 2026 06:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants