Skip to content

chore: add access enforcement to projects handler#2005

Open
tgmendes wants to merge 14 commits intomainfrom
tiago/add-rbac-enforcement-to-projects
Open

chore: add access enforcement to projects handler#2005
tgmendes wants to merge 14 commits intomainfrom
tiago/add-rbac-enforcement-to-projects

Conversation

@tgmendes
Copy link
Copy Markdown
Contributor

@tgmendes tgmendes commented Mar 26, 2026

This adds RBAC enforcement policies to the projects endpoints, as the first PR using this.

Noteworthy callouts:

  1. RBAC is for enterprise only, any other plans we bypass it
  2. This is behind a feature flag, so no RBAC will be applied until we explicitly set it
  3. API Keys keep their semantics separately, and keep working as is: if the request includes API key and not session, RBAC is bypassed

Open with Devin

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gram-docs-redirect Ready Ready Preview, Comment Mar 27, 2026 2:32pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 26, 2026

🦋 Changeset detected

Latest commit: 5b73d02

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

return ctx, err
}

ctx, err = access.LoadIntoContext(ctx, s.logger, s.db)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to hear suggestion on a better pattern here. This step loads grants into context so it can be used to verify downstream. I wanted this to be a Goa or HTTP middleware, but we don't have authcontext at that point.

One other option is to load these as part of the Authorize step above, but unsure if that's good practice.

Loading grants is a DB query, so a future step will be to cache these.

@tgmendes tgmendes changed the title Tiago/add rbac enforcement to projects chore: add access enforcement to projects handler Mar 27, 2026
@tgmendes tgmendes marked this pull request as ready for review March 27, 2026 10:13
@tgmendes tgmendes requested a review from a team as a code owner March 27, 2026 10:13
devin-ai-integration[bot]

This comment was marked as resolved.

Add isEnterpriseOrg guard to Require, RequireAny, and Filter so RBAC
enforcement is skipped for non-enterprise accounts at the access layer.
Update all tests to use explicit enterprise context to avoid vacuous passes.
devin-ai-integration[bot]

This comment was marked as resolved.

API key auth does not load grants (no session), so requireAccess and
filterByAccess now skip enforcement when grants are absent from context.
Also removes leftover fmt.Println debug statements from LoadIntoContext.
@blacksmith-sh

This comment has been minimized.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment on lines +115 to +117
if err := s.requireAccess(ctx, access.Check{Scope: access.ScopeBuildRead, ResourceID: proj.ID.String()}); err != nil {
return nil, err
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 GetProject reveals project existence to unauthorized RBAC users

In GetProject, the RBAC check (requireAccess) runs after the project is fetched from the DB (server/internal/projects/impl.go:103-117). This means an authenticated enterprise user without build:read access gets a 403 (Forbidden) for existing projects but a 404 (Not Found) for non-existent ones, leaking project existence information. This ordering is necessary because the project ID (needed for the access check) isn't known until after the DB lookup. Whether this is acceptable depends on the security posture for enterprise RBAC — if project existence is sensitive, consider returning 404 uniformly for both cases.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +138 to +140
if payload.OrganizationID != authCtx.ActiveOrganizationID {
return nil, oops.E(oops.CodeForbidden, nil, "organization does not match active organization context")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 CreateProject and ListProjects now restrict to active organization only

The new checks at lines 138 and 232 enforce that payload.OrganizationID must match authCtx.ActiveOrganizationID. Previously, users could create/list projects in any organization they belonged to (verified by the slices.IndexFunc org membership check). This is a behavioral narrowing — users switching between organizations in the same session can no longer operate on non-active orgs. This appears intentional for RBAC (grants are loaded for the active org), but callers relying on the old behavior will now get 403 errors.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

Comment on lines +32 to +59
func LoadIntoContext(ctx context.Context, logger *slog.Logger, db accessrepo.DBTX) (context.Context, error) {
logger = logger.With(attr.SlogComponent("access"))
authCtx, ok := contextvalues.GetAuthContext(ctx)
if !ok || authCtx == nil || authCtx.SessionID == nil || authCtx.ActiveOrganizationID == "" || authCtx.UserID == "" {
return ctx, nil
}

if authCtx.AccountType != "enterprise" {
return ctx, nil
}

principals := []urn.Principal{urn.NewPrincipal(urn.PrincipalTypeUser, authCtx.UserID)}
// TODO: once we have role mapping we need to also add grants for roles here
// principals = append(principals, roleMapping[authCtx.UserID]...)

grants, err := LoadGrants(ctx, db, authCtx.ActiveOrganizationID, principals)
if err != nil {
logger.ErrorContext(
ctx,
"failed to load access grants",
attr.SlogOrganizationID(authCtx.ActiveOrganizationID),
attr.SlogUserID(authCtx.UserID),
attr.SlogError(err),
)
return ctx, fmt.Errorf("load access grants: %w", err)
}

return GrantsToContext(ctx, grants), nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 LoadIntoContext runs a DB query on every authenticated request for enterprise orgs

The LoadIntoContext call in APIKeyAuth (server/internal/projects/impl.go:89) executes a database query (GetPrincipalGrants) for every request from an enterprise org with a valid session. Since the generated Goa endpoints route both API key and session auth through APIKeyAuth (see server/internal/access/context.go:47), this adds a DB round-trip per request. For high-traffic endpoints like ListProjects or GetProject, this could be a performance concern. Consider caching grants (e.g., in Redis with a short TTL) similar to how productfeatures.Client caches feature flags.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will Cache these later, DW boo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants