# OrchestrAD **OrchestrAD** is a modern, rule-based automation platform for Active Directory. It enables dynamic group membership, object lifecycle orchestration, and policy-driven directory operations through a clean UI, REST API, and powerful scheduling engine. --- ## ๐Ÿš€ Overview OrchestrAD replaces static scripts and JSON-based tooling with a centralized, database-backed system for managing directory automation. It allows administrators to define **rules** that evaluate Users, Computers, and Groups, then automatically perform actions such as: * Adding/removing group memberships * Moving objects to Organizational Units (OUs) * Creating groups dynamically * Nesting groups * Enforcing directory structure and policy consistency All configuration is managed through the UI or API โ€” **no manual file editing required**. --- ## ๐Ÿ“ธ Screenshots | Dashboard | Rules | | --- | --- | | [![Dashboard](docs/screenshots/dashboard.png)](docs/screenshots/dashboard.png) | [![Rules](docs/screenshots/rules.png)](docs/screenshots/rules.png) | | Credentials | Sign in | | --- | --- | | [![Credentials](docs/screenshots/credentials.png)](docs/screenshots/credentials.png) | [![Sign in](docs/screenshots/login.png)](docs/screenshots/login.png) | The entire UI is served by the single backend binary (embedded Next.js static export) โ€” no separate web server to run. --- ## โœจ Key Features ### ๐Ÿ” Rule-Based Automation * Visual rule builder with condition groups and logical operators * Supports multiple object types (Users, Computers, Groups) * Rich condition support (equals, regex, LDAP, comparisons, etc.) * Multiple actions per rule with ordered execution ### ๐Ÿง  Dynamic Directory Orchestration * Add/remove group membership with diff-based execution * Move objects to OUs with dynamic path generation * Auto-create groups and OUs when missing * Support for nested group relationships ### โฑ๏ธ Scheduling Engine * Easy schedules (e.g., every 5 minutes) * Advanced 6-field cron expressions * Per-rule execution control and concurrency handling ### ๐Ÿ” Secure Authentication & Access * Local authentication (Argon2-secured) * OIDC support (via NextAuth/Auth.js) * API key system (separate from user auth) * Role-based access control (RBAC) ### ๐Ÿ”‘ Credential Management * Reusable, encrypted credential objects * Secure storage using AEAD encryption * Credential testing and validation ### ๐Ÿ“Š Observability & Auditing * Centralized logging with rotation and retention * Human-readable logs with structured context * Full audit trail for all actions and changes * Rule execution history and summaries ### ๐Ÿ’พ Backup & Restore * Automatic database backups with retention * Manual backup and restore support * Safe restore with validation and rollback protection ### ๐Ÿ“ฆ Cross-Platform Runtime * Single Go binary * Runs as: * foreground process * system service * Docker container * Supports Windows, macOS, and Linux (amd64 + arm64) --- ## ๐Ÿงฑ Architecture * **Backend:** Go (Chi, sqlc, SQLite, robfig/cron) * **Frontend:** Next.js + Material UI + Tailwind (Spike Template) * **Database:** SQLite (WAL mode, migrations enabled) * **Auth:** NextAuth/Auth.js (UI sessions) + backend RBAC/API keys * **Directory Integration:** LDAP/LDAPS via go-ldap --- ## ๐Ÿ” Authentication Model OrchestrAD separates authentication concerns: * **Browser Sessions** * Managed via NextAuth/Auth.js * Supports OIDC and local login * **API Access** * Managed via API keys * Keys can expire or persist indefinitely * Shown only once at creation * **Backend Authorization** * All security decisions enforced server-side * RBAC controls access to resources and actions --- ## ๐Ÿ”Œ API & Automation Everything the UI does is available over the REST API, so rules can be created and run programmatically. * **Interactive docs (Swagger UI):** `https://:18090/api/docs` * **OpenAPI 3 spec:** `https://:18090/api/openapi.json` * **Compact route list:** `https://:18090/api/routes` The spec is **generated from the live router**, so it always reflects the endpoints the running build actually serves. Authenticate at `POST /api/v1/auth/login`, then send the returned token as `Authorization: Bearer `. ### Access to the docs The documentation endpoints are **not public** โ€” they require the same authentication as the rest of the API: * **From the dashboard:** sign in, then use the `` icon in the header or **Administration โ†’ API Docs** in the menu. Login sets a session cookie scoped to `/api/docs`, so the Swagger UI opens straight away in a new tab. Opening `/api/docs` while signed out redirects to the login page and returns you there afterwards. * **From a client:** send `Authorization: Bearer ` or `X-API-Key: ` to `/api/openapi.json` or `/api/routes`. The docs cookie is `HttpOnly` and path-scoped to `/api/docs`, so it is never sent to `/api/v1/*` and cannot be used to make API calls. Swagger's *Try it out* still needs a token or API key. ### Discovering routes Both `/api/routes` and `/api/openapi.json` accept the same filters, so a client can ask for just the part of the API it cares about: | Query | Meaning | | --- | --- | | `?method=post` | only POST operations | | `?method=get,post` | GET **or** POST | | `?path=rules` | paths containing `rules` | | `?method=post&path=connections` | POST operations on connection routes | `/api/routes` returns a flat list โ€” method, path, summary, tag, whether the route is public, and `allowed`, which is `false` when a **read-scoped** API key cannot invoke it: ```powershell $headers = New-Object 'System.Collections.Generic.Dictionary[String,String]' $headers.Add('X-API-Key', $apiKey) $routes = Invoke-RestMethod -Method 'Get' ` -Uri 'https://localhost:18090/api/routes?method=post&path=rules' ` -Headers $headers -SkipCertificateCheck $routes.data.routes | Format-Table -Property 'method', 'path', 'allowed', 'summary' ``` The same filters work on the Swagger UI itself (`/api/docs?path=rules`), which loads a spec narrowed to those operations. ### Create a rule from PowerShell A ready-to-run example lives at [`docs/examples/Create-OrchestrADRule.ps1`](docs/examples/Create-OrchestrADRule.ps1). It builds the headers and body as strongly-typed dictionaries, serializes the body with `ConvertTo-Json`, and uses full cmdlet names (no aliases): ```powershell # Authenticate $loginBody = New-Object 'System.Collections.Generic.Dictionary[String,Object]' $loginBody.Add('username', 'admin') $loginBody.Add('password', 'admin') $login = Invoke-RestMethod -Method 'Post' -Uri 'https://localhost:18090/api/v1/auth/login' ` -ContentType 'application/json' -Body ($loginBody | ConvertTo-Json) -SkipCertificateCheck $headers = New-Object 'System.Collections.Generic.Dictionary[String,String]' $headers.Add('Authorization', "Bearer $($login.data.token)") # Build the rule: users where department = Sales -> synced into a target group $condition = New-Object 'System.Collections.Generic.Dictionary[String,Object]' $condition.Add('attributeName', 'department'); $condition.Add('operator', 'Equals'); $condition.Add('comparisonValue', 'Sales') $conditions = New-Object 'System.Collections.Generic.List[Object]'; $conditions.Add($condition) $group = New-Object 'System.Collections.Generic.Dictionary[String,Object]' $group.Add('joinOperator', 'AND'); $group.Add('conditions', $conditions) $conditionGroups = New-Object 'System.Collections.Generic.List[Object]'; $conditionGroups.Add($group) $actionConfig = New-Object 'System.Collections.Generic.Dictionary[String,Object]' $actionConfig.Add('targetGroupDn', 'CN=Sales,OU=Groups,DC=corp,DC=com') $actionConfig.Add('syncMode', 'FullSync'); $actionConfig.Add('createIfMissing', $true) $action = New-Object 'System.Collections.Generic.Dictionary[String,Object]' $action.Add('actionType', 'SyncGroupMembership'); $action.Add('configurationJson', ($actionConfig | ConvertTo-Json -Compress)) $actions = New-Object 'System.Collections.Generic.List[Object]'; $actions.Add($action) $ruleBody = New-Object 'System.Collections.Generic.Dictionary[String,Object]' $ruleBody.Add('name', 'Sales Team'); $ruleBody.Add('adConnectionId', '') $ruleBody.Add('objectType', 'User'); $ruleBody.Add('executionMode', 'Apply'); $ruleBody.Add('groupJoinOperator', 'AND') $ruleBody.Add('conditionGroups', $conditionGroups); $ruleBody.Add('actions', $actions) Invoke-RestMethod -Method 'Post' -Uri 'https://localhost:18090/api/v1/rules' ` -Headers $headers -ContentType 'application/json' -Body ($ruleBody | ConvertTo-Json -Depth 10) -SkipCertificateCheck ``` Tip: `GET /api/v1/rules/metadata` returns the valid object types, operators, action types, and sync modes for building rule bodies. --- ## ๐Ÿงพ Logging & Retention Standard log format: ``` [TimestampUTC] - [Component] - [Level] - Message ``` **Log rotation** is automatic (max size, backups, and age; compressed): | Setting | Env var | Default | | --- | --- | --- | | Max file size (MB) | `ORCHESTRAD_LOG_MAX_SIZE_MB` | `5` | | Rotated copies kept | `ORCHESTRAD_LOG_MAX_BACKUPS` | `3` | | Max age (days) | `ORCHESTRAD_LOG_MAX_AGE_DAYS` | `30` | **Database maintenance** keeps history from growing forever โ€” old rule runs and audit events are pruned and the database is compacted (`VACUUM`) on a schedule: | Setting | Env var | Default | | --- | --- | --- | | Rule-run history retained (days) | `ORCHESTRAD_RUN_RETENTION_DAYS` | `90` | | Audit history retained (days) | `ORCHESTRAD_AUDIT_RETENTION_DAYS` | `180` | | Maintenance interval (hours) | `ORCHESTRAD_MAINTENANCE_INTERVAL_HOURS` | `24` | | Run VACUUM | `ORCHESTRAD_MAINTENANCE_VACUUM` | `true` | Error logs: ``` [TimestampUTC] - [Component] - [Level] - [File:Line:Column] - Message ``` Logs are designed to be: * Clear and human-readable * Operationally useful * Free of unnecessary noise --- ## โš™๏ธ Configuration Configuration is managed via: * Environment variables * Secure secrets * Database-backed runtime configuration Supports: * Export and import of configuration (JSON) * Schema versioning * Credential portability with key validation --- ## ๐Ÿ“ฅ Installation ### Windows (MSI) Download `OrchestrAD--x64.msi` from the [latest release](https://prod.git.gracesolution.info/gsadmin/OrchestrAD/releases) and run it. The installer: * Installs to `C:\Program Files\OrchestrAD` * Registers and starts the **OrchestrAD** Windows service (auto-start) * Records the install directory and version under `HKLM\Software\Grace Solutions\OrchestrAD` * On upgrade, updates only the binaries โ€” the database (`โ€ฆ\OrchestrAD\data`) is preserved * On uninstall, stops and removes the service and deletes the program files (the data directory is left in place) ### Standalone binary (Windows / macOS / Linux) Download the archive for your platform from the release (`โ€ฆ--.tar.gz` / `.zip`), extract the single `orchestrad` binary, and run it. See **Running & Service Management** below. ### Docker ```bash docker pull prod.git.gracesolution.info/gsadmin/orchestrad:latest docker run -d --name orchestrad -p 18090:18090 \ -e ORCHESTRAD_SECRET_KEY=<32+ byte secret> \ -v orchestrad-data:/data \ prod.git.gracesolution.info/gsadmin/orchestrad:latest ``` Or use the bundled [`docker-compose.yml`](docker-compose.yml). Images are tagged `latest` and the unified commit date `yyyy.MM.dd.HHmm`. --- ## โ–ถ๏ธ Running & Service Management The single binary is both the app and its own service manager (Windows service, systemd/upstart/sysv, or launchd). ``` orchestrad run Run in the foreground (also the service entry point; used by Docker) initialize Install and start the service (idempotent) remove Stop and remove the service (idempotent) install Alias for initialize uninstall Alias for remove start Start the installed service stop Stop the installed service init Initialize the database and apply migrations migrate Apply database migrations backup Create a manual backup restore --file Restore the database from a backup doctor Validate configuration and system health version Show version information ``` `initialize` and `remove` are idempotent, so they are safe to re-run (and are what the MSI uses under the hood). On first start the default admin account is `admin` / `admin`, and a password change is forced at first login. Key environment variables (see the CLI help for the full list): | Variable | Default | Purpose | | --- | --- | --- | | `ORCHESTRAD_DATA_PATH` | `./data` (next to the binary for the service) | Database, logs, backups | | `ORCHESTRAD_SECRET_KEY` | insecure dev key | AEAD key for credential encryption (**set in production**) | | `ORCHESTRAD_SECRET_KEY_FILE` | โ€” | Read the secret key from a file instead (for Docker/K8s secrets) | | `ORCHESTRAD_HOST` / `ORCHESTRAD_PORT` | `0.0.0.0` / `18090` | HTTP bind address | | `ORCHESTRAD_LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` | > โš ๏ธ **The secret key must stay the same for the life of the installation.** Stored credential passwords are encrypted with it, so a changed or lost key leaves them intact but unreadable, and every directory bind fails. Back it up somewhere durable. > > On startup OrchestrAD verifies that the stored credentials decrypt with the current key and logs a clear error naming the affected credentials if they do not; `orchestrad doctor` runs the same check on demand. If the original key is genuinely gone, re-enter the affected passwords to re-encrypt them under the new key. --- ## ๐Ÿ“ฆ Build & Versioning * Version format: `yyyy.MM.dd.HHmm` (derived from the HEAD commit date in CI) * **Pure Go** โ€” SQLite via `modernc.org/sqlite`, so `CGO_ENABLED=0` and every target cross-compiles from one machine with no C toolchain * Multi-platform: Windows, macOS, Linux (amd64 + arm64) * Windows binaries embed the application icon and version metadata from `/resources/icons` (via `goversioninfo`) Local build (Windows host, builds the frontend and stages it into the binary): ```powershell ./scripts/build.ps1 -All # or -Windows / -MacOS / -Linux ``` Output lands under `/binaries///`. --- ## ๐Ÿณ Docker OrchestrAD ships as a single small (~60 MB) image built by a multi-stage `Dockerfile` that compiles the Next.js UI, embeds it, and builds the static Go binary: * Runs in foreground mode by default; graceful shutdown on `SIGTERM` * `/data` holds the database, logs, and backups (bind-mount or named volume) * `HEALTHCHECK` probes `/health` (answers both GET and HEAD) --- ## ๐Ÿš€ CI/CD A single Gitea Actions workflow ([`.gitea/workflows/release.yml`](.gitea/workflows/release.yml)) runs **only on merge to `main`** (docs-only merges are skipped โ€” no per-commit or per-PR CI): 1. **`release`** job (`ubuntu-host`): runs the Go test suite as a gate, builds the frontend, cross-compiles all six binaries, builds and pushes the container image (`latest` + version), and creates the Gitea release with the binaries + checksums attached. 2. **`msi`** job (`windows-host`): wraps the Windows binary into the MSI with WiX and attaches it to the release. ### Required repository secret | Secret | Purpose | | --- | --- | | `REGISTRY_PASSWORD` | A Gitea personal access token with `write:package` scope, used to push images to the container registry (with `REGISTRY_HOST` + `REGISTRY_USERNAME`). | `REGISTRY_HOST` / `REGISTRY_USERNAME` point at the container registry (the Gitea instance itself by default). The release itself is created with the auto-injected Actions token, so no other secrets are needed. --- ## ๐Ÿ“Œ Use Cases * Dynamic group membership automation * Directory cleanup and normalization * Organizational policy enforcement * Zero-touch user and device placement * Identity lifecycle orchestration ---