Skip to content

Repository files navigation

Access Approval Tool for Omnissa

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.

CI CodeQL License: MIT Container

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.

Features

  • 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 chatSlack 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
  • Opsbackup & 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)

Screenshots

Dashboard Approval Queue
Dashboard Queue
Auto-Approval Rules Audit Trail
Rules Audit
Sign-in (local + OIDC) OAuth-only mode
Login OAuth-only login

Quick Start

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:latest

Or 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:

Documentation


Deployment Modes

Three Compose files are provided; pick one. Details in docs/deployment.md.

Docker + Caddy (automatic HTTPS)

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 -d

The application will be available at https://<APPROVAL_DOMAIN>.

Standalone TLS Mode

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 -d

Set SSL_KEYSTORE_PASSWORD in .env before starting.

Behind Your Own Reverse Proxy (nginx / NPM / CasaOS)

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 --build

The 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.

ZimaCube one-script deploy

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-run

Update 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.comhttp://<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).


Local Development

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=local

Open 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 dev

See CONTRIBUTING.md for PR guidelines.


Configuration

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.

Omnissa Access Setup

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.

First Login

  • Local admin (default) — on first startup, if OMNISSA_BOOTSTRAP_ADMIN_USERNAME and OMNISSA_BOOTSTRAP_ADMIN_PASSWORD are 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.

Roles

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-…:AUDITOR

Notes 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/claims to read the ids — it pairs each id with its display name.
  • Requires the group scope, which is requested by default. Without it Access emits no group claim at all. Check your tenant advertises it in scopes_supported at <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_MAP set, 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 WARN is 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=false plus 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.

Architecture

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)
Email Spring Mail + FreeMarker templates
Reverse proxy Caddy (bundled option) or your own (nginx/NPM), or embedded Tomcat TLS

License

MIT — Copyright (c) 2026 Dean Flaming. Trademark and non-production disclaimers in NOTICE.md.

About

Omnissa Access Application Approvals Tool

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages