fix: simplify dashboard and disable unauthorized actions
hermetic-ci / test (push) Canceled after 0s

This commit is contained in:
proxyctl coordinator
2026-08-30 15:49:04 +08:00
parent 416425a9bf
commit 30f7206b14
20 changed files with 182 additions and 221 deletions
+47 -16
View File
@@ -164,6 +164,7 @@ type Application struct {
targetDescriptions []target.Description
availabilities map[string]target.Availability
inspections map[string]target.Inspection
mutationBlockReasons map[string]string
detectionPhase map[string]TargetDetectionPhase
detectionErr map[string]error
detectionGeneration uint64
@@ -197,15 +198,16 @@ func NewWithTargetsAndRuntime(store profile.Store, targets []target.Target, muta
return nil, err
}
a := &Application{
store: store,
state: state,
targets: append([]target.Target(nil), targets...),
availabilities: make(map[string]target.Availability),
inspections: make(map[string]target.Inspection),
detectionPhase: make(map[string]TargetDetectionPhase),
detectionErr: make(map[string]error),
mutationLock: mutationLock,
logger: logger,
store: store,
state: state,
targets: append([]target.Target(nil), targets...),
availabilities: make(map[string]target.Availability),
inspections: make(map[string]target.Inspection),
mutationBlockReasons: make(map[string]string),
detectionPhase: make(map[string]TargetDetectionPhase),
detectionErr: make(map[string]error),
mutationLock: mutationLock,
logger: logger,
}
for _, adapter := range a.targets {
id := adapter.ID()
@@ -229,7 +231,7 @@ func (a *Application) snapshotLocked() Snapshot {
availability := a.availabilities[id]
inspection := a.inspections[id]
phase := a.detectionPhase[id]
primary, resolution := contextualTargetActions(phase, availability.Available, inspection.State, state.ActiveProfile != "")
primary, resolution := contextualTargetActions(phase, availability.Available, inspection.State, state.ActiveProfile != "", a.mutationBlockReasons[id])
snapshot.Targets = append(snapshot.Targets, TargetSnapshot{
ID: id, Description: a.targetDescriptions[i], DetectionPhase: phase, DetectionErr: a.detectionErr[id],
Available: availability.Available, AvailabilityEvidence: availability.Evidence, State: inspection.State, Evidence: inspection.Evidence,
@@ -239,7 +241,7 @@ func (a *Application) snapshotLocked() Snapshot {
return snapshot
}
func contextualTargetActions(phase TargetDetectionPhase, available bool, state target.State, hasActiveProfile bool) (TargetAction, []TargetAction) {
func contextualTargetActions(phase TargetDetectionPhase, available bool, state target.State, hasActiveProfile bool, mutationBlockReason string) (TargetAction, []TargetAction) {
primaryKind := primaryTargetActionKind(state)
primary := TargetAction{Kind: primaryKind, Available: primaryKind != 0}
if primaryKind == 0 {
@@ -253,6 +255,10 @@ func contextualTargetActions(phase TargetDetectionPhase, available bool, state t
primary.Available = false
primary.DisabledReason = "This action is unavailable because there is no Active Profile; open Profile management first"
}
if primary.Available && mutationBlockReason != "" {
primary.Available = false
primary.DisabledReason = mutationBlockReason
}
if state != target.Drifted {
return primary, nil
}
@@ -271,6 +277,10 @@ func contextualTargetActions(phase TargetDetectionPhase, available bool, state t
action.Available = false
action.DisabledReason = reason
}
if action.Available && mutationBlockReason != "" {
action.Available = false
action.DisabledReason = mutationBlockReason
}
resolution = append(resolution, action)
}
return primary, resolution
@@ -300,8 +310,9 @@ func targetActionDisabledReason(phase TargetDetectionPhase, available bool) stri
}
type targetDetectionOutcome struct {
targetID string
detection target.Detection
targetID string
detection target.Detection
mutationBlockReason string
}
type targetDetectionUpdate struct {
@@ -327,7 +338,10 @@ func (a *Application) beginTargetDetection(ctx context.Context) (Snapshot, <-cha
for i, adapter := range a.targets {
adapter, id := adapter, a.targetIDs[i]
go func() {
outcomes <- targetDetectionOutcome{targetID: id, detection: detectTarget(ctx, adapter, active)}
outcomes <- targetDetectionOutcome{
targetID: id, detection: detectTarget(ctx, adapter, active),
mutationBlockReason: mutationBlockReason(ctx, adapter),
}
}()
}
go func() {
@@ -345,6 +359,7 @@ func (a *Application) beginTargetDetection(ctx context.Context) (Snapshot, <-cha
}
a.availabilities[outcome.targetID] = outcome.detection.Availability
a.inspections[outcome.targetID] = outcome.detection.Inspection
a.mutationBlockReasons[outcome.targetID] = outcome.mutationBlockReason
a.detectionPhase[outcome.targetID] = phase
a.detectionErr[outcome.targetID] = outcome.detection.Err
update := targetDetectionUpdate{snapshot: a.snapshotLocked(), targetID: outcome.targetID}
@@ -372,6 +387,21 @@ func detectTarget(ctx context.Context, adapter target.Target, active *profile.Pr
}
}
func mutationBlockReason(ctx context.Context, adapter target.Target) string {
authorizer, ok := adapter.(target.MutationAuthorizer)
if !ok {
return ""
}
check := authorizer.MutationAuthority(ctx)
if check.OK {
return ""
}
if check.Evidence == "" {
return "The current process does not have authority to change this Target"
}
return check.Evidence
}
func (a *Application) Dispatch(intent Intent) Result {
ctx, cancel := context.WithTimeout(context.Background(), operationTimeout)
defer cancel()
@@ -642,12 +672,13 @@ func (a *Application) replaceInspection(id string, inspection target.Inspection)
// publishTargetDetection atomically replaces all availability, inspection, and
// failure fields from one targeted check. Detect runs before this method, so no
// Application state mutex is held while adapter code executes.
func (a *Application) publishTargetDetection(id string, detection target.Detection) Snapshot {
func (a *Application) publishTargetDetection(id string, detection target.Detection, mutationBlockReason string) Snapshot {
a.stateMu.Lock()
defer a.stateMu.Unlock()
a.detectionGeneration++
a.availabilities[id] = detection.Availability
a.inspections[id] = detection.Inspection
a.mutationBlockReasons[id] = mutationBlockReason
a.detectionErr[id] = detection.Err
a.detectionPhase[id] = TargetReady
if detection.Err != nil {
@@ -806,7 +837,7 @@ func (a *Application) setTargetDesired(ctx context.Context, intent Intent, publi
// before action validation. Adapter calls happen without stateMu; the full
// outcome is then published under one lock before policy is derived.
detection := detectTarget(ctx, selected, active)
snapshot := a.publishTargetDetection(intent.TargetID, detection)
snapshot := a.publishTargetDetection(intent.TargetID, detection, mutationBlockReason(ctx, selected))
selectedSnapshot := snapshot.Targets[selectedIndex]
policy, err := validateTargetAction(intent, selectedSnapshot)
if err != nil {
+29
View File
@@ -103,6 +103,35 @@ func TestApplicationRequiresTargetConfirmationBeforeMutation(t *testing.T) {
}
}
type authorityTarget struct {
*fakeTarget
allowed bool
reason string
}
func (t *authorityTarget) MutationAuthority(context.Context) target.Check {
return target.Check{Name: "mutation authority", OK: t.allowed, Evidence: t.reason}
}
func TestApplicationDisablesMutationWhenCurrentProcessLacksAuthority(t *testing.T) {
adapter := &authorityTarget{fakeTarget: &fakeTarget{available: true, state: target.Disabled}, reason: "requires elevated authority"}
application, err := app.NewWithTargets(profile.NewFileStore(filepath.Join(t.TempDir(), "profiles.json")), []target.Target{adapter})
if err != nil {
t.Fatal(err)
}
createAndActivate(t, application)
refreshApplicationTargets(t, application)
snapshot := application.Snapshot()
if snapshot.Targets[0].PrimaryAction.Available || snapshot.Targets[0].PrimaryAction.DisabledReason != adapter.reason {
t.Fatalf("authority policy = %#v", snapshot.Targets[0].PrimaryAction)
}
result := application.Dispatch(app.Intent{Kind: app.EnableTarget, TargetID: adapter.ID()})
if result.Err == nil || !strings.Contains(result.Err.Error(), adapter.reason) || len(adapter.calls) != 0 {
t.Fatalf("blocked mutation result = %#v calls=%v", result, adapter.calls)
}
}
type targetWithID struct {
*fakeTarget
id string