Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

chore: refactor /insights API routes for testing#21051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
spikecurtis wants to merge1 commit intomain
base:main
Choose a base branch
Loading
fromspike/coderd-api-test-refactor
Draft
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletionsMakefile
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -679,7 +679,7 @@ gen/db: $(DB_GEN_FILES)
gen/golden-files: \
agent/unit/testdata/.gen-golden \
cli/testdata/.gen-golden \
coderd/.gen-golden \
coderd/insightsapi/.gen-golden \
coderd/notifications/.gen-golden \
enterprise/cli/testdata/.gen-golden \
enterprise/tailnet/testdata/.gen-golden \
Expand DownExpand Up@@ -953,7 +953,7 @@ clean/golden-files:
find \
cli/testdata \
coderd/notifications/testdata \
coderd/testdata \
coderd/insightsapi/testdata \
enterprise/cli/testdata \
enterprise/tailnet/testdata \
helm/coder/tests/testdata \
Expand DownExpand Up@@ -991,8 +991,8 @@ helm/provisioner/tests/testdata/.gen-golden: $(wildcard helm/provisioner/tests/t
TZ=UTC go test ./helm/provisioner/tests -run=TestUpdateGoldenFiles -update
touch "$@"

coderd/.gen-golden: $(wildcard coderd/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/*_test.go)
TZ=UTC go test ./coderd -run="Test.*Golden$$" -update
coderd/insightsapi/.gen-golden: $(wildcard coderd/insightsapi/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/insightsapi/*_test.go)
TZ=UTC go test ./coderd/insightsapi -run="Test.*Golden$$" -update
touch "$@"

coderd/notifications/.gen-golden: $(wildcard coderd/notifications/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/notifications/*_test.go)
Expand Down
3 changes: 2 additions & 1 deletioncoderd/apikey.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpauthz"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
Expand DownExpand Up@@ -341,7 +342,7 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) {
}
}

keys, err =AuthorizeFilter(api.HTTPAuth, r, policy.ActionRead, keys)
keys, err =httpauthz.AuthorizationFilter(api.HTTPAuth, r, policy.ActionRead, keys)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching keys.",
Expand Down
79 changes: 0 additions & 79 deletionscoderd/authorize.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import (
"net/http"

"github.com/google/uuid"
"golang.org/x/xerrors"

"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/httpapi"
Expand All@@ -15,33 +14,6 @@ import (
"github.com/coder/coder/v2/codersdk"
)

// AuthorizeFilter takes a list of objects and returns the filtered list of
// objects that the user is authorized to perform the given action on.
// This is faster than calling Authorize() on each object.
func AuthorizeFilter[O rbac.Objecter](h *HTTPAuthorizer, r *http.Request, action policy.Action, objects []O) ([]O, error) {
roles := httpmw.UserAuthorization(r.Context())
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles, action, objects)
if err != nil {
// Log the error as Filter should not be erroring.
h.Logger.Error(r.Context(), "authorization filter failed",
slog.Error(err),
slog.F("user_id", roles.ID),
slog.F("username", roles),
slog.F("roles", roles.SafeRoleNames()),
slog.F("scope", roles.SafeScopeName()),
slog.F("route", r.URL.Path),
slog.F("action", action),
)
return nil, err
}
return objects, nil
}

type HTTPAuthorizer struct {
Authorizer rbac.Authorizer
Logger slog.Logger
}

// Authorize will return false if the user is not authorized to do the action.
// This function will log appropriately, but the caller must return an
// error to the api client.
Expand All@@ -55,57 +27,6 @@ func (api *API) Authorize(r *http.Request, action policy.Action, object rbac.Obj
return api.HTTPAuth.Authorize(r, action, object)
}

// Authorize will return false if the user is not authorized to do the action.
// This function will log appropriately, but the caller must return an
// error to the api client.
// Eg:
//
//if !h.Authorize(...) {
//httpapi.Forbidden(rw)
//return
//}
func (h *HTTPAuthorizer) Authorize(r *http.Request, action policy.Action, object rbac.Objecter) bool {
roles := httpmw.UserAuthorization(r.Context())
err := h.Authorizer.Authorize(r.Context(), roles, action, object.RBACObject())
if err != nil {
// Log the errors for debugging
internalError := new(rbac.UnauthorizedError)
logger := h.Logger
if xerrors.As(err, internalError) {
logger = h.Logger.With(slog.F("internal_error", internalError.Internal()))
}
// Log information for debugging. This will be very helpful
// in the early days
logger.Warn(r.Context(), "requester is not authorized to access the object",
slog.F("roles", roles.SafeRoleNames()),
slog.F("actor_id", roles.ID),
slog.F("actor_name", roles),
slog.F("scope", roles.SafeScopeName()),
slog.F("route", r.URL.Path),
slog.F("action", action),
slog.F("object", object),
)

return false
}
return true
}

// AuthorizeSQLFilter returns an authorization filter that can used in a
// SQL 'WHERE' clause. If the filter is used, the resulting rows returned
// from postgres are already authorized, and the caller does not need to
// call 'Authorize()' on the returned objects.
// Note the authorization is only for the given action and object type.
func (h *HTTPAuthorizer) AuthorizeSQLFilter(r *http.Request, action policy.Action, objectType string) (rbac.PreparedAuthorized, error) {
roles := httpmw.UserAuthorization(r.Context())
prepared, err := h.Authorizer.Prepare(r.Context(), roles, action, objectType)
if err != nil {
return nil, xerrors.Errorf("prepare filter: %w", err)
}

return prepared, nil
}

// checkAuthorization returns if the current API key can use the given
// permissions, factoring in the current user's roles and the API key scopes.
//
Expand Down
20 changes: 13 additions & 7 deletionscoderd/coderd.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,8 @@ import (
"sync/atomic"
"time"

"github.com/coder/coder/v2/coderd/httpauthz"
"github.com/coder/coder/v2/coderd/insightsapi"
"github.com/coder/coder/v2/coderd/oauth2provider"
"github.com/coder/coder/v2/coderd/pproflabel"
"github.com/coder/coder/v2/coderd/prebuilds"
Expand DownExpand Up@@ -584,7 +586,7 @@ func New(options *Options) *API {
ID: uuid.New(),
Options: options,
RootHandler: r,
HTTPAuth: &HTTPAuthorizer{
HTTPAuth: &httpauthz.HTTPAuthorizer{
Authorizer: options.Authorizer,
Logger: options.Logger,
},
Expand DownExpand Up@@ -800,6 +802,8 @@ func New(options *Options) *API {
APIKeyEncryptionKeycache: options.AppEncryptionKeyCache,
})

api.insightsAPI = insightsapi.NewAPI(api.Logger, api.Database, api.HTTPAuth)

apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
DB: options.Database,
ActivateDormantUser: ActivateDormantUser(options.Logger, &api.Auditor, options.Database),
Expand DownExpand Up@@ -1524,11 +1528,11 @@ func New(options *Options) *API {
})
r.Route("/insights", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Get("/daus", api.deploymentDAUs)
r.Get("/user-activity", api.insightsUserActivity)
r.Get("/user-status-counts", api.insightsUserStatusCounts)
r.Get("/user-latency", api.insightsUserLatency)
r.Get("/templates", api.insightsTemplates)
r.Get("/daus", api.insightsAPI.DeploymentDAUs)
r.Get("/user-activity", api.insightsAPI.UserActivity)
r.Get("/user-status-counts", api.insightsAPI.UserStatusCounts)
r.Get("/user-latency", api.insightsAPI.UserLatency)
r.Get("/templates", api.insightsAPI.Templates)
})
r.Route("/debug", func(r chi.Router) {
r.Use(
Expand DownExpand Up@@ -1802,7 +1806,7 @@ type API struct {

UpdatesProvider tailnet.WorkspaceUpdatesProvider

HTTPAuth *HTTPAuthorizer
HTTPAuth *httpauthz.HTTPAuthorizer

// APIHandler serves "/api/v2"
APIHandler chi.Router
Expand DownExpand Up@@ -1837,6 +1841,8 @@ type API struct {
// dbRolluper rolls up template usage stats from raw agent and app
// stats. This is used to provide insights in the WebUI.
dbRolluper *dbrollup.Rolluper

insightsAPI *insightsapi.API
}

// Close waits for all WebSocket connections to drain before returning.
Expand Down
4 changes: 0 additions & 4 deletionscoderd/coderd_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@ package coderd_test

import (
"context"
"flag"
"fmt"
"io"
"net/http"
Expand DownExpand Up@@ -35,9 +34,6 @@ import (
"github.com/coder/coder/v2/testutil"
)

// updateGoldenFiles is a flag that can be set to update golden files.
varupdateGoldenFiles=flag.Bool("update",false,"Update golden files")

funcTestMain(m*testing.M) {
goleak.VerifyTestMain(m,testutil.GoleakOptions...)
}
Expand Down
29 changes: 29 additions & 0 deletionscoderd/coderdtest/database.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
package coderdtest

import (
"sync/atomic"
"testing"

"github.com/prometheus/client_golang/prometheus"
"go.uber.org/mock/gomock"

"cdr.dev/slog"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/rbac"
)

func MockedDatabaseWithAuthz(t testing.TB, logger slog.Logger) (*gomock.Controller, *dbmock.MockStore, database.Store, rbac.Authorizer) {
ctrl := gomock.NewController(t)
mDB := dbmock.NewMockStore(ctrl)
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
accessControlStore := &atomic.Pointer[dbauthz.AccessControlStore]{}
var acs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
accessControlStore.Store(&acs)
// dbauthz will call Wrappers() to check for wrapped databases
mDB.EXPECT().Wrappers().Return([]string{}).AnyTimes()
authDB := dbauthz.New(mDB, auth, logger, accessControlStore)
return ctrl, mDB, authDB, auth
}
14 changes: 14 additions & 0 deletionscoderd/coderdtest/subjects.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
package coderdtest

import "github.com/coder/coder/v2/coderd/rbac"

func OwnerSubject() rbac.Subject {
return rbac.Subject{
FriendlyName: "coderdtest-owner",
Email: "owner@coderd.test",
Type: rbac.SubjectTypeUser,
ID: "coderdtest-owner-id",
Roles: rbac.RoleIdentifiers{rbac.RoleOwner()},
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
}
2 changes: 1 addition & 1 deletioncoderd/coderdtest/swagger_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import (
func TestEndpointsDocumented(t *testing.T) {
t.Parallel()

swaggerComments, err := coderdtest.ParseSwaggerComments("..")
swaggerComments, err := coderdtest.ParseSwaggerComments("..", "../insightsapi")
require.NoError(t, err, "can't parse swagger comments")
require.NotEmpty(t, swaggerComments, "swagger comments must be present")

Expand Down
91 changes: 91 additions & 0 deletionscoderd/httpauthz/httpauthz.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
package httpauthz

import (
"net/http"

"golang.org/x/xerrors"

"cdr.dev/slog"

"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
)

// AuthorizationFilter takes a list of objects and returns the filtered list of
// objects that the user is authorized to perform the given action on.
// This is faster than calling Authorize() on each object.
func AuthorizationFilter[O rbac.Objecter](h *HTTPAuthorizer, r *http.Request, action policy.Action, objects []O) ([]O, error) {
roles := httpmw.UserAuthorization(r.Context())
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles, action, objects)
if err != nil {
// Log the error as Filter should not be erroring.
h.Logger.Error(r.Context(), "authorization filter failed",
slog.Error(err),
slog.F("user_id", roles.ID),
slog.F("username", roles),
slog.F("roles", roles.SafeRoleNames()),
slog.F("scope", roles.SafeScopeName()),
slog.F("route", r.URL.Path),
slog.F("action", action),
)
return nil, err
}
return objects, nil
}

type HTTPAuthorizer struct {
Authorizer rbac.Authorizer
Logger slog.Logger
}

// AuthorizeSQLFilter returns an authorization filter that can used in a
// SQL 'WHERE' clause. If the filter is used, the resulting rows returned
// from postgres are already authorized, and the caller does not need to
// call 'Authorize()' on the returned objects.
// Note the authorization is only for the given action and object type.
func (h *HTTPAuthorizer) AuthorizeSQLFilter(r *http.Request, action policy.Action, objectType string) (rbac.PreparedAuthorized, error) {
roles := httpmw.UserAuthorization(r.Context())
prepared, err := h.Authorizer.Prepare(r.Context(), roles, action, objectType)
if err != nil {
return nil, xerrors.Errorf("prepare filter: %w", err)
}

return prepared, nil
}

// Authorize will return false if the user is not authorized to do the action.
// This function will log appropriately, but the caller must return an
// error to the api client.
// Eg:
//
//if !h.Authorize(...) {
//httpapi.Forbidden(rw)
//return
//}
func (h *HTTPAuthorizer) Authorize(r *http.Request, action policy.Action, object rbac.Objecter) bool {
roles := httpmw.UserAuthorization(r.Context())
err := h.Authorizer.Authorize(r.Context(), roles, action, object.RBACObject())
if err != nil {
// Log the errors for debugging
internalError := new(rbac.UnauthorizedError)
logger := h.Logger
if xerrors.As(err, internalError) {
logger = h.Logger.With(slog.F("internal_error", internalError.Internal()))
}
// Log information for debugging. This will be very helpful
// in the early days
logger.Warn(r.Context(), "requester is not authorized to access the object",
slog.F("roles", roles.SafeRoleNames()),
slog.F("actor_id", roles.ID),
slog.F("actor_name", roles),
slog.F("scope", roles.SafeScopeName()),
slog.F("route", r.URL.Path),
slog.F("action", action),
slog.F("object", object),
)

return false
}
return true
}
27 changes: 27 additions & 0 deletionscoderd/insightsapi/api.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
package insightsapi

import (
"cdr.dev/slog"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/httpauthz"
)

typeAPIstruct {
logger slog.Logger
authorizer*httpauthz.HTTPAuthorizer
database database.Store
}

funcNewAPI(
logger slog.Logger,
db database.Store,
authorizer*httpauthz.HTTPAuthorizer,
)*API {
a:=&API{
logger:logger.Named("insightsapi"),
authorizer:authorizer,
database:db,
}
returna
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp