mirror of
https://github.com/coder/coder.git
synced 2026-06-06 22:48:19 +00:00
fix: clamp template port sharing level in SubAgentAPI (#26061)
Fixes an issue where sub-agent apps created via CreateSubAgent would
bypass the check for the template's max port sharing level:
- Clamps dynamically inserted `workspace_apps` to the template max
sharing level in `coderd.agentapi.SubAgentAPI`.
- Emits a warning when clamping occurs.
- Adds unit test coverage for the max sharing level matrix.
- Adds an integration-ish test through the devcontainer sub-agent client
path.
> 🤖 Generated by Coder Agents with guidance from a human.
This commit is contained in:
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||
"github.com/coder/coder/v2/coderd/externalauth"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/portsharing"
|
||||
"github.com/coder/coder/v2/coderd/prometheusmetrics"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/coderd/workspacestats"
|
||||
@@ -90,6 +91,7 @@ type Options struct {
|
||||
NetworkTelemetryHandler func(batch []*tailnetproto.TelemetryEvent)
|
||||
BoundaryUsageTracker *boundaryusage.Tracker
|
||||
LifecycleMetrics *LifecycleMetrics
|
||||
PortSharer *atomic.Pointer[portsharing.PortSharer]
|
||||
|
||||
AccessURL *url.URL
|
||||
AppHostname string
|
||||
@@ -230,6 +232,7 @@ func New(opts Options, workspace database.Workspace, agent database.WorkspaceAge
|
||||
Log: opts.Log,
|
||||
Clock: opts.Clock,
|
||||
Database: opts.Database,
|
||||
PortSharer: opts.PortSharer,
|
||||
}
|
||||
|
||||
api.BoundaryLogsAPI = &BoundaryLogsAPI{
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/portsharing"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisioner"
|
||||
"github.com/coder/quartz"
|
||||
@@ -27,9 +29,10 @@ type SubAgentAPI struct {
|
||||
OrganizationID uuid.UUID
|
||||
AgentFn func(context.Context) (database.WorkspaceAgent, error)
|
||||
|
||||
Log slog.Logger
|
||||
Clock quartz.Clock
|
||||
Database database.Store
|
||||
Log slog.Logger
|
||||
Clock quartz.Clock
|
||||
Database database.Store
|
||||
PortSharer *atomic.Pointer[portsharing.PortSharer]
|
||||
}
|
||||
|
||||
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
|
||||
Detail: fmt.Sprintf("agent name %q does not match regex %q", agentName, provisioner.AgentNameRegex),
|
||||
}
|
||||
}
|
||||
var template database.Template
|
||||
if len(req.Apps) > 0 {
|
||||
workspace, err := a.Database.GetWorkspaceByAgentID(ctx, parentAgent.ID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get workspace by agent id: %w", err)
|
||||
}
|
||||
|
||||
// Intentional: SubAgentAPI auth context enforces template ACL.
|
||||
// Normal workspace operations depend on this.
|
||||
template, err = a.Database.GetTemplateByID(ctx, workspace.TemplateID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get template policy: %w. If template access was recently changed, restart the workspace to refresh agent permissions", err)
|
||||
}
|
||||
}
|
||||
|
||||
subAgent, err := a.Database.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{
|
||||
ID: uuid.New(),
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: parentAgent.ID},
|
||||
@@ -155,6 +173,14 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
|
||||
return nil, xerrors.Errorf("insert sub agent: %w", err)
|
||||
}
|
||||
|
||||
// A nil PortSharer uses the AGPL default, which permits all share levels.
|
||||
portSharer := portsharing.DefaultPortSharer
|
||||
if a.PortSharer != nil {
|
||||
if loaded := a.PortSharer.Load(); loaded != nil {
|
||||
portSharer = *loaded
|
||||
}
|
||||
}
|
||||
|
||||
var appCreationErrors []*agentproto.CreateSubAgentResponse_AppCreationError
|
||||
appSlugs := make(map[string]struct{})
|
||||
|
||||
@@ -198,6 +224,18 @@ func (a *SubAgentAPI) CreateSubAgent(ctx context.Context, req *agentproto.Create
|
||||
}
|
||||
}
|
||||
sharingLevel := database.AppSharingLevel(strings.ToLower(protoSharingLevel))
|
||||
// Clamp instead of rejecting so a too-permissive app share level does
|
||||
// not block the sub-agent from starting.
|
||||
if err := portSharer.AuthorizedLevel(template, codersdk.WorkspaceAgentPortShareLevel(sharingLevel)); err != nil {
|
||||
a.Log.Warn(ctx, "clamping sub-agent app sharing level to template max port sharing level",
|
||||
slog.F("sub_agent_name", subAgent.Name),
|
||||
slog.F("sub_agent_id", subAgent.ID),
|
||||
slog.F("app_slug", slug),
|
||||
slog.F("requested_share_level", sharingLevel),
|
||||
slog.F("max_port_share_level", template.MaxPortSharingLevel),
|
||||
slog.Error(err))
|
||||
sharingLevel = template.MaxPortSharingLevel
|
||||
}
|
||||
|
||||
var openIn database.WorkspaceAppOpenIn
|
||||
switch app.GetOpenIn() {
|
||||
|
||||
@@ -412,6 +412,11 @@ var (
|
||||
User: []rbac.Permission{},
|
||||
ByOrgID: map[string]rbac.OrgPermissions{
|
||||
orgID.String(): {
|
||||
Org: rbac.Permissions(map[string][]policy.Action{
|
||||
// SubAgentAPI needs to check metadata of templates
|
||||
// potentially shared via group_acl.
|
||||
rbac.ResourceTemplate.Type: {policy.ActionRead},
|
||||
}),
|
||||
Member: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
||||
}),
|
||||
|
||||
@@ -166,6 +166,7 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) {
|
||||
PublishWorkspaceAgentLogsUpdateFn: api.publishWorkspaceAgentLogsUpdate,
|
||||
NetworkTelemetryHandler: api.NetworkTelemetryBatcher.Handler,
|
||||
BoundaryUsageTracker: api.BoundaryUsageTracker,
|
||||
PortSharer: &api.PortSharer,
|
||||
|
||||
AccessURL: api.AccessURL,
|
||||
AppHostname: api.AppHostname,
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/agentapi"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
agpldbauthz "github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbmock"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
agplportsharing "github.com/coder/coder/v2/coderd/portsharing"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
entdbauthz "github.com/coder/coder/v2/enterprise/coderd/dbauthz"
|
||||
entportsharing "github.com/coder/coder/v2/enterprise/coderd/portsharing"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestSubAgentAPICreateSubAgentAppShareRespectsEnterpriseMaxPortShareLevel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type expectedApp struct {
|
||||
slugSuffix string
|
||||
sharingLevel database.AppSharingLevel
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
maxPortShareLevel database.AppSharingLevel
|
||||
apps []*proto.CreateSubAgentRequest_App
|
||||
expectedStoredApps []expectedApp
|
||||
}{
|
||||
{
|
||||
name: "AuthenticatedClampsPublicOnly",
|
||||
maxPortShareLevel: database.AppSharingLevelAuthenticated,
|
||||
apps: []*proto.CreateSubAgentRequest_App{
|
||||
{
|
||||
Slug: "public-app",
|
||||
Share: proto.CreateSubAgentRequest_App_PUBLIC.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8080"),
|
||||
},
|
||||
{
|
||||
Slug: "authenticated-app",
|
||||
Share: proto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8081"),
|
||||
},
|
||||
{
|
||||
Slug: "owner-app",
|
||||
Share: proto.CreateSubAgentRequest_App_OWNER.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8082"),
|
||||
},
|
||||
{
|
||||
Slug: "organization-app",
|
||||
Share: proto.CreateSubAgentRequest_App_ORGANIZATION.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8083"),
|
||||
},
|
||||
},
|
||||
expectedStoredApps: []expectedApp{
|
||||
{
|
||||
slugSuffix: "-authenticated-app",
|
||||
sharingLevel: database.AppSharingLevelAuthenticated,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-organization-app",
|
||||
sharingLevel: database.AppSharingLevelOrganization,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-owner-app",
|
||||
sharingLevel: database.AppSharingLevelOwner,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-public-app",
|
||||
sharingLevel: database.AppSharingLevelAuthenticated,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PublicAllowsPublicAuthenticatedOrganizationAndOwner",
|
||||
maxPortShareLevel: database.AppSharingLevelPublic,
|
||||
apps: []*proto.CreateSubAgentRequest_App{
|
||||
{
|
||||
Slug: "public-app",
|
||||
Share: proto.CreateSubAgentRequest_App_PUBLIC.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8080"),
|
||||
},
|
||||
{
|
||||
Slug: "authenticated-app",
|
||||
Share: proto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8081"),
|
||||
},
|
||||
{
|
||||
Slug: "owner-app",
|
||||
Share: proto.CreateSubAgentRequest_App_OWNER.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8082"),
|
||||
},
|
||||
{
|
||||
Slug: "organization-app",
|
||||
Share: proto.CreateSubAgentRequest_App_ORGANIZATION.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8083"),
|
||||
},
|
||||
},
|
||||
expectedStoredApps: []expectedApp{
|
||||
{
|
||||
slugSuffix: "-authenticated-app",
|
||||
sharingLevel: database.AppSharingLevelAuthenticated,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-organization-app",
|
||||
sharingLevel: database.AppSharingLevelOrganization,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-owner-app",
|
||||
sharingLevel: database.AppSharingLevelOwner,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-public-app",
|
||||
sharingLevel: database.AppSharingLevelPublic,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OrganizationClampsAuthenticatedAndPublic",
|
||||
maxPortShareLevel: database.AppSharingLevelOrganization,
|
||||
apps: []*proto.CreateSubAgentRequest_App{
|
||||
{
|
||||
Slug: "authenticated-app",
|
||||
Share: proto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8080"),
|
||||
},
|
||||
{
|
||||
Slug: "public-app",
|
||||
Share: proto.CreateSubAgentRequest_App_PUBLIC.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8081"),
|
||||
},
|
||||
{
|
||||
Slug: "owner-app",
|
||||
Share: proto.CreateSubAgentRequest_App_OWNER.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8082"),
|
||||
},
|
||||
{
|
||||
Slug: "organization-app",
|
||||
Share: proto.CreateSubAgentRequest_App_ORGANIZATION.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8083"),
|
||||
},
|
||||
},
|
||||
expectedStoredApps: []expectedApp{
|
||||
{
|
||||
slugSuffix: "-authenticated-app",
|
||||
sharingLevel: database.AppSharingLevelOrganization,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-organization-app",
|
||||
sharingLevel: database.AppSharingLevelOrganization,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-owner-app",
|
||||
sharingLevel: database.AppSharingLevelOwner,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-public-app",
|
||||
sharingLevel: database.AppSharingLevelOrganization,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OwnerClampsOrganizationAuthenticatedAndPublic",
|
||||
maxPortShareLevel: database.AppSharingLevelOwner,
|
||||
apps: []*proto.CreateSubAgentRequest_App{
|
||||
{
|
||||
Slug: "authenticated-app",
|
||||
Share: proto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8080"),
|
||||
},
|
||||
{
|
||||
Slug: "public-app",
|
||||
Share: proto.CreateSubAgentRequest_App_PUBLIC.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8081"),
|
||||
},
|
||||
{
|
||||
Slug: "owner-app",
|
||||
Share: proto.CreateSubAgentRequest_App_OWNER.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8082"),
|
||||
},
|
||||
{
|
||||
Slug: "organization-app",
|
||||
Share: proto.CreateSubAgentRequest_App_ORGANIZATION.Enum(),
|
||||
Url: ptr.Ref("http://localhost:8083"),
|
||||
},
|
||||
},
|
||||
expectedStoredApps: []expectedApp{
|
||||
{
|
||||
slugSuffix: "-authenticated-app",
|
||||
sharingLevel: database.AppSharingLevelOwner,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-organization-app",
|
||||
sharingLevel: database.AppSharingLevelOwner,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-owner-app",
|
||||
sharingLevel: database.AppSharingLevelOwner,
|
||||
},
|
||||
{
|
||||
slugSuffix: "-public-app",
|
||||
sharingLevel: database.AppSharingLevelOwner,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, api, upsertedApps := newMockSubAgentAPIWithMaxPortShareLevel(t, tt.maxPortShareLevel, len(tt.apps))
|
||||
resp, err := api.CreateSubAgent(ctx, &proto.CreateSubAgentRequest{
|
||||
Name: "child-agent",
|
||||
Directory: "/workspaces/coder",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
Apps: tt.apps,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.Agent)
|
||||
require.Empty(t, resp.AppCreationErrors)
|
||||
require.Len(t, *upsertedApps, len(tt.expectedStoredApps))
|
||||
|
||||
slices.SortFunc(*upsertedApps, func(a, b database.UpsertWorkspaceAppParams) int {
|
||||
return cmp.Compare(appSlugSuffix(a.Slug), appSlugSuffix(b.Slug))
|
||||
})
|
||||
slices.SortFunc(tt.expectedStoredApps, func(a, b expectedApp) int {
|
||||
return cmp.Compare(a.slugSuffix, b.slugSuffix)
|
||||
})
|
||||
|
||||
for i, expectedApp := range tt.expectedStoredApps {
|
||||
require.Equal(t, expectedApp.slugSuffix, appSlugSuffix((*upsertedApps)[i].Slug))
|
||||
require.Equal(t, expectedApp.sharingLevel, (*upsertedApps)[i].SharingLevel)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func appSlugSuffix(slug string) string {
|
||||
_, suffix, ok := strings.Cut(slug, "-")
|
||||
if !ok {
|
||||
return slug
|
||||
}
|
||||
return "-" + suffix
|
||||
}
|
||||
|
||||
func newMockSubAgentAPIWithMaxPortShareLevel(
|
||||
t *testing.T,
|
||||
maxPortShareLevel database.AppSharingLevel,
|
||||
appCount int,
|
||||
) (context.Context, *agentapi.SubAgentAPI, *[]database.UpsertWorkspaceAppParams) {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
log := testutil.Logger(t)
|
||||
clock := quartz.NewMock(t)
|
||||
ownerID := uuid.New()
|
||||
organizationID := uuid.New()
|
||||
templateID := uuid.New()
|
||||
parentAgent := database.WorkspaceAgent{
|
||||
ID: uuid.New(),
|
||||
ResourceID: uuid.New(),
|
||||
}
|
||||
workspace := database.Workspace{
|
||||
ID: uuid.New(),
|
||||
OwnerID: ownerID,
|
||||
OrganizationID: organizationID,
|
||||
TemplateID: templateID,
|
||||
}
|
||||
template := database.Template{
|
||||
ID: templateID,
|
||||
MaxPortSharingLevel: maxPortShareLevel,
|
||||
}
|
||||
upsertedApps := []database.UpsertWorkspaceAppParams{}
|
||||
|
||||
db := dbmock.NewMockStore(gomock.NewController(t))
|
||||
db.EXPECT().GetWorkspaceByAgentID(gomock.Any(), parentAgent.ID).Return(workspace, nil)
|
||||
db.EXPECT().GetTemplateByID(gomock.Any(), templateID).Return(template, nil)
|
||||
db.EXPECT().InsertWorkspaceAgent(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(_ context.Context, params database.InsertWorkspaceAgentParams) (database.WorkspaceAgent, error) {
|
||||
require.True(t, params.ParentID.Valid)
|
||||
require.Equal(t, parentAgent.ID, params.ParentID.UUID)
|
||||
|
||||
return database.WorkspaceAgent{
|
||||
ID: params.ID,
|
||||
Name: params.Name,
|
||||
AuthToken: params.AuthToken,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
db.EXPECT().UpsertWorkspaceApp(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(_ context.Context, params database.UpsertWorkspaceAppParams) (database.WorkspaceApp, error) {
|
||||
upsertedApps = append(upsertedApps, params)
|
||||
return database.WorkspaceApp{
|
||||
ID: params.ID,
|
||||
AgentID: params.AgentID,
|
||||
Slug: params.Slug,
|
||||
SharingLevel: params.SharingLevel,
|
||||
}, nil
|
||||
},
|
||||
).Times(appCount)
|
||||
|
||||
portSharer := &atomic.Pointer[agplportsharing.PortSharer]{}
|
||||
var ps agplportsharing.PortSharer = entportsharing.NewEnterprisePortSharer()
|
||||
portSharer.Store(&ps)
|
||||
api := &agentapi.SubAgentAPI{
|
||||
OwnerID: ownerID,
|
||||
OrganizationID: organizationID,
|
||||
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
|
||||
return parentAgent, nil
|
||||
},
|
||||
Log: log,
|
||||
Clock: clock,
|
||||
Database: db,
|
||||
PortSharer: portSharer,
|
||||
}
|
||||
|
||||
return ctx, api, &upsertedApps
|
||||
}
|
||||
|
||||
func TestDevcontainerSubAgentAppShareClampedByEnterpriseTemplateMaxPortShareLevel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, db, client := newDevcontainerSubAgentClientWithMaxPortShareLevel(t, database.AppSharingLevelAuthenticated)
|
||||
subAgent, err := client.Create(ctx, agentcontainers.SubAgent{
|
||||
Name: "devcontainer",
|
||||
Directory: "/workspaces/coder",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
Apps: []agentcontainers.SubAgentApp{
|
||||
{
|
||||
Slug: "public-app",
|
||||
URL: "http://localhost:8080",
|
||||
Share: codersdk.WorkspaceAppSharingLevelPublic,
|
||||
},
|
||||
{
|
||||
Slug: "owner-app",
|
||||
URL: "http://localhost:8081",
|
||||
Share: codersdk.WorkspaceAppSharingLevelOwner,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, uuid.Nil, subAgent.ID)
|
||||
|
||||
apps, err := db.GetWorkspaceAppsByAgentID(ctx, subAgent.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 2)
|
||||
slices.SortFunc(apps, func(a, b database.WorkspaceApp) int {
|
||||
return cmp.Compare(appSlugSuffix(a.Slug), appSlugSuffix(b.Slug))
|
||||
})
|
||||
require.Equal(t, "-owner-app", appSlugSuffix(apps[0].Slug))
|
||||
require.Equal(t, database.AppSharingLevelOwner, apps[0].SharingLevel)
|
||||
require.Equal(t, "-public-app", appSlugSuffix(apps[1].Slug))
|
||||
require.Equal(t, database.AppSharingLevelAuthenticated, apps[1].SharingLevel)
|
||||
}
|
||||
|
||||
func TestDevcontainerCoderAppShareClampedWithGroupRestrictedEnterpriseTemplateACL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, db, client := newDevcontainerSubAgentClientWithMaxPortShareLevel(t,
|
||||
database.AppSharingLevelAuthenticated,
|
||||
withGroupRestrictedTemplateACL,
|
||||
)
|
||||
subAgent, err := client.Create(ctx, agentcontainers.SubAgent{
|
||||
Name: "devcontainer",
|
||||
Directory: "/workspaces/coder",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
Apps: []agentcontainers.SubAgentApp{
|
||||
{
|
||||
Slug: "public-app",
|
||||
URL: "http://localhost:8080",
|
||||
Share: codersdk.WorkspaceAppSharingLevelPublic,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
apps, err := db.GetWorkspaceAppsByAgentID(ctx, subAgent.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 1)
|
||||
require.Equal(t, "-public-app", appSlugSuffix(apps[0].Slug))
|
||||
require.Equal(t, database.AppSharingLevelAuthenticated, apps[0].SharingLevel)
|
||||
}
|
||||
|
||||
type devcontainerSubAgentClientOption func(testing.TB, database.Store, database.Organization, database.User, *database.Template)
|
||||
|
||||
func newDevcontainerSubAgentClientWithMaxPortShareLevel(
|
||||
t *testing.T,
|
||||
maxPortShareLevel database.AppSharingLevel,
|
||||
options ...devcontainerSubAgentClientOption,
|
||||
) (context.Context, database.Store, agentcontainers.SubAgentClient) {
|
||||
t.Helper()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
log := testutil.Logger(t)
|
||||
clock := quartz.NewMock(t)
|
||||
|
||||
rawDB, _ := dbtestutil.NewDB(t)
|
||||
org := dbgen.Organization(t, rawDB, database.Organization{})
|
||||
user := dbgen.User(t, rawDB, database.User{})
|
||||
template := dbgen.Template(t, rawDB, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
MaxPortSharingLevel: maxPortShareLevel,
|
||||
})
|
||||
for _, option := range options {
|
||||
option(t, rawDB, org, user, &template)
|
||||
}
|
||||
templateVersion := dbgen.TemplateVersion(t, rawDB, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{Valid: true, UUID: template.ID},
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
workspace := dbgen.Workspace(t, rawDB, database.WorkspaceTable{
|
||||
OrganizationID: org.ID,
|
||||
TemplateID: template.ID,
|
||||
OwnerID: user.ID,
|
||||
})
|
||||
job := dbgen.ProvisionerJob(t, rawDB, nil, database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
build := dbgen.WorkspaceBuild(t, rawDB, database.WorkspaceBuild{
|
||||
JobID: job.ID,
|
||||
WorkspaceID: workspace.ID,
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
})
|
||||
resource := dbgen.WorkspaceResource(t, rawDB, database.WorkspaceResource{
|
||||
JobID: build.JobID,
|
||||
})
|
||||
parentAgent := dbgen.WorkspaceAgent(t, rawDB, database.WorkspaceAgent{
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
|
||||
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
accessControlStore := &atomic.Pointer[agpldbauthz.AccessControlStore]{}
|
||||
var acs agpldbauthz.AccessControlStore = entdbauthz.EnterpriseTemplateAccessControlStore{}
|
||||
accessControlStore.Store(&acs)
|
||||
db := agpldbauthz.New(rawDB, auth, log, accessControlStore)
|
||||
portSharer := &atomic.Pointer[agplportsharing.PortSharer]{}
|
||||
var ps agplportsharing.PortSharer = entportsharing.NewEnterprisePortSharer()
|
||||
portSharer.Store(&ps)
|
||||
api := &agentapi.SubAgentAPI{
|
||||
OwnerID: user.ID,
|
||||
OrganizationID: org.ID,
|
||||
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
|
||||
return parentAgent, nil
|
||||
},
|
||||
Log: log,
|
||||
Clock: clock,
|
||||
Database: db,
|
||||
PortSharer: portSharer,
|
||||
}
|
||||
|
||||
client := agentcontainers.NewSubAgentClientFromAPI(log, devcontainerSubAgentDRPCClient{api: api})
|
||||
return ctx, rawDB, client
|
||||
}
|
||||
|
||||
func withGroupRestrictedTemplateACL(t testing.TB, db database.Store, org database.Organization, user database.User, template *database.Template) {
|
||||
t.Helper()
|
||||
|
||||
group := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{
|
||||
GroupID: group.ID,
|
||||
UserID: user.ID,
|
||||
})
|
||||
template.GroupACL = database.TemplateACL{
|
||||
group.ID.String(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
|
||||
}
|
||||
template.UserACL = database.TemplateACL{}
|
||||
require.NoError(t, db.UpdateTemplateACLByID(context.Background(), database.UpdateTemplateACLByIDParams{
|
||||
ID: template.ID,
|
||||
GroupACL: template.GroupACL,
|
||||
UserACL: template.UserACL,
|
||||
}))
|
||||
}
|
||||
|
||||
type devcontainerSubAgentDRPCClient struct {
|
||||
proto.DRPCAgentClient28
|
||||
api *agentapi.SubAgentAPI
|
||||
}
|
||||
|
||||
func (c devcontainerSubAgentDRPCClient) CreateSubAgent(ctx context.Context, req *proto.CreateSubAgentRequest) (*proto.CreateSubAgentResponse, error) {
|
||||
return c.api.CreateSubAgent(ctx, req)
|
||||
}
|
||||
|
||||
func (c devcontainerSubAgentDRPCClient) DeleteSubAgent(ctx context.Context, req *proto.DeleteSubAgentRequest) (*proto.DeleteSubAgentResponse, error) {
|
||||
return c.api.DeleteSubAgent(ctx, req)
|
||||
}
|
||||
|
||||
func (c devcontainerSubAgentDRPCClient) ListSubAgents(ctx context.Context, req *proto.ListSubAgentsRequest) (*proto.ListSubAgentsResponse, error) {
|
||||
return c.api.ListSubAgents(ctx, req)
|
||||
}
|
||||
Reference in New Issue
Block a user