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/v1route requires an active internal user withapp_accessand 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
- Microsoft SSO (via oauth2-proxy) authenticates the user.
- The reverse proxy forwards trusted identity headers to the API. NetOS honors them only when
TRUST_PROXY_AUTH_HEADERS=trueand the request's client IP is insideTRUSTED_AUTH_PROXY_CIDRS(default127.0.0.1/32,172.16.0.0/12). The accepted headers, in precedence order, areX-Auth-Request-Email,X-Ms-Client-Principal-Name,X-Auth-Request-User,X-Forwarded-Email,X-Forwarded-User, andCF-Access-Authenticated-User-Email(withX-Ms-Client-Principal-Id/X-Auth-Request-Preferred-Usernameas the subject/Entra object id). - NetOS normalizes the email and optional Entra object ID.
- NetOS loads the internal
usersrecord. - If the user is inactive or has no record, access is denied unless they match the bootstrap Super Admin allowlist.
- Roles and direct user permission overrides are resolved (role permissions unioned, then direct
allow/denyoverrides applied). - 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.comauto-provisions as Super Admin. The server compose file defaults it tofalse; 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
usersrecord - 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_atwhen 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/meThe 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.comWhen 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, includingscope_typeandscope_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.
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.viewfield.circuit_cost.viewfield.customer_revenue.viewfield.margin.viewfield.capex.viewfield.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"] = NoneExports 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
The sidebar hides navigation items when the current user lacks the associated view permission.
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 headAfter 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 -vSecurity Considerations
- Keep Microsoft Conditional Access and MFA in Entra; NetOS remains compatible because it only consumes the authenticated identity.
- Do not grant
app_accessautomatically 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.
