chore: implement 'use' verb to template object, read has less scope now (#16075)

Template `use` is now a verb.
- Template admins can `use` all templates (org template admins same in
org)
- Members get the `use` perm from the `everyone` group in the
`group_acl`.
This commit is contained in:
Steven Masley
2025-01-17 11:55:41 -06:00
committed by GitHub
parent 3217cb85f6
commit f34e6fd92c
17 changed files with 128 additions and 28 deletions

View File

@ -17,6 +17,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/render"
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
"github.com/coder/coder/v2/codersdk"
@ -694,3 +695,13 @@ func MatchedProvisioners(provisionerDaemons []database.ProvisionerDaemon, now ti
}
return matched
}
func TemplateRoleActions(role codersdk.TemplateRole) []policy.Action {
switch role {
case codersdk.TemplateRoleAdmin:
return []policy.Action{policy.WildcardSymbol}
case codersdk.TemplateRoleUse:
return []policy.Action{policy.ActionRead, policy.ActionUse}
}
return []policy.Action{}
}

View File

@ -3169,6 +3169,14 @@ func (q *querier) InsertUserLink(ctx context.Context, arg database.InsertUserLin
func (q *querier) InsertWorkspace(ctx context.Context, arg database.InsertWorkspaceParams) (database.WorkspaceTable, error) {
obj := rbac.ResourceWorkspace.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID)
tpl, err := q.GetTemplateByID(ctx, arg.TemplateID)
if err != nil {
return database.WorkspaceTable{}, xerrors.Errorf("verify template by id: %w", err)
}
if err := q.authorizeContext(ctx, policy.ActionUse, tpl); err != nil {
return database.WorkspaceTable{}, xerrors.Errorf("use template for workspace: %w", err)
}
return insert(q.log, q.auth, obj, q.db.InsertWorkspace)(ctx, arg)
}

View File

@ -2459,7 +2459,7 @@ func (s *MethodTestSuite) TestWorkspace() {
OrganizationID: o.ID,
AutomaticUpdates: database.AutomaticUpdatesNever,
TemplateID: tpl.ID,
}).Asserts(rbac.ResourceWorkspace.WithOwner(u.ID.String()).InOrg(o.ID), policy.ActionCreate)
}).Asserts(tpl, policy.ActionRead, tpl, policy.ActionUse, rbac.ResourceWorkspace.WithOwner(u.ID.String()).InOrg(o.ID), policy.ActionCreate)
}))
s.Run("Start/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})

View File

@ -20,12 +20,13 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/testutil"
)
@ -75,7 +76,7 @@ func Template(t testing.TB, db database.Store, seed database.Template) database.
if seed.GroupACL == nil {
// By default, all users in the organization can read the template.
seed.GroupACL = database.TemplateACL{
seed.OrganizationID.String(): []policy.Action{policy.ActionRead},
seed.OrganizationID.String(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
}
}
if seed.UserACL == nil {

View File

@ -0,0 +1,5 @@
UPDATE
templates
SET
group_acl = replace(group_acl::text, '["read", "use"]', '["read"]')::jsonb,
user_acl = replace(user_acl::text, '["read", "use"]', '["read"]')::jsonb

View File

@ -0,0 +1,12 @@
-- With the "use" verb now existing for templates, we need to update the acl's to
-- include "use" where the permissions set ["read"] is present.
-- The other permission set is ["*"] which is unaffected.
UPDATE
templates
SET
-- Instead of trying to write a complicated SQL query to update the JSONB
-- object, a string replace is much simpler and easier to understand.
-- Both pieces of text are JSON arrays, so this safe to do.
group_acl = replace(group_acl::text, '["read"]', '["read", "use"]')::jsonb,
user_acl = replace(user_acl::text, '["read"]', '["read", "use"]')::jsonb

View File

@ -23,12 +23,12 @@ import (
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbrollup"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/workspaceapps"
"github.com/coder/coder/v2/coderd/workspacestats"
"github.com/coder/coder/v2/codersdk"
@ -675,7 +675,7 @@ func TestTemplateInsights_Golden(t *testing.T) {
OrganizationID: firstUser.OrganizationID,
CreatedBy: firstUser.UserID,
GroupACL: database.TemplateACL{
firstUser.OrganizationID.String(): []policy.Action{policy.ActionRead},
firstUser.OrganizationID.String(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
},
})
err := db.UpdateTemplateVersionByID(context.Background(), database.UpdateTemplateVersionByIDParams{
@ -1573,7 +1573,7 @@ func TestUserActivityInsights_Golden(t *testing.T) {
OrganizationID: firstUser.OrganizationID,
CreatedBy: firstUser.UserID,
GroupACL: database.TemplateACL{
firstUser.OrganizationID.String(): []policy.Action{policy.ActionRead},
firstUser.OrganizationID.String(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
},
})
err := db.UpdateTemplateVersionByID(context.Background(), database.UpdateTemplateVersionByIDParams{

View File

@ -256,6 +256,7 @@ var (
// - "ActionDelete" :: delete a template
// - "ActionRead" :: read template
// - "ActionUpdate" :: update a template
// - "ActionUse" :: use the template to initially create a workspace, then workspace lifecycle permissions take over
// - "ActionViewInsights" :: view insights
ResourceTemplate = Object{
Type: "template",

View File

@ -133,8 +133,8 @@ var RBACPermissions = map[string]PermissionDefinition{
},
"template": {
Actions: map[Action]ActionDefinition{
ActionCreate: actDef("create a template"),
// TODO: Create a use permission maybe?
ActionCreate: actDef("create a template"),
ActionUse: actDef("use the template to initially create a workspace, then workspace lifecycle permissions take over"),
ActionRead: actDef("read template"),
ActionUpdate: actDef("update a template"),
ActionDelete: actDef("delete a template"),

View File

@ -318,7 +318,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Identifier: RoleTemplateAdmin(),
DisplayName: "Template Admin",
Site: Permissions(map[string][]policy.Action{
ResourceTemplate.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights},
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
// CRUD all files, even those they did not upload.
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
ResourceWorkspace.Type: {policy.ActionRead},
@ -476,7 +476,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Site: []Permission{},
Org: map[string][]Permission{
organizationID.String(): Permissions(map[string][]policy.Action{
ResourceTemplate.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete, policy.ActionViewInsights},
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
ResourceWorkspace.Type: {policy.ActionRead},
// Assigning template perms requires this permission.

View File

@ -232,6 +232,17 @@ func TestRolePermissions(t *testing.T) {
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin, orgMemberMe},
},
},
{
Name: "UseTemplates",
Actions: []policy.Action{policy.ActionUse},
Resource: rbac.ResourceTemplate.InOrg(orgID).WithGroupACL(map[string][]policy.Action{
groupID.String(): {policy.ActionUse},
}),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, groupMemberMe},
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin, orgMemberMe},
},
},
{
Name: "Files",
Actions: []policy.Action{policy.ActionCreate},

View File

@ -14,6 +14,7 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
@ -382,7 +383,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
if !createTemplate.DisableEveryoneGroupAccess {
// The organization ID is used as the group ID for the everyone group
// in this organization.
defaultsGroups[organization.ID.String()] = []policy.Action{policy.ActionRead}
defaultsGroups[organization.ID.String()] = db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse)
}
err = api.Database.InTx(func(tx database.Store) error {
now := dbtime.Now()

View File

@ -525,6 +525,18 @@ func createWorkspace(
httpapi.ResourceNotFound(rw)
return
}
// The user also needs permission to use the template. At this point they have
// read perms, but not necessarily "use". This is also checked in `db.InsertWorkspace`.
// Doing this up front can save some work below if the user doesn't have permission.
if !api.Authorize(r, policy.ActionUse, template) {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: fmt.Sprintf("Unauthorized access to use the template %q.", template.Name),
Detail: "Although you are able to view the template, you are unable to create a workspace using it. " +
"Please contact an administrator about your permissions if you feel this is an error.",
Validations: nil,
})
return
}
templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template)
if templateAccessControl.IsDeprecated() {