Skip to content

Commit 4e8f73a

Browse files
authored
revert: remove group-variables feature entirely (#196) (#239)
Reverts the group-based variables feature shipped via 5 server PRs: - #197 (f0934bf): server storage + handlers + RBAC + events + migration 043_group_variables.sql + sqlc queries + group-variable validator + audit-redactor schema entries for the new event types - #198 (6dafcef): server-side {{ var.NAME }} template renderer + internal_handler.go SyncActions hook + main.go renderer wiring - #236 (3bb247a): drop device-labels resolver + add DispatchAction template gate - #237 (ee455b1): rewire ListAvailableVariables to group-scoped + SDK pin bump - #238 (05c7bc1): distinct error code for templated-dispatch refusal Squashed into one cohesive rollback commit alongside the SDK pin bump to power-manage-sdk@dda53a0 (which is the post-rollback SDK HEAD that no longer carries the group-vars proto). Conflicts resolved during revert: - cmd/control/main.go: dropped both the 'template' import (we are removing the package) AND the 'asynqutil' import the revert wanted to put back (it lives in valkey.go now post-#157 slice 4 extraction; not main.go's responsibility anymore). - go.mod: SDK pin set to dda53a0 (post-revert SDK HEAD) rather than the pre-#197 SHA the revert hunk targeted, so the server compiles against an SDK that also doesn't have the variables proto. - go.sum: regenerated via 'go mod tidy' against the new pin. Why: the design has fundamental scaling problems that were not caught at design time. Group-bound variables become monsters when many actions are assigned to a group; assignment-bound variables suffer the inverse N\xc3\x97M repetition problem; bundle-based reuse needs more design work before re-implementation. Cleaner to revert wholesale than to ship a half-shape that locks operators into the wrong API. Companion reverts: - SDK #64 (merged): the proto + TS-client wrappers - Web 09a8c75 (shipped): the variables UI + hint banner + error mapping Refs #196.
1 parent 05c7bc1 commit 4e8f73a

28 files changed

Lines changed: 15 additions & 2451 deletions

cmd/control/main.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"github.com/manchtools/power-manage/sdk/gen/go/pm/v1/pmv1connect"
1717
"github.com/manchtools/power-manage/sdk/go/logging"
1818
"github.com/manchtools/power-manage/server/internal/api"
19-
"github.com/manchtools/power-manage/server/internal/api/template"
2019
"github.com/manchtools/power-manage/server/internal/auth"
2120
"github.com/manchtools/power-manage/server/internal/ca"
2221
"github.com/manchtools/power-manage/server/internal/middleware"
@@ -278,12 +277,6 @@ func main() {
278277
// ProxyValidateTerminalToken.
279278
internalHandler.SetTerminalTokenStore(valkey.TerminalTokenStore)
280279
}
281-
// Server-side `{{ var.NAME }}` substitution for action params.
282-
// The renderer's StoreResolver reads device labels + group
283-
// variables from the projection and decrypts SECRET-typed values
284-
// via the same encryptor that wrote them. See manchtools/power-
285-
// manage-server#196 (group-based variables, design #59).
286-
internalHandler.SetRenderer(template.New(template.NewStoreResolver(st, encryptor, logger.With("component", "template"))))
287280
internalPath, internalH := pmv1connect.NewInternalServiceHandler(internalHandler)
288281

289282
// Peer-class gate: InternalService handles credential-bearing

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,4 @@ require (
106106
// whatever happens to be in a local ../sdk checkout. Developers who
107107
// want to iterate against a local SDK override this with a per-dev
108108
// go.work at their workspace root — see server/README.md for setup.
109-
replace github.com/manchtools/power-manage/sdk => github.com/manchtools/power-manage-sdk v0.4.1-0.20260511201104-c971debf896b
109+
replace github.com/manchtools/power-manage/sdk => github.com/manchtools/power-manage-sdk v0.4.1-0.20260512183037-dda53a00b743

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
111111
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
112112
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
113113
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
114-
github.com/manchtools/power-manage-sdk v0.4.1-0.20260511201104-c971debf896b h1:wBe6kBNNwZrqzG2jY+MD0of7s/xpLh72PWyt6V9IiEY=
115-
github.com/manchtools/power-manage-sdk v0.4.1-0.20260511201104-c971debf896b/go.mod h1:hG7NcdIGAG0VzhLL/W12FDAgVQJxeK+jHFtHb2eF3A8=
114+
github.com/manchtools/power-manage-sdk v0.4.1-0.20260512183037-dda53a00b743 h1:ANagVtDVc6PmBoFXVcT2gx23tYVrRHAE3OiQoK2sdms=
115+
github.com/manchtools/power-manage-sdk v0.4.1-0.20260512183037-dda53a00b743/go.mod h1:hG7NcdIGAG0VzhLL/W12FDAgVQJxeK+jHFtHb2eF3A8=
116116
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
117117
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
118118
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=

internal/api/action_dispatch.go

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"github.com/oklog/ulid/v2"
2323

2424
pm "github.com/manchtools/power-manage/sdk/gen/go/pm/v1"
25-
"github.com/manchtools/power-manage/server/internal/api/template"
2625
"github.com/manchtools/power-manage/server/internal/eventtypes"
2726
"github.com/manchtools/power-manage/server/internal/eventtypes/payloads"
2827
"github.com/manchtools/power-manage/server/internal/store"
@@ -147,35 +146,6 @@ func (h *ActionHandler) DispatchAction(ctx context.Context, req *connect.Request
147146
return nil, apiErrorCtx(ctx, ErrValidationFailed, connect.CodeInvalidArgument, "either action_id or inline_action is required")
148147
}
149148

150-
// Templated-params gate: ad-hoc DispatchAction has no group context
151-
// to resolve `{{ var.NAME }}` from (variables are exclusively a
152-
// group concept; the renderer only runs on the SyncActions path
153-
// where the agent's device → group memberships supply the values).
154-
// Refuse here rather than enqueueing literal `{{ ... }}` markers
155-
// the agent would consume verbatim.
156-
//
157-
// Marshals to JSON once for the scan, regardless of params shape
158-
// (map[string]any from the stored branch, []byte from the inline
159-
// branch, or the rare string fallback). The marshal happens again
160-
// downstream for signing — a one-extra-allocation cost in exchange
161-
// for one shared scan path. Marshal failure here MUST be fail-
162-
// closed: a params shape we can't serialise can't be scanned for
163-
// templates either, and silently bypassing the gate would let
164-
// templated content through that we couldn't see. The downstream
165-
// signing step would also fail on the same params, but failing
166-
// here is more honest about which gate is rejecting the call.
167-
paramsJSONForScan, err := json.Marshal(inputs.params)
168-
if err != nil {
169-
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal,
170-
fmt.Sprintf("failed to marshal action params for template scan: %v", err))
171-
}
172-
if template.HasReference(string(paramsJSONForScan)) {
173-
return nil, apiErrorCtx(ctx, ErrTemplatedDispatchRefused, connect.CodeFailedPrecondition,
174-
"this action contains templated parameters ({{ var.NAME }}) and cannot be dispatched ad-hoc to a single device. "+
175-
"Variables are resolved from device-group / user-group memberships at agent sync time. "+
176-
"Assign the action to a device-group or user-group instead of running it directly.")
177-
}
178-
179149
id := ulid.Make().String()
180150

181151
// Compute the deferred-dispatch delay. RunAt is optional; when set

internal/api/audit_handler.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,6 @@ var eventRedactionSchemas = map[string]map[string]redactionSchema{
116116
// `passphrase` at the top level.
117117
"LuksKeyRotated": {paths: []string{"passphrase"}},
118118
},
119-
"device_group_variable": {
120-
// Group-scoped variables (#59). The non-secret variants
121-
// (GroupVariableSet / Deleted) carry no secret-bearing fields
122-
// and need no redaction; the secret variants always carry the
123-
// AES-GCM ciphertext under "ciphertext", scrubbed here so the
124-
// audit log never reveals it. Same trust shape as
125-
// LpsPasswordRotated / LuksKeyRotated.
126-
string(eventtypes.GroupSecretVariableSet): {paths: []string{"ciphertext"}},
127-
},
128-
"user_group_variable": {
129-
string(eventtypes.GroupSecretVariableSet): {paths: []string{"ciphertext"}},
130-
},
131119
}
132120

133121
// actionRedactionSchemas maps an action's `params.type` value to the

internal/api/dispatch_validation_test.go

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package api_test
22

33
import (
4-
"errors"
54
"log/slog"
65
"testing"
76

@@ -179,59 +178,6 @@ func TestDispatchAction_PreconditionNoTaskQueue(t *testing.T) {
179178
assert.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(err))
180179
}
181180

182-
// TestDispatchAction_TemplatedParamsRefused pins the templated-params
183-
// gate added for #196 scope correction. Variables resolve only via
184-
// device-group / user-group memberships at agent SyncActions time; an
185-
// ad-hoc DispatchAction has no group context so the renderer can't
186-
// resolve `{{ var.NAME }}` references. The gate refuses with
187-
// CodeFailedPrecondition rather than enqueueing literal `{{ ... }}`
188-
// markers the agent would consume verbatim.
189-
func TestDispatchAction_TemplatedParamsRefused(t *testing.T) {
190-
st := testutil.SetupPostgres(t)
191-
h := api.NewActionHandler(st, slog.Default(), api.NoOpSigner{})
192-
193-
adminID := testutil.CreateTestUser(t, st, testutil.NewID()+"@test.com", "pass", "admin")
194-
deviceID := testutil.CreateTestDevice(t, st, "templated-host")
195-
ctx := testutil.AdminContext(adminID)
196-
197-
_, err := h.DispatchAction(ctx, connect.NewRequest(&pm.DispatchActionRequest{
198-
DeviceId: deviceID,
199-
ActionSource: &pm.DispatchActionRequest_InlineAction{
200-
InlineAction: &pm.Action{
201-
Id: &pm.ActionId{Value: testutil.NewID()},
202-
Type: pm.ActionType_ACTION_TYPE_SHELL,
203-
DesiredState: pm.DesiredState_DESIRED_STATE_PRESENT,
204-
Params: &pm.Action_Shell{
205-
Shell: &pm.ShellParams{
206-
Script: "echo {{ var.greeting }}",
207-
},
208-
},
209-
},
210-
},
211-
}))
212-
require.Error(t, err)
213-
assert.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(err),
214-
"templated params on ad-hoc dispatch MUST surface FailedPrecondition — anything else lets literal {{ var.X }} reach the agent verbatim")
215-
assert.Contains(t, err.Error(), "templated parameters",
216-
"error message must explain that variables are group-only and the action should be assigned to a group instead")
217-
218-
// The web client reads the structured ErrorDetail code (not the
219-
// human-readable message) to map the failure to a localized
220-
// "assign to a group instead" UI message. Pin the code here so a
221-
// future refactor that drops the structured detail breaks the test
222-
// instead of silently breaking the web error mapping.
223-
var connectErr *connect.Error
224-
require.True(t, errors.As(err, &connectErr))
225-
details := connectErr.Details()
226-
require.NotEmpty(t, details, "templated-dispatch refusal MUST carry the structured ErrorDetail — the web client maps by code, not by message")
227-
val, vErr := details[0].Value()
228-
require.NoError(t, vErr)
229-
detail, ok := val.(*pm.ErrorDetail)
230-
require.True(t, ok, "first detail must be ErrorDetail; got %T", val)
231-
assert.Equal(t, api.ErrTemplatedDispatchRefused, detail.Code,
232-
"error code MUST be templated_dispatch_refused — the web client switches on this exact string")
233-
}
234-
235181
// TestDispatchInstantAction_PreconditionNoTaskQueue pins the
236182
// matching fail-closed behaviour for the instant-action path.
237183
func TestDispatchInstantAction_PreconditionNoTaskQueue(t *testing.T) {

internal/api/errors.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,6 @@ const (
109109
ErrValidationFailed = "validation_failed"
110110
ErrInvalidPageToken = "invalid_page_token"
111111
ErrInvalidQuery = "invalid_query"
112-
113-
// ErrTemplatedDispatchRefused is returned when an operator tries to
114-
// dispatch an action whose params contain `{{ var.NAME }}` references
115-
// directly to a single device (DispatchAction / DispatchToMultiple /
116-
// DispatchActionSet / etc.). Variables resolve only via device-group
117-
// or user-group memberships at agent SyncActions time; the ad-hoc
118-
// dispatch path has no group context. Web maps this code to a
119-
// "assign to a group instead" UI message. See #196.
120-
ErrTemplatedDispatchRefused = "templated_dispatch_refused"
121112
)
122113

123114
// Internal error code (generic).

0 commit comments

Comments
 (0)