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 {
2729OrganizationID uuid.UUID
2830AgentFn 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}
34373538func (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
129132Detail: 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+132150subAgent, err := a.Database.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
133151ID: uuid.New(),
134152ParentID: uuid.NullUUID{Valid: true, UUID: parentAgent.ID},
@@ -155,6 +173,14 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
155173return 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+158184var appCreationErrors []*agentproto.CreateSubAgentResponse_AppCreationError
159185appSlugs := make(map[string]struct{})
160186@@ -198,6 +224,18 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
198224 }
199225 }
200226sharingLevel := 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+ }
201239202240var openIn database.WorkspaceAppOpenIn
203241switch app.GetOpenIn() {