A self-hosted approval gateway for Omnissa Access (Workspace ONE): users request applications from the catalog, administrators approve or deny them in a live web queue, and decisions flow back to Omnissa Access automatically.
NOTICE: This is an independent community project — not an Omnissa product and not affiliated with, endorsed by, or supported by Omnissa, LLC. It is provided as-is, without warranty, and is intended for testing, lab, and demo use only — not production. See NOTICE.md.
When a user requests access to an application, Omnissa Access POSTs a callout to this service; administrators approve or deny the request through a web UI, and the decision is sent back via the Omnissa Access service client API.
- Approval queue with live updates (Server-Sent Events) and a Deactivated list
- Native Omnissa Access callout integration — messaging-envelope parsing, decision posting, connectivity status tile
- Admin login: local account and/or "Sign in with Omnissa Access" (OIDC + PKCE), optional OAuth-only mode, automatic consent-screen disable
- Local account management — add, disable, delete and re-role local accounts, self-service password change, and a guard that refuses to remove the last enabled local admin (the break-glass when Access is unreachable)
- Access lifecycle — time-bound (JIT) grants that auto-expire, permanent vs temporary decline, on-demand revoke, and a reversible block (details)
- Approve from chat — Slack messages and Teams Adaptive Cards with deep-link decision buttons: the approver signs in, so roles apply and no inbound endpoint or shared secret is needed
- Auto-approval rules — match rules decide on arrival, expiry rules auto-reject requests left pending too long; both scope by wildcard app-name and/or Access group, first match wins, and approve rules can grant time-bound access
- Audit trail recording both the acting identity (including chat approvers) and who the access was for, so an entry survives the deletion of the request it describes — plus CSV export
- Notifications — SMTP email to requestors; webhooks in generic/Slack/Teams formats for new requests, decisions, and access-revoked / app-reopened lifecycle events
- Ops — backup & restore for the database and secrets, log bundle download, syslog export (UDP/TCP/TLS with client certs), and a dependency health API that separates "the container is down" from "something it depends on is unhealthy" — including detection of approval requests Omnissa Access is waiting on that never reached the queue
- Role-based access control — Admin / Approver / Viewer / Auditor, resolved from Omnissa Access group membership (details)
- API hardening — optional Basic auth and per-IP rate limiting on the callout endpoint, the only endpoint that must be internet-reachable
- Sign-in protection — failed local sign-ins are progressively delayed and a persistent source is refused, with no account lockout: locking would let anyone reach the login page and disable the break-glass credential. Password policy is configurable (details)
| Dashboard | Approval Queue |
|---|---|
![]() |
![]() |
| Auto-Approval Rules | Audit Trail |
|---|---|
![]() |
![]() |
| Sign-in (local + OIDC) | OAuth-only mode |
|---|---|
![]() |
![]() |
Pull the image (once published) and run it with an env file and a data volume:
docker pull ghcr.io/squidlyman/omnissa-access-approvals:latest
docker run -d --name omnissa-approvals \
--env-file omnissa-approvals.env \
-v ./data:/app/data \
-p 8081:8081 \
--restart unless-stopped \
ghcr.io/squidlyman/omnissa-access-approvals:latestOr build from source:
git clone https://github.com/SquidlyMan/Omnissa-Access-Approvals.git
cd Omnissa-Access-Approvals
docker build -t omnissa-access-approvals .Use deploy/zimacube/omnissa-approvals.env.example as the env-file template, put a TLS reverse proxy in front (only POST /api/approvals/new must be internet-reachable), and configure your tenant — full walkthroughs in the docs:
- Deployment — Docker/Compose, reverse-proxy requirements, inbound connectivity, ZimaCube
- Configuration reference — every environment variable
- Access lifecycle — JIT/time-bound grants, decline modes, revoke, allow re-request
- Actionable Teams approvals — Adaptive Card + Power Automate workflow setup
- Omnissa Access setup — OAuth clients, Settings > Approvals, per-app approval
- Actionable Slack approvals — Slack app setup, signature verification, approver mapping
- Monitoring — health endpoints, Uptime Kuma and UAG setup, per-component runbook
- Troubleshooting — real failure modes and fixes
- SECURITY.md — reporting vulnerabilities, endpoint scope, hardening
- CONTRIBUTING.md — dev setup and PR guidelines
- CHANGELOG.md — release history
Three Compose files are provided; pick one. Details in docs/deployment.md.
Caddy acts as a reverse proxy and obtains a TLS certificate automatically via Let's Encrypt. Requires a publicly reachable domain and open ports 80/443.
git clone https://github.com/SquidlyMan/Omnissa-Access-Approvals.git
cd Omnissa-Access-Approvals
cp .env.example .env # fill in values — see docs/configuration.md
docker compose up -dThe application will be available at https://<APPROVAL_DOMAIN>.
Use this when you have your own certificate and can't use Let's Encrypt:
# Generate a keystore from PEM files:
bash scripts/import-cert.sh
# Or generate a self-signed cert for testing:
bash scripts/gen-dev-cert.sh
docker compose -f docker-compose-standalone.yml up -dSet SSL_KEYSTORE_PASSWORD in .env before starting.
Use this mode when another proxy already owns ports 80/443 on the host:
cp .env.example .env # APPROVAL_DOMAIN and SSL_KEYSTORE_PASSWORD are unused here
docker compose -f docker-compose-proxy.yml up -d --buildThe app listens on plain HTTP port 8081; point your proxy at it. The proxy must pass X-Forwarded-Proto/X-Forwarded-Host (the app uses them for https:// OAuth2 redirect URIs — server.forward-headers-strategy=framework is set by default), and the SSE endpoint /api/approvals/stream needs proxy_buffering off. A complete nginx server block and Nginx Proxy Manager notes are in docs/deployment.md.
If nginx runs directly on the host, change the port mapping in docker-compose-proxy.yml to "127.0.0.1:8081:8081" so the unencrypted port is not reachable from the LAN; keep "8081:8081" if your proxy runs in a container.
For a ZimaCube specifically, deploy/zimacube/ contains a complete, idempotent deployment: repo checkout and H2 data on /media/ZIMARAID/omnissa-approvals/, env file with chmod 600, a CasaOS-adoption-safe compose (image pulled from ghcr.io/squidlyman/omnissa-access-approvals — no local build on the NAS, all state bind-mounted), and a systemd unit that keeps port 8081 LAN-only via a DOCKER-USER iptables rule. On the NAS:
git clone https://github.com/SquidlyMan/Omnissa-Access-Approvals.git /media/ZIMARAID/omnissa-approvals/src
sudo sh /media/ZIMARAID/omnissa-approvals/src/deploy/zimacube/deploy.sh
# first run creates the env file and stops — edit it, then re-runUpdate by re-running deploy.sh, or opt in to Watchtower-based auto-updates via the autoupdate compose profile (disabled by default; daily, label-scoped) — see docs/deployment.md. The CasaOS "Check and then update" button does not work for this container and always reports "is the latest version" — ZimaOS resolves updates by looking the app up in a CasaOS AppStore, and an externally-managed Compose app is never found there, so no registry check happens at all. No tag choice changes this. Use deploy.sh, docker compose pull && up -d, or Watchtower, and trust the dashboard version banner — see CasaOS updates.
Then add the NPM proxy host approvals.example.com → http://<nas-ip>:8081. If NPM returns 502 and its access log shows requests arriving from a 172.x Docker-bridge address, the LAN-only rule is dropping proxy traffic — insert an accept for the bridge network above the drop: iptables -I DOCKER-USER -p tcp --dport 8081 -s 172.16.0.0/12 -j ACCEPT (and add a matching ExecStart line to the systemd unit).
Uses an embedded H2 database and runs without Docker. Requires Java 17 (Node/npm are handled by frontend-maven-plugin).
cp config/application-local.properties.example config/application-local.properties
# fill in the required values, then:
./mvnw spring-boot:run -Dspring.config.additional-location=file:./config/ -Dspring.profiles.active=localOpen http://localhost:8081. To iterate on the frontend without a full Maven build, run the Vite dev server separately after the first Maven build has populated src/main/resources/static/:
cd src/main/frontend
npm install
npm run devSee CONTRIBUTING.md for PR guidelines.
All variables are set in .env for Docker deployments, or in config/application-local.properties for local development. The complete reference — service client, bootstrap admin, admin OAuth2 login, API security, mail, webhooks, syslog — lives in docs/configuration.md, and deploy/zimacube/omnissa-approvals.env.example is a fully commented template.
Two OAuth clients are required in your tenant (a Client Credentials service client for the approvals API, and optionally an OIDC client for admin login), plus Settings > Approvals pointed at https://<your-host>/api/approvals/new and License Approval Required on each gated app. The full walkthrough is in docs/omnissa-access-setup.md.
- Local admin (default) — on first startup, if
OMNISSA_BOOTSTRAP_ADMIN_USERNAMEandOMNISSA_BOOTSTRAP_ADMIN_PASSWORDare set and the user table is empty, a local admin account is created automatically. Sign in with those credentials. - Omnissa Access OAuth2 — if the
OMNISSA_ADMIN_OAUTH_*variables are configured, a Sign in with Omnissa Access button appears on the login page. What a user may then do is decided by their Omnissa Access group membership — see Roles below. Signing in successfully grants Viewer and nothing more until a group is mapped to a higher role.
Authorization is driven by Omnissa Access group membership, mapped to four roles:
| Role | Can |
|---|---|
| Admin | Manage users, auto-approval rules, tenant configuration and the log bundle; delete requests; everything below |
| Approver | Decide requests — approve, reject, revoke, revoke-and-block, allow re-request |
| Viewer | Read the queue, request details, statistics, rules and the audit trail on screen — no CSV export |
| Auditor | The audit trail only, including its CSV export — no live queue, no decisions |
Configure the mapping in the env file with OMNISSA_ROLE_MAP, as comma-separated <groupId>:<ROLE> pairs:
OMNISSA_ROLE_MAP=05eb7969-…:ADMIN,63173f00-…:APPROVER,4378e8f5-…:AUDITORNotes that matter in practice:
- The mapping keys on group ids, not names. Renaming a group in Access would otherwise silently drop everyone to Viewer with no error anywhere. Sign in and open
/api/auth/claimsto read the ids — it pairs each id with its display name. - Requires the
groupscope, which is requested by default. Without it Access emits no group claim at all. Check your tenant advertises it inscopes_supportedat<issuer>/.well-known/openid-configuration. - Viewer is a fallback, not a floor. A user whose groups match nothing gets Viewer; once any group matches, the matched roles are exactly what they hold. This is what makes Auditor meaningful — it grants less than Viewer, which is impossible if everyone starts as a Viewer. With no
OMNISSA_ROLE_MAPset, every user is a Viewer, so enabling the map is the deliberate act that grants privilege. - Matched roles are additive among themselves — someone in both the approver and auditor groups holds both.
- The most permissive role wins. Every rule admits any one of its listed roles, so a user holding several gets the union of their permissions.
- Changes apply at next sign-in. Roles are resolved from the token, which is a snapshot — adding or removing a group has no effect until the user signs out and back in.
- Exporting is gated separately from reading. A bulk export is an extraction, not a read: it produces a file that leaves the tool's controls entirely. A Viewer may read the audit trail on screen but cannot download it. The audit-trail export (
/api/audit/export.csv) is Admin and Auditor; the request export (/api/approvals/export.csv) is Admin, Approver and Auditor.
Auditor is the one restrictive role, so never combine it. Because resolution is additive, pairing Auditor with any other role silently defeats it: the union applies, the user keeps full access to the live queue, and Auditor contributes nothing. Adding an approver to the auditors group looks like a restriction and is not one. Paired with Admin or Approver it is also a separation-of-duties conflict — the same identity both decides requests and audits those decisions.
The combination is permitted rather than corrected (a group membership that removed access would be surprising in its own way, and could strand someone), but it is no longer invisible: a
WARNis written at each sign-in naming the user and the roles held. Grant Auditor on its own when the intent is to restrict.
- Use groups created for this purpose. Mapping an existing operational group (an AD IT Admins, say) means anyone added to it for unrelated reasons silently gains the ability to revoke and block entitlements in your tenant.
- Keep a way back in. If the mapping is wrong or Access is unreachable,
OMNISSA_AUTH_LOCAL_LOGIN_DISABLED=falseplus the bootstrap admin account is the recovery path. Note the bootstrap account is only created when the user table is empty, so its password cannot be rotated by changing the env var on an existing install.
| Layer | Technology |
|---|---|
| Backend | Java 17, Spring Boot 3.2.5, Spring Security |
| Database | H2 embedded (file-backed, survives restarts) |
| Frontend | React, TypeScript, Vite, Tailwind CSS |
| Live updates | Server-Sent Events (SSE) |
| Spring Mail + FreeMarker templates | |
| Reverse proxy | Caddy (bundled option) or your own (nginx/NPM), or embedded Tomcat TLS |
MIT — Copyright (c) 2026 Dean Flaming. Trademark and non-production disclaimers in NOTICE.md.





