refactor: consolidate template and workspace acl validation (#19192)

This commit is contained in:
ケイラ
2025-08-07 10:14:58 -06:00
committed by GitHub
parent 02de067d46
commit 26458cd6f0
14 changed files with 535 additions and 103 deletions
+130
View File
@@ -0,0 +1,130 @@
package acl
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/codersdk"
)
type UpdateValidator[Role codersdk.WorkspaceRole | codersdk.TemplateRole] interface {
// Users should return a map from user UUIDs (as strings) to the role they
// are being assigned. Additionally, it should return a string that will be
// used as the field name for the ValidationErrors returned from Validate.
Users() (map[string]Role, string)
// Groups should return a map from group UUIDs (as strings) to the role they
// are being assigned. Additionally, it should return a string that will be
// used as the field name for the ValidationErrors returned from Validate.
Groups() (map[string]Role, string)
// ValidateRole should return an error that will be used in the
// ValidationError if the role is invalid for the corresponding resource type.
ValidateRole(role Role) error
}
func Validate[Role codersdk.WorkspaceRole | codersdk.TemplateRole](
ctx context.Context,
db database.Store,
v UpdateValidator[Role],
) []codersdk.ValidationError {
// nolint:gocritic // Validate requires full read access to users and groups
ctx = dbauthz.AsSystemRestricted(ctx)
var validErrs []codersdk.ValidationError
groupRoles, groupsField := v.Groups()
groupIDs := make([]uuid.UUID, 0, len(groupRoles))
for idStr, role := range groupRoles {
// Validate the provided role names
if err := v.ValidateRole(role); err != nil {
validErrs = append(validErrs, codersdk.ValidationError{
Field: groupsField,
Detail: err.Error(),
})
}
// Validate that the IDs are UUIDs
id, err := uuid.Parse(idStr)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{
Field: groupsField,
Detail: fmt.Sprintf("%v is not a valid UUID.", idStr),
})
continue
}
// Don't check if the ID exists when setting the role to
// WorkspaceRoleDeleted or TemplateRoleDeleted. They might've existing at
// some point and got deleted. If we report that as an error here then they
// can't be removed.
if string(role) == "" {
continue
}
groupIDs = append(groupIDs, id)
}
// Validate that the groups exist
groupValidation, err := db.ValidateGroupIDs(ctx, groupIDs)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{
Field: groupsField,
Detail: fmt.Sprintf("failed to validate group IDs: %v", err.Error()),
})
}
if !groupValidation.Ok {
for _, id := range groupValidation.InvalidGroupIds {
validErrs = append(validErrs, codersdk.ValidationError{
Field: groupsField,
Detail: fmt.Sprintf("group with ID %v does not exist", id),
})
}
}
userRoles, usersField := v.Users()
userIDs := make([]uuid.UUID, 0, len(userRoles))
for idStr, role := range userRoles {
// Validate the provided role names
if err := v.ValidateRole(role); err != nil {
validErrs = append(validErrs, codersdk.ValidationError{
Field: usersField,
Detail: err.Error(),
})
}
// Validate that the IDs are UUIDs
id, err := uuid.Parse(idStr)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{
Field: usersField,
Detail: fmt.Sprintf("%v is not a valid UUID.", idStr),
})
continue
}
// Don't check if the ID exists when setting the role to
// WorkspaceRoleDeleted or TemplateRoleDeleted. They might've existing at
// some point and got deleted. If we report that as an error here then they
// can't be removed.
if string(role) == "" {
continue
}
userIDs = append(userIDs, id)
}
// Validate that the groups exist
userValidation, err := db.ValidateUserIDs(ctx, userIDs)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{
Field: usersField,
Detail: fmt.Sprintf("failed to validate user IDs: %v", err.Error()),
})
}
if !userValidation.Ok {
for _, id := range userValidation.InvalidUserIds {
validErrs = append(validErrs, codersdk.ValidationError{
Field: usersField,
Detail: fmt.Sprintf("user with ID %v does not exist", id),
})
}
}
return validErrs
}
+91
View File
@@ -0,0 +1,91 @@
package acl_test
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/rbac/acl"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
)
func TestOK(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
o := dbgen.Organization(t, db, database.Organization{})
g := dbgen.Group(t, db, database.Group{OrganizationID: o.ID})
u := dbgen.User(t, db, database.User{})
ctx := testutil.Context(t, testutil.WaitShort)
update := codersdk.UpdateWorkspaceACL{
UserRoles: map[string]codersdk.WorkspaceRole{
u.ID.String(): codersdk.WorkspaceRoleAdmin,
// An unknown ID is allowed if and only if the specified role is either
// codersdk.WorkspaceRoleDeleted or codersdk.TemplateRoleDeleted.
uuid.NewString(): codersdk.WorkspaceRoleDeleted,
},
GroupRoles: map[string]codersdk.WorkspaceRole{
g.ID.String(): codersdk.WorkspaceRoleAdmin,
// An unknown ID is allowed if and only if the specified role is either
// codersdk.WorkspaceRoleDeleted or codersdk.TemplateRoleDeleted.
uuid.NewString(): codersdk.WorkspaceRoleDeleted,
},
}
errors := acl.Validate(ctx, db, coderd.WorkspaceACLUpdateValidator(update))
require.Empty(t, errors)
}
func TestDeniesUnknownIDs(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)
update := codersdk.UpdateWorkspaceACL{
UserRoles: map[string]codersdk.WorkspaceRole{
uuid.NewString(): codersdk.WorkspaceRoleAdmin,
},
GroupRoles: map[string]codersdk.WorkspaceRole{
uuid.NewString(): codersdk.WorkspaceRoleAdmin,
},
}
errors := acl.Validate(ctx, db, coderd.WorkspaceACLUpdateValidator(update))
require.Len(t, errors, 2)
require.Equal(t, errors[0].Field, "group_roles")
require.ErrorContains(t, errors[0], "does not exist")
require.Equal(t, errors[1].Field, "user_roles")
require.ErrorContains(t, errors[1], "does not exist")
}
func TestDeniesUnknownRolesAndInvalidIDs(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitShort)
update := codersdk.UpdateWorkspaceACL{
UserRoles: map[string]codersdk.WorkspaceRole{
"Quifrey": "level 5",
},
GroupRoles: map[string]codersdk.WorkspaceRole{
"apprentices": "level 2",
},
}
errors := acl.Validate(ctx, db, coderd.WorkspaceACLUpdateValidator(update))
require.Len(t, errors, 4)
require.Equal(t, errors[0].Field, "group_roles")
require.ErrorContains(t, errors[0], "role \"level 2\" is not a valid workspace role")
require.Equal(t, errors[1].Field, "group_roles")
require.ErrorContains(t, errors[1], "not a valid UUID")
require.Equal(t, errors[2].Field, "user_roles")
require.ErrorContains(t, errors[2], "role \"level 5\" is not a valid workspace role")
require.Equal(t, errors[3].Field, "user_roles")
require.ErrorContains(t, errors[3], "not a valid UUID")
}