◐ Shell
clean mode source ↗

fix: clamp template port sharing level in SubAgentAPI (#26061) (#26256) · coder/coder@b78ec31

@@ -8,6 +8,7 @@ import (

88

"errors"

99

"fmt"

1010

"strings"

11+

"sync/atomic"

11121213

"github.com/google/uuid"

1314

"github.com/sqlc-dev/pqtype"

@@ -17,6 +18,7 @@ import (

1718

agentproto "github.com/coder/coder/v2/agent/proto"

1819

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

1920

"github.com/coder/coder/v2/coderd/database/dbauthz"

21+

"github.com/coder/coder/v2/coderd/portsharing"

2022

"github.com/coder/coder/v2/codersdk"

2123

"github.com/coder/coder/v2/provisioner"

2224

"github.com/coder/quartz"

@@ -27,9 +29,10 @@ type SubAgentAPI struct {

2729

OrganizationID uuid.UUID

2830

AgentFn func(context.Context) (database.WorkspaceAgent, error)

293130-

Log slog.Logger

31-

Clock quartz.Clock

32-

Database database.Store

32+

Log slog.Logger

33+

Clock quartz.Clock

34+

Database database.Store

35+

PortSharer *atomic.Pointer[portsharing.PortSharer]

3336

}

34373538

func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.CreateSubAgentRequest) (*agentproto.CreateSubAgentResponse, error) {

@@ -129,6 +132,21 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create

129132

Detail: fmt.Sprintf("agent name %q does not match regex %q", agentName, provisioner.AgentNameRegex),

130133

}

131134

}

135+

var template database.Template

136+

if len(req.Apps) > 0 {

137+

workspace, err := a.Database.GetWorkspaceByAgentID(ctx, parentAgent.ID)

138+

if err != nil {

139+

return nil, xerrors.Errorf("get workspace by agent id: %w", err)

140+

}

141+142+

// Intentional: SubAgentAPI auth context enforces template ACL.

143+

// Normal workspace operations depend on this.

144+

template, err = a.Database.GetTemplateByID(ctx, workspace.TemplateID)

145+

if err != nil {

146+

return nil, xerrors.Errorf("get template policy: %w. If template access was recently changed, restart the workspace to refresh agent permissions", err)

147+

}

148+

}

149+132150

subAgent, err := a.Database.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{

133151

ID: uuid.New(),

134152

ParentID: uuid.NullUUID{Valid: true, UUID: parentAgent.ID},

@@ -155,6 +173,14 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create

155173

return nil, xerrors.Errorf("insert sub agent: %w", err)

156174

}

157175176+

// A nil PortSharer uses the AGPL default, which permits all share levels.

177+

portSharer := portsharing.DefaultPortSharer

178+

if a.PortSharer != nil {

179+

if loaded := a.PortSharer.Load(); loaded != nil {

180+

portSharer = *loaded

181+

}

182+

}

183+158184

var appCreationErrors []*agentproto.CreateSubAgentResponse_AppCreationError

159185

appSlugs := make(map[string]struct{})

160186

@@ -198,6 +224,18 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create

198224

}

199225

}

200226

sharingLevel := database.AppSharingLevel(strings.ToLower(protoSharingLevel))

227+

// Clamp instead of rejecting so a too-permissive app share level does

228+

// not block the sub-agent from starting.

229+

if err := portSharer.AuthorizedLevel(template, codersdk.WorkspaceAgentPortShareLevel(sharingLevel)); err != nil {

230+

a.Log.Warn(ctx, "clamping sub-agent app sharing level to template max port sharing level",

231+

slog.F("sub_agent_name", subAgent.Name),

232+

slog.F("sub_agent_id", subAgent.ID),

233+

slog.F("app_slug", slug),

234+

slog.F("requested_share_level", sharingLevel),

235+

slog.F("max_port_share_level", template.MaxPortSharingLevel),

236+

slog.Error(err))

237+

sharingLevel = template.MaxPortSharingLevel

238+

}

201239202240

var openIn database.WorkspaceAppOpenIn

203241

switch app.GetOpenIn() {