Skip to content

Commit ae1a49d

Browse files
authored
Send a list of paths that should be redacted to the policy runtime (#38600)
1 parent c50585f commit ae1a49d

21 files changed

Lines changed: 747 additions & 354 deletions

internal/command/apply_policy_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ func TestApply_WithPlanPolicyDiagnosticsJSON(t *testing.T) {
307307

308308
policyClient.EvaluateFn = func(ctx context.Context, er policy.EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]) policy.EvaluationResponse {
309309
// This is what is returned during the post-plan policy evaluation
310-
if !er.Attrs.GetAttr("id").IsWhollyKnown() {
310+
if !er.Attrs.Raw.GetAttr("id").IsWhollyKnown() {
311311
return evalRespFn(proto.EvaluateResult_DENY_EVALUATE_RESULT)
312312
}
313313

internal/command/init_policy_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/google/go-cmp/cmp"
1616
"github.com/hashicorp/cli"
17+
"github.com/zclconf/go-cty/cty"
1718
"google.golang.org/protobuf/testing/protocmp"
1819

1920
"github.com/hashicorp/terraform/internal/policy"
@@ -510,7 +511,7 @@ func TestInit_WithProviderPolicy(t *testing.T) {
510511
Version: "1.0.0",
511512
}
512513
}
513-
if diff := cmp.Diff(expected, req.Meta, protocmp.Transform()); diff != "" {
514+
if diff := cmp.Diff(expected, req.Meta, protocmp.Transform(), cmp.Comparer(cty.Value.RawEquals)); diff != "" {
514515
t.Fatalf("wrong provider metadata\ngot: %s\nwant: %v", diff, expected)
515516
}
516517

internal/command/meta_policy.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func (h *policyModuleInstallHook) ModuleSourceResolved(ctx context.Context, req
138138
result := h.client.EvaluateModule(ctx, policy.EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]{
139139
// Configuration attributes may not be available during init, so we will send an unknown
140140
// dynamic value to the policy client.
141-
Attrs: cty.DynamicVal,
141+
Attrs: policy.PolicyValue{Raw: cty.DynamicVal},
142142
Target: req.SourceAddr.String(),
143143
Meta: &proto.PolicyEvaluateModuleRequest_ModuleMetadata{
144144
Address: moduleAddr,
@@ -201,7 +201,7 @@ func (p *providerPolicyHook) ProviderVersionSelected(ctx context.Context, provid
201201

202202
// Configuration attributes may not be available during init, so we will send an unknown
203203
// dynamic value to the policy client.
204-
Attrs: cty.DynamicVal,
204+
Attrs: policy.PolicyValue{Raw: cty.DynamicVal},
205205
Meta: &proto.PolicyEvaluateProviderRequest_ProviderMetadata{
206206
Name: provider.Type,
207207
Namespace: provider.Namespace,

internal/policy/client.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/hashicorp/go-hclog"
1515
"github.com/hashicorp/go-plugin"
1616
"github.com/zclconf/go-cty/cty"
17-
"github.com/zclconf/go-cty/cty/msgpack"
1817
"google.golang.org/grpc"
1918

2019
"github.com/hashicorp/terraform/internal/policy/callback"
@@ -163,7 +162,7 @@ func (c *client) EvaluateResource(ctx context.Context, req EvaluationRequest[*pr
163162

164163
req = normalizeRequest(req)
165164

166-
attrsBytes, err := msgpack.Marshal(req.Attrs, cty.DynamicPseudoType)
165+
attrs, err := resourceAttributesToProto(req.Attrs)
167166
if err != nil {
168167
return ErrorEvalFromDiags(append(diags, &proto.Diagnostic{
169168
Severity: proto.Severity_ERROR,
@@ -172,7 +171,7 @@ func (c *client) EvaluateResource(ctx context.Context, req EvaluationRequest[*pr
172171
}))
173172
}
174173

175-
priorAttrsBytes, err := msgpack.Marshal(req.PriorAttrs, cty.DynamicPseudoType)
174+
priorAttrs, err := resourceAttributesToProto(req.PriorAttrs)
176175
if err != nil {
177176
return ErrorEvalFromDiags(append(diags, &proto.Diagnostic{
178177
Severity: proto.Severity_ERROR,
@@ -185,9 +184,9 @@ func (c *client) EvaluateResource(ctx context.Context, req EvaluationRequest[*pr
185184
request := &proto.PolicyEvaluateResourceRequest{
186185
EvaluationId: evalID,
187186
Resource: req.Target,
188-
Attrs: attrsBytes,
187+
Attrs: attrs,
188+
PriorAttrs: priorAttrs,
189189
Metadata: req.Meta,
190-
PriorAttrs: priorAttrsBytes,
191190
}
192191

193192
// Register the callback functions with the callback service, so that they are available
@@ -214,7 +213,7 @@ func (c *client) EvaluateProvider(ctx context.Context, req EvaluationRequest[*pr
214213
var diags []*proto.Diagnostic
215214
req = normalizeRequest(req)
216215

217-
attrsBytes, err := msgpack.Marshal(req.Attrs, cty.DynamicPseudoType)
216+
attrs, err := resourceAttributesToProto(req.Attrs)
218217
if err != nil {
219218
return ErrorEvalFromDiags(append(diags, &proto.Diagnostic{
220219
Severity: proto.Severity_ERROR,
@@ -225,7 +224,7 @@ func (c *client) EvaluateProvider(ctx context.Context, req EvaluationRequest[*pr
225224

226225
request := &proto.PolicyEvaluateProviderRequest{
227226
ProviderType: req.Target,
228-
Attrs: attrsBytes,
227+
Attrs: attrs,
229228
Metadata: req.Meta,
230229
}
231230

@@ -247,8 +246,18 @@ func (c *client) EvaluateModule(ctx context.Context, req EvaluationRequest[*prot
247246

248247
req = normalizeRequest(req)
249248

249+
attrs, err := resourceAttributesToProto(req.Attrs)
250+
if err != nil {
251+
return ErrorEvalFromDiags(append(diags, &proto.Diagnostic{
252+
Severity: proto.Severity_ERROR,
253+
Summary: "Failed to serialize attributes",
254+
Detail: fmt.Sprintf("Failed to serialize attributes: %v.", err),
255+
}))
256+
}
257+
250258
request := &proto.PolicyEvaluateModuleRequest{
251259
ModuleSource: req.Target,
260+
Attrs: attrs,
252261
Metadata: req.Meta,
253262
}
254263

@@ -275,11 +284,11 @@ func (c *client) Stop() {
275284
func normalizeRequest[T any](req EvaluationRequest[T]) EvaluationRequest[T] {
276285
attrs := req.Attrs
277286
priorAttrs := req.PriorAttrs
278-
if attrs == cty.NilVal {
279-
attrs = cty.EmptyObjectVal
287+
if attrs.Raw == cty.NilVal {
288+
attrs.Raw = cty.EmptyObjectVal
280289
}
281-
if priorAttrs == cty.NilVal {
282-
priorAttrs = cty.EmptyObjectVal
290+
if priorAttrs.Raw == cty.NilVal {
291+
priorAttrs.Raw = cty.EmptyObjectVal
283292
}
284293

285294
return EvaluationRequest[T]{

internal/policy/client_test.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/hashicorp/terraform/internal/tfdiags"
1313
"github.com/zclconf/go-cty/cty"
1414
"google.golang.org/grpc"
15+
gproto "google.golang.org/protobuf/proto"
1516

1617
"github.com/hashicorp/terraform/internal/policy/callback"
1718
"github.com/hashicorp/terraform/internal/policy/proto"
@@ -42,8 +43,8 @@ func TestClientEvaluate(t *testing.T) {
4243

4344
tests := []struct {
4445
name string
45-
attrs cty.Value
46-
priorAttrs cty.Value
46+
attrs PolicyValue
47+
priorAttrs PolicyValue
4748

4849
// an optional function to override the default evaluateResourceFn
4950
evaluateResourceFn func(*proto.PolicyEvaluateResourceRequest) (*proto.PolicyEvaluateResourceResponse, error)
@@ -53,8 +54,8 @@ func TestClientEvaluate(t *testing.T) {
5354
}{
5455
{
5556
name: "nil attrs and prior attrs",
56-
attrs: cty.NilVal,
57-
priorAttrs: cty.NilVal,
57+
attrs: PolicyValue{Raw: cty.NilVal},
58+
priorAttrs: PolicyValue{Raw: cty.NilVal},
5859
assertResponse: func(t *testing.T, registry *callback.MockRegistry, req *proto.PolicyEvaluateResourceRequest, resp EvaluationResponse) {
5960
t.Helper()
6061
if resp.Overall != AllowResult {
@@ -69,9 +70,14 @@ func TestClientEvaluate(t *testing.T) {
6970
},
7071
},
7172
{
72-
name: "non-nil attrs and prior attrs",
73-
attrs: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("test")}),
74-
priorAttrs: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("prior")}),
73+
name: "non-nil attrs and prior attrs",
74+
attrs: PolicyValue{
75+
Raw: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("test")}),
76+
RedactedPaths: []cty.Path{cty.GetAttrPath("secret")},
77+
},
78+
priorAttrs: PolicyValue{
79+
Raw: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("prior")}),
80+
},
7581
assertResponse: func(t *testing.T, registry *callback.MockRegistry, req *proto.PolicyEvaluateResourceRequest, resp EvaluationResponse) {
7682
t.Helper()
7783
if resp.Overall != AllowResult {
@@ -80,12 +86,19 @@ func TestClientEvaluate(t *testing.T) {
8086
if len(resp.Diagnostics) != 0 {
8187
t.Fatalf("unexpected diagnostics: %#v", resp.Diagnostics)
8288
}
89+
90+
want := &proto.AttributePath{Steps: []*proto.AttributePath_Step{{
91+
Selector: &proto.AttributePath_Step_AttributeName{AttributeName: "secret"},
92+
}}}
93+
if len(req.Attrs.RedactedPaths) != 1 || !gproto.Equal(req.Attrs.RedactedPaths[0], want) {
94+
t.Fatalf("unexpected redacted paths: %#v", req.Attrs.RedactedPaths)
95+
}
8396
},
8497
},
8598
{
8699
name: "transforms diagnostics from response",
87-
attrs: cty.NilVal,
88-
priorAttrs: cty.NilVal,
100+
attrs: PolicyValue{Raw: cty.NilVal},
101+
priorAttrs: PolicyValue{Raw: cty.NilVal},
89102
evaluateResourceFn: func(req *proto.PolicyEvaluateResourceRequest) (*proto.PolicyEvaluateResourceResponse, error) {
90103
return &proto.PolicyEvaluateResourceResponse{
91104
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
@@ -204,13 +217,13 @@ func TestClientEvaluateProvider(t *testing.T) {
204217

205218
tests := []struct {
206219
name string
207-
attrs cty.Value
220+
attrs PolicyValue
208221
evaluateProviderFn func(*proto.PolicyEvaluateProviderRequest) (*proto.PolicyEvaluateProviderResponse, error)
209222
assertResponse func(*testing.T, EvaluationResponse)
210223
}{
211224
{
212225
name: "nil attrs",
213-
attrs: cty.NilVal,
226+
attrs: PolicyValue{Raw: cty.NilVal},
214227
assertResponse: func(t *testing.T, resp EvaluationResponse) {
215228
t.Helper()
216229
if resp.Overall != AllowResult {
@@ -223,7 +236,7 @@ func TestClientEvaluateProvider(t *testing.T) {
223236
},
224237
{
225238
name: "unknown attrs",
226-
attrs: cty.UnknownVal(cty.EmptyObject),
239+
attrs: PolicyValue{Raw: cty.UnknownVal(cty.EmptyObject)},
227240
assertResponse: func(t *testing.T, resp EvaluationResponse) {
228241
t.Helper()
229242
if resp.Overall != AllowResult {
@@ -236,7 +249,7 @@ func TestClientEvaluateProvider(t *testing.T) {
236249
},
237250
{
238251
name: "non-nil attrs",
239-
attrs: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("test")}),
252+
attrs: PolicyValue{Raw: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("test")})},
240253
assertResponse: func(t *testing.T, resp EvaluationResponse) {
241254
t.Helper()
242255
if resp.Overall != AllowResult {
@@ -249,7 +262,7 @@ func TestClientEvaluateProvider(t *testing.T) {
249262
},
250263
{
251264
name: "transforms diagnostics from response",
252-
attrs: cty.NilVal,
265+
attrs: PolicyValue{Raw: cty.NilVal},
253266
evaluateProviderFn: func(req *proto.PolicyEvaluateProviderRequest) (*proto.PolicyEvaluateProviderResponse, error) {
254267
return &proto.PolicyEvaluateProviderResponse{
255268
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,

internal/policy/convert.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright IBM Corp. 2014, 2026
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package policy
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/zclconf/go-cty/cty"
10+
"github.com/zclconf/go-cty/cty/msgpack"
11+
12+
"github.com/hashicorp/terraform/internal/policy/proto"
13+
)
14+
15+
func resourceAttributesToProto(value PolicyValue) (*proto.ResourceAttributes, error) {
16+
raw, err := msgpack.Marshal(value.Raw, cty.DynamicPseudoType)
17+
if err != nil {
18+
return nil, fmt.Errorf("error serializing raw value: %w", err)
19+
}
20+
21+
redactedPaths, err := pathsToProto(value.RedactedPaths)
22+
if err != nil {
23+
return nil, fmt.Errorf("error serializing redacted paths: %w", err)
24+
}
25+
26+
return &proto.ResourceAttributes{
27+
Raw: raw,
28+
RedactedPaths: redactedPaths,
29+
}, nil
30+
}
31+
32+
func pathToProto(path cty.Path) (*proto.AttributePath, error) {
33+
steps := make([]*proto.AttributePath_Step, 0, len(path))
34+
for _, step := range path {
35+
switch step := step.(type) {
36+
case cty.GetAttrStep:
37+
steps = append(steps, &proto.AttributePath_Step{
38+
Selector: &proto.AttributePath_Step_AttributeName{AttributeName: step.Name},
39+
})
40+
case cty.IndexStep:
41+
key := step.Key
42+
switch key.Type() {
43+
case cty.String:
44+
steps = append(steps, &proto.AttributePath_Step{
45+
Selector: &proto.AttributePath_Step_ElementKeyString{ElementKeyString: key.AsString()},
46+
})
47+
case cty.Number:
48+
v, _ := key.AsBigFloat().Int64()
49+
steps = append(steps, &proto.AttributePath_Step{
50+
Selector: &proto.AttributePath_Step_ElementKeyInt{ElementKeyInt: int64(v)},
51+
})
52+
default:
53+
return nil, fmt.Errorf("unsupported cty path step type %T", step)
54+
}
55+
default:
56+
return nil, fmt.Errorf("unsupported cty path step type %T", step)
57+
}
58+
}
59+
return &proto.AttributePath{Steps: steps}, nil
60+
}
61+
62+
func pathsToProto(paths []cty.Path) ([]*proto.AttributePath, error) {
63+
ret := make([]*proto.AttributePath, 0, len(paths))
64+
for _, path := range paths {
65+
protoPath, err := pathToProto(path)
66+
if err != nil {
67+
return nil, err
68+
}
69+
ret = append(ret, protoPath)
70+
}
71+
return ret, nil
72+
}

internal/policy/policy.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/zclconf/go-cty/cty"
1111

1212
"github.com/hashicorp/hcl/v2"
13+
"github.com/hashicorp/terraform/internal/lang/marks"
1314
"github.com/hashicorp/terraform/internal/policy/callback"
1415
"github.com/hashicorp/terraform/internal/policy/proto"
1516
)
@@ -68,15 +69,24 @@ type (
6869
CallbackService uint32
6970
}
7071

72+
PolicyValue struct {
73+
// Raw contains the Terraform value being sent to the policy engine.
74+
Raw cty.Value
75+
76+
// RedactedPaths contains attribute paths that should be redacted when
77+
// displaying values from Raw.
78+
RedactedPaths []cty.Path
79+
}
80+
7181
EvaluationRequest[T any] struct {
7282
// Target is the object being evaluated.
7383
Target string
7484

7585
// Attrs contains the attributes of the object being evaluated.
76-
Attrs cty.Value
86+
Attrs PolicyValue
7787

7888
// PriorAttrs contains the state of the object prior to the current operation.
79-
PriorAttrs cty.Value
89+
PriorAttrs PolicyValue
8090

8191
// Meta is additional metadata required for evaluation.
8292
Meta T
@@ -198,3 +208,14 @@ func ErrorEvalFromDiags(diags []*proto.Diagnostic) EvaluationResponse {
198208
Diagnostics: DiagsFromProto(diags, nil),
199209
}
200210
}
211+
212+
// CtyToPolicyValue converts a cty.Value to a PolicyValue, unmarking the value and
213+
// extracting sensitive paths.
214+
func CtyToPolicyValue(raw cty.Value) PolicyValue {
215+
raw, pvms := raw.UnmarkDeepWithPaths()
216+
redactedPaths, _ := marks.PathsWithMark(pvms, marks.Sensitive)
217+
return PolicyValue{
218+
Raw: raw,
219+
RedactedPaths: redactedPaths,
220+
}
221+
}

0 commit comments

Comments
 (0)