Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a494734
feat: count only workspace-capable users toward license seats
jscottmiller Jul 7, 2026
5965e69
fix(enterprise/coderd/license): propagate workspace-capable count errors
jscottmiller Jul 15, 2026
6c41d19
perf(enterprise/coderd/license): dedupe seat counting on roles only
jscottmiller Jul 15, 2026
b925888
fix(enterprise/coderd/license): tolerate malformed stored roles in se…
jscottmiller Jul 16, 2026
ac69c95
feat(enterprise/coderd/license): log a warning when a stored role fai…
jscottmiller Jul 16, 2026
211ae15
fix(enterprise/coderd/license): name workspace-capable users in user_…
jscottmiller Jul 16, 2026
d537e16
perf(enterprise/coderd/license): share the custom role cache across s…
jscottmiller Jul 16, 2026
ec0135b
perf(coderd/database): aggregate seat-counting roles once instead of …
jscottmiller Jul 16, 2026
6be238b
chore(coderd/database): drop redundant comment from seat-counting query
jscottmiller Jul 16, 2026
faca823
perf(enterprise/coderd/license): prefetch all custom roles for seat c…
jscottmiller Jul 16, 2026
2e22100
chore(enterprise/coderd/license): trim seat-consumption phrasing from…
jscottmiller Jul 16, 2026
de422d6
feat(enterprise/coderd/license): log the workspace-capable seat count
jscottmiller Jul 16, 2026
bcef917
test(enterprise/coderd/license): cover multi-org split capability in …
jscottmiller Jul 16, 2026
5a9ec68
chore(enterprise/coderd/license): address review nits around gating a…
jscottmiller Jul 16, 2026
bab7391
fix(enterprise/coderd/license): bind permission-based counting to the…
jscottmiller Jul 16, 2026
6b7db3d
chore(enterprise/coderd/license): fix typos-lint spelling and import …
jscottmiller Jul 16, 2026
031fcc9
test(enterprise/coderd/license): benchmark seat counting across user …
jscottmiller Jul 16, 2026
9f363f4
fix(enterprise/coderd/license): bound the workspace-capable count wit…
jscottmiller Jul 16, 2026
51e0d24
chore(codersdk): drop ExperimentsSafe exclusion comment
jscottmiller Jul 19, 2026
1bd3009
chore: trim seat-counting comments to describe mechanism over motivation
jscottmiller Jul 20, 2026
2e7c1cc
chore(enterprise/coderd/license): put context first in benchmark help…
jscottmiller Jul 20, 2026
45bcc6b
fix(enterprise/coderd/license): select the best user_limit pair acros…
jscottmiller Jul 20, 2026
97b3fd0
test(enterprise/coderd/license): cover best-pair user_limit selection…
jscottmiller Jul 20, 2026
e3b8385
test(enterprise/coderd/license): restore TestNextLicenseValidityPeriod
jscottmiller Jul 20, 2026
bc000dc
refactor(enterprise/coderd/license): drop the per-organization worksp…
jscottmiller Jul 21, 2026
000508d
refactor(enterprise/coderd/license): extract user_limit best-pair sel…
jscottmiller Jul 22, 2026
72f799a
chore(enterprise/coderd/license): name FeatureUserLimit in comments i…
jscottmiller Jul 22, 2026
3a1ebae
chore(enterprise/coderd/license): name the concrete counting modes in…
jscottmiller Jul 22, 2026
62d5153
refactor(enterprise/coderd/license): make the user counting mode expl…
jscottmiller Jul 22, 2026
f71d925
test(enterprise/coderd/license): cover count error paths and remainin…
jscottmiller Jul 23, 2026
c0315f3
test(enterprise/coderd/license): assert AI Governance seats are unaff…
jscottmiller Jul 23, 2026
e405351
refactor(enterprise/coderd/license): simplify candidate count resolution
jscottmiller Jul 24, 2026
0c17122
refactor: rename permission-based licensing to workspace-capable lice…
jscottmiller Jul 24, 2026
8bc6b9d
refactor(enterprise/coderd/license): always provide the workspace-cap…
jscottmiller Jul 24, 2026
5cd2ac1
docs(enterprise/coderd/license): illustrate selectUserLimit's pairing…
jscottmiller Jul 24, 2026
d6dd818
refactor(enterprise/coderd/license): key seat-count dedupe on hashed …
jscottmiller Jul 26, 2026
2fd635c
chore(enterprise/coderd/license): deduplicate canonicalization commen…
jscottmiller Jul 26, 2026
a93de24
chore: address review feedback on seat counting
jscottmiller Jul 27, 2026
0899b8d
refactor(enterprise/coderd/license): let selectUserLimit own the enfo…
jscottmiller Jul 27, 2026
b02ca8a
chore(enterprise/coderd/license): name licenses instead of pairs in t…
jscottmiller Jul 27, 2026
18e3599
test(enterprise/coderd/license): pass a real authorizer instead of nil
jscottmiller Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2955,6 +2955,13 @@ func (q *querier) GetActiveUserCount(ctx context.Context, includeSystem bool) (i
return q.db.GetActiveUserCount(ctx, includeSystem)
}

func (q *querier) GetActiveUsersAuthorizationRoles(ctx context.Context) ([]database.GetActiveUsersAuthorizationRolesRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.GetActiveUsersAuthorizationRoles(ctx)
}

func (q *querier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]database.WorkspaceBuild, error) {
// This is a system-only function.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5038,6 +5038,10 @@ func (s *MethodTestSuite) TestSystemFunctions() {
dbm.EXPECT().GetActiveUserCount(gomock.Any(), false).Return(int64(0), nil).AnyTimes()
check.Args(false).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(int64(0))
}))
s.Run("GetActiveUsersAuthorizationRoles", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().GetActiveUsersAuthorizationRoles(gomock.Any()).Return([]database.GetActiveUsersAuthorizationRolesRow{}, nil).AnyTimes()
check.Args().Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.GetActiveUsersAuthorizationRolesRow{})
}))
s.Run("GetAuthorizationUserRoles", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
u := testutil.Fake(s.T(), faker, database.User{})
dbm.EXPECT().GetAuthorizationUserRoles(gomock.Any(), u.ID).Return(database.GetAuthorizationUserRolesRow{}, nil).AnyTimes()
Expand Down
8 changes: 8 additions & 0 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions coderd/database/modelmethods.go
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,18 @@ func (r GetAuthorizationUserRolesRow) RoleNames() ([]rbac.RoleIdentifier, error)
return names, nil
}

func (r GetActiveUsersAuthorizationRolesRow) RoleNames() ([]rbac.RoleIdentifier, error) {
names := make([]rbac.RoleIdentifier, 0, len(r.Roles))
for _, role := range r.Roles {
value, err := rbac.RoleNameFromString(role)
if err != nil {
return nil, xerrors.Errorf("convert role %q: %w", role, err)
}
names = append(names, value)
}
return names, nil
}

func (k CryptoKey) ExpiresAt(keyDuration time.Duration) time.Time {
return k.StartsAt.Add(keyDuration).UTC()
}
Expand Down
10 changes: 10 additions & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 89 additions & 0 deletions coderd/database/querier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17723,3 +17723,92 @@ func requireAIGatewayKeysViolation(
require.FailNow(t, "test case must expect a constraint error")
}
}

// TestGetActiveUsersAuthorizationRolesParity verifies that the bulk
// GetActiveUsersAuthorizationRoles query returns, for every eligible
// user, the same roles and groups as the per-user
// GetAuthorizationUserRoles query. The two queries encode the implied
// member roles, organization default roles, and group memberships
// independently and must not drift.
func TestGetActiveUsersAuthorizationRolesParity(t *testing.T) {
t.Parallel()

db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitLong)

orgA := dbgen.Organization(t, db, database.Organization{})
orgB := dbgen.Organization(t, db, database.Organization{})

activeUser := func(seed database.User) database.User {
seed.Status = database.UserStatusActive
return dbgen.User(t, db, seed)
}
member := func(orgID uuid.UUID, user database.User, roles ...string) {
dbgen.OrganizationMember(t, db, database.OrganizationMember{
OrganizationID: orgID,
UserID: user.ID,
Roles: roles,
})
}

// Site-wide role, zero org memberships.
owner := activeUser(database.User{RBACRoles: []string{rbac.RoleOwner().Name}})

// Plain single-org member; effective roles come from the implied
// member role plus the org's default member roles.
plain := activeUser(database.User{})
member(orgA.ID, plain)

// Explicit org roles across two organizations.
multiOrg := activeUser(database.User{})
member(orgA.ID, multiOrg, rbac.RoleOrgAdmin())
member(orgB.ID, multiOrg)

// Custom org role.
customRole, err := db.InsertCustomRole(ctx, database.InsertCustomRoleParams{
Name: "parity-role",
DisplayName: "Parity Role",
OrganizationID: uuid.NullUUID{UUID: orgA.ID, Valid: true},
OrgPermissions: []database.CustomRolePermission{{
ResourceType: rbac.ResourceWorkspace.Type,
Action: policy.ActionCreate,
}},
})
require.NoError(t, err)
custom := activeUser(database.User{})
member(orgA.ID, custom, customRole.Name)

// Group memberships.
grouped := activeUser(database.User{})
member(orgA.ID, grouped)
for range 2 {
group := dbgen.Group(t, db, database.Group{OrganizationID: orgA.ID})
dbgen.GroupMember(t, db, database.GroupMemberTable{
UserID: grouped.ID,
GroupID: group.ID,
})
}

// Excluded from the bulk query: service accounts and non-active
// users.
sa := activeUser(database.User{IsServiceAccount: true})
member(orgA.ID, sa)
suspended := dbgen.User(t, db, database.User{Status: database.UserStatusSuspended})
member(orgA.ID, suspended)

rows, err := db.GetActiveUsersAuthorizationRoles(ctx)
require.NoError(t, err)

gotIDs := make([]uuid.UUID, 0, len(rows))
for _, row := range rows {
gotIDs = append(gotIDs, row.ID)
}
require.ElementsMatch(t, []uuid.UUID{owner.ID, plain.ID, multiOrg.ID, custom.ID, grouped.ID}, gotIDs)

for _, row := range rows {
single, err := db.GetAuthorizationUserRoles(ctx, row.ID)
require.NoError(t, err)
require.ElementsMatch(t, single.Roles, row.Roles, "roles diverged for user %s", row.ID)
require.ElementsMatch(t, single.Groups, row.Groups, "groups diverged for user %s", row.ID)
}
}
94 changes: 94 additions & 0 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading