diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 92b0a8a8accc2..6c862c6e11103 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -51,6 +51,8 @@ type Request[T Auditable] struct { Action database.AuditAction } +// UpdateOrganizationID can be used if the organization ID is not known +// at the initiation of an audit log request. func (r *Request[T]) UpdateOrganizationID(id uuid.UUID) { r.params.OrganizationID = id } diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 643a1dd59d70c..9a1640e620d31 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -1064,25 +1064,6 @@ func (w WorkspaceAgentWaiter) Wait() []codersdk.WorkspaceResource { // CreateWorkspace creates a workspace for the user and template provided. // A random name is generated for it. // To customize the defaults, pass a mutator func. -// -// Deprecated: Use CreateWorkspace. -func CreateWorkspaceByOrganization(t testing.TB, client *codersdk.Client, organization uuid.UUID, templateID uuid.UUID, mutators ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace { - t.Helper() - req := codersdk.CreateWorkspaceRequest{ - TemplateID: templateID, - Name: RandomUsername(t), - AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"), - TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()), - AutomaticUpdates: codersdk.AutomaticUpdatesNever, - } - for _, mutator := range mutators { - mutator(&req) - } - workspace, err := client.CreateWorkspace(context.Background(), organization, codersdk.Me, req) - require.NoError(t, err) - return workspace -} - func CreateWorkspace(t testing.TB, client *codersdk.Client, templateID uuid.UUID, mutators ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace { t.Helper() req := codersdk.CreateWorkspaceRequest{ diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 584651298f023..901e3723964bd 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -464,7 +464,7 @@ func createWorkspace( templateID := req.TemplateID if templateID == uuid.Nil { templateVersion, err := api.Database.GetTemplateVersionByID(ctx, req.TemplateVersionID) - if errors.Is(err, sql.ErrNoRows) { + if httpapi.Is404Error(err) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Template version %q doesn't exist.", templateID.String()), Validations: []codersdk.ValidationError{{ @@ -498,7 +498,7 @@ func createWorkspace( } template, err := api.Database.GetTemplateByID(ctx, templateID) - if errors.Is(err, sql.ErrNoRows) { + if httpapi.Is404Error(err) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Template %q doesn't exist.", templateID.String()), Validations: []codersdk.ValidationError{{ @@ -521,8 +521,18 @@ func createWorkspace( }) return } + + // Update audit log's organization auditReq.UpdateOrganizationID(template.OrganizationID) + // Do this upfront to save work. If this fails, the rest of the work + // would be wasted. + if !api.Authorize(r, policy.ActionCreate, + rbac.ResourceWorkspace.InOrg(template.OrganizationID).WithOwner(owner.ID.String())) { + httpapi.ResourceNotFound(rw) + return + } + templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template) if templateAccessControl.IsDeprecated() { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -578,7 +588,7 @@ func createWorkspace( // read other workspaces. Ideally we check the error on create and look for // a postgres conflict error. workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{ - OwnerID: user.ID, + OwnerID: owner.ID, Name: req.Name, }) if err == nil { @@ -611,7 +621,7 @@ func createWorkspace( ID: uuid.New(), CreatedAt: now, UpdatedAt: now, - OwnerID: user.ID, + OwnerID: owner.ID, OrganizationID: template.OrganizationID, TemplateID: template.ID, Name: req.Name, @@ -679,8 +689,8 @@ func createWorkspace( ProvisionerJob: *provisionerJob, QueuePosition: 0, }, - user.Username, - user.AvatarURL, + owner.Username, + owner.AvatarURL, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -702,8 +712,8 @@ func createWorkspace( workspace, apiBuild, template, - user.Username, - user.AvatarURL, + owner.Username, + owner.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 8d8a9304d60fd..277d41cf9ae52 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -475,19 +475,8 @@ func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, n // CreateWorkspace creates a new workspace for the template specified. // // Deprecated: Use CreateUserWorkspace instead. -func (c *Client) CreateWorkspace(ctx context.Context, organizationID uuid.UUID, user string, request CreateWorkspaceRequest) (Workspace, error) { - res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/members/%s/workspaces", organizationID, user), request) - if err != nil { - return Workspace{}, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusCreated { - return Workspace{}, ReadBodyAsError(res) - } - - var workspace Workspace - return workspace, json.NewDecoder(res.Body).Decode(&workspace) +func (c *Client) CreateWorkspace(ctx context.Context, _ uuid.UUID, user string, request CreateWorkspaceRequest) (Workspace, error) { + return c.CreateUserWorkspace(ctx, user, request) } // CreateUserWorkspace creates a new workspace for the template specified. diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 0f76da78c3da2..0b758e0491e1b 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -135,6 +135,58 @@ func TestCreateWorkspace(t *testing.T) { _, err = client1.CreateWorkspace(ctx, user.OrganizationID, user1.ID.String(), req) require.Error(t, err) }) + + t.Run("NoTemplateAccess", func(t *testing.T) { + t.Parallel() + ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, + }, + }) + + templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) + user, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleMember()) + + version := coderdtest.CreateTemplateVersion(t, templateAdmin, owner.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID) + template := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Remove everyone access + err := templateAdmin.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ + UserPerms: map[string]codersdk.TemplateRole{}, + GroupPerms: map[string]codersdk.TemplateRole{ + owner.OrganizationID.String(): codersdk.TemplateRoleDeleted, + }, + }) + require.NoError(t, err) + + // Test "everyone" access is revoked to the regular user + _, err = user.Template(ctx, template.ID) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + + _, err = user.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{ + TemplateID: template.ID, + Name: "random", + AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"), + TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()), + AutomaticUpdates: codersdk.AutomaticUpdatesNever, + }) + require.Error(t, err) + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + require.Contains(t, apiErr.Message, "doesn't exist") + }) } func TestCreateUserWorkspace(t *testing.T) {