mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: consolidate template and workspace acl validation (#19192)
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user