Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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: 5 additions & 3 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Postgres does not support removing enum values, so the down
-- migration for the `tailnet` connection_type is a no-op.

COMMENT ON COLUMN connection_logs.user_agent IS 'Null for SSH events. For web connections, this is the User-Agent header from the request.';

COMMENT ON COLUMN connection_logs.user_id IS 'Null for SSH events. For web connections, this is the ID of the user that made the request.';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TYPE connection_type ADD VALUE IF NOT EXISTS 'tailnet';

COMMENT ON COLUMN connection_logs.user_agent IS 'Null for agent-reported (SSH) events. For HTTP-initiated connections (workspace_app, port_forwarding, tailnet), this is the User-Agent header from the request.';

COMMENT ON COLUMN connection_logs.user_id IS 'Null for agent-reported (SSH) events. For HTTP-initiated connections (workspace_app, port_forwarding, tailnet), this is the ID of the user that made the request.';
9 changes: 6 additions & 3 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions coderd/database/queries/connectionlogs.sql
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ WHERE
((@status = 'ongoing' AND disconnect_time IS NULL) OR
(@status = 'completed' AND disconnect_time IS NOT NULL)) AND
-- Exclude web events, since we don't know their close time.
"type" NOT IN ('workspace_app', 'port_forwarding')
"type" NOT IN ('workspace_app', 'port_forwarding', 'tailnet')
ELSE true
END
-- Authorize Filter clause will be injected below in
Expand Down Expand Up @@ -231,7 +231,7 @@ SELECT COUNT(*) AS count FROM (
((@status = 'ongoing' AND disconnect_time IS NULL) OR
(@status = 'completed' AND disconnect_time IS NOT NULL)) AND
-- Exclude web events, since we don't know their close time.
"type" NOT IN ('workspace_app', 'port_forwarding')
"type" NOT IN ('workspace_app', 'port_forwarding', 'tailnet')
ELSE true
END
-- Authorize Filter clause will be injected below in
Expand Down
46 changes: 46 additions & 0 deletions coderd/workspaceagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,52 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
})
return
}

// Record a connection log entry for this tunnel so that enterprise
// auditors can attribute subsequent SSH/IDE activity inside the
// workspace back to the Coder user and client that established it.
// The agent-reported SSH connection log rows do not have this
// information (see coderd/agentapi/connectionlog.go). We only log
// when the caller is an authenticated user; requests proxied by a
// workspace proxy carry no API key on this route.
if apiKey, ok := httpmw.APIKeyOptional(r); ok {
userAgent := r.UserAgent()
connLogger := *api.ConnectionLogger.Load()
err := connLogger.Upsert(ctx, database.UpsertConnectionLogParams{
ID: uuid.New(),
Time: dbtime.Now(),
OrganizationID: waws.WorkspaceTable.OrganizationID,
WorkspaceOwnerID: waws.WorkspaceTable.OwnerID,
WorkspaceID: waws.WorkspaceTable.ID,
WorkspaceName: waws.WorkspaceTable.Name,
AgentName: waws.WorkspaceAgent.Name,
Type: database.ConnectionTypeTailnet,
IP: database.ParseIP(r.RemoteAddr),
Code: sql.NullInt32{
Int32: http.StatusSwitchingProtocols,
Valid: true,
},
UserAgent: sql.NullString{String: userAgent, Valid: userAgent != ""},
UserID: uuid.NullUUID{UUID: apiKey.UserID, Valid: true},
// ConnectionID is intentionally left unset so that each
// handshake produces its own row. Reusing peerID here
// would cause resume_token reconnects to upsert into the
// existing row without updating ip/user_agent.
ConnectionID: uuid.NullUUID{},
ConnectionStatus: database.ConnectionStatusConnected,
// N/A
SlugOrPort: sql.NullString{},
DisconnectReason: sql.NullString{},
})
if err != nil {
api.Logger.Error(ctx, "upsert tailnet connection log failed",
slog.F("workspace_id", waws.WorkspaceTable.ID),
slog.F("user_id", apiKey.UserID),
slog.Error(err),
)
}
}

ctx, wsNetConn := codersdk.WebsocketNetConn(ctx, conn, websocket.MessageBinary)
defer wsNetConn.Close()

Expand Down
44 changes: 44 additions & 0 deletions coderd/workspaceagents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/coder/coder/v2/coderd/agentapi/metadatabatcher"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
"github.com/coder/coder/v2/coderd/connectionlog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
Expand Down Expand Up @@ -916,6 +917,49 @@ func TestWorkspaceAgentTailnet(t *testing.T) {
require.Equal(t, "test", strings.TrimSpace(string(output)))
}

func TestWorkspaceAgentClientCoordinate_ConnectionLog(t *testing.T) {
t.Parallel()
connLogger := connectionlog.NewFake()
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
ConnectionLogger: connLogger,
})
user := coderdtest.CreateFirstUser(t, client)

r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
}).WithAgent().Do()

_ = agenttest.New(t, client.URL, r.AgentToken)
resources := coderdtest.AwaitWorkspaceAgents(t, client, r.Workspace.ID)

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

conn, err := workspacesdk.New(client).
DialAgent(ctx, resources[0].Agents[0].ID, &workspacesdk.DialAgentOptions{
Logger: testutil.Logger(t).Named("client"),
})
require.NoError(t, err)
defer conn.Close()
conn.AwaitReachable(ctx)

require.Eventually(t, func() bool {
return connLogger.Contains(t, database.UpsertConnectionLogParams{
OrganizationID: user.OrganizationID,
WorkspaceOwnerID: user.UserID,
WorkspaceID: r.Workspace.ID,
WorkspaceName: r.Workspace.Name,
AgentName: resources[0].Agents[0].Name,
Type: database.ConnectionTypeTailnet,
UserID: uuid.NullUUID{
UUID: user.UserID,
Valid: true,
},
})
}, testutil.WaitShort, testutil.IntervalFast)
}

func TestWorkspaceAgentClientCoordinate_BadVersion(t *testing.T) {
t.Parallel()
client, db := coderdtest.NewWithDatabase(t, nil)
Expand Down
7 changes: 7 additions & 0 deletions codersdk/connectionlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ConnectionLog struct {
// WebInfo is only set when `type` is one of:
// - `ConnectionTypePortForwarding`
// - `ConnectionTypeWorkspaceApp`
// - `ConnectionTypeTailnet`
WebInfo *ConnectionLogWebInfo `json:"web_info,omitempty"`

// SSHInfo is only set when `type` is one of:
Expand All @@ -46,6 +47,12 @@ const (
ConnectionTypeReconnectingPTY ConnectionType = "reconnecting_pty"
ConnectionTypeWorkspaceApp ConnectionType = "workspace_app"
ConnectionTypePortForwarding ConnectionType = "port_forwarding"
// ConnectionTypeTailnet is recorded when a client establishes a
// tailnet tunnel to a workspace agent via the coordinate endpoint.
// Unlike the SSH-family types above (which are reported by the
// agent and cannot identify the connecting user), this event is
// written by coderd and carries the authenticated user's identity.
ConnectionTypeTailnet ConnectionType = "tailnet"
)

// ConnectionLogStatus is the status of a connection log entry.
Expand Down
8 changes: 4 additions & 4 deletions docs/reference/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion enterprise/coderd/connectionlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ func convertConnectionLog(dblog database.GetConnectionLogsOffsetRow) codersdk.Co

switch dblog.ConnectionLog.Type {
case database.ConnectionTypeWorkspaceApp,
database.ConnectionTypePortForwarding:
database.ConnectionTypePortForwarding,
database.ConnectionTypeTailnet:
webInfo = &codersdk.ConnectionLogWebInfo{
UserAgent: dblog.ConnectionLog.UserAgent.String,
User: user,
Expand Down
2 changes: 2 additions & 0 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ export const JetBrains: Story = {
},
};

export const Tailnet: Story = {
args: {
connectionLog: {
...MockWebConnectionLog,
type: "tailnet",
},
},
};

export const WebTerminal: Story = {
args: {
connectionLog: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,25 @@ export const ConnectionLogDescription: FC<ConnectionLogDescriptionProps> = ({
</span>
);
}

case "tailnet": {
if (!web_info) return null;
const { user } = web_info;
const isOwnWorkspace = user
? workspace_owner_username === user.username
: false;
return (
<span>
{user ? user.username : "Unauthenticated user"} established a tailnet
tunnel to {isOwnWorkspace ? "their" : `${workspace_owner_username}'s`}{" "}
<Link asChild showExternalIcon={false} className="text-base">
<RouterLink to={`/@${workspace_owner_username}/${workspace_name}`}>
<strong>{workspace_name}</strong>
</RouterLink>
</Link>{" "}
workspace
</span>
);
}
}
};
Loading
Loading