Files
harmony/nats/callout/src/lib.rs
Jean-Gabriel Gill-Couture 6c45fb22ba feat(nats-callout): production callout + harmony module + e2e demo
harmony-nats-callout becomes a deployable service, not just a library:
- New [[bin]] target with env+secret-file driven config and
  SIGINT/SIGTERM-aware shutdown.
- Dockerfile (single-stage archlinux:base, non-root, matches
  harmony-fleet-operator convention).
- Refactored handler into a pure `decide()` function so the entire
  authorization decision tree is unit-testable without async-nats.
- New `roles` module with role resolution + a `validate_device_id`
  security gate that rejects NATS subject metacharacters in device_id
  (.>* whitespace) — closes a real escalation path through the
  `{device_id}` placeholder in the per-device permissions block.
- Configurable role claim path + admin/device role names; admin wins
  when both are present (privilege-escalation invariant).

57 unit tests cover every reachable branch of the security decision
tree; 4 e2e tests in nats/integration-test-callout exercise real NATS
in podman with: device pubsub on own subjects, cross-device subject
isolation, admin-can-read-anything, and JWT-without-role rejection.

harmony/src/modules/nats_auth_callout/:
- New `NatsAuthCalloutScore` deploys the callout as a K8s Deployment +
  Secret. fsGroup + 0o440 secret mode so the non-root container can
  read its mounted seed/password without leaving them in env vars.
- `render_auth_callout_block` helper produces the YAML for NATS Helm
  `config.merge.authorization.auth_callout` so both halves stay in
  sync.

examples/fleet_auth_callout/:
- `bring_up_stack()` orchestrates k3d -> Zitadel + Postgres ->
  CoreDNS rewrite -> project + roles + machine users with JWT keys
  -> NATS Helm with auth_callout block -> callout image build +
  sideload -> NatsAuthCalloutScore deploy. Idempotent across re-runs
  (issuer NKey persisted in a K8s secret so user JWTs survive
  restarts).
- `mint_access_token()` RFC 7523 JWT-bearer client. Uses Host header
  with port so Zitadel emits a matching issuer.
- main.rs prints URLs/creds/keyIds and waits for Ctrl-C.
- Three #[tokio::test] functions sharing one cluster via OnceCell:
  admin_can_read_any_device_subject, device_can_only_access_own_subjects,
  unknown_role_is_rejected. All green on real k3d.
2026-05-03 15:01:44 -04:00

18 lines
563 B
Rust

pub mod config;
pub mod handler;
pub mod permissions;
pub mod roles;
pub mod service;
pub mod zitadel;
pub use config::{
AuthCalloutConfig, AuthCalloutConfigBuilder, DEFAULT_ADMIN_ROLE, DEFAULT_DEVICE_ROLE,
DEFAULT_ROLES_CLAIM,
};
pub use permissions::{
InterpolatedPermissions, PermissionSubjects, PermissionsConfig, interpolate_permissions,
};
pub use roles::{DeviceIdError, ResolvedRole, resolve as resolve_role, validate_device_id};
pub use service::AuthCalloutService;
pub use zitadel::{ZitadelClaims, ZitadelValidationError, ZitadelValidator};