diff --git a/api/process/process.go b/api/process/process.go index 9c4beb3fc..a30f678fa 100644 --- a/api/process/process.go +++ b/api/process/process.go @@ -14,6 +14,7 @@ import ( "github.com/wippyai/runtime/api/registry" "github.com/wippyai/runtime/api/relay" "github.com/wippyai/runtime/api/runtime" + "github.com/wippyai/runtime/api/security" ) // System identifies the process system in the event bus. @@ -38,7 +39,10 @@ const ( type ( // Meta contains metadata about a process type. Meta struct { - Method string + // Security carries the entry-declared security configuration; hosts + // apply it to the process frame context at start. + Security *security.Config + Method string } // Start contains the configuration needed to start a new process. diff --git a/api/runtime/lua/config.go b/api/runtime/lua/config.go index 7f63bb12c..a056d1c90 100644 --- a/api/runtime/lua/config.go +++ b/api/runtime/lua/config.go @@ -79,11 +79,12 @@ type ( // ProcessConfig defines the configuration for a Lua processes. ProcessConfig struct { - Meta attrs.Bag `json:"meta"` // Metadata for the terminal - Source string `json:"source" resolve:"-"` // Lua source code - Method string `json:"method"` // Alias of the Lua method to execute - Imports map[string]registry.ID `json:"imports,omitempty"` // Imports aliases for the library - Modules []string `json:"modules,omitempty"` // Shortcut for importing modules + Meta attrs.Bag `json:"meta"` // Metadata for the terminal + Security *security.Config `json:"security,omitempty" yaml:"security,omitempty"` + Source string `json:"source" resolve:"-"` // Lua source code + Method string `json:"method"` // Alias of the Lua method to execute + Imports map[string]registry.ID `json:"imports,omitempty"` // Imports aliases for the library + Modules []string `json:"modules,omitempty"` // Shortcut for importing modules } // WorkflowConfig defines the configuration for a Lua workflow. @@ -131,13 +132,14 @@ type ( // BytecodeProcessConfig defines configuration for a precompiled Lua process. BytecodeProcessConfig struct { - Imports map[string]registry.ID `json:"imports,omitempty"` - Meta attrs.Bag `json:"meta,omitempty"` - FS string `json:"fs"` - Path string `json:"path"` - Hash string `json:"hash"` - Method string `json:"method"` - Modules []string `json:"modules,omitempty"` + Imports map[string]registry.ID `json:"imports,omitempty"` + Meta attrs.Bag `json:"meta,omitempty"` + Security *security.Config `json:"security,omitempty" yaml:"security,omitempty"` + FS string `json:"fs"` + Path string `json:"path"` + Hash string `json:"hash"` + Method string `json:"method"` + Modules []string `json:"modules,omitempty"` } // BytecodeWorkflowConfig defines configuration for a precompiled Lua workflow. diff --git a/runtime/lua/component/process/manager.go b/runtime/lua/component/process/manager.go index e30dafcb9..3d21fa0d9 100644 --- a/runtime/lua/component/process/manager.go +++ b/runtime/lua/component/process/manager.go @@ -13,6 +13,7 @@ import ( "github.com/wippyai/runtime/api/process" "github.com/wippyai/runtime/api/registry" api "github.com/wippyai/runtime/api/runtime/lua" + "github.com/wippyai/runtime/api/security" runtimelua "github.com/wippyai/runtime/runtime/lua" "github.com/wippyai/runtime/runtime/lua/code" "github.com/wippyai/runtime/runtime/lua/component" @@ -25,6 +26,7 @@ import ( type configEntry struct { source *api.ProcessConfig bytecode *api.BytecodeProcessConfig + security *security.Config method string } @@ -115,7 +117,7 @@ func (m *Manager) Invalidate(ctx context.Context, ids []registry.ID) error { } } - if err := m.registerFactory(ctx, id, cfg.method); err != nil { + if err := m.registerFactory(ctx, id, cfg.method, cfg.security); err != nil { m.log.Error("failed to invalidate process", zap.Error(err)) errs = append(errs, err) continue @@ -166,9 +168,9 @@ func (m *Manager) addSource(ctx context.Context, entry registry.Entry) error { return runtimelua.NewAddNodeError("process", err) } - m.configs.Store(entry.ID, &configEntry{method: cfg.Method, source: cfg}) + m.configs.Store(entry.ID, &configEntry{method: cfg.Method, source: cfg, security: cfg.Security}) - if err := m.registerFactory(ctx, entry.ID, cfg.Method); err != nil { + if err := m.registerFactory(ctx, entry.ID, cfg.Method, cfg.Security); err != nil { _ = m.code.DeleteNode(ctx, entry.ID) m.configs.Delete(entry.ID) return runtimelua.NewRegisterFactoryError(err) @@ -200,9 +202,9 @@ func (m *Manager) addBytecode(ctx context.Context, entry registry.Entry) error { return runtimelua.NewAddNodeError("process", err) } - m.configs.Store(entry.ID, &configEntry{method: cfg.Method, bytecode: cfg}) + m.configs.Store(entry.ID, &configEntry{method: cfg.Method, bytecode: cfg, security: cfg.Security}) - if err := m.registerFactory(ctx, entry.ID, cfg.Method); err != nil { + if err := m.registerFactory(ctx, entry.ID, cfg.Method, cfg.Security); err != nil { _ = m.code.DeleteNode(ctx, entry.ID) m.configs.Delete(entry.ID) return runtimelua.NewRegisterFactoryError(err) @@ -234,9 +236,9 @@ func (m *Manager) updateSource(ctx context.Context, entry registry.Entry) error return runtimelua.NewUpdateNodeError("process", err) } - m.configs.Store(entry.ID, &configEntry{method: cfg.Method, source: cfg}) + m.configs.Store(entry.ID, &configEntry{method: cfg.Method, source: cfg, security: cfg.Security}) - if err := m.registerFactory(ctx, entry.ID, cfg.Method); err != nil { + if err := m.registerFactory(ctx, entry.ID, cfg.Method, cfg.Security); err != nil { return runtimelua.NewUpdateFactoryError(err) } @@ -266,9 +268,9 @@ func (m *Manager) updateBytecode(ctx context.Context, entry registry.Entry) erro return runtimelua.NewUpdateNodeError("process", err) } - m.configs.Store(entry.ID, &configEntry{method: cfg.Method, bytecode: cfg}) + m.configs.Store(entry.ID, &configEntry{method: cfg.Method, bytecode: cfg, security: cfg.Security}) - if err := m.registerFactory(ctx, entry.ID, cfg.Method); err != nil { + if err := m.registerFactory(ctx, entry.ID, cfg.Method, cfg.Security); err != nil { return runtimelua.NewUpdateFactoryError(err) } @@ -277,7 +279,7 @@ func (m *Manager) updateBytecode(ctx context.Context, entry registry.Entry) erro } // registerFactory registers a process factory with the factory registry and waits for confirmation. -func (m *Manager) registerFactory(ctx context.Context, id registry.ID, method string) error { +func (m *Manager) registerFactory(ctx context.Context, id registry.ID, method string, sec *security.Config) error { // Create factory using ProcessFactory factoryFn, err := m.factory.CreateFactory(id, engine.WithModules(component.ExecutableAmbientModules()...)) if err != nil { @@ -308,7 +310,8 @@ func (m *Manager) registerFactory(ctx context.Context, id registry.ID, method st Data: &process.FactoryEntry{ Factory: factoryFn, Meta: process.Meta{ - Method: method, + Method: method, + Security: sec, }, }, }) diff --git a/runtime/lua/component/process/manager_test.go b/runtime/lua/component/process/manager_test.go index d1cab0a0f..ec3ed1560 100644 --- a/runtime/lua/component/process/manager_test.go +++ b/runtime/lua/component/process/manager_test.go @@ -17,6 +17,7 @@ import ( processapi "github.com/wippyai/runtime/api/process" "github.com/wippyai/runtime/api/registry" api "github.com/wippyai/runtime/api/runtime/lua" + "github.com/wippyai/runtime/api/security" "github.com/wippyai/runtime/runtime/lua/code" "github.com/wippyai/runtime/runtime/lua/engine" systempayload "github.com/wippyai/runtime/system/payload" @@ -212,7 +213,12 @@ func TestManager_registerFactory_PreparesBeforeSend(t *testing.T) { } ctx := event.WithAwaitService(ctxapi.NewRootContext(), awaitSvc) - err := manager.registerFactory(ctx, registry.NewID("app.test", "process"), "main") + securityConfig := &security.Config{Actor: security.Actor{ID: "app.test:process"}} + err := manager.registerFactory(ctx, registry.NewID("app.test", "process"), "main", securityConfig) require.NoError(t, err) assert.False(t, sendBeforePrepare, "factory register was sent before await prepare") + require.Len(t, bus.events, 1) + registered, ok := bus.events[0].Data.(*processapi.FactoryEntry) + require.True(t, ok) + assert.Same(t, securityConfig, registered.Meta.Security) } diff --git a/service/host/host.go b/service/host/host.go index f8c977671..cc74171f4 100644 --- a/service/host/host.go +++ b/service/host/host.go @@ -18,6 +18,7 @@ import ( hostapi "github.com/wippyai/runtime/api/service/host" "github.com/wippyai/runtime/api/topology" "github.com/wippyai/runtime/system/scheduler/actor" + securitysys "github.com/wippyai/runtime/system/security" "go.uber.org/zap" ) @@ -98,6 +99,9 @@ func (h *Host) Run(ctx context.Context, start *process.Start) (pid.PID, error) { processID := h.preparePID(ctx, start) frameCtx := h.prepareContext(ctx, processID, start) + if meta != nil && meta.Security != nil { + frameCtx = securitysys.WithSecurityConfig(frameCtx, meta.Security) + } method := "main" if meta != nil && meta.Method != "" { diff --git a/service/host/host_test.go b/service/host/host_test.go index 71e0a98da..4b11cad93 100644 --- a/service/host/host_test.go +++ b/service/host/host_test.go @@ -8,6 +8,7 @@ import ( "sync" "sync/atomic" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,10 +20,12 @@ import ( "github.com/wippyai/runtime/api/registry" "github.com/wippyai/runtime/api/relay" "github.com/wippyai/runtime/api/runtime" + secapi "github.com/wippyai/runtime/api/security" hostapi "github.com/wippyai/runtime/api/service/host" "github.com/wippyai/runtime/api/topology" "github.com/wippyai/runtime/internal/uniqid" "github.com/wippyai/runtime/system/scheduler/actor" + securitysys "github.com/wippyai/runtime/system/security" "go.uber.org/zap" ) @@ -748,6 +751,77 @@ func TestHost_ConcurrentSend(t *testing.T) { wg.Wait() } +// --- Entry Security --- + +type stubPolicy struct{ id registry.ID } + +func (p stubPolicy) ID() registry.ID { return p.id } +func (p stubPolicy) Evaluate(_ secapi.Actor, _, _ string, _ attrs.Bag) secapi.Result { + return secapi.Allow +} + +type stubPolicyRegistry struct{ policy secapi.Policy } + +func (r stubPolicyRegistry) GetPolicy(id registry.ID) (secapi.Policy, error) { + if r.policy != nil && r.policy.ID() == id { + return r.policy, nil + } + return nil, errors.New("policy not found") +} +func (r stubPolicyRegistry) GetPolicyGroup(registry.ID) (secapi.Scope, error) { + return nil, errors.New("group not found") +} +func (r stubPolicyRegistry) ListGroups() []registry.ID { return nil } +func (r stubPolicyRegistry) ListPolicies() []registry.ID { return nil } + +func TestHost_RunAppliesEntrySecurity(t *testing.T) { + entryPolicyID := registry.NewID("test", "entry_allow") + launchPolicy := stubPolicy{id: registry.NewID("test", "launch_allow")} + + var gotActor secapi.Actor + var gotScope secapi.Scope + done := make(chan struct{}) + + th := newTestHost(func(th *testHost) { + th.factory.meta = &process.Meta{ + Security: &secapi.Config{ + Actor: secapi.Actor{ID: "test:entry-actor"}, + Policies: []registry.ID{entryPolicyID}, + }, + } + th.lifecycle.onStartFunc = func(ctx context.Context, _ pid.PID, _ process.Process) { + gotActor, _ = secapi.GetActor(ctx) + gotScope, _ = secapi.GetScope(ctx) + close(done) + } + }) + ctx := secapi.WithRegistry(ctxWithAppContext(), stubPolicyRegistry{ + policy: stubPolicy{id: entryPolicyID}, + }) + _, err := th.host.Start(ctx) + require.NoError(t, err) + defer th.stop() + + _, err = th.host.Run(ctx, &process.Start{ + Source: registry.NewID("test", "proc"), + Context: []ctxapi.Pair{ + secapi.ActorPair(secapi.Actor{ID: "test:launch-actor"}), + secapi.ScopePair(securitysys.NewScope([]secapi.Policy{launchPolicy})), + }, + }) + require.NoError(t, err) + + select { + case <-done: + case <-time.After(3 * time.Second): + t.Fatal("process did not start") + } + assert.Equal(t, "test:entry-actor", gotActor.ID) + require.NotNil(t, gotScope) + assert.True(t, gotScope.Contains(entryPolicyID), "entry policy must be present") + assert.True(t, gotScope.Contains(launchPolicy.ID()), "launch policy must be preserved") +} + // --- Interface Compliance --- var _ process.Host = (*Host)(nil) diff --git a/service/terminal/host.go b/service/terminal/host.go index 295739a4b..534ae26f8 100644 --- a/service/terminal/host.go +++ b/service/terminal/host.go @@ -23,6 +23,7 @@ import ( supervisorapi "github.com/wippyai/runtime/api/supervisor" "github.com/wippyai/runtime/system/logs" "github.com/wippyai/runtime/system/scheduler/actor" + securitysys "github.com/wippyai/runtime/system/security" "go.uber.org/zap" ) @@ -205,6 +206,9 @@ func (h *Host) Run(ctx context.Context, start *process.Start) (pid.PID, error) { } frameCtx := h.prepareContext(ctx, processID, start) + if meta != nil && meta.Security != nil { + frameCtx = securitysys.WithSecurityConfig(frameCtx, meta.Security) + } method := "main" if meta != nil && meta.Method != "" { diff --git a/service/terminal/host_test.go b/service/terminal/host_test.go index 7bf5e57e0..248d8d5a4 100644 --- a/service/terminal/host_test.go +++ b/service/terminal/host_test.go @@ -21,6 +21,7 @@ import ( "github.com/wippyai/runtime/api/registry" "github.com/wippyai/runtime/api/relay" "github.com/wippyai/runtime/api/runtime" + secapi "github.com/wippyai/runtime/api/security" terminalapi "github.com/wippyai/runtime/api/service/terminal" "github.com/wippyai/runtime/internal/uniqid" "github.com/wippyai/runtime/system/logs" @@ -252,6 +253,66 @@ func TestHost_Run_ShuttingDown(t *testing.T) { _ = h.Stop(context.Background()) } +type securityCaptureProcess struct { + actor chan secapi.Actor +} + +func (p *securityCaptureProcess) Init(ctx context.Context, _ string, _ payload.Payloads) error { + actor, _ := secapi.GetActor(ctx) + p.actor <- actor + return nil +} + +func (*securityCaptureProcess) Step([]process.Event, *process.StepOutput) error { return nil } +func (*securityCaptureProcess) Close() {} + +type securityFactory struct { + proc process.Process + meta *process.Meta +} + +func (f securityFactory) Create(registry.ID) (process.Process, *process.Meta, error) { + return f.proc, f.meta, nil +} + +func TestHost_RunAppliesEntrySecurity(t *testing.T) { + actorCh := make(chan secapi.Actor, 1) + factory := securityFactory{ + proc: &securityCaptureProcess{actor: actorCh}, + meta: &process.Meta{Security: &secapi.Config{ + Actor: secapi.Actor{ID: "test:entry-actor"}, + }}, + } + scheduler := actor.NewScheduler(&mockCommandRegistry{}, actor.WithWorkers(1)) + h := NewHost( + registry.NewID("test", "host"), + &terminalapi.HostConfig{}, + scheduler, + factory, + logs.NewConfigurator(nil, zap.NewNop()), + zap.NewNop(), + ) + ctx := process.WithPIDGenerator(ctxapi.NewRootContext(), newTestPIDGen()) + _, err := h.Start(ctx) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, h.Stop(context.Background())) }) + + _, err = h.Run(ctx, &process.Start{ + Source: registry.NewID("test", "process"), + Context: []ctxapi.Pair{ + secapi.ActorPair(secapi.Actor{ID: "test:launch-actor"}), + }, + }) + require.NoError(t, err) + + select { + case got := <-actorCh: + assert.Equal(t, "test:entry-actor", got.ID) + case <-time.After(3 * time.Second): + t.Fatal("terminal process did not initialize") + } +} + func TestHost_Send_ShuttingDown(t *testing.T) { id := registry.ID{NS: "test", Name: "host1"} cfg := &terminalapi.HostConfig{}