fix(site): use RBAC-filtered organizations for embedded metadata (#27110)

This commit is contained in:
McKayla はな
2026-07-09 17:56:09 -06:00
committed by GitHub
parent d66e4d794f
commit e59a67d63f
2 changed files with 98 additions and 3 deletions
+4 -3
View File
@@ -436,9 +436,10 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht
return err
})
eg.Go(func() error {
orgs, err := h.opts.Database.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{
UserID: apiKey.UserID,
})
// Match the /api/v2/organizations endpoint: RBAC-filtered and
// excluding deleted organizations. Membership-scoped queries would
// hide organizations that roles like owner can view.
orgs, err := h.opts.Database.GetOrganizations(ctx, database.GetOrganizationsParams{})
if err == nil {
userOrgs = orgs
}
+94
View File
@@ -28,8 +28,10 @@ import (
"golang.org/x/exp/maps"
"github.com/coder/coder/v2/coderd/appearance"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
@@ -349,6 +351,98 @@ func TestInjectionFailureProducesCleanHTML(t *testing.T) {
assert.Equal(t, "<html></html>", body)
}
func TestOrganizationsMetadata(t *testing.T) {
t.Parallel()
// GIVEN: a site handler backed by an authz-wrapped database,
// matching production wiring, and a template that renders only
// the organizations metadata.
siteFS := fstest.MapFS{
"index.html": &fstest.MapFile{
Data: []byte("{{ .Organizations }}"),
},
}
rawDB, _ := dbtestutil.NewDB(t)
db := dbauthz.New(
rawDB,
rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()),
testutil.Logger(t),
coderdtest.AccessControlStorePointer(),
)
handler, err := site.New(&site.Options{
Telemetry: telemetry.NewNoop(),
Database: db,
SiteFS: siteFS,
})
require.NoError(t, err)
ctx := testutil.Context(t, testutil.WaitShort)
// GIVEN: an org with members, another org, and a soft-deleted org.
memberOrg := dbgen.Organization(t, rawDB, database.Organization{})
otherOrg := dbgen.Organization(t, rawDB, database.Organization{})
deletedOrg := dbgen.Organization(t, rawDB, database.Organization{})
err = rawDB.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
ID: deletedOrg.ID,
UpdatedAt: dbtime.Now(),
})
require.NoError(t, err)
fetchOrgIDs := func(t *testing.T, token string) []uuid.UUID {
t.Helper()
r := httptest.NewRequest("GET", "/", nil)
r.Header.Set(codersdk.SessionTokenHeader, token)
rw := httptest.NewRecorder()
handler.ServeHTTP(rw, r)
require.Equal(t, http.StatusOK, rw.Code)
var orgs []codersdk.Organization
err := json.Unmarshal([]byte(html.UnescapeString(rw.Body.String())), &orgs)
require.NoError(t, err)
ids := make([]uuid.UUID, 0, len(orgs))
for _, org := range orgs {
ids = append(ids, org.ID)
}
return ids
}
// WHEN: an owner who is a member of only one org loads the page.
owner := dbgen.User(t, rawDB, database.User{
RBACRoles: []string{codersdk.RoleOwner},
})
dbgen.OrganizationMember(t, rawDB, database.OrganizationMember{
OrganizationID: memberOrg.ID,
UserID: owner.ID,
})
_, ownerToken := dbgen.APIKey(t, rawDB, database.APIKey{
UserID: owner.ID,
ExpiresAt: time.Now().Add(time.Hour),
})
// THEN: the metadata includes every non-deleted org, not just the
// orgs the owner is a member of.
ownerOrgIDs := fetchOrgIDs(t, ownerToken)
assert.Contains(t, ownerOrgIDs, memberOrg.ID)
assert.Contains(t, ownerOrgIDs, otherOrg.ID)
assert.NotContains(t, ownerOrgIDs, deletedOrg.ID)
// WHEN: a regular member of a single org loads the page.
member := dbgen.User(t, rawDB, database.User{})
dbgen.OrganizationMember(t, rawDB, database.OrganizationMember{
OrganizationID: memberOrg.ID,
UserID: member.ID,
})
_, memberToken := dbgen.APIKey(t, rawDB, database.APIKey{
UserID: member.ID,
ExpiresAt: time.Now().Add(time.Hour),
})
// THEN: the metadata only includes orgs the member can read.
memberOrgIDs := fetchOrgIDs(t, memberToken)
assert.Contains(t, memberOrgIDs, memberOrg.ID)
assert.NotContains(t, memberOrgIDs, otherOrg.ID)
assert.NotContains(t, memberOrgIDs, deletedOrg.ID)
}
func TestCaching(t *testing.T) {
t.Parallel()