mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
This change adds a new `ReportConnection` endpoint to the `agentapi`. The protocol version was bumped previously, so it has been omitted here. This allows the agent to report connection events, for example when the user connects to the workspace via SSH or VS Code. Updates #15139
106 lines
3.3 KiB
Go
106 lines
3.3 KiB
Go
package agentapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"sync/atomic"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
|
|
"cdr.dev/slog"
|
|
|
|
agentproto "github.com/coder/coder/v2/agent/proto"
|
|
"github.com/coder/coder/v2/coderd/audit"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
|
)
|
|
|
|
type AuditAPI struct {
|
|
AgentFn func(context.Context) (database.WorkspaceAgent, error)
|
|
Auditor *atomic.Pointer[audit.Auditor]
|
|
Database database.Store
|
|
Log slog.Logger
|
|
}
|
|
|
|
func (a *AuditAPI) ReportConnection(ctx context.Context, req *agentproto.ReportConnectionRequest) (*emptypb.Empty, error) {
|
|
// We will use connection ID as request ID, typically this is the
|
|
// SSH session ID as reported by the agent.
|
|
connectionID, err := uuid.FromBytes(req.GetConnection().GetId())
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("connection id from bytes: %w", err)
|
|
}
|
|
|
|
action, err := db2sdk.AuditActionFromAgentProtoConnectionAction(req.GetConnection().GetAction())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
connectionType, err := agentsdk.ConnectionTypeFromProto(req.GetConnection().GetType())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Fetch contextual data for this audit event.
|
|
workspaceAgent, err := a.AgentFn(ctx)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get agent: %w", err)
|
|
}
|
|
workspace, err := a.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get workspace by agent id: %w", err)
|
|
}
|
|
build, err := a.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get latest workspace build by workspace id: %w", err)
|
|
}
|
|
|
|
// We pass the below information to the Auditor so that it
|
|
// can form a friendly string for the user to view in the UI.
|
|
type additionalFields struct {
|
|
audit.AdditionalFields
|
|
|
|
ConnectionType agentsdk.ConnectionType `json:"connection_type"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
resourceInfo := additionalFields{
|
|
AdditionalFields: audit.AdditionalFields{
|
|
WorkspaceID: workspace.ID,
|
|
WorkspaceName: workspace.Name,
|
|
WorkspaceOwner: workspace.OwnerUsername,
|
|
BuildNumber: strconv.FormatInt(int64(build.BuildNumber), 10),
|
|
BuildReason: database.BuildReason(string(build.Reason)),
|
|
},
|
|
ConnectionType: connectionType,
|
|
Reason: req.GetConnection().GetReason(),
|
|
}
|
|
|
|
riBytes, err := json.Marshal(resourceInfo)
|
|
if err != nil {
|
|
a.Log.Error(ctx, "marshal resource info for agent connection failed", slog.Error(err))
|
|
riBytes = []byte("{}")
|
|
}
|
|
|
|
audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.WorkspaceAgent]{
|
|
Audit: *a.Auditor.Load(),
|
|
Log: a.Log,
|
|
Time: req.GetConnection().GetTimestamp().AsTime(),
|
|
OrganizationID: workspace.OrganizationID,
|
|
RequestID: connectionID,
|
|
Action: action,
|
|
New: workspaceAgent,
|
|
Old: workspaceAgent,
|
|
IP: req.GetConnection().GetIp(),
|
|
Status: int(req.GetConnection().GetStatusCode()),
|
|
AdditionalFields: riBytes,
|
|
|
|
// It's not possible to tell which user connected. Once we have
|
|
// the capability, this may be reported by the agent.
|
|
UserID: uuid.Nil,
|
|
})
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|