diff --git a/cli/autostart_test.go b/cli/autostart_test.go index 78f11a5380145..8c9ff40ee25d0 100644 --- a/cli/autostart_test.go +++ b/cli/autostart_test.go @@ -110,7 +110,7 @@ func TestAutostart(t *testing.T) { clitest.SetupConfig(t, client, root) err := cmd.Execute() - require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error") + require.ErrorContains(t, err, "status code 403: forbidden", "unexpected error") }) t.Run("Disable_NotFound", func(t *testing.T) { @@ -128,7 +128,7 @@ func TestAutostart(t *testing.T) { clitest.SetupConfig(t, client, root) err := cmd.Execute() - require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error") + require.ErrorContains(t, err, "status code 403: forbidden", "unexpected error") }) t.Run("Enable_DefaultSchedule", func(t *testing.T) { diff --git a/cli/autostop_test.go b/cli/autostop_test.go index 2d5205ad9c732..14447ac037ee4 100644 --- a/cli/autostop_test.go +++ b/cli/autostop_test.go @@ -109,7 +109,7 @@ func TestAutostop(t *testing.T) { clitest.SetupConfig(t, client, root) err := cmd.Execute() - require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error") + require.ErrorContains(t, err, "status code 403: forbidden", "unexpected error") }) t.Run("Disable_NotFound", func(t *testing.T) { @@ -127,7 +127,7 @@ func TestAutostop(t *testing.T) { clitest.SetupConfig(t, client, root) err := cmd.Execute() - require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error") + require.ErrorContains(t, err, "status code 403: forbidden", "unexpected error") }) t.Run("Enable_DefaultSchedule", func(t *testing.T) { diff --git a/coderd/authorize.go b/coderd/authorize.go new file mode 100644 index 0000000000000..69b9cf8c596c9 --- /dev/null +++ b/coderd/authorize.go @@ -0,0 +1,43 @@ +package coderd + +import ( + "net/http" + + "golang.org/x/xerrors" + + "cdr.dev/slog" + + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/coderd/rbac" +) + +func (api *api) Authorize(rw http.ResponseWriter, r *http.Request, action rbac.Action, object rbac.Object) bool { + roles := httpmw.UserRoles(r) + err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object) + if err != nil { + httpapi.Write(rw, http.StatusForbidden, httpapi.Response{ + Message: err.Error(), + }) + + // Log the errors for debugging + internalError := new(rbac.UnauthorizedError) + logger := api.Logger + if xerrors.As(err, internalError) { + logger = api.Logger.With(slog.F("internal", internalError.Internal())) + } + // Log information for debugging. This will be very helpful + // in the early days + logger.Warn(r.Context(), "unauthorized", + slog.F("roles", roles.Roles), + slog.F("user_id", roles.ID), + slog.F("username", roles.Username), + slog.F("route", r.URL.Path), + slog.F("action", action), + slog.F("object", object), + ) + + return false + } + return true +} diff --git a/coderd/coderd.go b/coderd/coderd.go index 27e5a667cb75f..6bf4aa8920189 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -50,7 +50,7 @@ type Options struct { SecureAuthCookie bool SSHKeygenAlgorithm gitsshkey.Algorithm TURNServer *turnconn.Server - Authorizer *rbac.RegoAuthorizer + Authorizer rbac.Authorizer } // New constructs the Coder API into an HTTP handler. @@ -83,10 +83,6 @@ func New(options *Options) (http.Handler, func()) { // TODO: @emyrk we should just move this into 'ExtractAPIKey'. authRolesMiddleware := httpmw.ExtractUserRoles(options.Database) - authorize := func(f http.HandlerFunc, actions rbac.Action) http.HandlerFunc { - return httpmw.Authorize(api.Logger, api.Authorizer, actions)(f).ServeHTTP - } - r := chi.NewRouter() r.Use( @@ -158,10 +154,7 @@ func New(options *Options) (http.Handler, func()) { }) }) r.Route("/members", func(r chi.Router) { - r.Route("/roles", func(r chi.Router) { - r.Use(httpmw.WithRBACObject(rbac.ResourceUserRole)) - r.Get("/", authorize(api.assignableOrgRoles, rbac.ActionRead)) - }) + r.Get("/roles", api.assignableOrgRoles) r.Route("/{user}", func(r chi.Router) { r.Use( httpmw.ExtractUserParam(options.Database), @@ -232,8 +225,7 @@ func New(options *Options) (http.Handler, func()) { r.Get("/", api.users) // These routes query information about site wide roles. r.Route("/roles", func(r chi.Router) { - r.Use(httpmw.WithRBACObject(rbac.ResourceUserRole)) - r.Get("/", authorize(api.assignableSiteRoles, rbac.ActionRead)) + r.Get("/", api.assignableSiteRoles) }) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) @@ -244,8 +236,7 @@ func New(options *Options) (http.Handler, func()) { r.Put("/active", api.putUserStatus(database.UserStatusActive)) }) r.Route("/password", func(r chi.Router) { - r.Use(httpmw.WithRBACObject(rbac.ResourceUserPasswordRole)) - r.Put("/", authorize(api.putUserPassword, rbac.ActionUpdate)) + r.Put("/", api.putUserPassword) }) r.Get("/organizations", api.organizationsByUser) r.Post("/organizations", api.postOrganizationsByUser) @@ -302,6 +293,7 @@ func New(options *Options) (http.Handler, func()) { r.Route("/workspaces/{workspace}", func(r chi.Router) { r.Use( apiKeyMiddleware, + authRolesMiddleware, httpmw.ExtractWorkspaceParam(options.Database), ) r.Get("/", api.workspace) diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index 73d3c3d308def..d096682957538 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -2,14 +2,19 @@ package coderd_test import ( "context" + "net/http" + "strings" "testing" - "go.uber.org/goleak" - + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/goleak" + "golang.org/x/xerrors" "github.com/coder/coder/buildinfo" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/coderd/rbac" ) func TestMain(m *testing.M) { @@ -24,3 +29,197 @@ func TestBuildInfo(t *testing.T) { require.Equal(t, buildinfo.ExternalURL(), buildInfo.ExternalURL, "external URL") require.Equal(t, buildinfo.Version(), buildInfo.Version, "version") } + +// TestAuthorizeAllEndpoints will check `authorize` is called on every endpoint registered. +func TestAuthorizeAllEndpoints(t *testing.T) { + t.Parallel() + + authorizer := &fakeAuthorizer{} + srv, client := coderdtest.NewMemoryCoderd(t, &coderdtest.Options{ + Authorizer: authorizer, + }) + admin := coderdtest.CreateFirstUser(t, client) + organization, err := client.Organization(context.Background(), admin.OrganizationID) + require.NoError(t, err, "fetch org") + + // Setup some data in the database. + coderdtest.NewProvisionerDaemon(t, client) + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, admin.OrganizationID, template.ID) + + // Always fail auth from this point forward + authorizer.AlwaysReturn = rbac.ForbiddenWithInternal(xerrors.New("fake implementation"), nil, nil) + + // skipRoutes allows skipping routes from being checked. + type routeCheck struct { + NoAuthorize bool + AssertObject rbac.Object + StatusCode int + } + assertRoute := map[string]routeCheck{ + // These endpoints do not require auth + "GET:/api/v2": {NoAuthorize: true}, + "GET:/api/v2/buildinfo": {NoAuthorize: true}, + "GET:/api/v2/users/first": {NoAuthorize: true}, + "POST:/api/v2/users/first": {NoAuthorize: true}, + "POST:/api/v2/users/login": {NoAuthorize: true}, + "POST:/api/v2/users/logout": {NoAuthorize: true}, + "GET:/api/v2/users/authmethods": {NoAuthorize: true}, + + // All workspaceagents endpoints do not use rbac + "POST:/api/v2/workspaceagents/aws-instance-identity": {NoAuthorize: true}, + "POST:/api/v2/workspaceagents/azure-instance-identity": {NoAuthorize: true}, + "POST:/api/v2/workspaceagents/google-instance-identity": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/me/gitsshkey": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/me/iceservers": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/me/listen": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/me/metadata": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/me/turn": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/{workspaceagent}": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/{workspaceagent}/": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/{workspaceagent}/dial": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true}, + "GET:/api/v2/workspaceagents/{workspaceagent}/pty": {NoAuthorize: true}, + "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/users/oauth2/github/callback": {NoAuthorize: true}, + + "POST:/api/v2/users/{user}/organizations/": {NoAuthorize: true}, + "PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true}, + "GET:/api/v2/organizations/{organization}/provisionerdaemons": {NoAuthorize: true}, + "POST:/api/v2/organizations/{organization}/templates": {NoAuthorize: true}, + "GET:/api/v2/organizations/{organization}/templates": {NoAuthorize: true}, + "GET:/api/v2/organizations/{organization}/templates/{templatename}": {NoAuthorize: true}, + "POST:/api/v2/organizations/{organization}/templateversions": {NoAuthorize: true}, + "POST:/api/v2/organizations/{organization}/workspaces": {NoAuthorize: true}, + + "POST:/api/v2/parameters/{scope}/{id}": {NoAuthorize: true}, + "GET:/api/v2/parameters/{scope}/{id}": {NoAuthorize: true}, + "DELETE:/api/v2/parameters/{scope}/{id}/{name}": {NoAuthorize: true}, + + "GET:/api/v2/provisionerdaemons/me/listen": {NoAuthorize: true}, + + "DELETE:/api/v2/templates/{template}": {NoAuthorize: true}, + "GET:/api/v2/templates/{template}": {NoAuthorize: true}, + "GET:/api/v2/templates/{template}/versions": {NoAuthorize: true}, + "PATCH:/api/v2/templates/{template}/versions": {NoAuthorize: true}, + "GET:/api/v2/templates/{template}/versions/{templateversionname}": {NoAuthorize: true}, + + "GET:/api/v2/templateversions/{templateversion}": {NoAuthorize: true}, + "PATCH:/api/v2/templateversions/{templateversion}/cancel": {NoAuthorize: true}, + "GET:/api/v2/templateversions/{templateversion}/logs": {NoAuthorize: true}, + "GET:/api/v2/templateversions/{templateversion}/parameters": {NoAuthorize: true}, + "GET:/api/v2/templateversions/{templateversion}/resources": {NoAuthorize: true}, + "GET:/api/v2/templateversions/{templateversion}/schema": {NoAuthorize: true}, + + "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}, + + "POST:/api/v2/files": {NoAuthorize: true}, + "GET:/api/v2/files/{hash}": {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)}, + "GET:/api/v2/users/{user}/organizations": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceOrganization}, + "GET:/api/v2/users/{user}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace}, + "GET:/api/v2/organizations/{organization}/workspaces/{user}": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace}, + "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/organizations/{organization}/workspaces": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceWorkspace}, + + // These endpoints need payloads to get to the auth part. + "PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true}, + } + + c, _ := srv.Config.Handler.(*chi.Mux) + err = chi.Walk(c, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + name := method + ":" + route + t.Run(name, func(t *testing.T) { + authorizer.reset() + routeAssertions, ok := assertRoute[strings.TrimRight(name, "/")] + if !ok { + // By default, all omitted routes check for just "authorize" called + routeAssertions = routeCheck{} + } + if routeAssertions.StatusCode == 0 { + routeAssertions.StatusCode = http.StatusForbidden + } + + // Replace all url params with known values + 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) + + resp, err := client.Request(context.Background(), method, route, nil) + require.NoError(t, err, "do req") + _ = 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.AssertObject.Type != "" { + assert.Equal(t, routeAssertions.AssertObject.Type, authorizer.Called.Object.Type, "resource type") + } + if routeAssertions.AssertObject.Owner != "" { + assert.Equal(t, routeAssertions.AssertObject.Owner, authorizer.Called.Object.Owner, "resource owner") + } + if routeAssertions.AssertObject.OrgID != "" { + assert.Equal(t, routeAssertions.AssertObject.OrgID, authorizer.Called.Object.OrgID, "resource org") + } + if routeAssertions.AssertObject.ResourceID != "" { + assert.Equal(t, routeAssertions.AssertObject.ResourceID, authorizer.Called.Object.ResourceID, "resource ID") + } + } + } else { + assert.Nil(t, authorizer.Called, "authorize not expected") + } + }) + return nil + }) + require.NoError(t, err) +} + +type authCall struct { + SubjectID string + Roles []string + Action rbac.Action + Object rbac.Object +} + +type fakeAuthorizer struct { + Called *authCall + AlwaysReturn error +} + +func (f *fakeAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error { + f.Called = &authCall{ + SubjectID: subjectID, + Roles: roleNames, + Action: action, + Object: object, + } + return f.AlwaysReturn +} + +func (f *fakeAuthorizer) reset() { + f.Called = nil +} diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index f2cf11cfc02ae..b9c043075f2ff 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -56,6 +56,7 @@ import ( type Options struct { AWSCertificates awsidentity.Certificates + Authorizer rbac.Authorizer AzureCertificates x509.VerifyOptions GithubOAuth2Config *coderd.GithubOAuth2Config GoogleTokenValidator *idtoken.Validator @@ -66,7 +67,7 @@ type Options struct { // New constructs an in-memory coderd instance and returns // the connected client. -func New(t *testing.T, options *Options) *codersdk.Client { +func NewMemoryCoderd(t *testing.T, options *Options) (*httptest.Server, *codersdk.Client) { if options == nil { options = &Options{} } @@ -147,6 +148,7 @@ func New(t *testing.T, options *Options) *codersdk.Client { SSHKeygenAlgorithm: options.SSHKeygenAlgorithm, TURNServer: turnServer, APIRateLimit: options.APIRateLimit, + Authorizer: options.Authorizer, }) t.Cleanup(func() { cancelFunc() @@ -155,7 +157,14 @@ func New(t *testing.T, options *Options) *codersdk.Client { closeWait() }) - return codersdk.New(serverURL) + return srv, codersdk.New(serverURL) +} + +// New constructs an in-memory coderd instance and returns +// the connected client. +func New(t *testing.T, options *Options) *codersdk.Client { + _, cli := NewMemoryCoderd(t, options) + return cli } // NewProvisionerDaemon launches a provisionerd instance configured to work @@ -252,9 +261,8 @@ func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uui for _, r := range user.Roles { siteRoles = append(siteRoles, r.Name) } - // TODO: @emyrk switch "other" to "client" when we support updating other - // users. - _, err := other.UpdateUserRoles(context.Background(), user.ID.String(), codersdk.UpdateRoles{Roles: siteRoles}) + + _, err := client.UpdateUserRoles(context.Background(), user.ID.String(), codersdk.UpdateRoles{Roles: siteRoles}) require.NoError(t, err, "update site roles") // Update org roles diff --git a/coderd/gitsshkey.go b/coderd/gitsshkey.go index 1543980ab6eb2..d5b2b049f892c 100644 --- a/coderd/gitsshkey.go +++ b/coderd/gitsshkey.go @@ -8,11 +8,17 @@ import ( "github.com/coder/coder/coderd/gitsshkey" "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) regenerateGitSSHKey(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) + + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) { + return + } + privateKey, publicKey, err := gitsshkey.Generate(api.SSHKeygenAlgorithm) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -53,6 +59,11 @@ func (api *api) regenerateGitSSHKey(rw http.ResponseWriter, r *http.Request) { func (api *api) gitSSHKey(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUserData.WithOwner(user.ID.String())) { + return + } + gitSSHKey, err := api.Database.GetGitSSHKey(r.Context(), user.ID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 331ec527e4328..afdb4685ed063 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -62,6 +62,12 @@ type Error struct { Detail string `json:"detail" validate:"required"` } +func Forbidden(rw http.ResponseWriter) { + Write(rw, http.StatusForbidden, Response{ + Message: "forbidden", + }) +} + // Write outputs a standardized format to an HTTP response body. func Write(rw http.ResponseWriter, status int, response interface{}) { buf := &bytes.Buffer{} diff --git a/coderd/httpmw/authorize.go b/coderd/httpmw/authorize.go index 2eb221f1893eb..84bf7cbfa04b4 100644 --- a/coderd/httpmw/authorize.go +++ b/coderd/httpmw/authorize.go @@ -4,92 +4,10 @@ import ( "context" "net/http" - "golang.org/x/xerrors" - - "cdr.dev/slog" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" - "github.com/coder/coder/coderd/rbac" ) -// Authorize will enforce if the user roles can complete the action on the AuthObject. -// The organization and owner are found using the ExtractOrganization and -// ExtractUser middleware if present. -func Authorize(logger slog.Logger, auth *rbac.RegoAuthorizer, action rbac.Action) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - roles := UserRoles(r) - object := rbacObject(r) - - if object.Type == "" { - panic("developer error: auth object has no type") - } - - // First extract the object's owner and organization if present. - unknownOrg := r.Context().Value(organizationParamContextKey{}) - if organization, castOK := unknownOrg.(database.Organization); unknownOrg != nil { - if !castOK { - panic("developer error: organization param middleware not provided for authorize") - } - object = object.InOrg(organization.ID) - } - - unknownOwner := r.Context().Value(userParamContextKey{}) - if owner, castOK := unknownOwner.(database.User); unknownOwner != nil { - if !castOK { - panic("developer error: user param middleware not provided for authorize") - } - object = object.WithOwner(owner.ID.String()) - } - - err := auth.AuthorizeByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object) - if err != nil { - internalError := new(rbac.UnauthorizedError) - if xerrors.As(err, internalError) { - logger = logger.With(slog.F("internal", internalError.Internal())) - } - // Log information for debugging. This will be very helpful - // in the early days if we over secure endpoints. - logger.Warn(r.Context(), "unauthorized", - slog.F("roles", roles.Roles), - slog.F("user_id", roles.ID), - slog.F("username", roles.Username), - slog.F("route", r.URL.Path), - slog.F("action", action), - slog.F("object", object), - ) - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: err.Error(), - }) - return - } - next.ServeHTTP(rw, r) - }) - } -} - -type authObjectKey struct{} - -// APIKey returns the API key from the ExtractAPIKey handler. -func rbacObject(r *http.Request) rbac.Object { - obj, ok := r.Context().Value(authObjectKey{}).(rbac.Object) - if !ok { - panic("developer error: auth object middleware not provided") - } - return obj -} - -// WithRBACObject sets the object for 'Authorize()' for all routes handled -// by this middleware. The important field to set is 'Type' -func WithRBACObject(object rbac.Object) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), authObjectKey{}, object) - next.ServeHTTP(rw, r.WithContext(ctx)) - }) - } -} - // User roles are the 'subject' field of Authorize() type userRolesKey struct{} diff --git a/coderd/httpmw/oauth2.go b/coderd/httpmw/oauth2.go index ba14f8dafdfc0..92ba5da38162b 100644 --- a/coderd/httpmw/oauth2.go +++ b/coderd/httpmw/oauth2.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "reflect" "golang.org/x/oauth2" @@ -46,7 +47,8 @@ func OAuth2(r *http.Request) OAuth2State { func ExtractOAuth2(config OAuth2Config) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if config == nil { + // Interfaces can hold a nil value + if config == nil || reflect.ValueOf(config).IsNil() { httpapi.Write(rw, http.StatusPreconditionRequired, httpapi.Response{ Message: "The oauth2 method requested is not configured!", }) diff --git a/coderd/httpmw/organizationparam.go b/coderd/httpmw/organizationparam.go index d66e49236672e..8b088ee76b4d8 100644 --- a/coderd/httpmw/organizationparam.go +++ b/coderd/httpmw/organizationparam.go @@ -63,7 +63,7 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler UserID: apiKey.UserID, }) if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + httpapi.Write(rw, http.StatusForbidden, httpapi.Response{ Message: "not a member of the organization", }) return diff --git a/coderd/httpmw/organizationparam_test.go b/coderd/httpmw/organizationparam_test.go index 2e4a8eddf4414..b062e63bc3819 100644 --- a/coderd/httpmw/organizationparam_test.go +++ b/coderd/httpmw/organizationparam_test.go @@ -141,7 +141,7 @@ func TestOrganizationParam(t *testing.T) { rtr.ServeHTTP(rw, r) res := rw.Result() defer res.Body.Close() - require.Equal(t, http.StatusUnauthorized, res.StatusCode) + require.Equal(t, http.StatusForbidden, res.StatusCode) }) t.Run("Success", func(t *testing.T) { diff --git a/coderd/organizations.go b/coderd/organizations.go index feb7a7ba9dc18..b0b57f748ccd6 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -6,11 +6,19 @@ 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) organization(rw http.ResponseWriter, r *http.Request) { +func (api *api) organization(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceOrganization. + InOrg(organization.ID). + WithID(organization.ID.String())) { + return + } + httpapi.Write(rw, http.StatusOK, convertOrganization(organization)) } diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index e6338f61cb7fe..01b5822a15611 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -30,7 +30,7 @@ func TestOrganizationByUserAndName(t *testing.T) { _, err := client.OrganizationByName(context.Background(), codersdk.Me, "nothing") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) }) t.Run("NoMember", func(t *testing.T) { @@ -38,14 +38,14 @@ func TestOrganizationByUserAndName(t *testing.T) { client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) - org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ + org, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ Name: "another", }) require.NoError(t, err) - _, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name) + _, err = other.OrganizationByName(context.Background(), codersdk.Me, org.Name) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) }) t.Run("Valid", func(t *testing.T) { diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 39cd7ed102906..6325a4b8c506b 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -9,6 +9,10 @@ import ( "github.com/open-policy-agent/opa/rego" ) +type Authorizer interface { + ByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error +} + // RegoAuthorizer will use a prepared rego query for performing authorize() type RegoAuthorizer struct { query rego.PreparedEvalQuery @@ -38,10 +42,10 @@ type authSubject struct { Roles []Role `json:"roles"` } -// AuthorizeByRoleName will expand all roleNames into roles before calling Authorize(). +// ByRoleName will expand all roleNames into roles before calling Authorize(). // This is the function intended to be used outside this package. // The role is fetched from the builtin map located in memory. -func (a RegoAuthorizer) AuthorizeByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error { +func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error { roles := make([]Role, 0, len(roleNames)) for _, n := range roleNames { r, err := RoleByName(n) diff --git a/coderd/rbac/builtin.go b/coderd/rbac/builtin.go index 6a85cfe3256a2..71b0f059f628d 100644 --- a/coderd/rbac/builtin.go +++ b/coderd/rbac/builtin.go @@ -64,6 +64,10 @@ var ( return Role{ Name: member, DisplayName: "Member", + Site: permissions(map[Object][]Action{ + // All users can read all other users and know they exist. + ResourceUser: {ActionRead}, + }), User: permissions(map[Object][]Action{ ResourceWildcard: {WildcardSymbol}, }), @@ -111,7 +115,20 @@ var ( Name: roleName(orgMember, organizationID), DisplayName: "Organization Member", Org: map[string][]Permission{ - organizationID: {}, + organizationID: { + { + // All org members can read the other members in their org. + ResourceType: ResourceOrganizationMember.Type, + Action: ActionRead, + ResourceID: "*", + }, + { + // All org members can read the organization + ResourceType: ResourceOrganization.Type, + Action: ActionRead, + ResourceID: "*", + }, + }, }, } }, diff --git a/coderd/rbac/error.go b/coderd/rbac/error.go index 593ca4d0fc23a..6b63bb88602db 100644 --- a/coderd/rbac/error.go +++ b/coderd/rbac/error.go @@ -6,7 +6,7 @@ const ( // errUnauthorized is the error message that should be returned to // clients when an action is forbidden. It is intentionally vague to prevent // disclosing information that a client should not have access to. - errUnauthorized = "unauthorized" + errUnauthorized = "forbidden" ) // UnauthorizedError is the error type for authorization errors diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index e4fa5013a16ce..862653f50286e 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -9,6 +9,10 @@ const WildcardSymbol = "*" // Resources are just typed objects. Making resources this way allows directly // passing them into an Authorize function and use the chaining api. var ( + // ResourceWorkspace CRUD. Org + User owner + // create/delete = make or delete workspaces + // read = access workspace + // update = edit workspace variables ResourceWorkspace = Object{ Type: "workspace", } @@ -17,19 +21,60 @@ var ( Type: "template", } - ResourceUser = Object{ - Type: "user", + ResourceFile = Object{ + Type: "file", + } + + // ResourceOrganization CRUD. Has an org owner on all but 'create'. + // create/delete = make or delete organizations + // read = view org information (Can add user owner for read) + // update = ?? + ResourceOrganization = Object{ + Type: "organization", } - // ResourceUserRole might be expanded later to allow more granular permissions + // ResourceRoleAssignment might be expanded later to allow more granular permissions // to modifying roles. For now, this covers all possible roles, so having this permission // allows granting/deleting **ALL** roles. - ResourceUserRole = Object{ - Type: "user_role", + // create = Assign roles + // update = ?? + // read = View available roles to assign + // delete = Remove role + ResourceRoleAssignment = Object{ + Type: "assign_role", + } + + // ResourceAPIKey is owned by a user. + // create = Create a new api key for user + // update = ?? + // read = View api key + // delete = Delete api key + ResourceAPIKey = Object{ + Type: "api_key", + } + + // ResourceUser is the user in the 'users' table. + // ResourceUser never has any owners or in an org, as it's site wide. + // create/delete = make or delete a new user. + // read = view all 'user' table data + // update = update all 'user' table data + ResourceUser = Object{ + Type: "user", + } + + // ResourceUserData is any data associated with a user. A user has control + // over their data (profile, password, etc). So this resource has an owner. + ResourceUserData = Object{ + Type: "user_data", } - ResourceUserPasswordRole = Object{ - Type: "user_password", + // ResourceOrganizationMember is a user's membership in an organization. + // Has ONLY an organization owner. The resource ID is the user's ID + // create/delete = Create/delete member from org. + // update = Update organization member + // read = View member + ResourceOrganizationMember = Object{ + Type: "organization_member", } // ResourceWildcard represents all resource types diff --git a/coderd/roles.go b/coderd/roles.go index 205e8633b4bbe..308b1bf791984 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -11,32 +11,43 @@ import ( ) // assignableSiteRoles returns all site wide roles that can be assigned. -func (*api) assignableSiteRoles(rw http.ResponseWriter, _ *http.Request) { +func (api *api) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) { // TODO: @emyrk in the future, allow granular subsets of roles to be returned based on the // role of the user. + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceRoleAssignment) { + return + } + roles := rbac.SiteRoles() httpapi.Write(rw, http.StatusOK, convertRoles(roles)) } // assignableSiteRoles returns all site wide roles that can be assigned. -func (*api) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { +func (api *api) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) { // TODO: @emyrk in the future, allow granular subsets of roles to be returned based on the // role of the user. organization := httpmw.OrganizationParam(r) + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceRoleAssignment.InOrg(organization.ID)) { + return + } + roles := rbac.OrganizationRoles(organization.ID) httpapi.Write(rw, http.StatusOK, convertRoles(roles)) } func (api *api) checkPermissions(rw http.ResponseWriter, r *http.Request) { - roles := httpmw.UserRoles(r) user := httpmw.UserParam(r) - if user.ID != roles.ID { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - // TODO: @Emyrk in the future we could have an rbac check here. - // If the user can masquerade/impersonate as the user passed in, - // we could allow this or something like that. - Message: "only allowed to check permissions on yourself", - }) + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser.WithOwner(user.ID.String())) { + return + } + + // use the roles of the user specified, not the person making the request. + roles, err := api.Database.GetAllUserRoles(r.Context(), user.ID) + if err != nil { + httpapi.Forbidden(rw) return } @@ -57,7 +68,7 @@ func (api *api) checkPermissions(rw http.ResponseWriter, r *http.Request) { if v.Object.OwnerID == "me" { v.Object.OwnerID = roles.ID.String() } - err := api.Authorizer.AuthorizeByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.Action(v.Action), + err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.Action(v.Action), rbac.Object{ ResourceID: v.Object.ResourceID, Owner: v.Object.OwnerID, diff --git a/coderd/roles_test.go b/coderd/roles_test.go index 83d4f1f23d83a..0350bcf835377 100644 --- a/coderd/roles_test.go +++ b/coderd/roles_test.go @@ -112,7 +112,7 @@ func TestListRoles(t *testing.T) { }) require.NoError(t, err, "create org") - const unauth = "unauthorized" + const unauth = "forbidden" const notMember = "not a member of the organization" testCases := []struct { @@ -191,7 +191,7 @@ func TestListRoles(t *testing.T) { if c.AuthorizedError != "" { var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) require.Contains(t, apiErr.Message, c.AuthorizedError) } else { require.NoError(t, err) diff --git a/coderd/users.go b/coderd/users.go index 5f34b951223f6..fbbbde5e250c1 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -109,6 +109,11 @@ func (api *api) users(rw http.ResponseWriter, r *http.Request) { statusFilter = r.URL.Query().Get("status") ) + // Reading all users across the site + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser) { + return + } + paginationParams, ok := parsePagination(rw, r) if !ok { return @@ -157,12 +162,24 @@ func (api *api) users(rw http.ResponseWriter, r *http.Request) { // Creates a new user. func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { - apiKey := httpmw.APIKey(r) + // Create the user on the site + if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceUser) { + return + } var createUser codersdk.CreateUserRequest if !httpapi.Read(rw, r, &createUser) { return } + + // Create the organization member in the org. + if !api.Authorize(rw, r, rbac.ActionCreate, + rbac.ResourceOrganizationMember.InOrg(createUser.OrganizationID)) { + return + } + + // TODO: @emyrk Authorize the organization create if the createUser will do that. + _, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{ Username: createUser.Username, Email: createUser.Email, @@ -180,7 +197,7 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { return } - organization, err := api.Database.GetOrganizationByID(r.Context(), createUser.OrganizationID) + _, err = api.Database.GetOrganizationByID(r.Context(), createUser.OrganizationID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ Message: "organization does not exist with the provided id", @@ -193,23 +210,6 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { }) return } - // Check if the caller has permissions to the organization requested. - _, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{ - OrganizationID: organization.ID, - UserID: apiKey.UserID, - }) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: "you are not authorized to add members to that organization", - }) - return - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organization member: %s", err), - }) - return - } user, _, err := api.createUser(r.Context(), createUser) if err != nil { @@ -228,6 +228,10 @@ func (api *api) userByName(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) organizationIDs, err := userOrganizationIDs(r.Context(), api, user) + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser.WithID(user.ID.String())) { + return + } + if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("get organization IDs: %s", err.Error()), @@ -241,6 +245,10 @@ func (api *api) userByName(rw http.ResponseWriter, r *http.Request) { func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUser.WithOwner(user.ID.String())) { + return + } + var params codersdk.UpdateUserProfileRequest if !httpapi.Read(rw, r, ¶ms) { return @@ -307,6 +315,11 @@ func (api *api) putUserStatus(status database.UserStatus) func(rw http.ResponseW return func(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) apiKey := httpmw.APIKey(r) + + if !api.Authorize(rw, r, rbac.ActionDelete, rbac.ResourceUser.WithID(user.ID.String())) { + return + } + if status == database.UserStatusSuspended && user.ID == apiKey.UserID { httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ Message: "You cannot suspend yourself", @@ -344,6 +357,11 @@ func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) { user = httpmw.UserParam(r) params codersdk.UpdateUserPasswordRequest ) + + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) { + return + } + if !httpapi.Read(rw, r, ¶ms) { return } @@ -371,6 +389,12 @@ func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) { func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) + roles := httpmw.UserRoles(r) + + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUserData. + WithOwner(user.ID.String())) { + return + } resp := codersdk.UserRoles{ Roles: user.RBACRoles, @@ -386,7 +410,16 @@ func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) { } for _, mem := range memberships { - resp.OrganizationRoles[mem.OrganizationID] = mem.Roles + err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.ActionRead, + rbac.ResourceOrganizationMember. + WithID(user.ID.String()). + InOrg(mem.OrganizationID), + ) + + // If we can read the org member, include the roles + if err == nil { + resp.OrganizationRoles[mem.OrganizationID] = mem.Roles + } } httpapi.Write(rw, http.StatusOK, resp) @@ -394,22 +427,41 @@ func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) { func (api *api) putUserRoles(rw http.ResponseWriter, r *http.Request) { // User is the user to modify - // TODO: Until rbac authorize is implemented, only be able to change your - // own roles. This also means you can grant yourself whatever roles you want. user := httpmw.UserParam(r) - apiKey := httpmw.APIKey(r) - if apiKey.UserID != user.ID { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: "modifying other users is not supported at this time", - }) - return - } + roles := httpmw.UserRoles(r) var params codersdk.UpdateRoles if !httpapi.Read(rw, r, ¶ms) { return } + has := make(map[string]struct{}) + for _, exists := range roles.Roles { + has[exists] = struct{}{} + } + + for _, roleName := range params.Roles { + // If the user already has the role assigned, we don't need to check the permission + // to reassign it. Only run permission checks on the difference in the set of + // roles. + if _, ok := has[roleName]; ok { + delete(has, roleName) + continue + } + + // Assigning a role requires the create permission. + if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceRoleAssignment.WithID(roleName)) { + return + } + } + + // Any roles that were removed also need to be checked. + for roleName := range has { + if !api.Authorize(rw, r, rbac.ActionDelete, rbac.ResourceRoleAssignment.WithID(roleName)) { + return + } + } + updatedUser, err := api.updateSiteUserRoles(r.Context(), database.UpdateUserRolesParams{ GrantedRoles: params.Roles, ID: user.ID, @@ -432,6 +484,8 @@ func (api *api) putUserRoles(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertUser(updatedUser, organizationIDs)) } +// updateSiteUserRoles will ensure only site wide roles are passed in as arguments. +// If an organization role is included, an error is returned. func (api *api) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) { // Enforce only site wide roles for _, r := range args.GrantedRoles { @@ -454,6 +508,7 @@ func (api *api) updateSiteUserRoles(ctx context.Context, args database.UpdateUse // Returns organizations the parameterized user has access to. func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) + roles := httpmw.UserRoles(r) organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID) if errors.Is(err, sql.ErrNoRows) { @@ -469,42 +524,38 @@ func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) { publicOrganizations := make([]codersdk.Organization, 0, len(organizations)) for _, organization := range organizations { - publicOrganizations = append(publicOrganizations, convertOrganization(organization)) + err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.ActionRead, + rbac.ResourceOrganization. + WithID(organization.ID.String()). + InOrg(organization.ID), + ) + if err == nil { + // Only return orgs the user can read + publicOrganizations = append(publicOrganizations, convertOrganization(organization)) + } } httpapi.Write(rw, http.StatusOK, publicOrganizations) } func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Request) { - user := httpmw.UserParam(r) organizationName := chi.URLParam(r, "organizationname") organization, err := api.Database.GetOrganizationByName(r.Context(), organizationName) if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("no organization found by name %q", organizationName), - }) + // Return unauthorized rather than a 404 to not leak if the organization + // exists. + httpapi.Forbidden(rw) return } if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organization by name: %s", err), - }) - return - } - _, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{ - OrganizationID: organization.ID, - UserID: user.ID, - }) - if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: fmt.Sprintf("no organization found by name %q", organizationName), - }) + httpapi.Forbidden(rw) return } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organization member: %s", err), - }) + + if !api.Authorize(rw, r, rbac.ActionRead, + rbac.ResourceOrganization. + InOrg(organization.ID). + WithID(organization.ID.String())) { return } @@ -617,12 +668,8 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) { // Creates a new session key, used for logging in via the CLI func (api *api) postAPIKey(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) - apiKey := httpmw.APIKey(r) - if user.ID != apiKey.UserID { - httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ - Message: "Keys can only be generated for the authenticated user", - }) + if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceAPIKey.WithOwner(user.ID.String())) { return } diff --git a/coderd/users_test.go b/coderd/users_test.go index ef4eabe74972e..a39f733cf87d0 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -172,13 +172,14 @@ func TestPostUsers(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) + notInOrg := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{ Name: "another", }) require.NoError(t, err) - _, err = client.CreateUser(context.Background(), codersdk.CreateUserRequest{ + _, err = notInOrg.CreateUser(context.Background(), codersdk.CreateUserRequest{ Email: "some@domain.com", Username: "anotheruser", Password: "testing", @@ -186,7 +187,7 @@ func TestPostUsers(t *testing.T) { }) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) + require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) }) t.Run("Create", func(t *testing.T) { @@ -401,10 +402,11 @@ func TestGrantRoles(t *testing.T) { []string{rbac.RoleOrgMember(first.OrganizationID)}, ) + memberUser, err := member.User(ctx, codersdk.Me) + require.NoError(t, err, "fetch member") + // Grant - // TODO: @emyrk this should be 'admin.UpdateUserRoles' once proper authz - // is enforced. - _, err = member.UpdateUserRoles(ctx, codersdk.Me, codersdk.UpdateRoles{ + _, err = admin.UpdateUserRoles(ctx, memberUser.ID.String(), codersdk.UpdateRoles{ Roles: []string{ // Promote to site admin rbac.RoleMember(), diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 7176f3d20198a..0e473fbdbcd79 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -58,12 +58,18 @@ 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)) } func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) { organization := httpmw.OrganizationParam(r) + roles := httpmw.UserRoles(r) workspaces, err := api.Database.GetWorkspacesByOrganizationID(r.Context(), database.GetWorkspacesByOrganizationIDParams{ OrganizationID: organization.ID, Deleted: false, @@ -77,7 +83,18 @@ func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request }) return } - apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces) + + allowedWorkspaces := make([]database.Workspace, 0) + for _, ws := range workspaces { + ws := ws + err = api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.ActionRead, + rbac.ResourceWorkspace.InOrg(ws.OrganizationID).WithOwner(ws.OwnerID.String()).WithID(ws.ID.String())) + if err == nil { + allowedWorkspaces = append(allowedWorkspaces, ws) + } + } + + apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allowedWorkspaces) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("convert workspaces: %s", err), @@ -91,42 +108,7 @@ func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) roles := httpmw.UserRoles(r) - organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organizations: %s", err), - }) - return - } - organizationIDs := make([]uuid.UUID, 0) - for _, organization := range organizations { - err = api.Authorizer.AuthorizeByRoleName(r.Context(), user.ID.String(), roles.Roles, rbac.ActionRead, rbac.ResourceWorkspace.All().InOrg(organization.ID)) - var apiErr *rbac.UnauthorizedError - if xerrors.As(err, &apiErr) { - continue - } - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("authorize: %s", err), - }) - return - } - organizationIDs = append(organizationIDs, organization.ID) - } - - workspaceIDs := map[uuid.UUID]struct{}{} - allWorkspaces, err := api.Database.GetWorkspacesByOrganizationIDs(r.Context(), database.GetWorkspacesByOrganizationIDsParams{ - Ids: organizationIDs, - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspaces for organizations: %s", err), - }) - return - } - for _, ws := range allWorkspaces { - workspaceIDs[ws.ID] = struct{}{} - } + allWorkspaces := make([]database.Workspace, 0) userWorkspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{ OwnerID: user.ID, }) @@ -137,11 +119,12 @@ func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) { return } for _, ws := range userWorkspaces { - _, exists := workspaceIDs[ws.ID] - if exists { - continue + ws := ws + err = api.Authorizer.ByRoleName(r.Context(), user.ID.String(), roles.Roles, rbac.ActionRead, + rbac.ResourceWorkspace.InOrg(ws.OrganizationID).WithOwner(ws.OwnerID.String()).WithID(ws.ID.String())) + if err == nil { + allWorkspaces = append(allWorkspaces, ws) } - allWorkspaces = append(allWorkspaces, ws) } apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allWorkspaces) @@ -156,6 +139,7 @@ func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) { func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) { owner := httpmw.UserParam(r) + roles := httpmw.UserRoles(r) workspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{ OwnerID: owner.ID, }) @@ -168,7 +152,18 @@ func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) { }) return } - apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces) + + allowedWorkspaces := make([]database.Workspace, 0) + for _, ws := range workspaces { + ws := ws + err = api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.ActionRead, + rbac.ResourceWorkspace.InOrg(ws.OrganizationID).WithOwner(ws.OwnerID.String()).WithID(ws.ID.String())) + if err == nil { + allowedWorkspaces = append(allowedWorkspaces, ws) + } + } + + apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allowedWorkspaces) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: fmt.Sprintf("convert workspaces: %s", err), @@ -188,9 +183,8 @@ func (api *api) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) Name: workspaceName, }) if errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("no workspace found by name %q", workspaceName), - }) + // Do not leak information if the workspace exists or not + httpapi.Forbidden(rw) return } if err != nil { @@ -207,6 +201,11 @@ func (api *api) workspaceByOwnerAndName(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 + } + build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 3e4d8b57244c5..6183b853c1733 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -158,7 +158,7 @@ func TestWorkspaceByOwnerAndName(t *testing.T) { _, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, "something") var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + require.Equal(t, http.StatusForbidden, apiErr.StatusCode()) }) t.Run("Get", func(t *testing.T) { t.Parallel() diff --git a/codersdk/buildinfo.go b/codersdk/buildinfo.go index a3aecc1ffdfed..0233047caf98c 100644 --- a/codersdk/buildinfo.go +++ b/codersdk/buildinfo.go @@ -18,7 +18,7 @@ type BuildInfoResponse struct { // BuildInfo returns build information for this instance of Coder. func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) { - res, err := c.request(ctx, http.MethodGet, "/api/v2/buildinfo", nil) + res, err := c.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil) if err != nil { return BuildInfoResponse{}, err } diff --git a/codersdk/client.go b/codersdk/client.go index 1654c141e6827..48571adff5d0b 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -35,9 +35,9 @@ type Client struct { type requestOption func(*http.Request) -// request performs an HTTP request with the body provided. +// Request performs an HTTP request with the body provided. // The caller is responsible for closing the response body. -func (c *Client) request(ctx context.Context, method, path string, body interface{}, opts ...requestOption) (*http.Response, error) { +func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...requestOption) (*http.Response, error) { serverURL, err := c.URL.Parse(path) if err != nil { return nil, xerrors.Errorf("parse url: %w", err) diff --git a/codersdk/files.go b/codersdk/files.go index 15c30f30f87f7..52fcf0215081b 100644 --- a/codersdk/files.go +++ b/codersdk/files.go @@ -20,7 +20,7 @@ type UploadResponse struct { // Upload uploads an arbitrary file with the content type provided. // This is used to upload a source-code archive. func (c *Client) Upload(ctx context.Context, contentType string, content []byte) (UploadResponse, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/files", content, func(r *http.Request) { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/files", content, func(r *http.Request) { r.Header.Set("Content-Type", contentType) }) if err != nil { @@ -36,7 +36,7 @@ func (c *Client) Upload(ctx context.Context, contentType string, content []byte) // Download fetches a file by uploaded hash. func (c *Client) Download(ctx context.Context, hash string) ([]byte, string, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/files/%s", hash), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/files/%s", hash), nil) if err != nil { return nil, "", err } diff --git a/codersdk/gitsshkey.go b/codersdk/gitsshkey.go index f20c0666caf0f..e345a2733ab02 100644 --- a/codersdk/gitsshkey.go +++ b/codersdk/gitsshkey.go @@ -25,7 +25,7 @@ type AgentGitSSHKey struct { // GitSSHKey returns the user's git SSH public key. func (c *Client) GitSSHKey(ctx context.Context, user string) (GitSSHKey, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/gitsshkey", user), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/gitsshkey", user), nil) if err != nil { return GitSSHKey{}, xerrors.Errorf("execute request: %w", err) } @@ -41,7 +41,7 @@ func (c *Client) GitSSHKey(ctx context.Context, user string) (GitSSHKey, error) // RegenerateGitSSHKey will create a new SSH key pair for the user and return it. func (c *Client) RegenerateGitSSHKey(ctx context.Context, user string) (GitSSHKey, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/gitsshkey", user), nil) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/gitsshkey", user), nil) if err != nil { return GitSSHKey{}, xerrors.Errorf("execute request: %w", err) } @@ -57,7 +57,7 @@ func (c *Client) RegenerateGitSSHKey(ctx context.Context, user string) (GitSSHKe // AgentGitSSHKey will return the user's SSH key pair for the workspace. func (c *Client) AgentGitSSHKey(ctx context.Context) (AgentGitSSHKey, error) { - res, err := c.request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/gitsshkey", nil) + res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/gitsshkey", nil) if err != nil { return AgentGitSSHKey{}, xerrors.Errorf("execute request: %w", err) } diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 0843e6ddbcaa6..7ebb16aedca97 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -62,7 +62,7 @@ type CreateWorkspaceRequest struct { } func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", id.String()), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", id.String()), nil) if err != nil { return Organization{}, xerrors.Errorf("execute request: %w", err) } @@ -78,7 +78,7 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, // ProvisionerDaemonsByOrganization returns provisioner daemons available for an organization. func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerDaemon, error) { - res, err := c.request(ctx, http.MethodGet, + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organizationID.String()), nil, ) @@ -98,7 +98,7 @@ func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organizat // CreateTemplateVersion processes source-code and optionally associates the version with a template. // Executing without a template is useful for validating source-code. func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid.UUID, req CreateTemplateVersionRequest) (TemplateVersion, error) { - res, err := c.request(ctx, http.MethodPost, + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/templateversions", organizationID.String()), req, ) @@ -117,7 +117,7 @@ func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid. // CreateTemplate creates a new template inside an organization. func (c *Client) CreateTemplate(ctx context.Context, organizationID uuid.UUID, request CreateTemplateRequest) (Template, error) { - res, err := c.request(ctx, http.MethodPost, + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/templates", organizationID.String()), request, ) @@ -136,7 +136,7 @@ func (c *Client) CreateTemplate(ctx context.Context, organizationID uuid.UUID, r // TemplatesByOrganization lists all templates inside of an organization. func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uuid.UUID) ([]Template, error) { - res, err := c.request(ctx, http.MethodGet, + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/templates", organizationID.String()), nil, ) @@ -155,7 +155,7 @@ func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uui // TemplateByName finds a template inside the organization provided with a case-insensitive name. func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, name string) (Template, error) { - res, err := c.request(ctx, http.MethodGet, + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/templates/%s", organizationID.String(), name), nil, ) @@ -174,7 +174,7 @@ func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, n // CreateWorkspace creates a new workspace for the template specified. func (c *Client) CreateWorkspace(ctx context.Context, organizationID uuid.UUID, request CreateWorkspaceRequest) (Workspace, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/workspaces", organizationID), request) + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/workspaces", organizationID), request) if err != nil { return Workspace{}, err } @@ -190,7 +190,7 @@ func (c *Client) CreateWorkspace(ctx context.Context, organizationID uuid.UUID, // WorkspacesByOrganization returns all workspaces in the specified organization. func (c *Client) WorkspacesByOrganization(ctx context.Context, organizationID uuid.UUID) ([]Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces", organizationID), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces", organizationID), nil) if err != nil { return nil, err } @@ -206,7 +206,7 @@ func (c *Client) WorkspacesByOrganization(ctx context.Context, organizationID uu // WorkspacesByOwner returns all workspaces contained in the organization owned by the user. func (c *Client) WorkspacesByOwner(ctx context.Context, organizationID uuid.UUID, user string) ([]Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s", organizationID, user), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s", organizationID, user), nil) if err != nil { return nil, err } @@ -222,7 +222,7 @@ func (c *Client) WorkspacesByOwner(ctx context.Context, organizationID uuid.UUID // WorkspaceByOwnerAndName returns a workspace by the owner's UUID and the workspace's name. func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, organization uuid.UUID, owner string, name string) (Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s/%s", organization, owner, name), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s/%s", organization, owner, name), nil) if err != nil { return Workspace{}, err } diff --git a/codersdk/pagination.go b/codersdk/pagination.go index a4adee6b6e567..c059266dd34c4 100644 --- a/codersdk/pagination.go +++ b/codersdk/pagination.go @@ -26,7 +26,7 @@ type Pagination struct { Offset int `json:"offset,omitempty"` } -// asRequestOption returns a function that can be used in (*Client).request. +// asRequestOption returns a function that can be used in (*Client).Request. // It modifies the request query parameters. func (p Pagination) asRequestOption() requestOption { return func(r *http.Request) { diff --git a/codersdk/parameters.go b/codersdk/parameters.go index 4697d07c51190..9fd9d5d9cad8d 100644 --- a/codersdk/parameters.go +++ b/codersdk/parameters.go @@ -43,7 +43,7 @@ type CreateParameterRequest struct { } func (c *Client) CreateParameter(ctx context.Context, scope ParameterScope, id uuid.UUID, req CreateParameterRequest) (Parameter, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id.String()), req) + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id.String()), req) if err != nil { return Parameter{}, err } @@ -58,7 +58,7 @@ func (c *Client) CreateParameter(ctx context.Context, scope ParameterScope, id u } func (c *Client) DeleteParameter(ctx context.Context, scope ParameterScope, id uuid.UUID, name string) error { - res, err := c.request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/parameters/%s/%s/%s", scope, id.String(), name), nil) + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/parameters/%s/%s/%s", scope, id.String(), name), nil) if err != nil { return err } @@ -73,7 +73,7 @@ func (c *Client) DeleteParameter(ctx context.Context, scope ParameterScope, id u } func (c *Client) Parameters(ctx context.Context, scope ParameterScope, id uuid.UUID) ([]Parameter, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id.String()), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id.String()), nil) if err != nil { return nil, err } diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index c726f8f255eef..1c906fa8c23b8 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -99,7 +99,7 @@ func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, befo if !before.IsZero() { values["before"] = []string{strconv.FormatInt(before.UTC().UnixMilli(), 10)} } - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("%s?%s", path, values.Encode()), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("%s?%s", path, values.Encode()), nil) if err != nil { return nil, err } @@ -118,7 +118,7 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after if !after.IsZero() { afterQuery = fmt.Sprintf("&after=%d", after.UTC().UnixMilli()) } - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("%s?follow%s", path, afterQuery), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("%s?follow%s", path, afterQuery), nil) if err != nil { return nil, err } diff --git a/codersdk/roles.go b/codersdk/roles.go index 09aa19b806ccd..377565c06d404 100644 --- a/codersdk/roles.go +++ b/codersdk/roles.go @@ -17,7 +17,7 @@ type Role struct { // ListSiteRoles lists all available site wide roles. // This is not user specific. func (c *Client) ListSiteRoles(ctx context.Context) ([]Role, error) { - res, err := c.request(ctx, http.MethodGet, "/api/v2/users/roles", nil) + res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/roles", nil) if err != nil { return nil, err } @@ -32,7 +32,7 @@ func (c *Client) ListSiteRoles(ctx context.Context) ([]Role, error) { // ListOrganizationRoles lists all available roles for a given organization. // This is not user specific. func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]Role, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/roles/", org.String()), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/roles", org.String()), nil) if err != nil { return nil, err } @@ -45,7 +45,7 @@ func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]Ro } func (c *Client) CheckPermissions(ctx context.Context, checks UserAuthorizationRequest) (UserAuthorizationResponse, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/authorization", Me), checks) + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/authorization", Me), checks) if err != nil { return nil, err } diff --git a/codersdk/templates.go b/codersdk/templates.go index 14a14ed976650..972a8a5b2b8dd 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -32,7 +32,7 @@ type UpdateActiveTemplateVersion struct { // Template returns a single template. func (c *Client) Template(ctx context.Context, template uuid.UUID) (Template, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s", template), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s", template), nil) if err != nil { return Template{}, nil } @@ -45,7 +45,7 @@ func (c *Client) Template(ctx context.Context, template uuid.UUID) (Template, er } func (c *Client) DeleteTemplate(ctx context.Context, template uuid.UUID) error { - res, err := c.request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/templates/%s", template), nil) + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/templates/%s", template), nil) if err != nil { return err } @@ -59,7 +59,7 @@ func (c *Client) DeleteTemplate(ctx context.Context, template uuid.UUID) error { // UpdateActiveTemplateVersion updates the active template version to the ID provided. // The template version must be attached to the template. func (c *Client) UpdateActiveTemplateVersion(ctx context.Context, template uuid.UUID, req UpdateActiveTemplateVersion) error { - res, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/templates/%s/versions", template), req) + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/templates/%s/versions", template), req) if err != nil { return nil } @@ -79,7 +79,7 @@ type TemplateVersionsByTemplateRequest struct { // TemplateVersionsByTemplate lists versions associated with a template. func (c *Client) TemplateVersionsByTemplate(ctx context.Context, req TemplateVersionsByTemplateRequest) ([]TemplateVersion, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/versions", req.TemplateID), nil, req.Pagination.asRequestOption()) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/versions", req.TemplateID), nil, req.Pagination.asRequestOption()) if err != nil { return nil, err } @@ -94,7 +94,7 @@ func (c *Client) TemplateVersionsByTemplate(ctx context.Context, req TemplateVer // TemplateVersionByName returns a template version by it's friendly name. // This is used for path-based routing. Like: /templates/example/versions/helloworld func (c *Client) TemplateVersionByName(ctx context.Context, template uuid.UUID, name string) (TemplateVersion, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/versions/%s", template, name), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/versions/%s", template, name), nil) if err != nil { return TemplateVersion{}, err } diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go index f7cf29006e514..3b8b2e21711d4 100644 --- a/codersdk/templateversions.go +++ b/codersdk/templateversions.go @@ -31,7 +31,7 @@ type TemplateVersionParameter parameter.ComputedValue // TemplateVersion returns a template version by ID. func (c *Client) TemplateVersion(ctx context.Context, id uuid.UUID) (TemplateVersion, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s", id), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s", id), nil) if err != nil { return TemplateVersion{}, err } @@ -45,7 +45,7 @@ func (c *Client) TemplateVersion(ctx context.Context, id uuid.UUID) (TemplateVer // CancelTemplateVersion marks a template version job as canceled. func (c *Client) CancelTemplateVersion(ctx context.Context, version uuid.UUID) error { - res, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/templateversions/%s/cancel", version), nil) + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/templateversions/%s/cancel", version), nil) if err != nil { return err } @@ -58,7 +58,7 @@ func (c *Client) CancelTemplateVersion(ctx context.Context, version uuid.UUID) e // TemplateVersionSchema returns schemas for a template version by ID. func (c *Client) TemplateVersionSchema(ctx context.Context, version uuid.UUID) ([]TemplateVersionParameterSchema, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/schema", version), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/schema", version), nil) if err != nil { return nil, err } @@ -72,7 +72,7 @@ func (c *Client) TemplateVersionSchema(ctx context.Context, version uuid.UUID) ( // TemplateVersionParameters returns computed parameters for a template version. func (c *Client) TemplateVersionParameters(ctx context.Context, version uuid.UUID) ([]TemplateVersionParameter, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/parameters", version), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/parameters", version), nil) if err != nil { return nil, err } @@ -86,7 +86,7 @@ func (c *Client) TemplateVersionParameters(ctx context.Context, version uuid.UUI // TemplateVersionResources returns resources a template version declares. func (c *Client) TemplateVersionResources(ctx context.Context, version uuid.UUID) ([]WorkspaceResource, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/resources", version), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/resources", version), nil) if err != nil { return nil, err } diff --git a/codersdk/users.go b/codersdk/users.go index 8af79d720fff0..228885d48fe7f 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -155,7 +155,7 @@ type AuthMethods struct { // HasFirstUser returns whether the first user has been created. func (c *Client) HasFirstUser(ctx context.Context) (bool, error) { - res, err := c.request(ctx, http.MethodGet, "/api/v2/users/first", nil) + res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/first", nil) if err != nil { return false, err } @@ -172,7 +172,7 @@ func (c *Client) HasFirstUser(ctx context.Context) (bool, error) { // CreateFirstUser attempts to create the first user on a Coder deployment. // This initial user has superadmin privileges. If >0 users exist, this request will fail. func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest) (CreateFirstUserResponse, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/users/first", req) + res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/first", req) if err != nil { return CreateFirstUserResponse{}, err } @@ -186,7 +186,7 @@ func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest // CreateUser creates a new user. func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/users", req) + res, err := c.Request(ctx, http.MethodPost, "/api/v2/users", req) if err != nil { return User{}, err } @@ -200,7 +200,7 @@ func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, e // UpdateUserProfile enables callers to update profile information func (c *Client) UpdateUserProfile(ctx context.Context, user string, req UpdateUserProfileRequest) (User, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", user), req) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", user), req) if err != nil { return User{}, err } @@ -224,7 +224,7 @@ func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserS return User{}, xerrors.Errorf("status %q is not supported", status) } - res, err := c.request(ctx, http.MethodPut, path, nil) + res, err := c.Request(ctx, http.MethodPut, path, nil) if err != nil { return User{}, err } @@ -240,7 +240,7 @@ func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserS // UpdateUserPassword updates a user password. // It calls PUT /users/{user}/password func (c *Client) UpdateUserPassword(ctx context.Context, user string, req UpdateUserPasswordRequest) error { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", user), req) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", user), req) if err != nil { return err } @@ -254,7 +254,7 @@ func (c *Client) UpdateUserPassword(ctx context.Context, user string, req Update // UpdateUserRoles grants the userID the specified roles. // Include ALL roles the user has. func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRoles) (User, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/roles", user), req) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/roles", user), req) if err != nil { return User{}, err } @@ -269,7 +269,7 @@ func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRol // UpdateOrganizationMemberRoles grants the userID the specified roles in an org. // Include ALL roles the user has. func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organizationID uuid.UUID, user string, req UpdateRoles) (OrganizationMember, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/organizations/%s/members/%s/roles", organizationID, user), req) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/organizations/%s/members/%s/roles", organizationID, user), req) if err != nil { return OrganizationMember{}, err } @@ -283,7 +283,7 @@ func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organization // GetUserRoles returns all roles the user has func (c *Client) GetUserRoles(ctx context.Context, user string) (UserRoles, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", user), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", user), nil) if err != nil { return UserRoles{}, err } @@ -297,7 +297,7 @@ func (c *Client) GetUserRoles(ctx context.Context, user string) (UserRoles, erro // CreateAPIKey generates an API key for the user ID provided. func (c *Client) CreateAPIKey(ctx context.Context, user string) (*GenerateAPIKeyResponse, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil) + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil) if err != nil { return nil, err } @@ -312,7 +312,7 @@ func (c *Client) CreateAPIKey(ctx context.Context, user string) (*GenerateAPIKey // LoginWithPassword creates a session token authenticating with an email and password. // Call `SetSessionToken()` to apply the newly acquired token to the client. func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordRequest) (LoginWithPasswordResponse, error) { - res, err := c.request(ctx, http.MethodPost, "/api/v2/users/login", req) + res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/login", req) if err != nil { return LoginWithPasswordResponse{}, err } @@ -333,7 +333,7 @@ func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordReq func (c *Client) Logout(ctx context.Context) error { // Since `LoginWithPassword` doesn't actually set a SessionToken // (it requires a call to SetSessionToken), this is essentially a no-op - res, err := c.request(ctx, http.MethodPost, "/api/v2/users/logout", nil) + res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/logout", nil) if err != nil { return err } @@ -343,7 +343,7 @@ func (c *Client) Logout(ctx context.Context) error { // User returns a user for the ID/username provided. func (c *Client) User(ctx context.Context, userIdent string) (User, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", userIdent), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", userIdent), nil) if err != nil { return User{}, err } @@ -358,7 +358,7 @@ func (c *Client) User(ctx context.Context, userIdent string) (User, error) { // Users returns all users according to the request parameters. If no parameters are set, // the default behavior is to return all users in a single page. func (c *Client) Users(ctx context.Context, req UsersRequest) ([]User, error) { - res, err := c.request(ctx, http.MethodGet, "/api/v2/users", nil, + res, err := c.Request(ctx, http.MethodGet, "/api/v2/users", nil, req.Pagination.asRequestOption(), func(r *http.Request) { q := r.URL.Query() @@ -382,7 +382,7 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) ([]User, error) { // OrganizationsByUser returns all organizations the user is a member of. func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organization, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", user), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", user), nil) if err != nil { return nil, err } @@ -395,7 +395,7 @@ func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organi } func (c *Client) OrganizationByName(ctx context.Context, user string, name string) (Organization, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil) if err != nil { return Organization{}, err } @@ -409,7 +409,7 @@ func (c *Client) OrganizationByName(ctx context.Context, user string, name strin // CreateOrganization creates an organization and adds the provided user as an admin. func (c *Client) CreateOrganization(ctx context.Context, user string, req CreateOrganizationRequest) (Organization, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/organizations", user), req) + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/organizations", user), req) if err != nil { return Organization{}, err } @@ -425,7 +425,7 @@ func (c *Client) CreateOrganization(ctx context.Context, user string, req Create // AuthMethods returns types of authentication available to the user. func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { - res, err := c.request(ctx, http.MethodGet, "/api/v2/users/authmethods", nil) + res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/authmethods", nil) if err != nil { return AuthMethods{}, err } @@ -441,7 +441,7 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { // WorkspacesByUser returns all workspaces a user has access to. func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", user), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", user), nil) if err != nil { return nil, err } diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index be98b4696fd8b..d64b42bc5faaa 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -65,7 +65,7 @@ func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, servic if err != nil { return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err) } - res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceagents/google-instance-identity", GoogleInstanceIdentityToken{ + res, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/google-instance-identity", GoogleInstanceIdentityToken{ JSONWebToken: jwt, }) if err != nil { @@ -129,7 +129,7 @@ func (c *Client) AuthWorkspaceAWSInstanceIdentity(ctx context.Context) (Workspac return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err) } - res, err = c.request(ctx, http.MethodPost, "/api/v2/workspaceagents/aws-instance-identity", AWSInstanceIdentityToken{ + res, err = c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/aws-instance-identity", AWSInstanceIdentityToken{ Signature: string(signature), Document: string(document), }) @@ -164,7 +164,7 @@ func (c *Client) AuthWorkspaceAzureInstanceIdentity(ctx context.Context) (Worksp return WorkspaceAgentAuthenticateResponse{}, err } - res, err = c.request(ctx, http.MethodPost, "/api/v2/workspaceagents/azure-instance-identity", token) + res, err = c.Request(ctx, http.MethodPost, "/api/v2/workspaceagents/azure-instance-identity", token) if err != nil { return WorkspaceAgentAuthenticateResponse{}, err } @@ -213,7 +213,7 @@ func (c *Client) ListenWorkspaceAgent(ctx context.Context, logger slog.Logger) ( } listener, err := peerbroker.Listen(session, func(ctx context.Context) ([]webrtc.ICEServer, *peer.ConnOptions, error) { // This can be cached if it adds to latency too much. - res, err := c.request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/iceservers", nil) + res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/iceservers", nil) if err != nil { return nil, nil, err } @@ -240,7 +240,7 @@ func (c *Client) ListenWorkspaceAgent(ctx context.Context, logger slog.Logger) ( if err != nil { return agent.Metadata{}, nil, xerrors.Errorf("listen peerbroker: %w", err) } - res, err = c.request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil) + res, err = c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/metadata", nil) if err != nil { return agent.Metadata{}, nil, err } @@ -292,7 +292,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti return nil, xerrors.Errorf("negotiate connection: %w", err) } - res, err = c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/iceservers", agentID.String()), nil) + res, err = c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/iceservers", agentID.String()), nil) if err != nil { return nil, err } @@ -326,7 +326,7 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti // WorkspaceAgent returns an agent by ID. func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s", id), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s", id), nil) if err != nil { return WorkspaceAgent{}, err } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index ef6e68d6bab8f..3c1ee5154b540 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -32,7 +32,7 @@ type WorkspaceBuild struct { // WorkspaceBuild returns a single workspace build for a workspace. // If history is "", the latest version is returned. func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s", id), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s", id), nil) if err != nil { return WorkspaceBuild{}, err } @@ -46,7 +46,7 @@ func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (WorkspaceBui // CancelWorkspaceBuild marks a workspace build job as canceled. func (c *Client) CancelWorkspaceBuild(ctx context.Context, id uuid.UUID) error { - res, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/workspacebuilds/%s/cancel", id), nil) + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/workspacebuilds/%s/cancel", id), nil) if err != nil { return err } @@ -59,7 +59,7 @@ func (c *Client) CancelWorkspaceBuild(ctx context.Context, id uuid.UUID) error { // WorkspaceResourcesByBuild returns resources for a workspace build. func (c *Client) WorkspaceResourcesByBuild(ctx context.Context, build uuid.UUID) ([]WorkspaceResource, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s/resources", build), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s/resources", build), nil) if err != nil { return nil, err } @@ -83,7 +83,7 @@ func (c *Client) WorkspaceBuildLogsAfter(ctx context.Context, build uuid.UUID, a // WorkspaceBuildState returns the provisioner state of the build. func (c *Client) WorkspaceBuildState(ctx context.Context, build uuid.UUID) ([]byte, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s/state", build), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s/state", build), nil) if err != nil { return nil, err } diff --git a/codersdk/workspaceresources.go b/codersdk/workspaceresources.go index b21451bbc63ea..f1dc5d74a04f9 100644 --- a/codersdk/workspaceresources.go +++ b/codersdk/workspaceresources.go @@ -69,7 +69,7 @@ type WorkspaceAgentInstanceMetadata struct { } func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceresources/%s", id), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceresources/%s", id), nil) if err != nil { return WorkspaceResource{}, err } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 644f90422ff07..76693d6efad11 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -40,7 +40,7 @@ type CreateWorkspaceBuildRequest struct { // Workspace returns a single workspace. func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil) if err != nil { return Workspace{}, err } @@ -53,7 +53,7 @@ func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (Workspace, error) } func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]WorkspaceBuild, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), nil) if err != nil { return nil, err } @@ -67,7 +67,7 @@ func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]Wo // CreateWorkspaceBuild queues a new build to occur for a workspace. func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, request CreateWorkspaceBuildRequest) (WorkspaceBuild, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), request) + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), request) if err != nil { return WorkspaceBuild{}, err } @@ -80,7 +80,7 @@ func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, } func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, name string) (WorkspaceBuild, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds/%s", workspace, name), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds/%s", workspace, name), nil) if err != nil { return WorkspaceBuild{}, err } @@ -101,7 +101,7 @@ type UpdateWorkspaceAutostartRequest struct { // If the provided schedule is empty, autostart is disabled for the workspace. func (c *Client) UpdateWorkspaceAutostart(ctx context.Context, id uuid.UUID, req UpdateWorkspaceAutostartRequest) error { path := fmt.Sprintf("/api/v2/workspaces/%s/autostart", id.String()) - res, err := c.request(ctx, http.MethodPut, path, req) + res, err := c.Request(ctx, http.MethodPut, path, req) if err != nil { return xerrors.Errorf("update workspace autostart: %w", err) } @@ -121,7 +121,7 @@ type UpdateWorkspaceAutostopRequest struct { // If the provided schedule is empty, autostop is disabled for the workspace. func (c *Client) UpdateWorkspaceAutostop(ctx context.Context, id uuid.UUID, req UpdateWorkspaceAutostopRequest) error { path := fmt.Sprintf("/api/v2/workspaces/%s/autostop", id.String()) - res, err := c.request(ctx, http.MethodPut, path, req) + res, err := c.Request(ctx, http.MethodPut, path, req) if err != nil { return xerrors.Errorf("update workspace autostop: %w", err) }