mirror of https://github.com/coder/coder.git
feat: Add RBAC to /workspace endpoints (#1566)
* feat: Add RBAC to /workspace endpoints
This commit is contained in:
parent
f3fe2a08ce
commit
c034e8389e
|
@ -149,7 +149,7 @@ func New(options *Options) (http.Handler, func()) {
|
|||
r.Get("/", api.workspacesByOrganization)
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractUserParam(options.Database))
|
||||
r.Get("/{workspace}", api.workspaceByOwnerAndName)
|
||||
r.Get("/{workspacename}", api.workspaceByOwnerAndName)
|
||||
r.Get("/", api.workspacesByOwner)
|
||||
})
|
||||
})
|
||||
|
@ -237,8 +237,6 @@ func New(options *Options) (http.Handler, func()) {
|
|||
r.Route("/password", func(r chi.Router) {
|
||||
r.Put("/", api.putUserPassword)
|
||||
})
|
||||
r.Get("/organizations", api.organizationsByUser)
|
||||
r.Post("/organizations", api.postOrganizationsByUser)
|
||||
// These roles apply to the site wide permissions.
|
||||
r.Put("/roles", api.putUserRoles)
|
||||
r.Get("/roles", api.userRoles)
|
||||
|
@ -316,6 +314,7 @@ func New(options *Options) (http.Handler, func()) {
|
|||
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
authRolesMiddleware,
|
||||
httpmw.ExtractWorkspaceBuildParam(options.Database),
|
||||
httpmw.ExtractWorkspaceParam(options.Database),
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@ package coderd_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -48,13 +49,18 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, admin.OrganizationID, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Always fail auth from this point forward
|
||||
authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil)
|
||||
|
||||
// Some quick reused objects
|
||||
workspaceRBACObj := rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String())
|
||||
|
||||
// skipRoutes allows skipping routes from being checked.
|
||||
type routeCheck struct {
|
||||
NoAuthorize bool
|
||||
AssertAction rbac.Action
|
||||
AssertObject rbac.Object
|
||||
StatusCode int
|
||||
}
|
||||
|
@ -84,13 +90,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
|
||||
|
||||
// TODO: @emyrk these need to be fixed by adding authorize calls
|
||||
"GET:/api/v2/workspaceresources/{workspaceresource}": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspacebuilds/{workspacebuild}": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspacebuilds/{workspacebuild}/logs": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspacebuilds/{workspacebuild}/resources": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspacebuilds/{workspacebuild}/state": {NoAuthorize: true},
|
||||
"PATCH:/api/v2/workspacebuilds/{workspacebuild}/cancel": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceresources/{workspaceresource}": {NoAuthorize: true},
|
||||
|
||||
"GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true},
|
||||
|
||||
|
@ -123,15 +123,9 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
|
||||
"POST:/api/v2/users/{user}/organizations": {NoAuthorize: true},
|
||||
|
||||
"GET:/api/v2/workspaces/{workspace}": {NoAuthorize: true},
|
||||
"PUT:/api/v2/workspaces/{workspace}/autostart": {NoAuthorize: true},
|
||||
"PUT:/api/v2/workspaces/{workspace}/autostop": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true},
|
||||
"POST:/api/v2/workspaces/{workspace}/builds": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true},
|
||||
|
||||
"POST:/api/v2/files": {NoAuthorize: true},
|
||||
"GET:/api/v2/files/{hash}": {NoAuthorize: true},
|
||||
"POST:/api/v2/files": {NoAuthorize: true},
|
||||
"GET:/api/v2/files/{hash}": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true},
|
||||
|
||||
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
|
||||
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(admin.OrganizationID)},
|
||||
|
@ -141,11 +135,60 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
"GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspace}": {
|
||||
AssertObject: rbac.ResourceWorkspace.InOrg(organization.ID).WithID(workspace.ID.String()).WithOwner(workspace.OwnerID.String()),
|
||||
},
|
||||
"GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {
|
||||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"GET:/api/v2/organizations/{organization}/workspaces/{user}/{workspacename}": {
|
||||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"GET:/api/v2/organizations/{organization}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
|
||||
"GET:/api/v2/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace},
|
||||
"GET:/api/v2/workspacebuilds/{workspacebuild}": {
|
||||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"GET:/api/v2/workspacebuilds/{workspacebuild}/logs": {
|
||||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"GET:/api/v2/workspaces/{workspace}/builds": {
|
||||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"GET:/api/v2/workspaces/{workspace}": {
|
||||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"PUT:/api/v2/workspaces/{workspace}/autostart": {
|
||||
AssertAction: rbac.ActionUpdate,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"PUT:/api/v2/workspaces/{workspace}/autostop": {
|
||||
AssertAction: rbac.ActionUpdate,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"PATCH:/api/v2/workspacebuilds/{workspacebuild}/cancel": {
|
||||
AssertAction: rbac.ActionUpdate,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"GET:/api/v2/workspacebuilds/{workspacebuild}/resources": {
|
||||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"GET:/api/v2/workspacebuilds/{workspacebuild}/state": {
|
||||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"GET:/api/v2/workspaces/": {
|
||||
StatusCode: http.StatusOK,
|
||||
AssertAction: rbac.ActionRead,
|
||||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
|
||||
// These endpoints need payloads to get to the auth part.
|
||||
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
// These endpoints need payloads to get to the auth part. Payloads will be required
|
||||
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
}
|
||||
|
||||
for k, v := range assertRoute {
|
||||
|
@ -175,16 +218,24 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
route = strings.ReplaceAll(route, "{organization}", admin.OrganizationID.String())
|
||||
route = strings.ReplaceAll(route, "{user}", admin.UserID.String())
|
||||
route = strings.ReplaceAll(route, "{organizationname}", organization.Name)
|
||||
route = strings.ReplaceAll(route, "{workspace}", workspace.Name)
|
||||
route = strings.ReplaceAll(route, "{workspace}", workspace.ID.String())
|
||||
route = strings.ReplaceAll(route, "{workspacebuild}", workspace.LatestBuild.ID.String())
|
||||
route = strings.ReplaceAll(route, "{workspacename}", workspace.Name)
|
||||
route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name)
|
||||
|
||||
resp, err := client.Request(context.Background(), method, route, nil)
|
||||
require.NoError(t, err, "do req")
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Logf("Response Body: %q", string(body))
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if !routeAssertions.NoAuthorize {
|
||||
assert.NotNil(t, authorizer.Called, "authorizer expected")
|
||||
assert.Equal(t, routeAssertions.StatusCode, resp.StatusCode, "expect unauthorized")
|
||||
if authorizer.Called != nil {
|
||||
if routeAssertions.AssertAction != "" {
|
||||
assert.Equal(t, routeAssertions.AssertAction, authorizer.Called.Action, "resource action")
|
||||
}
|
||||
if routeAssertions.AssertObject.Type != "" {
|
||||
assert.Equal(t, routeAssertions.AssertObject.Type, authorizer.Called.Object.Type, "resource type")
|
||||
}
|
||||
|
|
|
@ -568,6 +568,14 @@ func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request)
|
|||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create organization uses the organization resource without an OrgID.
|
||||
// This means you need the site wide permission to make a new organization.
|
||||
if !api.Authorize(rw, r, rbac.ActionCreate,
|
||||
rbac.ResourceOrganization) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := api.Database.GetOrganizationByName(r.Context(), req.Name)
|
||||
if err == nil {
|
||||
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
||||
|
|
|
@ -173,7 +173,7 @@ func TestPostUsers(t *testing.T) {
|
|||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
notInOrg := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleAdmin(), rbac.RoleMember())
|
||||
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: "another",
|
||||
})
|
||||
|
|
|
@ -15,11 +15,25 @@ import (
|
|||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: "no workspace exists for this job",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
|
@ -34,6 +48,11 @@ func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
|||
func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
paginationParams, ok := parsePagination(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
|
@ -90,6 +109,11 @@ func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
workspaceBuildName := chi.URLParam(r, "workspacebuildname")
|
||||
workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{
|
||||
WorkspaceID: workspace.ID,
|
||||
|
@ -125,6 +149,25 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
if !httpapi.Read(rw, r, &createBuild) {
|
||||
return
|
||||
}
|
||||
|
||||
// Rbac action depends on the transition
|
||||
var action rbac.Action
|
||||
switch createBuild.Transition {
|
||||
case database.WorkspaceTransitionDelete:
|
||||
action = rbac.ActionDelete
|
||||
case database.WorkspaceTransitionStart, database.WorkspaceTransitionStop:
|
||||
action = rbac.ActionUpdate
|
||||
default:
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("transition not supported: %q", createBuild.Transition),
|
||||
})
|
||||
return
|
||||
}
|
||||
if !api.Authorize(rw, r, action, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
if createBuild.TemplateVersionID == uuid.Nil {
|
||||
latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
||||
if err != nil {
|
||||
|
@ -269,6 +312,19 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: "no workspace exists for this job",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
|
@ -308,6 +364,19 @@ func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
|
|||
|
||||
func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: "no workspace exists for this job",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
|
@ -320,6 +389,19 @@ func (api *api) workspaceBuildResources(rw http.ResponseWriter, r *http.Request)
|
|||
|
||||
func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: "no workspace exists for this job",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
|
@ -330,8 +412,20 @@ func (api *api) workspaceBuildLogs(rw http.ResponseWriter, r *http.Request) {
|
|||
api.provisionerJobLogs(rw, r, job)
|
||||
}
|
||||
|
||||
func (*api) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
|
||||
func (api *api) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspaceBuild.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: "no workspace exists for this job",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
|
|
|
@ -29,6 +29,10 @@ import (
|
|||
|
||||
func (api *api) workspace(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
if !api.Authorize(rw, r, rbac.ActionRead,
|
||||
rbac.ResourceWorkspace.InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
||||
if err != nil {
|
||||
|
@ -63,11 +67,6 @@ func (api *api) workspace(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !api.Authorize(rw, r, rbac.ActionRead,
|
||||
rbac.ResourceWorkspace.InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK,
|
||||
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner))
|
||||
}
|
||||
|
@ -219,7 +218,7 @@ func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) {
|
|||
func (api *api) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) {
|
||||
owner := httpmw.UserParam(r)
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
workspaceName := chi.URLParam(r, "workspace")
|
||||
workspaceName := chi.URLParam(r, "workspacename")
|
||||
|
||||
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
|
||||
OwnerID: owner.ID,
|
||||
|
@ -477,6 +476,12 @@ func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
|||
}
|
||||
|
||||
func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
var req codersdk.UpdateWorkspaceAutostartRequest
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
|
@ -495,7 +500,6 @@ func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
|||
dbSched.Valid = true
|
||||
}
|
||||
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
err := api.Database.UpdateWorkspaceAutostart(r.Context(), database.UpdateWorkspaceAutostartParams{
|
||||
ID: workspace.ID,
|
||||
AutostartSchedule: dbSched,
|
||||
|
@ -509,6 +513,12 @@ func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace.
|
||||
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
||||
return
|
||||
}
|
||||
|
||||
var req codersdk.UpdateWorkspaceAutostopRequest
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
|
@ -527,7 +537,6 @@ func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) {
|
|||
dbSched.Valid = true
|
||||
}
|
||||
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
err := api.Database.UpdateWorkspaceAutostop(r.Context(), database.UpdateWorkspaceAutostopParams{
|
||||
ID: workspace.ID,
|
||||
AutostopSchedule: dbSched,
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
@ -18,7 +20,7 @@ import (
|
|||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
func TestWorkspace(t *testing.T) {
|
||||
func TestAdminViewAllWorkspaces(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -27,8 +29,25 @@ func TestWorkspace(t *testing.T) {
|
|||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
_, err := client.Workspace(context.Background(), workspace.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
otherOrg, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: "default-test",
|
||||
})
|
||||
require.NoError(t, err, "create other org")
|
||||
|
||||
// This other user is not in the first user's org. Since other is an admin, they can
|
||||
// still see the "first" user's workspace.
|
||||
other := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.RoleAdmin(), rbac.RoleMember())
|
||||
otherWorkspaces, err := other.Workspaces(context.Background(), codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err, "(other) fetch workspaces")
|
||||
|
||||
firstWorkspaces, err := other.Workspaces(context.Background(), codersdk.WorkspaceFilter{})
|
||||
require.NoError(t, err, "(first) fetch workspaces")
|
||||
|
||||
require.ElementsMatch(t, otherWorkspaces, firstWorkspaces)
|
||||
}
|
||||
|
||||
func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||
|
@ -52,7 +71,7 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
|
|||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleMember(), rbac.RoleAdmin())
|
||||
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: "another",
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue