package service import ( "errors" "fmt" "sort" edgenode "iop/apps/edge/internal/node" "iop/packages/go/config" ) var ( ErrSingleRequestWorkspaceUnavailable = errors.New("single-request workspace: approved workspace is unavailable") ErrSingleRequestWorkspaceStale = errors.New("single-request workspace: ready connection changed before executor handoff") ) // bindSingleRequestWorkspace compiles the caller's already-authorized opaque // workspace ref into a request-stable capability projection. It deliberately // resolves only the configured catalog owner and then only that exact node id's // ready owner; aliases, caller Node/path input, and implicit registry selection // are not admission paths. func bindSingleRequestWorkspace(binding *SingleRequestBinding, store *edgenode.NodeStore, registry *edgenode.Registry) (*SingleRequestBinding, error) { base, err := cloneValidatedSingleRequestBinding(binding) if err != nil { return nil, fmt.Errorf("%w: %v", ErrSingleRequestInvalidBinding, err) } if store == nil || registry == nil { return nil, ErrSingleRequestWorkspaceUnavailable } owner, workspace, err := store.ResolveWorkspace(base.WorkspaceRef) if err != nil || owner == nil || owner.ID == "" || workspace.Ref != base.WorkspaceRef { return nil, fmt.Errorf("%w: workspace ref %q", ErrSingleRequestWorkspaceUnavailable, base.WorkspaceRef) } ready, ok := registry.ReadyOwnerSnapshot(owner.ID) if !ok || ready == nil || ready.NodeID != owner.ID || ready.ConnectionGeneration == 0 { return nil, fmt.Errorf("%w: node %q is not ready", ErrSingleRequestWorkspaceUnavailable, owner.ID) } workspaceBinding, err := compileSingleRequestWorkspaceBinding(workspace, ready) if err != nil { return nil, fmt.Errorf("%w: %v", ErrSingleRequestWorkspaceUnavailable, err) } base.Workspace = workspaceBinding if err := applySingleRequestWorkspaceEffectiveLimits(base); err != nil { return nil, fmt.Errorf("%w: %v", ErrSingleRequestWorkspaceUnavailable, err) } return base, nil } func compileSingleRequestWorkspaceBinding(workspace config.WorkspaceDefinition, ready *edgenode.NodeEntry) (*SingleRequestWorkspaceBinding, error) { if ready == nil || workspace.Ref == "" || ready.NodeID == "" || ready.ConnectionGeneration == 0 { return nil, errSingleRequestWorkspaceMalformed } operations := make([]string, 0, len(workspace.Operations)) commandEnabled := false for _, operation := range workspace.Operations { switch operation { case config.WorkspaceOpRead, config.WorkspaceOpList, config.WorkspaceOpWrite, config.WorkspaceOpDelete, config.WorkspaceOpCommand: operations = append(operations, string(operation)) commandEnabled = commandEnabled || operation == config.WorkspaceOpCommand default: return nil, errSingleRequestWorkspaceMalformed } } if len(operations) == 0 || (len(workspace.Commands) > 0 && !commandEnabled) { return nil, errSingleRequestWorkspaceMalformed } sort.Strings(operations) if !sortedUniqueNonEmpty(operations) { return nil, errSingleRequestWorkspaceMalformed } commandIDs := make([]string, 0, len(workspace.Commands)) for _, command := range workspace.Commands { if command.ID == "" { return nil, errSingleRequestWorkspaceMalformed } commandIDs = append(commandIDs, command.ID) } sort.Strings(commandIDs) if !sortedUniqueNonEmpty(commandIDs) { return nil, errSingleRequestWorkspaceMalformed } environmentNames := append([]string(nil), workspace.EnvironmentAllowlist...) sort.Strings(environmentNames) if !sortedUniqueNonEmpty(environmentNames) { return nil, errSingleRequestWorkspaceMalformed } for _, name := range environmentNames { if !validEnvironmentName(name) { return nil, errSingleRequestWorkspaceMalformed } } limits := SingleRequestWorkspaceLimits{ MaxReadBytes: workspace.MaxReadBytes, MaxWriteBytes: workspace.MaxWriteBytes, MaxOutputBytes: workspace.MaxOutputBytes, MaxCommandTimeoutMS: workspace.MaxCommandTimeoutMS, } if err := validateSingleRequestWorkspaceCapabilities(operations, commandIDs, limits); err != nil { return nil, errSingleRequestWorkspaceMalformed } return &SingleRequestWorkspaceBinding{ Ref: workspace.Ref, NodeID: ready.NodeID, ConnectionGeneration: ready.ConnectionGeneration, OperationIDs: operations, CommandIDs: commandIDs, EnvironmentNames: environmentNames, Limits: limits, }, nil } func applySingleRequestWorkspaceEffectiveLimits(binding *SingleRequestBinding) error { if binding == nil || binding.Workspace == nil { return errSingleRequestWorkspaceMalformed } if binding.Limits.MaxOutputBytes < binding.Workspace.Limits.MaxOutputBytes { binding.Workspace.Limits.MaxOutputBytes = binding.Limits.MaxOutputBytes } if binding.Limits.StageTimeoutMS < binding.Workspace.Limits.MaxCommandTimeoutMS { binding.Workspace.Limits.MaxCommandTimeoutMS = binding.Limits.StageTimeoutMS } _, err := validateAndCloneWorkspaceBinding(binding.Workspace) return err }