NetOSField accessMenu
Signed in user unavailable
Documentation/Authorization & RBAC
Download PDF

Xiber NetOS — Authorization & RBAC

Internal authorization model — users, roles, permissions, scopes, and field-level filtering layered on top of Microsoft SSO identity.


NetOS uses Microsoft SSO / Microsoft Entra ID for authentication only. Microsoft proves identity. NetOS decides authorization with internal users, roles, permissions, scopes, and field-level filtering.


Security Model

  • Default deny: every /api/v1 route requires an active internal user with app_access and the route permission.
  • Backend enforcement is authoritative. Frontend hiding is only a usability layer.
  • Financial and executive fields are filtered before API responses are returned.
  • Permission changes are written to the activity audit log with event_type="authorization".
  • Sensitive financial access is covered by the normal API activity log and should be expanded for especially sensitive exports or investor reporting endpoints as those are added.

Identity Flow

  1. Microsoft SSO (via oauth2-proxy) authenticates the user.
  2. The reverse proxy forwards trusted identity headers to the API. NetOS honors them only when TRUST_PROXY_AUTH_HEADERS=true and the request's client IP is inside TRUSTED_AUTH_PROXY_CIDRS (default 127.0.0.1/32,172.16.0.0/12). The accepted headers, in precedence order, are X-Auth-Request-Email, X-Ms-Client-Principal-Name, X-Auth-Request-User, X-Forwarded-Email, X-Forwarded-User, and CF-Access-Authenticated-User-Email (with X-Ms-Client-Principal-Id / X-Auth-Request-Preferred-Username as the subject/Entra object id).
  3. NetOS normalizes the email and optional Entra object ID.
  4. NetOS loads the internal users record.
  5. If the user is inactive or has no record, access is denied unless they match the bootstrap Super Admin allowlist.
  6. Roles and direct user permission overrides are resolved (role permissions unioned, then direct allow/deny overrides applied).
  7. Route and field authorization is applied.

Authenticated does not mean authorized.

Development auth shim

When ALLOW_DEV_AUTH=true, the API bypasses SSO and accepts x-user-email / x-user-role headers with no identity verification (intended for local development and the test harness). If no email is supplied it falls back to DEV_AUTH_DEFAULT_EMAIL (dev@xiber.com), which is on the bootstrap Super Admin allowlist while dev auth is enabled — so that identity comes up as Super Admin.

Production must set `ALLOW_DEV_AUTH=false`. Otherwise anyone able to reach the API can assert an arbitrary identity and dev@xiber.com auto-provisions as Super Admin. The server compose file defaults it to false; keep it that way.


Machine Access / API Tokens

Machine users and coding agents should use NetOS API access tokens instead of browser SSO sessions.

API tokens:

  • are created by authorized NetOS admins
  • are bound to an internal users record
  • inherit that user's roles, direct permissions, scopes, and field-level permissions
  • are stored only as hashes in api_access_tokens
  • are returned in full only once at creation time
  • can be revoked without deleting the owning user
  • write last_used_at when used
  • produce principals with auth_source="api_token"

Supported request headers:

Authorization: Bearer ntos_live_...

or:

X-NetOS-API-Key: ntos_live_...

Admin endpoints:

GET /api/v1/authz/api-tokens
POST /api/v1/authz/api-tokens
POST /api/v1/authz/api-tokens/{api_token_id}/rotate
DELETE /api/v1/authz/api-tokens/{api_token_id}

Creating and revoking tokens requires users_edit; listing tokens requires users_view.

Recommended practice:

  • Create a dedicated active internal user for each integration or agent, such as agent-netos@xiber.net.
  • Assign the least-privilege role set to that user.
  • Prefer expiring tokens.
  • Store the full token in the external system's secret manager.
  • Rotate tokens periodically and revoke unused tokens.
  • Do not create API tokens for human Super Admin users unless there is a short-lived emergency need.

User Lifecycle Management

Admins can now:

  • create internal users
  • activate and deactivate users
  • delete users
  • assign and remove scoped roles
  • assign direct permission overrides
  • generate API tokens for a user
  • rotate API tokens, immediately invalidating the previous secret
  • revoke API tokens
  • audit user and token activity

Deleting a user is a soft delete. NetOS marks the user inactive/deleted and revokes active API tokens for that user. Audit history remains intact.

Rotating a token keeps the same token record but replaces the stored hash and prefix. The old secret stops working immediately, and the new token value is returned once.


Seeing The Current User

The current API-resolved user is available at:

/api/v1/authz/me

The main navigation footer also shows the email that NetOS is authorizing and the source of that identity:

  • Microsoft SSO: the API received and trusted proxy authentication headers.
  • Dev fallback: the API did not use Microsoft proxy headers and fell back to development auth.

Production should show Microsoft SSO. If it shows Dev fallback, confirm ALLOW_DEV_AUTH=false and TRUST_PROXY_AUTH_HEADERS=true in the running API environment and verify the SSO proxy forwards identity headers such as X-Auth-Request-Email.


Bootstrap Super Admin

Set:

RBAC_BOOTSTRAP_SUPER_ADMIN_EMAILS=admin@example.com,second-admin@example.com

When one of those Microsoft-authenticated users first reaches the app, NetOS creates or activates the internal user and assigns the Super Admin role. If the variable is not set, NetOS falls back to ADMIN_EMAIL; in dev auth mode it also allows DEV_AUTH_DEFAULT_EMAIL.


Data Model

Core tables:

  • users: internal user mapped to email and optional Entra object ID.
  • roles: named permission bundles.
  • permissions: granular permission catalog.
  • role_permissions: permissions included in roles.
  • user_roles: role assignments, including scope_type and scope_ref_id.
  • user_permissions: optional direct allow/deny overrides.
  • permission_scopes: reusable labels for global, project, site, and department scopes.
  • user_activity_logs: audit history, including authorization changes.

Scopes currently support global, project, site, and department. Global enforcement is live; entity-scope query filters are intended to be applied module by module where project/site assignment data exists.


Default Roles

  • Super Admin: all permissions.
  • Executive: operational dashboards and financial summaries, including executive financial visibility.
  • Finance Admin: full finance, invoices, reporting, and financial field visibility.
  • Project Manager: project and assigned operational data with project/asset/circuit costs.
  • Network Engineer: technical inventory edit access with relevant asset and circuit costs.
  • Field Technician: assigned operational/site tooling, no financial fields by default.
  • Read-Only User: non-sensitive operational read access only.

Roles are composed of permissions. Do not hard-code role names in new features except for bootstrap or emergency super-admin behavior.

Super Admin bypass. Super Admin is seeded with the full permission catalog, and the code additionally short-circuits for it: require_roles, require_write_role, and can_view_field all return early when Super Admin is among the principal's roles, so a Super Admin sees unmasked fields and passes role checks regardless of individual permission rows.

Automatic seeding. On startup (first authenticated request) NetOS runs seed_default_rbac, which idempotently upserts the permission catalog (by code) and the default roles and their role→permission links (by name). Seeding is guarded so it runs once per process and is safe to re-run — you do not seed roles manually. Editing a role's permission composition afterward in the Admin UI persists in the database and is not overwritten. This is also how a new permission code introduced by a code change reaches *existing* roles in a deployed database: the first authenticated request after deploy re-runs the upsert, and any (role, permission) pair in DEFAULT_ROLE_PERMISSIONS that isn't already a row in role_permissions gets inserted — no manual seeding command is required.

Site media permissions. site_links_view, site_links_edit, and site_media_ingest (category "Site Media") gate /api/v1/site-links (site links pointing an infrastructure asset or customer at a SharePoint/OneDrive/Teams/URL resource, plus the SharePoint browse/ingest flow). By default, site_links_view is granted to every role that already has assets_view (Executive, Finance Admin, Project Manager, Network Engineer, Field Technician, Read-Only User, and the Integration: XSurvey service role); site_links_edit and site_media_ingest are granted to every role that already has assets_edit (Project Manager, Network Engineer, Field Technician). Super Admin has all three via the full permission catalog.

Site findings & comments permissions (Phase 2). findings_view, findings_edit, and comments_edit (category "Site Media") gate /api/v1/findings and /api/v1/comments. findings_view and comments_edit are granted together to every role that already has assets_view/OPERATIONAL_VIEW (Executive, Finance Admin, Project Manager, Network Engineer, Field Technician, Read-Only User) — a comment is intentionally as open as viewing the finding it's posted on. findings_edit (create/edit/delete a finding, and both AI-draft endpoints) is granted to every role that already has assets_edit/TECHNICAL_EDIT (Network Engineer via TECHNICAL_EDIT, plus Field Technician, which lists it directly rather than via that set — Project Manager notably does *not* have findings_edit despite having other TECHNICAL_EDIT-equivalent grants, a pre-existing asymmetry in DEFAULT_ROLE_PERMISSIONS, not something Phase 3 changed). Editing or deleting someone else's *comment* additionally requires settings_edit (the author can always edit/delete their own). Super Admin has all three via the full permission catalog.

Sharing & Sonar ticket permissions (Phase 3). findings_share (email/Teams/Slack a finding) and sonar_ticket_create (create a Sonar ticket from a finding) — category "Site Media" — gate the new /api/v1/findings/{id}/actions/* endpoints (app/api/v1/findings_actions.py). Both are granted everywhere findings_edit is granted: added to the TECHNICAL_EDIT set (so Network Engineer gets them the same way it gets findings_edit) and to Field Technician's explicit permission list. The read-only directory helpers under /api/v1/findings/directory/* (people/teams/channels/Sonar-user lookups, and the capabilities probe) require findings_edit itself, not the new permissions — they're read-only lookups needed to *compose* a share, not to send one. Super Admin has both via the full permission catalog.

Contributions & Rewards permissions (Phase 1). contributions_view (category "Recognition") gates GET /api/v1/contributions/me and GET /api/v1/contributions/leaderboard — granted to every human role (added to OPERATIONAL_VIEW plus Field Technician's explicit list), matching the design's "every human role can see their own history and the leaderboards" intent. recognition_admin gates the rules editor (GET/PUT /api/v1/contributions/rules*), the audit search and reversal endpoints (/api/v1/contributions/events*), the flags queue (/api/v1/contributions/flags*), and the dHash backfill (/api/v1/contributions/admin/backfill-dhash) — Super Admin only, via the full permission catalog; no other default role is granted it. Integration: XSurvey (and any other Integration: * role, and API-token principals) gets neither — the scoring engine (app/services/contributions/engine.py) independently refuses to award points to those principals regardless of what permissions they hold, so a service account can still read the endpoints it needs without ever appearing on a leaderboard.

Verification & missions permissions (Phase 2). verifications_edit (category "Recognition") gates POST /api/v1/verifications ("Confirm this record"); GET /api/v1/verifications and GET /api/v1/contributions/next-actions fall back to contributions_view (any signed-in human role can see the chips, only some can act on them). verifications_edit is granted to every role that already has assets_edit, customers_edit, or providers_edit on any entity it can confirm — Project Manager and Network Engineer (via their explicit lists) and Field Technician (explicit list) — plus Super Admin via the full catalog; Executive, Finance Admin, and Read-Only User do not have it, matching "the same roles that can edit the underlying entity." The endpoint additionally checks, per request, that the caller holds the specific view permission for the entity being confirmed (assets_view/customers_view/providers_view) — a 403 distinct from the route-level verifications_edit gate, since a role could in principle hold verifications_edit without holding every entity's view permission. GET /api/v1/contributions/missions uses contributions_view; POST/PATCH /api/v1/contributions/missions* (custom mission creation, archive/edit, generate-now) and POST /api/v1/contributions/admin/sync-departments and POST /api/v1/contributions/admin/recognition-email/send all require recognition_admin (Super Admin only by default), matching Phase 1's admin-surface convention.


Protecting Routes

Add a permission to apps/api/app/core/permissions.py, then add route mapping in ROUTE_PERMISSION_PREFIXES.

For explicit endpoint checks:

from app.core.auth import require_permission

@router.post("/example", dependencies=[Depends(require_permission("assets_edit"))])
async def create_example(...):
    ...

Every route under /api/v1 also runs require_route_permission.


Protecting Fields

Financial fields must not be sent and hidden only in the browser. Use field permissions:

  • field.asset_cost.view
  • field.circuit_cost.view
  • field.customer_revenue.view
  • field.margin.view
  • field.capex.view
  • field.executive_summary.view

Example:

from app.core.auth import can_view_field

if not can_view_field(principal, "field.circuit_cost.view"):
    payload["mrc_usd"] = None
    payload["nrc_usd"] = None

Exports and search endpoints must use the same filtered payload strategy and must not include protected fields in labels, descriptions, or metadata.


Admin UI

The Admin page includes an Authorization section for:

  • creating users
  • activating and deactivating users
  • assigning roles
  • assigning global/project/site/department scopes
  • editing role permission composition
  • assigning a user's functional team (a select in the user detail card,

saved immediately) -- see below

The sidebar hides navigation items when the current user lacks the associated view permission.

Functional teams

A Team (app.models.entities.Team, migration 0072_functional_teams) is a NetOS-managed group an admin curates and assigns by hand -- deliberately separate from User.department, which is synced automatically from Microsoft Graph (see Contributions & Rewards). Eight teams are seeded on migration: Project Management, Field Operations, Break/Fix, Transmission Engineering, Executive, Engineering, Xperts, and Billing.

Right below the Authorization panel, a Teams panel (components/ teams-admin.tsx) lists every team with its member count and lets an admin add a team, rename it inline, pick a color, reorder it (up/down swaps sort_order with its neighbor), and deactivate/reactivate it. Deactivating a team is always soft (is_active = false): members keep their team_id -- there is no hard delete, so a team's leaderboard history is never silently rewritten.

Endpoints (app/api/v1/teams.py): GET /api/v1/teams (any authenticated user, app_access) returns every non-deleted team with a live member_count; POST, PATCH /{team_id}, and DELETE /{team_id} (soft delete) all require users_edit, the same permission that gates the rest of user administration. Assigning a user to a team is PATCH /api/v1/authz/users/{user_id} with {"team_id": "<uuid>" | null} (also users_edit); the response's team field becomes {team_id, name} or null. GET /api/v1/authz/me and the Contributions endpoints (/contributions/me, /contributions/users/{id}, and each leaderboard row) all include the same team shape.


Setup

The RBAC tables are created by migrations 0030_rbac_authorization and 0031_rbac_audit_columns; API tokens by 0046_api_access_tokens. Apply migrations with a one-off container (see Admin & Operations for the full deploy sequence and the Compose v1 recreate bug):

cd ~/netos/infra/docker
docker-compose -p netos -f docker-compose.server.yml run --rm api alembic upgrade head

After a code/image change, rebuild and recreate the API/web containers (a plain docker restart re-runs the old image and will not pick up new code). Use the stop + rm + up -d sequence documented in Admin & Operations.

Run the RBAC tests from the API venv against the circuitos_test database (not the container image, which does not carry test dependencies):

cd apps/api
.venv/bin/pytest tests/test_rbac_authorization.py tests/test_field_masking.py tests/test_auth_hardening.py -v

Security Considerations

  • Keep Microsoft Conditional Access and MFA in Entra; NetOS remains compatible because it only consumes the authenticated identity.
  • Do not grant app_access automatically to every Microsoft user.
  • Prefer scoped roles for project/site/department work.
  • Use direct user permissions sparingly and audit them.
  • Never add wildcard permissions.
  • New financial, payroll, compensation, debt, investor reporting, export, or search features must define explicit route and field permissions before release.