diff --git a/coderd/coderd.go b/coderd/coderd.go index 4d394cb9362ae..8a62e4cb7f11f 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,8 +83,8 @@ 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 + authorize := func(f http.HandlerFunc, actions ...rbac.Action) http.HandlerFunc { + return httpmw.Authorize(api.Logger, api.Authorizer, actions...)(f).ServeHTTP } r := chi.NewRouter() @@ -127,9 +127,13 @@ func New(options *Options) (http.Handler, func()) { r.Route("/files", func(r chi.Router) { r.Use( apiKeyMiddleware, + authRolesMiddleware, // This number is arbitrary, but reading/writing // file content is expensive so it should be small. httpmw.RateLimitPerMinute(12), + // TODO: @emyrk (rbac) Currently files are owned by the site? + // Should files be org scoped? User scoped? + httpmw.WithRBACObject(rbac.ResourceFile), ) r.Get("/{hash}", api.fileByHash) r.Post("/", api.postFile) @@ -137,10 +141,11 @@ func New(options *Options) (http.Handler, func()) { r.Route("/organizations/{organization}", func(r chi.Router) { r.Use( apiKeyMiddleware, - httpmw.ExtractOrganizationParam(options.Database), authRolesMiddleware, + httpmw.ExtractOrganizationParam(options.Database), ) - r.Get("/", api.organization) + r.With(httpmw.WithRBACObject(rbac.ResourceOrganization)). + Get("/", authorize(api.organization, rbac.ActionRead)) r.Get("/provisionerdaemons", api.provisionerDaemonsByOrganization) r.Post("/templateversions", api.postTemplateVersionsByOrganization) r.Route("/templates", func(r chi.Router) { @@ -149,12 +154,17 @@ func New(options *Options) (http.Handler, func()) { r.Get("/{templatename}", api.templateByOrganizationAndName) }) r.Route("/workspaces", func(r chi.Router) { + r.Use(httpmw.WithRBACObject(rbac.ResourceWorkspace)) + // Posting a workspace is inherently owned by the api key creating it. + r.With(httpmw.WithAPIKeyAsOwner()). + Post("/", authorize(api.postWorkspacesByOrganization, rbac.ActionCreate)) + r.Get("/", authorize(api.workspacesByOrganization, rbac.ActionRead)) r.Post("/", api.postWorkspacesByOrganization) - r.Get("/", api.workspacesByOrganization) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) - r.Get("/{workspace}", api.workspaceByOwnerAndName) - r.Get("/", api.workspacesByOwner) + // TODO: @emyrk add the resource id to this authorize. + r.Get("/{workspace}", authorize(api.workspaceByOwnerAndName, rbac.ActionRead)) + r.Get("/", authorize(api.workspacesByOwner, rbac.ActionRead)) }) }) r.Route("/members", func(r chi.Router) { @@ -228,8 +238,12 @@ func New(options *Options) (http.Handler, func()) { apiKeyMiddleware, authRolesMiddleware, ) - r.Post("/", api.postUser) - r.Get("/", api.users) + r.Group(func(r chi.Router) { + // Site wide, all users + r.Use(httpmw.WithRBACObject(rbac.ResourceUser)) + r.Post("/", authorize(api.postUser, rbac.ActionCreate)) + r.Get("/", authorize(api.users, rbac.ActionRead)) + }) // These routes query information about site wide roles. r.Route("/roles", func(r chi.Router) { r.Use(httpmw.WithRBACObject(rbac.ResourceUserRole)) @@ -237,30 +251,45 @@ func New(options *Options) (http.Handler, func()) { }) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) - r.Get("/", api.userByName) - r.Put("/profile", api.putUserProfile) - r.Put("/suspend", api.putUserSuspend) - r.Route("/password", func(r chi.Router) { - r.Use(httpmw.WithRBACObject(rbac.ResourceUserPasswordRole)) - r.Put("/", authorize(api.putUserPassword, rbac.ActionUpdate)) + r.Group(func(r chi.Router) { + r.Use(httpmw.WithRBACObject(rbac.ResourceUser)) + r.Get("/", authorize(api.userByName, rbac.ActionRead)) + r.Put("/profile", authorize(api.putUserProfile, rbac.ActionUpdate)) + // suspension is deleting for a user + r.Put("/suspend", authorize(api.putUserSuspend, rbac.ActionDelete)) + r.Route("/password", func(r chi.Router) { + r.Put("/", authorize(api.putUserPassword, rbac.ActionUpdate)) + }) + // This route technically also fetches the organization member struct, but only + // returns the roles. + r.Get("/roles", authorize(api.userRoles, rbac.ActionRead)) + + // This has 2 authorize calls. The second is explicitly called + // in the handler. + r.Put("/roles", authorize(api.putUserRoles, rbac.ActionUpdate)) + + // For now, just use the "user" role for their ssh keys. + // We can always split this out to it's own resource if we need to. + r.Get("/gitsshkey", authorize(api.gitSSHKey, rbac.ActionRead)) + r.Put("/gitsshkey", authorize(api.regenerateGitSSHKey, rbac.ActionUpdate)) + + r.Post("/authorization", authorize(api.checkPermissions, rbac.ActionRead)) }) - r.Get("/organizations", api.organizationsByUser) - r.Post("/organizations", api.postOrganizationsByUser) - // These roles apply to the site wide permissions. - r.Put("/roles", api.putUserRoles) - r.Get("/roles", api.userRoles) - r.Post("/authorization", api.checkPermissions) + r.With(httpmw.WithRBACObject(rbac.ResourceAPIKey)).Post("/keys", authorize(api.postAPIKey, rbac.ActionCreate)) + r.Get("/workspaces", api.workspacesByUser) - r.Post("/keys", api.postAPIKey) r.Route("/organizations", func(r chi.Router) { + // TODO: @emyrk This creates an organization, so why is it nested under {user}? + // Shouldn't this be outside the {user} param subpath? Maybe in the organizations/ + // path? r.Post("/", api.postOrganizationsByUser) + r.Get("/", api.organizationsByUser) + + // TODO: @emyrk why is this nested under {user} when the user param is not used? r.Get("/{organizationname}", api.organizationByUserAndName) }) - r.Get("/gitsshkey", api.gitSSHKey) - r.Put("/gitsshkey", api.regenerateGitSSHKey) - r.Get("/workspaces", api.workspacesByUser) }) }) }) diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index 73d3c3d308def..7ac5736dedf27 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,179 @@ 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") + + // 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}, + } + + 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.StatusUnauthorized + } + + // 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) + + 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 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 75f49cea88380..5e4fac7cb72d3 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, codersdk.UpdateRoles{Roles: siteRoles}) + + _, err := client.UpdateUserRoles(context.Background(), user.ID, codersdk.UpdateRoles{Roles: siteRoles}) require.NoError(t, err, "update site roles") // Update org roles diff --git a/coderd/httpmw/authorize.go b/coderd/httpmw/authorize.go index 2eb221f1893eb..44fdb8a2b0e04 100644 --- a/coderd/httpmw/authorize.go +++ b/coderd/httpmw/authorize.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/google/uuid" "golang.org/x/xerrors" "cdr.dev/slog" @@ -12,14 +13,15 @@ import ( "github.com/coder/coder/coderd/rbac" ) -// Authorize will enforce if the user roles can complete the action on the AuthObject. +// Authorize will enforce if the user roles can complete the action on the RBACObject. // 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 { +func Authorize(logger slog.Logger, auth rbac.Authorizer, actions ...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) + authObject := rbacObject(r) + object := authObject.Object if object.Type == "" { panic("developer error: auth object has no type") @@ -34,34 +36,42 @@ func Authorize(logger slog.Logger, auth *rbac.RegoAuthorizer, action rbac.Action 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") + if authObject.WithOwner != nil { + owner := authObject.WithOwner(r) + object = object.WithOwner(owner.String()) + } else { + // Attempt to find the resource owner 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()) } - 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())) + for _, action := range actions { + err := auth.ByRoleName(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 } - // 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) }) @@ -70,21 +80,61 @@ func Authorize(logger slog.Logger, auth *rbac.RegoAuthorizer, action rbac.Action 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) +type RBACObject struct { + Object rbac.Object + + // WithOwner will set the Object.Owner field based on the request. + // This allows the Owner field to be set dynamically based on the context + // of the request. + WithOwner func(r *http.Request) uuid.UUID +} + +func rbacObject(r *http.Request) RBACObject { + obj, ok := r.Context().Value(authObjectKey{}).(RBACObject) if !ok { panic("developer error: auth object middleware not provided") } return obj } +func WithAPIKeyAsOwner() func(http.Handler) http.Handler { + return WithOwner(func(r *http.Request) uuid.UUID { + key := APIKey(r) + return key.UserID + }) +} + +// WithOwner sets the object owner for 'Authorize()' for all routes handled +// by this middleware. +func WithOwner(withOwner func(r *http.Request) uuid.UUID) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + obj, ok := r.Context().Value(authObjectKey{}).(RBACObject) + if ok { + obj.WithOwner = withOwner + } else { + obj = RBACObject{WithOwner: withOwner} + } + + ctx := context.WithValue(r.Context(), authObjectKey{}, obj) + next.ServeHTTP(rw, r.WithContext(ctx)) + }) + } +} + // 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) + obj, ok := r.Context().Value(authObjectKey{}).(RBACObject) + if ok { + obj.Object = object + } else { + obj = RBACObject{Object: object} + } + + ctx := context.WithValue(r.Context(), authObjectKey{}, obj) next.ServeHTTP(rw, r.WithContext(ctx)) }) } 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/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..a314addab3afb 100644 --- a/coderd/rbac/builtin.go +++ b/coderd/rbac/builtin.go @@ -64,6 +64,7 @@ var ( return Role{ Name: member, DisplayName: "Member", + Site: permissions(map[Object][]Action{}), User: permissions(map[Object][]Action{ ResourceWildcard: {WildcardSymbol}, }), @@ -111,7 +112,18 @@ 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, + }, + { + // All org members can read the organization + ResourceType: ResourceOrganization.Type, + Action: ActionRead, + }, + }, }, } }, diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index e4fa5013a16ce..f1a3ea6ff5954 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,53 @@ var ( Type: "template", } - ResourceUser = Object{ - Type: "user", + ResourceFile = Object{ + Type: "file", + } + + // ResourceOrganization CRUD. Always has an org owner. + // 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 // to modifying roles. For now, this covers all possible roles, so having this permission // allows granting/deleting **ALL** roles. + // create = Assign roles + // update = ?? + // read = View available roles to assign + // delete = Remove role ResourceUserRole = Object{ Type: "user_role", } - ResourceUserPasswordRole = Object{ - Type: "user_password", + // 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. + // create/delete = make or delete a new user. + // read = view all user's settings + // update = update all user field & settings + ResourceUser = Object{ + Type: "user", + } + + // ResourceOrganizationMember is a user's membership in an organization. + // Has ONLY an organization owner. + // 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..31bf5875be052 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -57,7 +57,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/users.go b/coderd/users.go index f6dc26ccb51fe..ead9e4d5ef74d 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -385,22 +385,53 @@ 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. The middleware checks if + // we can update this user, so the combination of the 2 permissions enables + // assigning new roles. + err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, + rbac.ActionCreate, rbac.ResourceUserRole.WithID(roleName)) + if err != nil { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "unauthorized", + }) + return + } + } + + // Any roles that were removed also need to be checked. + for roleName := range has { + err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, + rbac.ActionDelete, rbac.ResourceUserRole.WithID(roleName)) + if err != nil { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "unauthorized", + }) + return + } + } + updatedUser, err := api.updateSiteUserRoles(r.Context(), database.UpdateUserRolesParams{ GrantedRoles: params.Roles, ID: user.ID, @@ -445,6 +476,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) { @@ -460,19 +492,30 @@ 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) + roles := httpmw.UserRoles(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.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "unauthorized", }) return } @@ -482,19 +525,15 @@ func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques }) 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: "you are not a member of that organization", - }) - return - } + + err = api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, rbac.ActionRead, + rbac.ResourceOrganization. + InOrg(organization.ID). + WithID(organization.ID.String()), + ) if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get organization member: %s", err), + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: err.Error(), }) return } @@ -776,6 +815,7 @@ func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest) }) } +// func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) roles := httpmw.UserRoles(r) @@ -789,7 +829,7 @@ func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) { } 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)) + err = api.Authorizer.ByRoleName(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 diff --git a/coderd/users_test.go b/coderd/users_test.go index 99d1849ca6cf8..a9fd04b123897 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -401,10 +401,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, codersdk.UpdateRoles{ Roles: []string{ // Promote to site admin rbac.RoleMember(), @@ -588,12 +589,13 @@ func TestOrganizationByUserAndName(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) + notMember := 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.OrganizationByName(context.Background(), codersdk.Me, org.Name) + _, err = notMember.OrganizationByName(context.Background(), codersdk.Me, org.Name) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode()) 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 3cc4333e735e3..c71ddd85ba630 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, userID uuid.UUID) (GitSSHKey, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/gitsshkey", uuidOrMe(userID)), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/gitsshkey", uuidOrMe(userID)), nil) if err != nil { return GitSSHKey{}, xerrors.Errorf("execute request: %w", err) } @@ -41,7 +41,7 @@ func (c *Client) GitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, er // RegenerateGitSSHKey will create a new SSH key pair for the user and return it. func (c *Client) RegenerateGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/gitsshkey", uuidOrMe(userID)), nil) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/gitsshkey", uuidOrMe(userID)), nil) if err != nil { return GitSSHKey{}, xerrors.Errorf("execute request: %w", err) } @@ -57,7 +57,7 @@ func (c *Client) RegenerateGitSSHKey(ctx context.Context, userID uuid.UUID) (Git // 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 ca85dfbbc139e..52362f3de6034 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -58,7 +58,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) } @@ -74,7 +74,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, ) @@ -94,7 +94,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, ) @@ -113,7 +113,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, ) @@ -132,7 +132,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, ) @@ -151,7 +151,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, ) @@ -170,7 +170,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 } @@ -186,7 +186,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 } @@ -202,7 +202,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, userID uuid.UUID) ([]Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s", organizationID, uuidOrMe(userID)), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s", organizationID, uuidOrMe(userID)), nil) if err != nil { return nil, err } @@ -218,7 +218,7 @@ func (c *Client) WorkspacesByOwner(ctx context.Context, organizationID, userID u // WorkspaceByOwnerAndName returns a workspace by the owner's UUID and the workspace's name. func (c *Client) WorkspaceByOwnerAndName(ctx context.Context, organization, owner uuid.UUID, name string) (Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s/%s", organization, uuidOrMe(owner), name), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/workspaces/%s/%s", organization, uuidOrMe(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 59b52330b5fb1..dbbf8e7e53555 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", uuidOrMe(Me)), checks) + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/authorization", uuidOrMe(Me)), checks) if err != nil { return nil, err } diff --git a/codersdk/templates.go b/codersdk/templates.go index d1f3361c85671..dceb2bcbc2cc3 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -31,7 +31,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 } @@ -44,7 +44,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 } @@ -58,7 +58,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 } @@ -78,7 +78,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 } @@ -93,7 +93,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 3946d485799dc..d0d5695263c49 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -154,7 +154,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 } @@ -171,7 +171,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 } @@ -185,7 +185,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 } @@ -199,7 +199,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, userID uuid.UUID, req UpdateUserProfileRequest) (User, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", uuidOrMe(userID)), req) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", uuidOrMe(userID)), req) if err != nil { return User{}, err } @@ -213,7 +213,7 @@ func (c *Client) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req Up // SuspendUser enables callers to suspend a user func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/suspend", uuidOrMe(userID)), nil) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/suspend", uuidOrMe(userID)), nil) if err != nil { return User{}, err } @@ -229,7 +229,7 @@ func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error // UpdateUserPassword updates a user password. // It calls PUT /users/{user}/password func (c *Client) UpdateUserPassword(ctx context.Context, userID uuid.UUID, req UpdateUserPasswordRequest) error { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", uuidOrMe(userID)), req) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", uuidOrMe(userID)), req) if err != nil { return err } @@ -243,7 +243,7 @@ func (c *Client) UpdateUserPassword(ctx context.Context, userID uuid.UUID, req U // UpdateUserRoles grants the userID the specified roles. // Include ALL roles the user has. func (c *Client) UpdateUserRoles(ctx context.Context, userID uuid.UUID, req UpdateRoles) (User, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/roles", uuidOrMe(userID)), req) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/roles", uuidOrMe(userID)), req) if err != nil { return User{}, err } @@ -258,7 +258,7 @@ func (c *Client) UpdateUserRoles(ctx context.Context, userID uuid.UUID, req Upda // UpdateOrganizationMemberRoles grants the userID the specified roles in an org. // Include ALL roles the user has. func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organizationID, userID uuid.UUID, req UpdateRoles) (OrganizationMember, error) { - res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/organizations/%s/members/%s/roles", organizationID, uuidOrMe(userID)), req) + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/organizations/%s/members/%s/roles", organizationID, uuidOrMe(userID)), req) if err != nil { return OrganizationMember{}, err } @@ -272,7 +272,7 @@ func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organization // GetUserRoles returns all roles the user has func (c *Client) GetUserRoles(ctx context.Context, userID uuid.UUID) (UserRoles, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", uuidOrMe(userID)), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", uuidOrMe(userID)), nil) if err != nil { return UserRoles{}, err } @@ -286,7 +286,7 @@ func (c *Client) GetUserRoles(ctx context.Context, userID uuid.UUID) (UserRoles, // CreateAPIKey generates an API key for the user ID provided. func (c *Client) CreateAPIKey(ctx context.Context, userID uuid.UUID) (*GenerateAPIKeyResponse, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", uuidOrMe(userID)), nil) + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", uuidOrMe(userID)), nil) if err != nil { return nil, err } @@ -301,7 +301,7 @@ func (c *Client) CreateAPIKey(ctx context.Context, userID uuid.UUID) (*GenerateA // 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 } @@ -322,7 +322,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 } @@ -342,7 +342,7 @@ func (c *Client) UserByUsername(ctx context.Context, username string) (User, err } func (c *Client) userByIdentifier(ctx context.Context, ident string) (User, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", ident), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", ident), nil) if err != nil { return User{}, err } @@ -357,7 +357,7 @@ func (c *Client) userByIdentifier(ctx context.Context, ident string) (User, erro // 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() @@ -381,7 +381,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, userID uuid.UUID) ([]Organization, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", uuidOrMe(userID)), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", uuidOrMe(userID)), nil) if err != nil { return nil, err } @@ -394,7 +394,7 @@ func (c *Client) OrganizationsByUser(ctx context.Context, userID uuid.UUID) ([]O } func (c *Client) OrganizationByName(ctx context.Context, userID uuid.UUID, name string) (Organization, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", uuidOrMe(userID), name), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", uuidOrMe(userID), name), nil) if err != nil { return Organization{}, err } @@ -408,7 +408,7 @@ func (c *Client) OrganizationByName(ctx context.Context, userID uuid.UUID, name // CreateOrganization creates an organization and adds the provided user as an admin. func (c *Client) CreateOrganization(ctx context.Context, userID uuid.UUID, req CreateOrganizationRequest) (Organization, error) { - res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/organizations", uuidOrMe(userID)), req) + res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/organizations", uuidOrMe(userID)), req) if err != nil { return Organization{}, err } @@ -424,7 +424,7 @@ func (c *Client) CreateOrganization(ctx context.Context, userID uuid.UUID, req C // 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 } @@ -440,7 +440,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, userID uuid.UUID) ([]Workspace, error) { - res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", uuidOrMe(userID)), nil) + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces", uuidOrMe(userID)), 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 2fa5dd4b7611a..6729af94551a0 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -39,7 +39,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 } @@ -52,7 +52,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 } @@ -66,7 +66,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 } @@ -79,7 +79,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 } @@ -100,7 +100,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) } @@ -120,7 +120,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) }