Repository navigation
Group-based RBAC for integrations, enforced at the runtime proxy - #89
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Devin Review found 2 new potential issues.
1 security issue not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| func (s *Scheduler) injectProxyEnv(env map[string]string, tenantID string, caller models.CallerIdentity) { | ||
| if s.internalProxyURL != "" { | ||
| env["VELANE_PROXY_URL"] = s.internalProxyURL | ||
| } | ||
| env["VELANE_TENANT_ID"] = tenantID | ||
|
|
||
| caller.TenantID = tenantID | ||
| if len(caller.GroupIDs) > 0 { | ||
| env["VELANE_CALLER_GROUPS"] = strings.Join(caller.GroupIDs, ",") | ||
| } | ||
| if caller.UserID == "" && caller.Role == "" && len(caller.GroupIDs) == 0 { | ||
| // Nothing to attest — legacy tenant-wide invocation. | ||
| return | ||
| } | ||
| token, err := callerid.Sign(s.encKey, callerid.Claims{ | ||
| TenantID: caller.TenantID, | ||
| UserID: caller.UserID, | ||
| Role: caller.Role, | ||
| GroupIDs: caller.GroupIDs, | ||
| }, callerid.DefaultTTL) | ||
| if err != nil { | ||
| return | ||
| } | ||
| env[callerid.EnvVar] = token |
There was a problem hiding this comment.
🔍 Non-interactive invocations lose access to restricted integrations
Caller is populated only in InvokeByToken. Replay, git-webhook, and event-triggered invocations build the request with no caller, so injectProxyEnv mints no token and the proxy treats them as unidentified: they can reach only ungranted profiles, and are denied all restricted ones once strict mode is on.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Correct, and intended for now — it is called out in the PR description. Replay, git-webhook and event-triggered invocations have no human principal to attest, so they run as legacy tenant-wide callers: unaffected while rbac_strict_mode is off, denied restricted profiles once it is on. Giving them identity needs a product decision (persist the original invoker on the invocation record and re-mint from it, or grant system-triggered runs an explicit service principal per trigger). I'd rather make that call with @abskrj than pick one here.
| token, err := callerid.Sign(s.encKey, callerid.Claims{ | ||
| TenantID: caller.TenantID, | ||
| UserID: caller.UserID, | ||
| Role: caller.Role, | ||
| GroupIDs: caller.GroupIDs, | ||
| }, callerid.DefaultTTL) |
There was a problem hiding this comment.
🔍 Caller token can expire before a queued job runs
The token minted by injectProxyEnv carries a 1h TTL and travels through Redis into async/streaming workers. A job that begins executing more than an hour after enqueue presents an expired token, and the proxy then treats it as an unidentified caller and denies restricted integrations.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
True for jobs that sit in the queue longer than the 1h TTL. Raising the TTL trades that off against how long a leaked token stays usable, so the durable fix is to stop carrying a pre-minted token through Redis and instead persist the caller identity on the invocation record and mint the token in the worker at execution time — which also gives the invocation binding the token currently lacks. Flagging rather than changing it here since it reshapes the job schema; happy to do it as a follow-up.
Summary
Integrations can now be restricted to tenant user groups, and the restriction is enforced where it actually matters — the
/v1/proxypath a snippet uses at runtime — not just in list endpoints.Model (migration
029_rbac_user_groups.sql):user_groups,user_group_members,integration_group_grants(group_id, credential_profile_id), nullableapi_keys.user_id,tenants.rbac_strict_mode, and widened role CHECKs (owner,integration_manager,member,vieweralongsideadmin/manage/invoke). IDs areTEXTto match the existing schema convention rather than theuuidin the request.Visibility rule (
handlers/access.go), chosen so enabling RBAC changes nothing retroactively:Runtime enforcement is the load-bearing part. The proxy's only trust input today is
X-Velane-Tenant, so a plainVELANE_CALLER_GROUPSenv var would be trivially forgeable from inside the sandbox. Instead the scheduler mints an HMAC-signed, 1h-expiry token over the caller's claims and the proxy verifies it:VELANE_CALLER_GROUPSis still exported for introspection but is never trusted. The token rides inSecretEnvVars, so it survives the Redis queue into async/streaming workers. Both platform libraries forward it asX-Velane-Calleron every proxy call.Roles map onto existing scopes in
roleHasScope(viamodels.RoleGrantsScope) soRequireScopekeeps working unchanged; group management uses a newRequireRole(admin/owner; grants also allowintegration_manager).Notes / deliberate gaps:
viewercurrently grants no scope, so viewers cannot hit routes that requireinvoke. Read-only scope semantics need a follow-up decision.rbac_strict_modehas store accessors and enforcement but no management endpoint yet — it is opt-in per tenant viaSetTenantRBACStrictMode.worker/integration_events.go) carry no caller identity and therefore behave as legacy tenant-wide callers; they would be denied under strict mode.Testing
go test ./...(withTEST_DATABASE_URL,-p 1),go build ./...,make openapi-check,make test-platform-libs,npx tsc --noEmitinapps/admin— all pass. New coverage: store group/grant lifecycle and tenant scoping, API tests for granted vs. ungranted member visibility and admin see-all, proxy 200/403 for granted/ungranted/admin/legacy/strict-mode, andcalleridsignature, tamper, foreign-key and expiry rejection.Link to Devin session: https://app.devin.ai/sessions/ed1ea4614fc74dc1bb804634fcb75d86
Open in Devin Desktop: https://app.devin.ai/desktop/session/ed1ea4614fc74dc1bb804634fcb75d86?variant=devin
Requested by: @abskrj