Skip to content

Commit da7f19e

Browse files
authored
Apply network permissions from registry (#1041)
1 parent b6cd2ab commit da7f19e

File tree

7 files changed

+370
-377
lines changed

7 files changed

+370
-377
lines changed

cmd/thv/app/run.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/stacklok/toolhive/pkg/container"
1212
"github.com/stacklok/toolhive/pkg/container/runtime"
1313
"github.com/stacklok/toolhive/pkg/logger"
14-
"github.com/stacklok/toolhive/pkg/permissions"
1514
"github.com/stacklok/toolhive/pkg/process"
1615
"github.com/stacklok/toolhive/pkg/registry"
1716
"github.com/stacklok/toolhive/pkg/runner"
@@ -114,7 +113,7 @@ func init() {
114113
runCmd.Flags().StringVar(
115114
&runPermissionProfile,
116115
"permission-profile",
117-
permissions.ProfileNetwork,
116+
"",
118117
"Permission profile to use (none, network, or path to JSON file)",
119118
)
120119
runCmd.Flags().StringArrayVarP(

docs/cli/thv_run.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/api/v1/workloads.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,6 @@ func (s *WorkloadRoutes) createWorkload(w http.ResponseWriter, r *http.Request)
225225
return
226226
}
227227

228-
// Mimic behavior of the CLI by defaulting to the "network" permission profile.
229-
// TODO: Consider moving this into the run config creation logic.
230-
if req.PermissionProfile == nil {
231-
req.PermissionProfile = permissions.BuiltinNetworkProfile()
232-
}
233-
234228
// Fetch or build the requested image
235229
// TODO: Make verification configurable and return errors over the API.
236230
imageURL, imageMetadata, err := retriever.GetMCPServer(

pkg/runner/config.go

Lines changed: 0 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9-
"strings"
109

1110
"github.com/stacklok/toolhive/pkg/audit"
1211
"github.com/stacklok/toolhive/pkg/auth"
@@ -256,36 +255,6 @@ func (c *RunConfig) WithPorts(port, targetPort int) (*RunConfig, error) {
256255
return c, nil
257256
}
258257

259-
// ParsePermissionProfile loads and sets the permission profile
260-
func (c *RunConfig) ParsePermissionProfile() (*RunConfig, error) {
261-
if c.PermissionProfile != nil {
262-
return c, nil
263-
}
264-
265-
if c.PermissionProfileNameOrPath == "" {
266-
return c, fmt.Errorf("permission profile name or path is required")
267-
}
268-
269-
var permProfile *permissions.Profile
270-
var err error
271-
272-
switch c.PermissionProfileNameOrPath {
273-
case permissions.ProfileNone, "stdio":
274-
permProfile = permissions.BuiltinNoneProfile()
275-
case permissions.ProfileNetwork:
276-
permProfile = permissions.BuiltinNetworkProfile()
277-
default:
278-
// Try to load from file
279-
permProfile, err = permissions.FromFile(c.PermissionProfileNameOrPath)
280-
if err != nil {
281-
return c, fmt.Errorf("failed to load permission profile: %v", err)
282-
}
283-
}
284-
285-
c.PermissionProfile = permProfile
286-
return c, nil
287-
}
288-
289258
// WithEnvironmentVariables parses and sets environment variables
290259
func (c *RunConfig) WithEnvironmentVariables(envVarStrings []string) (*RunConfig, error) {
291260
envVars, err := environment.ParseEnvironmentVariables(envVarStrings)
@@ -360,79 +329,3 @@ func (c *RunConfig) WithStandardLabels() *RunConfig {
360329
labels.AddStandardLabels(c.ContainerLabels, containerName, c.BaseName, transportLabel, c.Port)
361330
return c
362331
}
363-
364-
// ProcessVolumeMounts processes volume mounts and adds them to the permission profile
365-
func (c *RunConfig) ProcessVolumeMounts() error {
366-
// Skip if no volumes to process
367-
if len(c.Volumes) == 0 {
368-
return nil
369-
}
370-
371-
// Ensure permission profile is loaded
372-
if c.PermissionProfile == nil {
373-
if c.PermissionProfileNameOrPath == "" {
374-
return fmt.Errorf("permission profile is required when using volume mounts")
375-
}
376-
377-
// Load the permission profile from the specified name or path
378-
_, err := c.ParsePermissionProfile()
379-
if err != nil {
380-
return fmt.Errorf("failed to load permission profile: %v", err)
381-
}
382-
}
383-
384-
// Create a map of existing mount targets for quick lookup
385-
existingMounts := make(map[string]string)
386-
387-
// Add existing read mounts to the map
388-
for _, m := range c.PermissionProfile.Read {
389-
source, target, _ := m.Parse()
390-
existingMounts[target] = source
391-
}
392-
393-
// Add existing write mounts to the map
394-
for _, m := range c.PermissionProfile.Write {
395-
source, target, _ := m.Parse()
396-
existingMounts[target] = source
397-
}
398-
399-
// Process each volume mount
400-
for _, volume := range c.Volumes {
401-
// Parse read-only flag
402-
readOnly := strings.HasSuffix(volume, ":ro")
403-
volumeSpec := volume
404-
if readOnly {
405-
volumeSpec = strings.TrimSuffix(volume, ":ro")
406-
}
407-
408-
// Create and parse mount declaration
409-
mount := permissions.MountDeclaration(volumeSpec)
410-
source, target, err := mount.Parse()
411-
if err != nil {
412-
return fmt.Errorf("invalid volume format: %s (%v)", volume, err)
413-
}
414-
415-
// Check for duplicate mount target
416-
if existingSource, isDuplicate := existingMounts[target]; isDuplicate {
417-
logger.Warnf("Skipping duplicate mount target: %s (already mounted from %s)",
418-
target, existingSource)
419-
continue
420-
}
421-
422-
// Add the mount to the appropriate permission list
423-
if readOnly {
424-
c.PermissionProfile.Read = append(c.PermissionProfile.Read, mount)
425-
} else {
426-
c.PermissionProfile.Write = append(c.PermissionProfile.Write, mount)
427-
}
428-
429-
// Add to the map of existing mounts
430-
existingMounts[target] = source
431-
432-
logger.Infof("Adding volume mount: %s -> %s (%s)",
433-
source, target,
434-
map[bool]string{true: "read-only", false: "read-write"}[readOnly])
435-
}
436-
437-
return nil
438-
}

pkg/runner/config_builder.go

Lines changed: 105 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -308,24 +308,15 @@ func (b *RunConfigBuilder) validateConfig(imageMetadata *registry.ImageMetadata)
308308
return err
309309
}
310310

311-
// If we are missing the permission profile, attempt to load one from the image metadata.
312-
if c.PermissionProfileNameOrPath == "" && c.PermissionProfile == nil && imageMetadata != nil {
313-
permProfilePath, err := CreatePermissionProfileFile(c.Name, imageMetadata.Permissions)
314-
if err != nil {
315-
// Just log the error and continue with the default permission profile
316-
logger.Warnf("Warning: failed to create permission profile file: %v", err)
317-
} else {
318-
// Update the permission profile path
319-
c.PermissionProfileNameOrPath = permProfilePath
320-
}
321-
}
322-
// Set permission profile (mandatory)
323-
if _, err = c.ParsePermissionProfile(); err != nil {
311+
// Load or default the permission profile
312+
// NOTE: This must be done before processing volume mounts
313+
c.PermissionProfile, err = b.loadPermissionProfile(imageMetadata)
314+
if err != nil {
324315
return err
325316
}
326317

327318
// Process volume mounts
328-
if err = c.ProcessVolumeMounts(); err != nil {
319+
if err = b.processVolumeMounts(); err != nil {
329320
return err
330321
}
331322

@@ -362,3 +353,103 @@ func (b *RunConfigBuilder) validateConfig(imageMetadata *registry.ImageMetadata)
362353

363354
return nil
364355
}
356+
357+
func (b *RunConfigBuilder) loadPermissionProfile(imageMetadata *registry.ImageMetadata) (*permissions.Profile, error) {
358+
// The permission profile object takes precedence over the name or path.
359+
if b.config.PermissionProfile != nil {
360+
return b.config.PermissionProfile, nil
361+
}
362+
363+
// Try to load the permission profile by name or path.
364+
if b.config.PermissionProfileNameOrPath != "" {
365+
switch b.config.PermissionProfileNameOrPath {
366+
case permissions.ProfileNone, "stdio":
367+
return permissions.BuiltinNoneProfile(), nil
368+
case permissions.ProfileNetwork:
369+
return permissions.BuiltinNetworkProfile(), nil
370+
default:
371+
// Try to load from file
372+
return permissions.FromFile(b.config.PermissionProfileNameOrPath)
373+
}
374+
}
375+
376+
// If a profile was not set by name or path, check the image metadata.
377+
if imageMetadata != nil && imageMetadata.Permissions != nil {
378+
379+
logger.Debugf("Using registry permission profile: %v", imageMetadata.Permissions)
380+
return imageMetadata.Permissions, nil
381+
}
382+
383+
// If no metadata is available, use the network permission profile as default.
384+
logger.Debugf("Using default permission profile: %s", permissions.ProfileNetwork)
385+
return permissions.BuiltinNetworkProfile(), nil
386+
}
387+
388+
// processVolumeMounts processes volume mounts and adds them to the permission profile
389+
func (b *RunConfigBuilder) processVolumeMounts() error {
390+
391+
// Skip if no volumes to process
392+
if len(b.config.Volumes) == 0 {
393+
return nil
394+
}
395+
396+
// Ensure permission profile is loaded
397+
if b.config.PermissionProfile == nil {
398+
return fmt.Errorf("permission profile is required when using volume mounts")
399+
}
400+
401+
// Create a map of existing mount targets for quick lookup
402+
existingMounts := make(map[string]string)
403+
404+
// Add existing read mounts to the map
405+
for _, m := range b.config.PermissionProfile.Read {
406+
source, target, _ := m.Parse()
407+
existingMounts[target] = source
408+
}
409+
410+
// Add existing write mounts to the map
411+
for _, m := range b.config.PermissionProfile.Write {
412+
source, target, _ := m.Parse()
413+
existingMounts[target] = source
414+
}
415+
416+
// Process each volume mount
417+
for _, volume := range b.config.Volumes {
418+
// Parse read-only flag
419+
readOnly := strings.HasSuffix(volume, ":ro")
420+
volumeSpec := volume
421+
if readOnly {
422+
volumeSpec = strings.TrimSuffix(volume, ":ro")
423+
}
424+
425+
// Create and parse mount declaration
426+
mount := permissions.MountDeclaration(volumeSpec)
427+
source, target, err := mount.Parse()
428+
if err != nil {
429+
return fmt.Errorf("invalid volume format: %s (%v)", volume, err)
430+
}
431+
432+
// Check for duplicate mount target
433+
if existingSource, isDuplicate := existingMounts[target]; isDuplicate {
434+
logger.Warnf("Skipping duplicate mount target: %s (already mounted from %s)",
435+
target, existingSource)
436+
continue
437+
}
438+
439+
// Add the mount to the appropriate permission list
440+
if readOnly {
441+
b.config.PermissionProfile.Read = append(b.config.PermissionProfile.Read, mount)
442+
} else {
443+
b.config.PermissionProfile.Write = append(b.config.PermissionProfile.Write, mount)
444+
}
445+
446+
// Add to the map of existing mounts
447+
existingMounts[target] = source
448+
449+
logger.Infof("Adding volume mount: %s -> %s (%s)",
450+
source, target,
451+
map[bool]string{true: "read-only", false: "read-write"}[readOnly])
452+
}
453+
454+
return nil
455+
}

0 commit comments

Comments
 (0)