feat: safely control Shell Target
This commit is contained in:
+82
-8
@@ -1,10 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"proxyctl/internal/profile"
|
||||
"proxyctl/internal/target"
|
||||
)
|
||||
|
||||
type IntentKind int
|
||||
@@ -14,18 +16,31 @@ const (
|
||||
UpdateProfile
|
||||
DeleteProfile
|
||||
ActivateProfile
|
||||
SetTargetDesired
|
||||
)
|
||||
|
||||
type Intent struct {
|
||||
Kind IntentKind
|
||||
Name string
|
||||
Profile profile.Profile
|
||||
Confirm bool
|
||||
Kind IntentKind
|
||||
Name string
|
||||
Profile profile.Profile
|
||||
Confirm bool
|
||||
TargetID string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type TargetSnapshot struct {
|
||||
ID string
|
||||
Description target.Description
|
||||
Available bool
|
||||
AvailabilityEvidence string
|
||||
State target.State
|
||||
Evidence string
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Profiles []profile.Profile
|
||||
ActiveProfile string
|
||||
Targets []TargetSnapshot
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -40,24 +55,41 @@ type Module interface {
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
store profile.Store
|
||||
state profile.State
|
||||
store profile.Store
|
||||
state profile.State
|
||||
targets []target.Target
|
||||
inspections map[string]target.Inspection
|
||||
}
|
||||
|
||||
func New(store profile.Store) (*Application, error) {
|
||||
return NewWithTargets(store, nil)
|
||||
}
|
||||
|
||||
func NewWithTargets(store profile.Store, targets []target.Target) (*Application, error) {
|
||||
state, err := store.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Application{store: store, state: state}, nil
|
||||
a := &Application{store: store, state: state, targets: append([]target.Target(nil), targets...), inspections: make(map[string]target.Inspection)}
|
||||
a.refreshTargets()
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *Application) Snapshot() Snapshot {
|
||||
profiles := append([]profile.Profile(nil), a.state.Profiles...)
|
||||
return Snapshot{Profiles: profiles, ActiveProfile: a.state.ActiveProfile}
|
||||
snapshot := Snapshot{Profiles: profiles, ActiveProfile: a.state.ActiveProfile}
|
||||
for _, adapter := range a.targets {
|
||||
availability := adapter.Availability(context.Background())
|
||||
inspection := a.inspections[adapter.ID()]
|
||||
snapshot.Targets = append(snapshot.Targets, TargetSnapshot{ID: adapter.ID(), Description: adapter.Description(), Available: availability.Available, AvailabilityEvidence: availability.Evidence, State: inspection.State, Evidence: inspection.Evidence})
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func (a *Application) Dispatch(intent Intent) Result {
|
||||
if intent.Kind == SetTargetDesired {
|
||||
return a.setTargetDesired(intent)
|
||||
}
|
||||
var notice string
|
||||
next, err := a.store.Mutate(func(current profile.State) (profile.State, error) {
|
||||
var mutationErr error
|
||||
@@ -77,6 +109,7 @@ func (a *Application) Dispatch(intent Intent) Result {
|
||||
})
|
||||
if err == nil {
|
||||
a.state = next
|
||||
a.refreshTargets()
|
||||
} else {
|
||||
var committed *profile.CommitError
|
||||
if errors.As(err, &committed) {
|
||||
@@ -94,6 +127,47 @@ func (a *Application) Dispatch(intent Intent) Result {
|
||||
return Result{Snapshot: a.Snapshot(), Notice: notice, Err: err}
|
||||
}
|
||||
|
||||
func (a *Application) activeProfile() *profile.Profile {
|
||||
for i := range a.state.Profiles {
|
||||
if a.state.Profiles[i].Name == a.state.ActiveProfile {
|
||||
value := a.state.Profiles[i]
|
||||
return &value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) refreshTargets() {
|
||||
active := a.activeProfile()
|
||||
for _, adapter := range a.targets {
|
||||
a.inspections[adapter.ID()] = adapter.Inspect(context.Background(), active)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) setTargetDesired(intent Intent) Result {
|
||||
var selected target.Target
|
||||
for _, adapter := range a.targets {
|
||||
if adapter.ID() == intent.TargetID {
|
||||
selected = adapter
|
||||
break
|
||||
}
|
||||
}
|
||||
if selected == nil {
|
||||
return Result{Snapshot: a.Snapshot(), Err: fmt.Errorf("Target %q does not exist", intent.TargetID)}
|
||||
}
|
||||
active := a.activeProfile()
|
||||
if intent.Enabled && active == nil {
|
||||
return Result{Snapshot: a.Snapshot(), Err: errors.New("no Active Profile; open Profile management before enabling a Target")}
|
||||
}
|
||||
result := selected.SetDesired(context.Background(), intent.Enabled, active)
|
||||
a.inspections[selected.ID()] = result.Inspection
|
||||
notice := ""
|
||||
if result.Err == nil {
|
||||
notice = fmt.Sprintf("%s Target is now %s", selected.Description().Name, result.Inspection.State)
|
||||
}
|
||||
return Result{Snapshot: a.Snapshot(), Notice: notice, Err: result.Err}
|
||||
}
|
||||
|
||||
func create(state profile.State, input profile.Profile) (profile.State, string, error) {
|
||||
p, err := profile.ValidateAndNormalize(input)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package app_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/profile"
|
||||
"proxyctl/internal/target"
|
||||
)
|
||||
|
||||
type fakeTarget struct {
|
||||
available bool
|
||||
state target.State
|
||||
setErr error
|
||||
calls []bool
|
||||
}
|
||||
|
||||
func (f *fakeTarget) ID() string { return "shell" }
|
||||
func (f *fakeTarget) Description() target.Description {
|
||||
return target.Description{Name: "Shell", Summary: "Shell summary", Details: "Shell details"}
|
||||
}
|
||||
func (f *fakeTarget) Availability(context.Context) target.Availability {
|
||||
return target.Availability{Available: f.available, Evidence: "fake detection"}
|
||||
}
|
||||
func (f *fakeTarget) Inspect(context.Context, *profile.Profile) target.Inspection {
|
||||
return target.Inspection{State: f.state, Evidence: "actual state"}
|
||||
}
|
||||
func (f *fakeTarget) SetDesired(_ context.Context, enabled bool, _ *profile.Profile) target.OperationResult {
|
||||
f.calls = append(f.calls, enabled)
|
||||
if f.setErr == nil {
|
||||
if enabled {
|
||||
f.state = target.Enabled
|
||||
} else {
|
||||
f.state = target.Disabled
|
||||
}
|
||||
}
|
||||
return target.OperationResult{Inspection: target.Inspection{State: f.state, Evidence: "re-inspected actual state"}, Err: f.setErr}
|
||||
}
|
||||
|
||||
func TestApplicationTargetSeamRequiresActiveProfileAndReturnsTruthfulResults(t *testing.T) {
|
||||
adapter := &fakeTarget{available: true, state: target.External}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
initial := application.Snapshot()
|
||||
if len(initial.Targets) != 1 || initial.Targets[0].State != target.External || initial.Targets[0].Description.Summary != "Shell summary" {
|
||||
t.Fatalf("initial Target snapshot = %#v", initial.Targets)
|
||||
}
|
||||
|
||||
result := application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: "shell", Enabled: true})
|
||||
if result.Err == nil || !strings.Contains(result.Err.Error(), "Profile management") || len(adapter.calls) != 0 || result.Snapshot.Targets[0].State != target.External {
|
||||
t.Fatalf("enable without Active Profile = %#v, calls=%v", result, adapter.calls)
|
||||
}
|
||||
|
||||
createAndActivate(t, application)
|
||||
result = application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: "shell", Enabled: true})
|
||||
if result.Err != nil || result.Snapshot.Targets[0].State != target.Enabled || len(adapter.calls) != 1 {
|
||||
t.Fatalf("enable result = %#v", result)
|
||||
}
|
||||
|
||||
adapter.setErr = errors.New("permission denied")
|
||||
adapter.state = target.Drifted
|
||||
result = application.Dispatch(app.Intent{Kind: app.SetTargetDesired, TargetID: "shell", Enabled: false})
|
||||
if result.Err == nil || result.Snapshot.Targets[0].State != target.Drifted || result.Snapshot.Targets[0].Evidence != "re-inspected actual state" {
|
||||
t.Fatalf("failed operation not truthful: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationHidesUnavailableTargetButKeepsExplanation(t *testing.T) {
|
||||
adapter := &fakeTarget{available: false, state: target.Disabled}
|
||||
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := application.Snapshot().Targets[0]
|
||||
if got.Available || got.AvailabilityEvidence != "fake detection" {
|
||||
t.Fatalf("unavailable Target = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func createAndActivate(t *testing.T, application *app.Application) {
|
||||
t.Helper()
|
||||
if result := application.Dispatch(app.Intent{Kind: app.CreateProfile, Profile: profile.Profile{Name: "office", SingleURL: "http://proxy.example:8080"}}); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
if result := application.Dispatch(app.Intent{Kind: app.ActivateProfile, Name: "office"}); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user