fix: simplify dashboard and disable unauthorized actions
hermetic-ci / test (push) Canceled after 0s
hermetic-ci / test (push) Canceled after 0s
This commit is contained in:
+47
-16
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user