mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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},
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -86,7 +86,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||
ResourceReplicas: {ActionRead},
|
||||
ResourceSystem: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceTailnetCoordinator: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceTemplate: {ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionViewInsights},
|
||||
ResourceTemplate: {ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionUse, ActionViewInsights},
|
||||
ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal},
|
||||
ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
|
||||
ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate},
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@@ -222,7 +223,7 @@ func (api *API) patchTemplateACL(rw http.ResponseWriter, r *http.Request) {
|
||||
delete(template.UserACL, id)
|
||||
continue
|
||||
}
|
||||
template.UserACL[id] = convertSDKTemplateRole(role)
|
||||
template.UserACL[id] = db2sdk.TemplateRoleActions(role)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ func (api *API) patchTemplateACL(rw http.ResponseWriter, r *http.Request) {
|
||||
delete(template.GroupACL, id)
|
||||
continue
|
||||
}
|
||||
template.GroupACL[id] = convertSDKTemplateRole(role)
|
||||
template.GroupACL[id] = db2sdk.TemplateRoleActions(role)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,8 +317,8 @@ func convertTemplateUsers(tus []database.TemplateUser, orgIDsByUserIDs map[uuid.
|
||||
}
|
||||
|
||||
func validateTemplateRole(role codersdk.TemplateRole) error {
|
||||
actions := convertSDKTemplateRole(role)
|
||||
if actions == nil && role != codersdk.TemplateRoleDeleted {
|
||||
actions := db2sdk.TemplateRoleActions(role)
|
||||
if len(actions) == 0 && role != codersdk.TemplateRoleDeleted {
|
||||
return xerrors.Errorf("role %q is not a valid Template role", role)
|
||||
}
|
||||
|
||||
@@ -326,7 +327,7 @@ func validateTemplateRole(role codersdk.TemplateRole) error {
|
||||
|
||||
func convertToTemplateRole(actions []policy.Action) codersdk.TemplateRole {
|
||||
switch {
|
||||
case len(actions) == 1 && actions[0] == policy.ActionRead:
|
||||
case len(actions) == 2 && slice.SameElements(actions, []policy.Action{policy.ActionUse, policy.ActionRead}):
|
||||
return codersdk.TemplateRoleUse
|
||||
case len(actions) == 1 && actions[0] == policy.WildcardSymbol:
|
||||
return codersdk.TemplateRoleAdmin
|
||||
@@ -335,17 +336,6 @@ func convertToTemplateRole(actions []policy.Action) codersdk.TemplateRole {
|
||||
return ""
|
||||
}
|
||||
|
||||
func convertSDKTemplateRole(role codersdk.TemplateRole) []policy.Action {
|
||||
switch role {
|
||||
case codersdk.TemplateRoleAdmin:
|
||||
return []policy.Action{policy.WildcardSymbol}
|
||||
case codersdk.TemplateRoleUse:
|
||||
return []policy.Action{policy.ActionRead}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO move to api.RequireFeatureMW when we are OK with changing the behavior.
|
||||
func (api *API) templateRBACEnabledMW(next http.Handler) http.Handler {
|
||||
return api.RequireFeatureMW(codersdk.FeatureTemplateRBAC)(next)
|
||||
|
||||
@@ -193,6 +193,53 @@ func TestCreateWorkspace(t *testing.T) {
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
require.Contains(t, apiErr.Message, "doesn't exist")
|
||||
})
|
||||
|
||||
// Auditors cannot "use" templates, they can only read them.
|
||||
t.Run("Auditor", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
owner, first := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
codersdk.FeatureMultipleOrganizations: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// A member of the org as an auditor
|
||||
auditor, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleAuditor())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Given: a template with a version without the "use" permission on everyone
|
||||
version := coderdtest.CreateTemplateVersion(t, owner, first.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, owner, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, owner, first.OrganizationID, version.ID)
|
||||
|
||||
//nolint:gocritic // This should be run as the owner user.
|
||||
err := owner.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
|
||||
UserPerms: nil,
|
||||
GroupPerms: map[string]codersdk.TemplateRole{
|
||||
first.OrganizationID.String(): codersdk.TemplateRoleDeleted,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = auditor.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: template.ID,
|
||||
Name: "workspace",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
|
||||
require.Contains(t, apiErr.Message, "Unauthorized access to use the template")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateUserWorkspace(t *testing.T) {
|
||||
|
||||
@@ -144,6 +144,7 @@ export const RBACResourceActions: Partial<
|
||||
delete: "delete a template",
|
||||
read: "read template",
|
||||
update: "update a template",
|
||||
use: "use the template to initially create a workspace, then workspace lifecycle permissions take over",
|
||||
view_insights: "view insights",
|
||||
},
|
||||
user: {
|
||||
|
||||
Reference in New Issue
Block a user