mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: expose Everyone group through UI (#9117)
- Allows setting quota allowances on the 'Everyone' group.
This commit is contained in:
+48
-27
@@ -46,9 +46,9 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == database.AllUsersGroup {
|
||||
if req.Name == database.EveryoneGroup {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("%q is a reserved keyword and cannot be used for a group name.", database.AllUsersGroup),
|
||||
Message: fmt.Sprintf("%q is a reserved keyword and cannot be used for a group name.", database.EveryoneGroup),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -102,36 +102,56 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
defer commitAudit()
|
||||
|
||||
currentMembers, currentMembersErr := api.Database.GetGroupMembers(ctx, group.ID)
|
||||
if currentMembersErr != nil {
|
||||
httpapi.InternalServerError(rw, currentMembersErr)
|
||||
return
|
||||
}
|
||||
|
||||
aReq.Old = group.Auditable(currentMembers)
|
||||
|
||||
var req codersdk.PatchGroupRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" && req.Name == database.AllUsersGroup {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("%q is a reserved group name!", database.AllUsersGroup),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// If the name matches the existing group name pretend we aren't
|
||||
// updating the name at all.
|
||||
if req.Name == group.Name {
|
||||
req.Name = ""
|
||||
}
|
||||
|
||||
if group.IsEveryone() && req.Name != "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Cannot rename the %q group!", database.EveryoneGroup),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if group.IsEveryone() && (req.DisplayName != nil && *req.DisplayName != "") {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Cannot update the Display Name for the %q group!", database.EveryoneGroup),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == database.EveryoneGroup {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("%q is a reserved group name!", database.EveryoneGroup),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
users := make([]string, 0, len(req.AddUsers)+len(req.RemoveUsers))
|
||||
users = append(users, req.AddUsers...)
|
||||
users = append(users, req.RemoveUsers...)
|
||||
|
||||
if len(users) > 0 && group.Name == database.EveryoneGroup {
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: fmt.Sprintf("Cannot add or remove users from the %q group!", database.EveryoneGroup),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
currentMembers, err := api.Database.GetGroupMembers(ctx, group.ID)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
aReq.Old = group.Auditable(currentMembers)
|
||||
|
||||
for _, id := range users {
|
||||
if _, err := uuid.Parse(id); err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
@@ -156,6 +176,7 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Name != "" && req.Name != group.Name {
|
||||
_, err := api.Database.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{
|
||||
OrganizationID: group.OrganizationID,
|
||||
@@ -169,8 +190,7 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
err := api.Database.InTx(func(tx database.Store) error {
|
||||
var err error
|
||||
err = database.ReadModifyUpdate(api.Database, func(tx database.Store) error {
|
||||
group, err = tx.GetGroupByID(ctx, group.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get group by ID: %w", err)
|
||||
@@ -230,7 +250,8 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
})
|
||||
|
||||
if database.IsUniqueViolation(err) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Cannot add the same user to a group twice!",
|
||||
@@ -283,6 +304,13 @@ func (api *API) deleteGroup(rw http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
defer commitAudit()
|
||||
|
||||
if group.Name == database.EveryoneGroup {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("%q is a reserved group and cannot be deleted!", database.EveryoneGroup),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
groupMembers, getMembersErr := api.Database.GetGroupMembers(ctx, group.ID)
|
||||
if getMembersErr != nil {
|
||||
httpapi.InternalServerError(rw, getMembersErr)
|
||||
@@ -291,13 +319,6 @@ func (api *API) deleteGroup(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
aReq.Old = group.Auditable(groupMembers)
|
||||
|
||||
if group.Name == database.AllUsersGroup {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("%q is a reserved group and cannot be deleted!", database.AllUsersGroup),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := api.Database.DeleteGroupByID(ctx, group.ID)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestCreateGroup(t *testing.T) {
|
||||
}})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: database.AllUsersGroup,
|
||||
Name: database.EveryoneGroup,
|
||||
})
|
||||
require.Error(t, err)
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
@@ -399,7 +399,7 @@ func TestPatchGroup(t *testing.T) {
|
||||
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("allUsers", func(t *testing.T) {
|
||||
t.Run("ReservedName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
@@ -414,13 +414,114 @@ func TestPatchGroup(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
|
||||
Name: database.AllUsersGroup,
|
||||
Name: database.EveryoneGroup,
|
||||
})
|
||||
require.Error(t, err)
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Everyone", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NoUpdateName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
}})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
|
||||
Name: "hi",
|
||||
})
|
||||
require.Error(t, err)
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoUpdateDisplayName", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
}})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
|
||||
DisplayName: ptr.Ref("hi"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoAddUsers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
}})
|
||||
_, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
|
||||
AddUsers: []string{user2.ID.String()},
|
||||
})
|
||||
require.Error(t, err)
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, http.StatusForbidden, cerr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoRmUsers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
}})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
|
||||
RemoveUsers: []string{user.UserID.String()},
|
||||
})
|
||||
require.Error(t, err)
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, http.StatusForbidden, cerr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("UpdateQuota", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
}})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
group, err := client.Group(ctx, user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 0, group.QuotaAllowance)
|
||||
|
||||
expectedQuota := 123
|
||||
group, err = client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
|
||||
QuotaAllowance: ptr.Ref(expectedQuota),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedQuota, group.QuotaAllowance)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: test auth.
|
||||
@@ -591,13 +692,17 @@ func TestGroup(t *testing.T) {
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
}})
|
||||
_, user1 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||
_, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
// The 'Everyone' group always has an ID that matches the organization ID.
|
||||
group, err := client.Group(ctx, user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, group.Members, 0)
|
||||
require.Len(t, group.Members, 3)
|
||||
require.Equal(t, "Everyone", group.Name)
|
||||
require.Equal(t, user.OrganizationID, group.OrganizationID)
|
||||
require.Contains(t, group.Members, user1, user2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -641,7 +746,8 @@ func TestGroups(t *testing.T) {
|
||||
|
||||
groups, err := client.GroupsByOrganization(ctx, user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, groups, 2)
|
||||
// 'Everyone' group + 2 custom groups.
|
||||
require.Len(t, groups, 3)
|
||||
require.Contains(t, groups, group1)
|
||||
require.Contains(t, groups, group2)
|
||||
})
|
||||
|
||||
@@ -373,8 +373,7 @@ func TestTemplateACL(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, acl.Groups, 1)
|
||||
// We don't return members for the 'Everyone' group.
|
||||
require.Len(t, acl.Groups[0].Members, 0)
|
||||
require.Len(t, acl.Groups[0].Members, 2)
|
||||
require.Len(t, acl.Users, 0)
|
||||
})
|
||||
|
||||
|
||||
@@ -525,7 +525,7 @@ func TestGroupSync(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, group := range orgGroups {
|
||||
if slice.Contains(tc.initialOrgGroups, group.Name) {
|
||||
if slice.Contains(tc.initialOrgGroups, group.Name) || group.IsEveryone() {
|
||||
require.Equal(t, group.Source, codersdk.GroupSourceUser)
|
||||
} else {
|
||||
require.Equal(t, group.Source, codersdk.GroupSourceOIDC)
|
||||
@@ -543,6 +543,7 @@ func TestGroupSync(t *testing.T) {
|
||||
}
|
||||
delete(orgGroupsMap, expected)
|
||||
}
|
||||
delete(orgGroupsMap, database.EveryoneGroup)
|
||||
require.Empty(t, orgGroupsMap, "unexpected groups found")
|
||||
|
||||
expectedUserGroups := make(map[string]struct{})
|
||||
@@ -554,7 +555,9 @@ func TestGroupSync(t *testing.T) {
|
||||
userInGroup := slice.ContainsCompare(group.Members, codersdk.User{Email: user.Email}, func(a, b codersdk.User) bool {
|
||||
return a.Email == b.Email
|
||||
})
|
||||
if _, ok := expectedUserGroups[group.Name]; ok {
|
||||
if group.IsEveryone() {
|
||||
require.True(t, userInGroup, "user cannot be removed from 'Everyone' group")
|
||||
} else if _, ok := expectedUserGroups[group.Name]; ok {
|
||||
require.Truef(t, userInGroup, "user should be in group %s", group.Name)
|
||||
} else {
|
||||
require.Falsef(t, userInGroup, "user should not be in group %s", group.Name)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/util/ptr"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/enterprise/coderd/license"
|
||||
@@ -53,7 +54,14 @@ func TestWorkspaceQuota(t *testing.T) {
|
||||
|
||||
verifyQuota(ctx, t, client, 0, 0)
|
||||
|
||||
// Add user to two groups, granting them a total budget of 3.
|
||||
// Patch the 'Everyone' group to verify its quota allowance is being accounted for.
|
||||
_, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
|
||||
QuotaAllowance: ptr.Ref(1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyQuota(ctx, t, client, 0, 1)
|
||||
|
||||
// Add user to two groups, granting them a total budget of 4.
|
||||
group1, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "test-1",
|
||||
QuotaAllowance: 1,
|
||||
@@ -76,7 +84,7 @@ func TestWorkspaceQuota(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyQuota(ctx, t, client, 0, 3)
|
||||
verifyQuota(ctx, t, client, 0, 4)
|
||||
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
@@ -105,7 +113,7 @@ func TestWorkspaceQuota(t *testing.T) {
|
||||
|
||||
// Spin up three workspaces fine
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := 0; i < 4; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -115,14 +123,14 @@ func TestWorkspaceQuota(t *testing.T) {
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
verifyQuota(ctx, t, client, 3, 3)
|
||||
verifyQuota(ctx, t, client, 4, 4)
|
||||
|
||||
// Next one must fail
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Consumed shouldn't bump
|
||||
verifyQuota(ctx, t, client, 3, 3)
|
||||
verifyQuota(ctx, t, client, 4, 4)
|
||||
require.Equal(t, codersdk.WorkspaceStatusFailed, build.Status)
|
||||
require.Contains(t, build.Job.Error, "quota")
|
||||
|
||||
@@ -138,7 +146,7 @@ func TestWorkspaceQuota(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
verifyQuota(ctx, t, client, 2, 3)
|
||||
verifyQuota(ctx, t, client, 3, 4)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -146,7 +154,7 @@ func TestWorkspaceQuota(t *testing.T) {
|
||||
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
verifyQuota(ctx, t, client, 3, 3)
|
||||
verifyQuota(ctx, t, client, 4, 4)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user