-
Notifications
You must be signed in to change notification settings - Fork 954
feat(enterprise/coderd): allow system users to be added to groups #18341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…tem user so that they can configure quotas
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering about this approach. Do we need to add IncludeSystem: true
to all these queries just to support the prebuilds system user?
This means someone will need to manually create a group for the prebuilds user and assign it a quota, right?
Wouldn't it be more robust to automatically create a group for the prebuilds system user with a default quota, and allow users to override it if needed? That way, we avoid having to include system users in these queries and reduce the risk of system users leaking into user-facing features.
The prebuilds user has already leaked into user facing features. Administrators search for prebuilds by specifying the prebuilds user on the workspace list page. For consistency and clarity, I think we should embrace listing and showing the prebuilds user. I like the idea of automatically creating a prebuilds group, but we'll need to decide what a sensible default quota would be if there is such a value that makes sense. |
It’s true that the prebuilds user is already exposed in some user-facing features, like showing prebuilt workspaces, but that might not be the ideal approach 😕 The issue isn’t just about the prebuilds user, it extends to listing all current and future system users.
We already have a precedent with the coder/coderd/database/queries/groups.sql Lines 102 to 109 in 0cdcf89
We could apply the same pattern here: automatically create the prebuilds group with a default quota of 0, and clearly document this behavior along with instructions on how users can update the quota if needed. |
WalkthroughThis change updates the logic for retrieving organization and group members to consistently include system users, such as the prebuilds system user, by default. It introduces a new boolean parameter to control this behavior in both SQL queries and Go code, and updates related tests to expect the presence of system users in member lists. Changes
Sequence Diagram(s)sequenceDiagram
participant API
participant MembersService
participant Database
API->>MembersService: Request organization/group members
MembersService->>Database: Query members (IncludeSystem: true)
Database-->>MembersService: Return all members (including system users)
MembersService-->>API: Return member list with system users
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we also add a test case that verifies the behavior when the prebuilds quota is actually used? I'm curious about what QuotaAllowance: 0
means in practice, does this mean prebuild creation will always fail due to no quota, or does 0 represent unlimited/no enforcement?
It would be helpful to have a test that actually attempts to create prebuilds after the group is set up to clarify this behavior and ensure the default quota setting works as intended.
_, err = s.store.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ | ||
// Create a "prebuilds" group in the organization and add the system user to it | ||
// This group will have a quota of 0 by default, which users can adjust based on their needs | ||
prebuildsGroup, err := s.store.InsertGroup(ctx, database.InsertGroupParams{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't this be done inside the if !alreadyOrgMember {
condition? This way there was no need for all the additional unique violation checks, right? Or am I missing something? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do it inside the !alreadyOrgMember
condition then an edge case exists and fails.
Consider what happens when where a prebuilds
group might already exist for that organization. In this case, we would want to enforce membership.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, this is because this code was already running, so we might currently have prebuilds that belong to organizations without a corresponding prebuilds
group, right? Could you add this context to the function comment?
This code is a bit tricky for me to follow 🤔 just some suggestions to improve readability (use them if you think it makes sense):
- Could you update the function description to clarify that this function: 1) ensures the user is a member of each preset's organization, and 2) ensures a "prebuilds" group exists per organization and the user is a member of it?
- Would it make sense to follow a similar pattern for the
prebuilds
groups as we do for organization memberships? We could have an SQL query upfront (maybeGetGroups
) to get all the prebuilds groups the user already belongs to, then create a map similar toorgMemberShips
to avoid the "try-insert-then-get-if-exists" pattern in the loop.
@@ -156,7 +156,7 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { | |||
|
|||
currentMembers, err := api.Database.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ | |||
GroupID: group.ID, | |||
IncludeSystem: false, | |||
IncludeSystem: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need all these IncludeSystem: true
cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so, because I maintain that the prebuilds user should be visible. If we decide that we should hide the prebuilds user, then we can revert this back to false.
preExistingMembership bool | ||
name string | ||
includePreset []bool | ||
preExistingOrgMembership []bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused here, why are we now using a slice of bool in these cases? Could you add a comment here as well as in the test cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The slices of bool allow us to test all the combinations of these scenarios. so in two "test cases", we actually technically have 16 tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, thanks for clarifying! I think for test readability it makes sense to be as explicit as possible about what we're actually testing. I'd personally prefer separate test cases for each scenario, but if that's too much work to change now, no worries.
|
||
if tc.expectUserInGroup != nil && !*tc.expectUserInGroup { | ||
// Check that the system user is NOT a member of the prebuilds group | ||
groupMembers, err := db.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the prebuilds user always be part of the prebuilds group? Or is this the case where an organization doesn't have prebuilds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should always be a member of the prebuilds group, yes. This is a defensive measure against an edge case where it isn't. This should never happen, but this defends against the case where it does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case, I think we should always assert that the prebuilds user is a member of the prebuilds
group (regardless of the test case parameters), since that should be an invariant of the function.
@ssncferreira I've added unit tests to verify the scope of quotas. You were right. Quotas are org scoped. Prebuilds might reach it's quota in one organization while still having some budget left in another. The added tests prove the desired behaviour for all users, including prebuilds. I'm planning a follow up where the prebuilds user checks its quota before it attempts to create a new workspace and fails early if it would exceed the quota. |
Because unclaimed prebuilt workspaces are owned by the `prebuilds` user, you can: | ||
|
||
1. Configure quotas for any group that includes this user. | ||
1. Set appropriate limits to balance prebuilt workspace availability with resource constraints. | ||
|
||
When prebuilt workspaces are configured for an organization, Coder creates a "prebuilds" group in that organization and adds the prebuilds user to it. This group has a default quota allowance of 0, which you should adjust based on your needs: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you update the PR description to match?
rbac.ResourceGroup.Type: { | ||
policy.ActionRead, | ||
policy.ActionCreate, | ||
policy.ActionUpdate, | ||
}, | ||
rbac.ResourceGroupMember.Type: { | ||
policy.ActionRead, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add comments explaining why the prebuilds user needs this privilege?
ELSE | ||
user_is_system = false | ||
END; | ||
WHERE (@include_system::bool OR NOT user_is_system); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order of evaluation of subexpressions is not defined. In particular, the inputs of an operator or function are not necessarily evaluated left-to-right or in any other fixed order.
https://www.postgresql.org/docs/17/sql-expressions.html#SYNTAX-EXPRESS-EVAL
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was the one who suggested this change #18341 (comment)
I thought PostgreSQL would guarantee short-circuit evaluation for AND
and OR
operators, but you're absolutely right, the documentation explicitly states that evaluation order is not defined. TIL. Thanks for the correction!
Sorry about that @SasSwart 🙇♀️
workspaceZeroCost := coderdtest.CreateWorkspace(t, client, templateZeroCost.ID) | ||
buildZeroCost := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceZeroCost.LatestBuild.ID) | ||
|
||
// Verify the build failed due to quota |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
©️ 🍝
// Verify the build failed due to quota | |
// Verify the build did not fail due to quota |
// ZeroQuota tests that a user with a zero quota allowance can't create a workspace. | ||
// Although relevant for all users, this test ensures that the prebuilds system user | ||
// cannot create workspaces in an organization for which it has exhausted its quota. | ||
t.Run("ZeroQuota", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While it's good to have this test, it would be best to explicitly test this scenario with the prebuilds reconciler.
-- Filter by system type | ||
AND (@include_system::bool OR NOT is_system) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we adding this filter here?
@@ -44,37 +44,74 @@ func (s StoreMembershipReconciler) ReconcileAll(ctx context.Context, userID uuid | |||
return xerrors.Errorf("determine prebuild organization membership: %w", err) | |||
} | |||
|
|||
systemUserMemberships := make(map[uuid.UUID]struct{}, 0) | |||
orgMemberShips := make(map[uuid.UUID]struct{}, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small nit
orgMemberShips := make(map[uuid.UUID]struct{}, 0) | |
orgMemberships := make(map[uuid.UUID]struct{}, 0) |
_, err = s.store.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ | ||
// Create a "prebuilds" group in the organization and add the system user to it | ||
// This group will have a quota of 0 by default, which users can adjust based on their needs | ||
prebuildsGroup, err := s.store.InsertGroup(ctx, database.InsertGroupParams{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, this is because this code was already running, so we might currently have prebuilds that belong to organizations without a corresponding prebuilds
group, right? Could you add this context to the function comment?
This code is a bit tricky for me to follow 🤔 just some suggestions to improve readability (use them if you think it makes sense):
- Could you update the function description to clarify that this function: 1) ensures the user is a member of each preset's organization, and 2) ensures a "prebuilds" group exists per organization and the user is a member of it?
- Would it make sense to follow a similar pattern for the
prebuilds
groups as we do for organization memberships? We could have an SQL query upfront (maybeGetGroups
) to get all the prebuilds groups the user already belongs to, then create a map similar toorgMemberShips
to avoid the "try-insert-then-get-if-exists" pattern in the loop.
require.Equal(t, "Everyone", group.Name) | ||
require.Equal(t, user.OrganizationID, group.OrganizationID) | ||
require.Contains(t, group.Members, user1.ReducedUser) | ||
require.Contains(t, group.Members, user2.ReducedUser) | ||
require.NotContains(t, group.Members, prebuildsUser.ReducedUser) | ||
require.Contains(t, group.Members, prebuildsUser.ReducedUser) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we include system users in the Everyone
group? IIUC with this change, prebuilds users will inherit the quota from this group, is that something that we want? Shouldn't it be limited to the prebuilds
group? As previously discussed, I'm not entirely convinced we should treat system users as normal users in these workflows 👀 but if there is a general consensus, I'm ok with it
preExistingMembership bool | ||
name string | ||
includePreset []bool | ||
preExistingOrgMembership []bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, thanks for clarifying! I think for test readability it makes sense to be as explicit as possible about what we're actually testing. I'd personally prefer separate test cases for each scenario, but if that's too much work to change now, no worries.
|
||
if tc.expectUserInGroup != nil && !*tc.expectUserInGroup { | ||
// Check that the system user is NOT a member of the prebuilds group | ||
groupMembers, err := db.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case, I think we should always assert that the prebuilds user is a member of the prebuilds
group (regardless of the test case parameters), since that should be an invariant of the function.
// ZeroQuota tests that a user with a zero quota allowance can't create a workspace. | ||
// Although relevant for all users, this test ensures that the prebuilds system user | ||
// cannot create workspaces in an organization for which it has exhausted its quota. | ||
t.Run("ZeroQuota", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice tests 👍
workspaceZeroCost := coderdtest.CreateWorkspace(t, client, templateZeroCost.ID) | ||
buildZeroCost := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceZeroCost.LatestBuild.ID) | ||
|
||
// Verify the build failed due to quota |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Verify the build failed due to quota |
closes #18274
This pull request makes system users visible in various group related queries so that they can be added to groups. This allows system user quotas to be configured. System users are still ignored in certain queries, such as when license seat consumption is determined.
Summary by CodeRabbit