Dynamic groups, activity intelligence, API keys, Swagger, maintenance #11

Merged
gsadmin merged 34 commits from development into main 2026-09-02 23:03:12 +00:00
Owner

Merges the development line into main to cut a release. Includes: dynamic-group reconciliation + rule editor, activity/history, connection field fixes, attribute/value pickers, built-in schedules, trusted-proxy defaults, DB maintenance/retention, config import robustness, OpenAPI/Swagger + PowerShell example, and working API keys with read/read-write scopes.

Merges the development line into main to cut a release. Includes: dynamic-group reconciliation + rule editor, activity/history, connection field fixes, attribute/value pickers, built-in schedules, trusted-proxy defaults, DB maintenance/retention, config import robustness, OpenAPI/Swagger + PowerShell example, and working API keys with read/read-write scopes.
gsadmin added 34 commits 2026-09-02 23:02:57 +00:00
Add Installation (MSI/binary/Docker), Running & Service Management (the CLI
commands incl. idempotent initialize/remove), and CI/CD (two-job release +
required REGISTRY_PASSWORD secret) sections; note the pure-Go build. Add
dashboard/rules/credentials/login/schedules screenshots and a Screenshots
section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change the default HTTP port from 8080 to 18090 and propagate it through the
Dockerfile (EXPOSE + healthcheck), docker-compose, and the README. Host default
stays 0.0.0.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add SettingsService.ResolveString/Bool/Int resolving runtime config as
app_settings (UI) > environment variable > default. Env seeds bootstrap; any
value set in the UI persists to app_settings and wins. Foundation for OIDC and
TLS UI configuration. Unit-tested for the precedence order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add internal/pki: generate a Root CA -> Intermediate CA -> server leaf, re-issue
leaves under a persisted CA for renewal, and export the chain as PEM
(cert/key/fullchain), DER (.cer), and PKCS#12 (.pfx). Unit-tested: the chain
verifies, the PFX decodes with the CA chain, and a reloaded CA re-issues a
valid leaf.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add internal/tlsmgr: on startup, load-or-generate the CA, issue/reuse the leaf,
export the chain under <data>/tls, and hot-swap the cert on renewal via
GetCertificate. The server listens HTTPS when a manager is present. TLS is on by
default for the standalone binary/service (tls.enabled setting or
ORCHESTRAD_TLS_ENABLED, UI wins over env); the container image defaults it off so
it runs plain HTTP behind a TLS-terminating proxy. Startup logs a clickable URL.
Verified end to end: HTTPS serves, plain HTTP rejected, disable serves HTTP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add runtime TLS modes resolved via settings (UI wins over env): auto
(self-managed), provided (bring-your-own upload), and windows-store (Windows
cert store, enumeration/serving scaffolded). New /api/v1/tls endpoints: status,
mode, certificate upload, and windows-store list; changes reload the live
manager without a restart, falling back to the self-managed cert if a configured
source is unavailable. Auto SANs now cover the hostname (CN), localhost, every
non-loopback IP, and host.<suffix> + the suffix for each detected DNS suffix
(Windows registry / resolv.conf). Bring-your-own is validated and unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Connection-specific DNS suffixes are set per adapter (and via DHCP) without a
domain join, so read Domain/DhcpDomain from each Tcpip Interfaces subkey in
addition to the global Domain/NV Domain/SearchList. Verified on a workgroup host:
the cert now covers host.<adapter-suffix> and the suffix for every adapter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement ListWindowsCerts (LocalMachine + CurrentUser My stores: thumbprint,
subject, issuer, validity, SANs, has-private-key) and windows-store serving:
find the cert by thumbprint, acquire its CNG key, and serve TLS via a
crypto.Signer that signs through NCryptSignHash (RSA PKCS#1 v1.5 and PSS) so the
private key is never exported. Legacy CSP / non-RSA keys report a clear error.
Verified end to end: a CNG cert in LocalMachine\My serves a completed HTTPS
handshake (openssl confirms the served subject).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On Windows, resolve the listen address/port with precedence env > registry
(HKLM\Software\Grace Solutions\OrchestrAD ListenAddress/ListenPort, written by
the MSI wizard) > default. No-op off Windows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a WiX UI wizard (WixToolset.UI.wixext): Welcome -> License (GPLv3) ->
Install directory -> Network (listen address + port) -> Ready -> Finish. The
install dir, listen address, and port are recorded in the registry and the
service reads them; the finish page shows https://localhost:<port>/ with the
default admin/admin credentials and an optional 'open in browser' box. Schedule
InitializeService after WriteRegistryValues so the first start binds the chosen
port. CI msi job adds the UI extension. Verified: install to a custom dir + port
binds that port (health OK), registry recorded, upgrade + uninstall clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add backend OIDC: /api/v1/auth/oidc/status|login|callback (authorization code +
PKCE, state+nonce cookies, ID-token verification via go-oidc) and admin
/auth/oidc/config (GET/PUT, validates the issuer via discovery on enable).
Config resolves app_settings (UI) > env > default, so SSO can be configured from
the UI while env still seeds it. LoginOIDC provisions/links federated users by
the stable (issuer, subject) pair, refusing to reuse a username held by a
different account. Verified: discovery + real authorize redirect + PKCE cookies;
config API with DB-over-env precedence; provisioning unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add build-tagged (adtest) + env-gated integration tests that run the rule engine
against a live Active Directory over plain LDAP: create a unique test OU with
users/groups, execute a rule, and assert the AD outcome, then tree-delete the OU
on cleanup. Covers add-to-group-by-condition (matching users added, others not),
create-missing-group, and move-to-OU. Excluded from normal builds and CI; run
with -tags adtest and ORCHESTRAD_AD_TEST_* env. Verified green against the test
domain; no objects left behind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ldap.CanonicalName: prefer the directory's canonicalName attribute (now
requested in searches) and otherwise build domain.com/OU/OU/CN from the DN.
Populate MatchedObject.CanonicalName and return it in the rule preview API so
operators see canonical names instead of raw DNs. Unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add CanonicalToOUDN/NormalizeOUTarget so an OU target can be given as a canonical
path (domain.com/OU/OU) or a DN, and Client.EnsureOUPath which idempotently
creates every OU down the path (parents before children). MoveToOu and group
creation now normalize the target and ensure the full OU path instead of only
the leaf. Unit-tested (conversion, escaped split) and verified against the test
AD: a canonical nested target creates each OU and moves the object; re-running is
a no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Default the theme to dark (toggle still available). Rebrand the logo to the
OrchestrAD icon + wordmark. Redesign the login into a calm, centered card on a
soft themed backdrop (removed the 3D character illustration and decorative
circles). Add a 'Sign in with SSO' button shown when OIDC is enabled, and adopt
the OIDC callback token from the URL fragment (hydrating the user via /auth/me).
Replace the hardcoded 'Mike/Admin' sidebar card with the real signed-in user and
a working sign-out; de-jar the search label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Security admin page wiring the OIDC and TLS backends: an SSO/OIDC form
(issuer, client id/secret, redirect, scopes, claim mappings, default role) and a
TLS card showing the live certificate and a source picker - self-managed,
bring-your-own (PEM upload), or a Windows-store certificate (enumerate + select).
Add OidcApi/TlsApi clients + types, a Security nav entry, and a Canonical Name
column to the rule preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The modernc.org/sqlite driver returns TEXT timestamp columns as strings
rather than time.Time (unlike the CGO mattn driver), so scanning directly
into *time.Time failed with "unsupported Scan" and surfaced as 500 errors
on the credentials and connections list endpoints.

Scan created/updated/started timestamps into strings and convert with the
existing parseTimeOrZero/parseNullTime helpers across credentials,
connections, schedules, rules, and rule_runs repositories. Add a
regression test that round-trips a credential and connection through a
migrated DB and asserts the timestamps come back parsed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the flag/language dropdown from both the vertical and horizontal
headers and delete the now-unused Language component. OrchestrAD ships a
single language, so the selector was dead UI chrome.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn the rule_run_actions ledger into operator intelligence. A new
ActivityService rolls the ledger up by time window (24h / 7d / all-time)
into syncs, operations and removals with success/failure counts and the
number of distinct objects affected, plus all-time totals per action type
and a most-active-rules ranking. A companion filtered, paginated feed
answers the operational questions directly: what action ran, what
happened, to which object, triggered by whom, and when.

Backend:
- ActivityService.Summary() and ListActions(filter) over the joined
  rule_run_actions / rule_runs / rules tables, categorising each action
  type as Sync (AddToGroup/AddGroupToGroup), Removal
  (RemoveFromGroupIfNoLongerMatched) or Operation (everything else).
- GET /api/v1/activity/summary and GET /api/v1/activity (category,
  actionType, status, ruleId, search, pagination).
- Table-driven test seeding a run with mixed action types/statuses and
  asserting window roll-ups, distinct-object counts, top rules, and the
  category/status feed filters.

Frontend:
- New Activity page: window summary cards, by-action-type and
  most-active-rules tables, and a drill-in feed with category/result/
  object filters and a detail dialog that pretty-prints the action's
  details JSON and error.
- Wired into the vertical sidebar and horizontal navbar under Automation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ad_connections List query selected only a subset of columns, so
use_start_tls, allow_invalid_certs, timeout_seconds, paging_enabled and
page_size came back as zero-values in the list response. Because the edit
form pre-populates directly from the list row, opening and saving a
connection silently overwrote those stored values with their zero values
— e.g. "Allow invalid certs" would never stick, and timeout/page size
reset to 0.

Select the full column set in List (matching GetByID) and scan the
previously dropped fields. Extend the repository round-trip test to set
these fields on create and assert List surfaces them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns rules into Adaxes/Active-Roles style dynamic groups. The core new
capability is a set-level SyncGroupMembership action that reconciles a
target group's membership against the matched object set in one pass
instead of the old add-only, per-object behaviour.

Engine / reconciliation:
- New ActionSyncGroupMembership runs once per target group after the
  match: resolve (and optionally create) the group, read its current
  members, diff against the matched set, and apply the adds/removes.
- Three per-rule sync modes (types.SyncMode): FullSync (membership ==
  matched set; removes stale members incl. manual adds), ManagedAdd (adds
  matches, removes only members this rule added), AddOnly (never removes).
- Managed ownership tracked in a new managed_group_members table
  (migration 005) + repository, wired into the runner's engine so
  ManagedAdd removes only what it added.
- Adds/removes are recorded as AddToGroup / RemoveFromGroupIfNoLongerMatched
  run-actions so the activity feed categorises them as syncs/removals.
- Preview now computes an accurate, non-mutating diff for sync actions
  (+add / -remove / already-in-sync counts and per-member entries).
- memberOf and memberOf-recursive (LDAP_MATCHING_RULE_IN_CHAIN) operators;
  Regex no longer silently degrades to equals.
- Canonical group targets: CanonicalToLeafDN / NormalizeGroupTarget so a
  target group can be given as domain.com/OU/Group as well as a DN.

Editor-facing APIs (backend-first; UI comes next):
- Rule create/update now accept conditionGroups + actions and persist them
  via RuleRepository.ReplaceLogic (soft-delete + insert, preserving the
  rule_run_actions FK). Omitting them leaves existing logic untouched.
- POST /api/v1/rules/preview evaluates an unsaved draft (live match panel).
- GET /api/v1/rules/metadata serves the operator vocabulary (object types,
  operators, action types, sync modes, common attributes) so UI dropdowns
  stay in lock-step with the backend.
- GET /api/v1/ad-connections/{id}/directory searches groups/OUs for the
  target pickers.

Tests: reconciliation across all three modes + create-if-missing and the
missing-group error path (fake directory client); canonical leaf-DN
conversion; ReplaceLogic round-trip. Full suite green.

Note: RuleRepository.GetByID nests a query (getConditionGroups holds a
cursor while calling getConditions); safe under the production pool (25)
but a follow-up should flatten it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getConditionGroups held an open rows cursor while calling getConditions
per group, so each GetByID needed two pooled connections at once and could
deadlock under a small/exhausted pool. Read all groups first, close the
cursor, then load every group's conditions in a single bulk IN-query keyed
by condition_group_id. The repository round-trip test now runs against a
single-connection pool, which guards the no-nested-cursor invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the old read-only rule form (which could only edit scalar metadata
and showed "author via config import" for conditions/actions) with a full
tabbed editor that authors the whole rule:

- Scope: connection, object type, and a searchable base-OU picker.
- Filter: a condition-group builder with attribute autocomplete (from the
  backend metadata), operator dropdowns, per-condition/-group NOT, AND/OR
  join within and across groups, and a raw-LDAP escape hatch. memberOf
  operators render a group picker for the value.
- Target & action: choose the action (Sync membership to group / Move to OU
  / Ensure group exists / Add to group), pick or paste the target group/OU,
  set the sync mode (Full / Managed / Add-only) and create-if-missing;
  multiple actions supported.
- Schedule: run manually, on an existing schedule, or create an interval or
  cron schedule inline.
- Preview: live dry-run against the directory (POST /rules/preview) showing
  match count, generated filter, planned +add/-remove, and sample matches.

New DirectoryPicker (search-or-type Autocomplete backed by the connection
directory endpoint) is reused for base OU, target group, target OU, and
memberOf values. API layer gains RuleMetadata/DirectoryObject/RuleInput
types and RulesApi.metadata/previewSpec + ConnectionsApi.directory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add two directory introspection endpoints the rule filter builder uses:

- GET /ad-connections/{id}/attributes?objectType=&q= returns the schema
  attributes that APPLY to the given object type (User/Computer/Group). The
  applicable set is derived by walking the classSchema hierarchy from the
  object's class up through subClassOf to top, plus auxiliary classes,
  unioning each class's may/must-contain attributes; results are filtered by
  substring and returned with adminDescription. The per-(connection,object
  type) set is cached for 10 minutes so the walk is not repeated per
  keystroke. (Computer inherits user attributes, since AD's computer class
  subclasses user — reflected correctly.)
- GET /ad-connections/{id}/attribute-values?attribute=&objectType=&q= samples
  objects and returns the distinct values present for one attribute, so the
  value field can suggest real directory values.

Backed by a new bounded ldap Client.SearchWithLimit that tolerates the
server's size-limit response. Attribute names are validated against the LDAP
descriptor charset before use in a filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The filter builder's attribute field now searches the selected connection's
full directory schema (scoped to the rule's object type) instead of a fixed
curated list, and the value field suggests the distinct values actually
present in the directory for the chosen attribute — both search-as-you-type
with the curated common attributes still shown first.

New AttributePicker and ValuePicker components (async MUI autocompletes over
ConnectionsApi.attributes / attributeValues), wired into each condition row;
memberOf still uses the group picker and raw-LDAP keeps a plain field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Proxy / base-URL:
- ORCHESTRAD_TRUSTED_PROXIES now defaults to "local", trusting reverse
  proxies in loopback + RFC1918 + link-local/ULA ranges out of the box, so
  X-Forwarded-* (client IP, scheme, host) is honored behind an edge proxy
  without extra config. New keywords: local/private, all/any, none.
- OIDC redirect URI derivation now uses the trust-gated request base URL
  instead of reading X-Forwarded-Proto directly, and audit client IP now
  trusts the middleware-rewritten RemoteAddr rather than the raw (spoofable)
  X-Forwarded-For header. Both honor forwarded values only from trusted
  peers.

Schedules:
- Seed eight built-in schedules on startup (every 5/15/30 min, hourly,
  every 6/12h, daily, weekly), idempotent by name, so operators have
  ready-made cadences in the Schedules page and the rule editor's schedule
  dropdown without hand-building one.

Test covers the trusted-proxy keyword expansion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Schedule column to the rules table that resolves each rule's
scheduleId to the schedule name and its next run time (or "Manual"),
pairing with the newly seeded built-in schedules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The runner updated the rule_runs history but never wrote the rules table's
last_run_utc / last_run_result columns, so the rules list showed blank Last
Run / Last Result. Add RuleRepository.UpdateLastRun and call it from the
runner's finalize and fail paths with the run's completion time and status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a History icon to each rule that opens a dialog listing the actual
objects this rule acted on — added, removed, moved — scoped to the rule via
the activity feed's ruleId filter, with category/result filters, paging, and
the per-action detail drill-in. Colour a "Completed" last-run result as
success (not amber) now that the rules list surfaces it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Database maintenance:
- New MaintenanceService prunes rule_runs (+ their action detail) and
  audit_events older than their retention windows, then VACUUMs to reclaim
  space. Runs once at startup and then on an interval, bound to the run
  context. Configurable via ORCHESTRAD_RUN_RETENTION_DAYS (90),
  ORCHESTRAD_AUDIT_RETENTION_DAYS (180),
  ORCHESTRAD_MAINTENANCE_INTERVAL_HOURS (24), ORCHESTRAD_MAINTENANCE_VACUUM.
  This stops the database growing forever. (Log rotation already existed via
  lumberjack: ORCHESTRAD_LOG_MAX_SIZE_MB/_MAX_BACKUPS/_MAX_AGE_DAYS.)

Config import:
- Import now accepts either the wrapped {payload,dryRun} shape or a bare
  exported config object, so a file downloaded from Export re-imports
  directly (dryRun via ?dryRun=true) — useful for automation.

Test covers retention pruning with FK-cascaded action rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Count tiles link to their resource pages; the run-stat cards open Activity,
Recent Rule Runs opens Rule Runs, and Connection Health opens Connections.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GET /api/docs serves a Swagger UI and GET /api/openapi.json serves an
  OpenAPI 3 spec built by walking the live chi router, so documented paths
  always match what the build serves. A small registry adds rich detail
  (request bodies, params, schemas) for the automation-critical operations
  (auth login, rule create/update/preview, connection introspection);
  RuleInput and friends are defined as reusable component schemas.
- Add docs/examples/Create-OrchestrADRule.ps1: a no-alias PowerShell sample
  that builds headers/body as typed dictionaries, serializes with
  ConvertTo-Json, logs in, creates a SyncGroupMembership rule, and runs it.
- README: new "API & Automation" section (Swagger + PowerShell), and a
  "Logging & Retention" section documenting log rotation and the new
  database maintenance/retention knobs.

Tests cover spec generation from a router and registry well-formedness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
API keys were never validated — the middleware only checked session tokens,
so an X-API-Key request always 401'd. Add auth.Service.ValidateAPIKey (looks
up the key hash, enforces enabled/revoked/expiry, loads the user + roles,
stamps last_used_utc) and route X-API-Key / bearer auth through it.

Add per-key scopes (migration 006): "read" (GET/HEAD only) or "readwrite"
(full access, default). The middleware rejects mutating requests from a
read-scoped key with 403. Create accepts a scope; list and create responses
include it; the UI create dialog has a scope selector and the list shows a
scope chip. Disable/re-enable and revoke (permanent) were already correct.

Verified live: RW key GET/POST ok; read key GET ok, POST 403; disable→401,
re-enable→200; revoke→401 and re-enable blocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lower ORCHESTRAD_LOG_MAX_SIZE_MB default from 100 to 5 so log files roll
sooner by default; README updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gsadmin merged commit 2f408e3c6e into main 2026-09-02 23:03:12 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gsadmin/OrchestrAD#11