mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore!: route connection logs to new table (#18340)
### Breaking Change (changelog note): > User connections to workspaces, and the opening of workspace apps or ports will no longer create entries in the audit log. Those events will now be included in the 'Connection Log'. Please see the 'Connection Log' page in the dashboard, and the Connection Log [documentation](https://coder.com/docs/admin/monitoring/connection-logs) for details. Those with permission to view the Audit Log will also be able to view the Connection Log. The new Connection Log has the same licensing restrictions as the Audit Log, and requires a Premium Coder deployment. ### Context This is the first PR of a few for moving connection events out of the audit log, and into a new database table and web UI page called the 'Connection Log'. This PR: - Creates the new table - Adds and tests queries for inserting and reading, including reading with an RBAC filter. - Implements the corresponding RBAC changes, such that anyone who can view the audit log can read from the table - Implements, under the enterprise package, a `ConnectionLogger` abstraction to replace the `Auditor` abstraction for these logs. (No-op'd in AGPL, like the `Auditor`) - Routes SSH connection and Workspace App events into the new `ConnectionLogger` - Updates all existing tests to check the values of the `ConnectionLogger` instead of the `Auditor`. Future PRs: - Add filtering to the query - Add an enterprise endpoint to query the new table - Write a query to delete old events from the audit log, call it from dbpurge. - Implement a table in the Web UI for viewing connection logs. > [!NOTE] > The PRs in this stack obviously won't be (completely) atomic. Whilst they'll each pass CI, the stack is designed to be merged all at once. I'm splitting them up for the sake of those reviewing, and so changes can be reviewed as early as possible. Despite this, it's really hard to make this PR any smaller than it already is. I'll be keeping it in draft until it's actually ready to merge.
This commit is contained in:
+62
-81
@@ -3,7 +3,6 @@ package workspaceapps
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -18,7 +17,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/connectionlog"
|
||||
"github.com/coder/coder/v2/coderd/cryptokeys"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
@@ -40,7 +39,7 @@ type DBTokenProvider struct {
|
||||
// DashboardURL is the main dashboard access URL for error pages.
|
||||
DashboardURL *url.URL
|
||||
Authorizer rbac.Authorizer
|
||||
Auditor *atomic.Pointer[audit.Auditor]
|
||||
ConnectionLogger *atomic.Pointer[connectionlog.ConnectionLogger]
|
||||
Database database.Store
|
||||
DeploymentValues *codersdk.DeploymentValues
|
||||
OAuth2Configs *httpmw.OAuth2Configs
|
||||
@@ -54,7 +53,7 @@ var _ SignedTokenProvider = &DBTokenProvider{}
|
||||
func NewDBTokenProvider(log slog.Logger,
|
||||
accessURL *url.URL,
|
||||
authz rbac.Authorizer,
|
||||
auditor *atomic.Pointer[audit.Auditor],
|
||||
connectionLogger *atomic.Pointer[connectionlog.ConnectionLogger],
|
||||
db database.Store,
|
||||
cfg *codersdk.DeploymentValues,
|
||||
oauth2Cfgs *httpmw.OAuth2Configs,
|
||||
@@ -73,7 +72,7 @@ func NewDBTokenProvider(log slog.Logger,
|
||||
Logger: log,
|
||||
DashboardURL: accessURL,
|
||||
Authorizer: authz,
|
||||
Auditor: auditor,
|
||||
ConnectionLogger: connectionLogger,
|
||||
Database: db,
|
||||
DeploymentValues: cfg,
|
||||
OAuth2Configs: oauth2Cfgs,
|
||||
@@ -95,7 +94,7 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r *
|
||||
// // permissions.
|
||||
dangerousSystemCtx := dbauthz.AsSystemRestricted(ctx)
|
||||
|
||||
aReq, commitAudit := p.auditInitRequest(ctx, rw, r)
|
||||
aReq, commitAudit := p.connLogInitRequest(ctx, rw, r)
|
||||
defer commitAudit()
|
||||
|
||||
appReq := issueReq.AppRequest.Normalize()
|
||||
@@ -386,20 +385,20 @@ func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *rbac.Subj
|
||||
return false, warnings, nil
|
||||
}
|
||||
|
||||
type auditRequest struct {
|
||||
type connLogRequest struct {
|
||||
time time.Time
|
||||
apiKey *database.APIKey
|
||||
dbReq *databaseRequest
|
||||
}
|
||||
|
||||
// auditInitRequest creates a new audit session and audit log for the given
|
||||
// request, if one does not already exist. If an audit session already exists,
|
||||
// it will be updated with the current timestamp. A session is used to reduce
|
||||
// the number of audit logs created.
|
||||
// connLogInitRequest creates a new connection log session and connect log for the
|
||||
// given request, if one does not already exist. If a connection log session
|
||||
// already exists, it will be updated with the current timestamp. A session is used to
|
||||
// reduce the number of connection logs created.
|
||||
//
|
||||
// A session is unique to the agent, app, user and users IP. If any of these
|
||||
// values change, a new session and audit log is created.
|
||||
func (p *DBTokenProvider) auditInitRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (aReq *auditRequest, commit func()) {
|
||||
// values change, a new session and connect log is created.
|
||||
func (p *DBTokenProvider) connLogInitRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (aReq *connLogRequest, commit func()) {
|
||||
// Get the status writer from the request context so we can figure
|
||||
// out the HTTP status and autocommit the audit log.
|
||||
sw, ok := w.(*tracing.StatusWriter)
|
||||
@@ -407,12 +406,12 @@ func (p *DBTokenProvider) auditInitRequest(ctx context.Context, w http.ResponseW
|
||||
panic("dev error: http.ResponseWriter is not *tracing.StatusWriter")
|
||||
}
|
||||
|
||||
aReq = &auditRequest{
|
||||
aReq = &connLogRequest{
|
||||
time: dbtime.Now(),
|
||||
}
|
||||
|
||||
// Set the commit function on the status writer to create an audit
|
||||
// log, this ensures that the status and response body are available.
|
||||
// Set the commit function on the status writer to create a connection log
|
||||
// this ensures that the status and response body are available.
|
||||
var committed bool
|
||||
return aReq, func() {
|
||||
if committed {
|
||||
@@ -422,7 +421,7 @@ func (p *DBTokenProvider) auditInitRequest(ctx context.Context, w http.ResponseW
|
||||
|
||||
if aReq.dbReq == nil {
|
||||
// App doesn't exist, there's information in the Request
|
||||
// struct but we need UUIDs for audit logging.
|
||||
// struct but we need UUIDs for connection logging.
|
||||
return
|
||||
}
|
||||
|
||||
@@ -434,28 +433,25 @@ func (p *DBTokenProvider) auditInitRequest(ctx context.Context, w http.ResponseW
|
||||
ip := r.RemoteAddr
|
||||
|
||||
// Approximation of the status code.
|
||||
statusCode := sw.Status
|
||||
// #nosec G115 - Safe conversion as HTTP status code is expected to be within int32 range (typically 100-599)
|
||||
var statusCode int32 = int32(sw.Status)
|
||||
if statusCode == 0 {
|
||||
statusCode = http.StatusOK
|
||||
}
|
||||
|
||||
type additionalFields struct {
|
||||
audit.AdditionalFields
|
||||
SlugOrPort string `json:"slug_or_port,omitempty"`
|
||||
}
|
||||
appInfo := additionalFields{
|
||||
AdditionalFields: audit.AdditionalFields{
|
||||
WorkspaceOwner: aReq.dbReq.Workspace.OwnerUsername,
|
||||
WorkspaceName: aReq.dbReq.Workspace.Name,
|
||||
WorkspaceID: aReq.dbReq.Workspace.ID,
|
||||
},
|
||||
}
|
||||
var (
|
||||
connType database.ConnectionType
|
||||
slugOrPort = aReq.dbReq.AppSlugOrPort
|
||||
)
|
||||
|
||||
switch {
|
||||
case aReq.dbReq.AccessMethod == AccessMethodTerminal:
|
||||
appInfo.SlugOrPort = "terminal"
|
||||
connType = database.ConnectionTypeWorkspaceApp
|
||||
slugOrPort = "terminal"
|
||||
case aReq.dbReq.App.ID == uuid.Nil:
|
||||
// If this isn't an app or a terminal, it's a port.
|
||||
appInfo.SlugOrPort = aReq.dbReq.AppSlugOrPort
|
||||
connType = database.ConnectionTypePortForwarding
|
||||
default:
|
||||
connType = database.ConnectionTypeWorkspaceApp
|
||||
}
|
||||
|
||||
// If we end up logging, ensure relevant fields are set.
|
||||
@@ -465,7 +461,7 @@ func (p *DBTokenProvider) auditInitRequest(ctx context.Context, w http.ResponseW
|
||||
slog.F("app_id", aReq.dbReq.App.ID),
|
||||
slog.F("user_id", userID),
|
||||
slog.F("user_agent", userAgent),
|
||||
slog.F("app_slug_or_port", appInfo.SlugOrPort),
|
||||
slog.F("app_slug_or_port", slugOrPort),
|
||||
slog.F("status_code", statusCode),
|
||||
)
|
||||
|
||||
@@ -485,9 +481,8 @@ func (p *DBTokenProvider) auditInitRequest(ctx context.Context, w http.ResponseW
|
||||
UserID: userID, // Can be unset, in which case uuid.Nil is fine.
|
||||
Ip: ip,
|
||||
UserAgent: userAgent,
|
||||
SlugOrPort: appInfo.SlugOrPort,
|
||||
// #nosec G115 - Safe conversion as HTTP status code is expected to be within int32 range (typically 100-599)
|
||||
StatusCode: int32(statusCode),
|
||||
SlugOrPort: slugOrPort,
|
||||
StatusCode: statusCode,
|
||||
StartedAt: aReq.time,
|
||||
UpdatedAt: aReq.time,
|
||||
})
|
||||
@@ -500,7 +495,7 @@ func (p *DBTokenProvider) auditInitRequest(ctx context.Context, w http.ResponseW
|
||||
if err != nil {
|
||||
logger.Error(ctx, "update workspace app audit session failed", slog.Error(err))
|
||||
|
||||
// Avoid spamming the audit log if deduplication failed, this should
|
||||
// Avoid spamming the connection log if deduplication failed, this should
|
||||
// only happen if there are problems communicating with the database.
|
||||
return
|
||||
}
|
||||
@@ -511,51 +506,37 @@ func (p *DBTokenProvider) auditInitRequest(ctx context.Context, w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
// Marshal additional fields only if we're writing an audit log entry.
|
||||
appInfoBytes, err := json.Marshal(appInfo)
|
||||
connLogger := *p.ConnectionLogger.Load()
|
||||
|
||||
err = connLogger.Upsert(ctx, database.UpsertConnectionLogParams{
|
||||
ID: uuid.New(),
|
||||
Time: aReq.time,
|
||||
OrganizationID: aReq.dbReq.Workspace.OrganizationID,
|
||||
WorkspaceOwnerID: aReq.dbReq.Workspace.OwnerID,
|
||||
WorkspaceID: aReq.dbReq.Workspace.ID,
|
||||
WorkspaceName: aReq.dbReq.Workspace.Name,
|
||||
AgentName: aReq.dbReq.Agent.Name,
|
||||
Type: connType,
|
||||
Code: sql.NullInt32{
|
||||
Int32: statusCode,
|
||||
Valid: true,
|
||||
},
|
||||
Ip: database.ParseIP(ip),
|
||||
UserAgent: sql.NullString{Valid: userAgent != "", String: userAgent},
|
||||
UserID: uuid.NullUUID{
|
||||
UUID: userID,
|
||||
Valid: userID != uuid.Nil,
|
||||
},
|
||||
SlugOrPort: sql.NullString{Valid: slugOrPort != "", String: slugOrPort},
|
||||
ConnectionStatus: database.ConnectionStatusConnected,
|
||||
|
||||
// N/A
|
||||
ConnectionID: uuid.NullUUID{},
|
||||
DisconnectReason: sql.NullString{},
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(ctx, "marshal additional fields failed", slog.Error(err))
|
||||
}
|
||||
|
||||
// We use the background audit function instead of init request
|
||||
// here because we don't know the resource type ahead of time.
|
||||
// This also allows us to log unauthenticated access.
|
||||
auditor := *p.Auditor.Load()
|
||||
requestID := httpmw.RequestID(r)
|
||||
switch {
|
||||
case aReq.dbReq.App.ID != uuid.Nil:
|
||||
audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.WorkspaceApp]{
|
||||
Audit: auditor,
|
||||
Log: logger,
|
||||
|
||||
Action: database.AuditActionOpen,
|
||||
OrganizationID: aReq.dbReq.Workspace.OrganizationID,
|
||||
UserID: userID,
|
||||
RequestID: requestID,
|
||||
Time: aReq.time,
|
||||
Status: statusCode,
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
New: aReq.dbReq.App,
|
||||
AdditionalFields: appInfoBytes,
|
||||
})
|
||||
default:
|
||||
// Web terminal, port app, etc.
|
||||
audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.WorkspaceAgent]{
|
||||
Audit: auditor,
|
||||
Log: logger,
|
||||
|
||||
Action: database.AuditActionOpen,
|
||||
OrganizationID: aReq.dbReq.Workspace.OrganizationID,
|
||||
UserID: userID,
|
||||
RequestID: requestID,
|
||||
Time: aReq.time,
|
||||
Status: statusCode,
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
New: aReq.dbReq.Agent,
|
||||
AdditionalFields: appInfoBytes,
|
||||
})
|
||||
logger.Error(ctx, "upsert connection log failed", slog.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+119
-151
@@ -3,7 +3,6 @@ package workspaceapps_test
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -22,10 +21,9 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agenttest"
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/connectionlog"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/jwtutils"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
@@ -83,12 +81,12 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
deploymentValues.Dangerous.AllowPathAppSharing = true
|
||||
deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = true
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
t.Cleanup(func() {
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
assert.Len(t, auditor.AuditLogs(), 0, "one or more test cases produced unexpected audit logs, did you replace the auditor or forget to call ResetLogs?")
|
||||
assert.Len(t, connLogger.ConnectionLogs(), 0, "one or more test cases produced unexpected connection logs, did you replace the auditor or forget to call ResetLogs?")
|
||||
})
|
||||
client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
||||
AppHostname: "*.test.coder.com",
|
||||
@@ -105,7 +103,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
"CF-Connecting-IP",
|
||||
},
|
||||
},
|
||||
Auditor: auditor,
|
||||
ConnectionLogger: connLogger,
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
_ = closer.Close()
|
||||
@@ -231,23 +229,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
}
|
||||
require.NotEqual(t, uuid.Nil, agentID)
|
||||
|
||||
//nolint:gocritic // This is a test, allow dbauthz.AsSystemRestricted.
|
||||
agent, err := api.Database.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), agentID)
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:gocritic // This is a test, allow dbauthz.AsSystemRestricted.
|
||||
apps, err := api.Database.GetWorkspaceAppsByAgentID(dbauthz.AsSystemRestricted(ctx), agentID)
|
||||
require.NoError(t, err)
|
||||
appsBySlug := make(map[string]database.WorkspaceApp, len(apps))
|
||||
for _, app := range apps {
|
||||
appsBySlug[app.Slug] = app
|
||||
}
|
||||
|
||||
// Reset audit logs so cleanup check can pass.
|
||||
auditor.ResetLogs()
|
||||
|
||||
assertAuditAgent := auditAsserter[database.WorkspaceAgent](workspace)
|
||||
assertAuditApp := auditAsserter[database.WorkspaceApp](workspace)
|
||||
connLogger.Reset()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -285,9 +268,9 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: app,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
auditableUA := "Tidua"
|
||||
auditableUA := "Noitcennoc"
|
||||
|
||||
t.Log("app", app)
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -297,7 +280,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set("User-Agent", auditableUA)
|
||||
|
||||
// Try resolving the request without a token.
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -333,8 +316,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.Equal(t, codersdk.SignedAppTokenCookie, cookie.Name)
|
||||
require.Equal(t, req.BasePath, cookie.Path)
|
||||
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[app], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "audit log count")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, app, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
|
||||
var parsedToken workspaceapps.SignedToken
|
||||
err := jwtutils.Verify(ctx, api.AppSigningKeyCache, cookie.Value, &parsedToken)
|
||||
@@ -350,7 +333,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.AddCookie(cookie)
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
secondToken, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
secondToken, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -363,7 +346,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.WithinDuration(t, token.Expiry.Time(), secondToken.Expiry.Time(), 2*time.Second)
|
||||
secondToken.Expiry = token.Expiry
|
||||
require.Equal(t, token, secondToken)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "no new audit log, FromRequest returned the same token and is not audited")
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1, "no new connection log, FromRequest returned the same token and is not logged")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -382,7 +365,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: app,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
t.Log("app", app)
|
||||
@@ -391,7 +374,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, secondUserClient.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -406,14 +389,15 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.Nil(t, token)
|
||||
require.NotZero(t, w.StatusCode)
|
||||
require.Equal(t, http.StatusNotFound, w.StatusCode)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
return
|
||||
}
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, token)
|
||||
require.Zero(t, w.StatusCode)
|
||||
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[app], secondUser.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, app, database.ConnectionTypeWorkspaceApp, secondUser.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -430,14 +414,14 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: app,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
t.Log("app", app)
|
||||
rw := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/app", nil)
|
||||
r.RemoteAddr = auditableIP
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -452,8 +436,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.NotZero(t, rw.Code)
|
||||
require.NotEqual(t, http.StatusOK, rw.Code)
|
||||
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[app], uuid.Nil, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "audit log for unauthenticated requests")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, app, database.ConnectionTypeWorkspaceApp, uuid.Nil)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
} else {
|
||||
if !assert.True(t, ok) {
|
||||
dump, err := httputil.DumpResponse(w, true)
|
||||
@@ -466,8 +450,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
t.Fatalf("expected 200 (or unset) response code, got %d", rw.Code)
|
||||
}
|
||||
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[app], uuid.Nil, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, app, database.ConnectionTypeWorkspaceApp, uuid.Nil)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
}
|
||||
_ = w.Body.Close()
|
||||
}
|
||||
@@ -479,12 +463,12 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
req := (workspaceapps.Request{
|
||||
AccessMethod: "invalid",
|
||||
}).Normalize()
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
rw := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/app", nil)
|
||||
r.RemoteAddr = auditableIP
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -494,7 +478,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
})
|
||||
require.False(t, ok)
|
||||
require.Nil(t, token)
|
||||
require.Len(t, auditor.AuditLogs(), 0, "no audit logs for invalid requests")
|
||||
require.Len(t, connLogger.ConnectionLogs(), 0)
|
||||
})
|
||||
|
||||
t.Run("SplitWorkspaceAndAgent", func(t *testing.T) {
|
||||
@@ -562,7 +546,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: appNamePublic,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -570,7 +554,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -591,11 +575,11 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.Equal(t, token.AgentNameOrID, c.agent)
|
||||
require.Equal(t, token.WorkspaceID, workspace.ID)
|
||||
require.Equal(t, token.AgentID, agentID)
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[token.AppSlugOrPort], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, token.AppSlugOrPort, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
} else {
|
||||
require.Nil(t, token)
|
||||
require.Len(t, auditor.AuditLogs(), 0, "no audit logs")
|
||||
require.Len(t, connLogger.ConnectionLogs(), 0)
|
||||
}
|
||||
_ = w.Body.Close()
|
||||
})
|
||||
@@ -637,7 +621,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: appNameOwner,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -651,7 +635,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
|
||||
// Even though the token is invalid, we should still perform request
|
||||
// resolution without failure since we'll just ignore the bad token.
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -676,8 +660,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, appNameOwner, parsedToken.AppSlugOrPort)
|
||||
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[appNameOwner], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, appNameOwner, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
})
|
||||
|
||||
t.Run("PortPathBlocked", func(t *testing.T) {
|
||||
@@ -692,7 +676,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: "8080",
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -700,7 +684,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -715,7 +699,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
_ = w.Body.Close()
|
||||
// TODO(mafredri): Verify this is the correct status code.
|
||||
require.Equal(t, http.StatusInternalServerError, w.StatusCode)
|
||||
require.Len(t, auditor.AuditLogs(), 0, "no audit logs for port path blocked requests")
|
||||
require.Len(t, connLogger.ConnectionLogs(), 0, "no connection logs for port path blocked requests")
|
||||
})
|
||||
|
||||
t.Run("PortSubdomain", func(t *testing.T) {
|
||||
@@ -730,7 +714,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: "9090",
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -738,7 +722,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -749,11 +733,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
require.Equal(t, req.AppSlugOrPort, token.AppSlugOrPort)
|
||||
require.Equal(t, "http://127.0.0.1:9090", token.AppURL)
|
||||
|
||||
assertAuditAgent(t, rw, r, auditor, agent, me.ID, map[string]any{
|
||||
"slug_or_port": "9090",
|
||||
})
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, "9090", database.ConnectionTypePortForwarding, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
})
|
||||
|
||||
t.Run("PortSubdomainHTTPSS", func(t *testing.T) {
|
||||
@@ -768,7 +749,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: "9090ss",
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -776,7 +757,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
_, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
_, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -792,7 +773,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(b), "404 - Application Not Found")
|
||||
require.Equal(t, http.StatusNotFound, w.StatusCode)
|
||||
require.Len(t, auditor.AuditLogs(), 0, "no audit logs for invalid requests")
|
||||
require.Len(t, connLogger.ConnectionLogs(), 0)
|
||||
})
|
||||
|
||||
t.Run("SubdomainEndsInS", func(t *testing.T) {
|
||||
@@ -807,7 +788,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: appNameEndsInS,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -815,7 +796,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -825,8 +806,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, req.AppSlugOrPort, token.AppSlugOrPort)
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[appNameEndsInS], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, appNameEndsInS, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
})
|
||||
|
||||
t.Run("Terminal", func(t *testing.T) {
|
||||
@@ -838,7 +819,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AgentNameOrID: agentID.String(),
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -846,7 +827,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -862,10 +843,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.Equal(t, req.AgentNameOrID, token.Request.AgentNameOrID)
|
||||
require.Empty(t, token.AppSlugOrPort)
|
||||
require.Empty(t, token.AppURL)
|
||||
assertAuditAgent(t, rw, r, auditor, agent, me.ID, map[string]any{
|
||||
"slug_or_port": "terminal",
|
||||
})
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, "terminal", database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
})
|
||||
|
||||
t.Run("InsufficientPermissions", func(t *testing.T) {
|
||||
@@ -880,7 +859,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: appNameOwner,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -888,7 +867,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, secondUserClient.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -898,8 +877,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
})
|
||||
require.False(t, ok)
|
||||
require.Nil(t, token)
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[appNameOwner], secondUser.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, appNameOwner, database.ConnectionTypeWorkspaceApp, secondUser.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
})
|
||||
|
||||
t.Run("UserNotFound", func(t *testing.T) {
|
||||
@@ -913,7 +892,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: appNameOwner,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -921,7 +900,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -931,7 +910,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
})
|
||||
require.False(t, ok)
|
||||
require.Nil(t, token)
|
||||
require.Len(t, auditor.AuditLogs(), 0, "no audit logs for user not found")
|
||||
require.Len(t, connLogger.ConnectionLogs(), 0)
|
||||
})
|
||||
|
||||
t.Run("RedirectSubdomainAuth", func(t *testing.T) {
|
||||
@@ -946,7 +925,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: appNameOwner,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -955,7 +934,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Host = "app.com"
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -972,8 +951,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
require.Equal(t, http.StatusSeeOther, w.StatusCode)
|
||||
// Note that we don't capture the owner UUID here because the apiKey
|
||||
// check/authorization exits early.
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[appNameOwner], uuid.Nil, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "autit log entry for redirect")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, appNameOwner, database.ConnectionTypeWorkspaceApp, uuid.Nil)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
|
||||
loc, err := w.Location()
|
||||
require.NoError(t, err)
|
||||
@@ -1012,7 +991,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: appNameAgentUnhealthy,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -1020,7 +999,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -1034,8 +1013,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
w := rw.Result()
|
||||
defer w.Body.Close()
|
||||
require.Equal(t, http.StatusBadGateway, w.StatusCode)
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[appNameAgentUnhealthy], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentNameUnhealthy, appNameAgentUnhealthy, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
|
||||
body, err := io.ReadAll(w.Body)
|
||||
require.NoError(t, err)
|
||||
@@ -1075,7 +1054,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: appNameInitializing,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -1083,7 +1062,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -1093,8 +1072,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
})
|
||||
require.True(t, ok, "ResolveRequest failed, should pass even though app is initializing")
|
||||
require.NotNil(t, token)
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[token.AppSlugOrPort], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, token.AppSlugOrPort, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
})
|
||||
|
||||
// Unhealthy apps are now permitted to connect anyways. This wasn't always
|
||||
@@ -1133,7 +1112,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: appNameUnhealthy,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -1141,7 +1120,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
token, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -1151,11 +1130,11 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
})
|
||||
require.True(t, ok, "ResolveRequest failed, should pass even though app is unhealthy")
|
||||
require.NotNil(t, token)
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[token.AppSlugOrPort], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, token.AppSlugOrPort, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
})
|
||||
|
||||
t.Run("AuditLogging", func(t *testing.T) {
|
||||
t.Run("ConnectionLogging", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, app := range allApps {
|
||||
@@ -1168,18 +1147,18 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppSlugOrPort: app,
|
||||
}).Normalize()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
connLogger := connectionlog.NewFake()
|
||||
auditableIP := testutil.RandomIPv6(t)
|
||||
|
||||
t.Log("app", app)
|
||||
|
||||
// First request, new audit log.
|
||||
// First request, new connection log.
|
||||
rw := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/app", nil)
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
_, ok := workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
_, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -1188,8 +1167,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppRequest: req,
|
||||
})
|
||||
require.True(t, ok)
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[app], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, app, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1)
|
||||
|
||||
// Second request, no audit log because the session is active.
|
||||
rw = httptest.NewRecorder()
|
||||
@@ -1197,7 +1176,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
_, ok = workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
_, ok = workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -1206,7 +1185,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppRequest: req,
|
||||
})
|
||||
require.True(t, ok)
|
||||
require.Len(t, auditor.AuditLogs(), 1, "single audit log, previous session active")
|
||||
require.Len(t, connLogger.ConnectionLogs(), 1, "single connection log, previous session active")
|
||||
|
||||
// Third request, session timed out, new audit log.
|
||||
rw = httptest.NewRecorder()
|
||||
@@ -1214,7 +1193,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
sessionTimeoutTokenProvider := signedTokenProviderWithAuditor(t, api.WorkspaceAppsProvider, auditor, 0)
|
||||
sessionTimeoutTokenProvider := signedTokenProviderWithConnLogger(t, api.WorkspaceAppsProvider, connLogger, 0)
|
||||
_, ok = workspaceappsResolveRequest(t, nil, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: sessionTimeoutTokenProvider,
|
||||
@@ -1224,8 +1203,8 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppRequest: req,
|
||||
})
|
||||
require.True(t, ok)
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[app], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 2, "two audit logs, session timed out")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, app, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 2, "two connection logs, session timed out")
|
||||
|
||||
// Fourth request, new IP produces new audit log.
|
||||
auditableIP = testutil.RandomIPv6(t)
|
||||
@@ -1234,7 +1213,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
|
||||
r.RemoteAddr = auditableIP
|
||||
|
||||
_, ok = workspaceappsResolveRequest(t, auditor, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
_, ok = workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
|
||||
Logger: api.Logger,
|
||||
SignedTokenProvider: api.WorkspaceAppsProvider,
|
||||
DashboardURL: api.AccessURL,
|
||||
@@ -1243,16 +1222,16 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
AppRequest: req,
|
||||
})
|
||||
require.True(t, ok)
|
||||
assertAuditApp(t, rw, r, auditor, appsBySlug[app], me.ID, nil)
|
||||
require.Len(t, auditor.AuditLogs(), 3, "three audit logs, new IP")
|
||||
assertConnLogContains(t, rw, r, connLogger, workspace, agentName, app, database.ConnectionTypeWorkspaceApp, me.ID)
|
||||
require.Len(t, connLogger.ConnectionLogs(), 3, "three connection logs, new IP")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func workspaceappsResolveRequest(t testing.TB, auditor audit.Auditor, w http.ResponseWriter, r *http.Request, opts workspaceapps.ResolveRequestOptions) (token *workspaceapps.SignedToken, ok bool) {
|
||||
func workspaceappsResolveRequest(t testing.TB, connLogger connectionlog.ConnectionLogger, w http.ResponseWriter, r *http.Request, opts workspaceapps.ResolveRequestOptions) (token *workspaceapps.SignedToken, ok bool) {
|
||||
t.Helper()
|
||||
if opts.SignedTokenProvider != nil && auditor != nil {
|
||||
opts.SignedTokenProvider = signedTokenProviderWithAuditor(t, opts.SignedTokenProvider, auditor, time.Hour)
|
||||
if opts.SignedTokenProvider != nil && connLogger != nil {
|
||||
opts.SignedTokenProvider = signedTokenProviderWithConnLogger(t, opts.SignedTokenProvider, connLogger, time.Hour)
|
||||
}
|
||||
|
||||
tracing.StatusWriterMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1264,52 +1243,41 @@ func workspaceappsResolveRequest(t testing.TB, auditor audit.Auditor, w http.Res
|
||||
return token, ok
|
||||
}
|
||||
|
||||
func signedTokenProviderWithAuditor(t testing.TB, provider workspaceapps.SignedTokenProvider, auditor audit.Auditor, sessionTimeout time.Duration) workspaceapps.SignedTokenProvider {
|
||||
func signedTokenProviderWithConnLogger(t testing.TB, provider workspaceapps.SignedTokenProvider, connLogger connectionlog.ConnectionLogger, sessionTimeout time.Duration) workspaceapps.SignedTokenProvider {
|
||||
t.Helper()
|
||||
p, ok := provider.(*workspaceapps.DBTokenProvider)
|
||||
require.True(t, ok, "provider is not a DBTokenProvider")
|
||||
|
||||
shallowCopy := *p
|
||||
shallowCopy.Auditor = &atomic.Pointer[audit.Auditor]{}
|
||||
shallowCopy.Auditor.Store(&auditor)
|
||||
shallowCopy.ConnectionLogger = &atomic.Pointer[connectionlog.ConnectionLogger]{}
|
||||
shallowCopy.ConnectionLogger.Store(&connLogger)
|
||||
shallowCopy.WorkspaceAppAuditSessionTimeout = sessionTimeout
|
||||
return &shallowCopy
|
||||
}
|
||||
|
||||
func auditAsserter[T audit.Auditable](workspace codersdk.Workspace) func(t testing.TB, rr *httptest.ResponseRecorder, r *http.Request, auditor *audit.MockAuditor, auditable T, userID uuid.UUID, additionalFields map[string]any) {
|
||||
return func(t testing.TB, rr *httptest.ResponseRecorder, r *http.Request, auditor *audit.MockAuditor, auditable T, userID uuid.UUID, additionalFields map[string]any) {
|
||||
t.Helper()
|
||||
func assertConnLogContains(t *testing.T, rr *httptest.ResponseRecorder, r *http.Request, connLogger *connectionlog.FakeConnectionLogger, workspace codersdk.Workspace, agentName string, slugOrPort string, typ database.ConnectionType, userID uuid.UUID) {
|
||||
t.Helper()
|
||||
|
||||
resp := rr.Result()
|
||||
defer resp.Body.Close()
|
||||
resp := rr.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.True(t, auditor.Contains(t, database.AuditLog{
|
||||
OrganizationID: workspace.OrganizationID,
|
||||
Action: database.AuditActionOpen,
|
||||
ResourceType: audit.ResourceType(auditable),
|
||||
ResourceID: audit.ResourceID(auditable),
|
||||
ResourceTarget: audit.ResourceTarget(auditable),
|
||||
UserID: userID,
|
||||
Ip: audit.ParseIP(r.RemoteAddr),
|
||||
UserAgent: sql.NullString{Valid: r.UserAgent() != "", String: r.UserAgent()},
|
||||
StatusCode: int32(resp.StatusCode), //nolint:gosec
|
||||
}), "audit log")
|
||||
|
||||
// Verify additional fields, assume the last log entry.
|
||||
alog := auditor.AuditLogs()[len(auditor.AuditLogs())-1]
|
||||
|
||||
// Contains does not verify uuid.Nil.
|
||||
if userID == uuid.Nil {
|
||||
require.Equal(t, uuid.Nil, alog.UserID, "unauthenticated user")
|
||||
}
|
||||
|
||||
add := make(map[string]any)
|
||||
if len(alog.AdditionalFields) > 0 {
|
||||
err := json.Unmarshal([]byte(alog.AdditionalFields), &add)
|
||||
require.NoError(t, err, "audit log unmarhsal additional fields")
|
||||
}
|
||||
for k, v := range additionalFields {
|
||||
require.Equal(t, v, add[k], "audit log additional field %s: additional fields: %v", k, add)
|
||||
}
|
||||
}
|
||||
require.True(t, connLogger.Contains(t, database.UpsertConnectionLogParams{
|
||||
OrganizationID: workspace.OrganizationID,
|
||||
WorkspaceOwnerID: workspace.OwnerID,
|
||||
WorkspaceID: workspace.ID,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: agentName,
|
||||
Type: typ,
|
||||
Ip: database.ParseIP(r.RemoteAddr),
|
||||
UserAgent: sql.NullString{Valid: r.UserAgent() != "", String: r.UserAgent()},
|
||||
Code: sql.NullInt32{
|
||||
Int32: int32(resp.StatusCode), // nolint:gosec
|
||||
Valid: true,
|
||||
},
|
||||
UserID: uuid.NullUUID{
|
||||
UUID: userID,
|
||||
Valid: true,
|
||||
},
|
||||
SlugOrPort: sql.NullString{Valid: slugOrPort != "", String: slugOrPort},
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user