-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathplaintext_payload.go
More file actions
249 lines (217 loc) · 9.82 KB
/
Copy pathplaintext_payload.go
File metadata and controls
249 lines (217 loc) · 9.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
package interceptors
import (
"context"
commandpb "go.temporal.io/api/command/v1"
commonpb "go.temporal.io/api/common/v1"
failurepb "go.temporal.io/api/failure/v1"
schedulepb "go.temporal.io/api/schedule/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/server/common/api"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/metrics"
"google.golang.org/grpc"
)
const metricPlaintextPayload = "plaintext_payload_detected_total"
// unencryptedEncodings is the set of encoding metadata values that indicate
// an unencrypted payload. binary/null is intentionally excluded — null payloads
// carry no data and are not a privacy concern.
var unencryptedEncodings = map[string]bool{
"json/plain": true,
"binary/plain": true,
}
// Tag helpers for dimensions that have no existing function in the metrics/tag packages.
// Defined once here so the key strings are not scattered across the file.
func metricPayloadField(v string) metrics.Tag { return metrics.StringTag("payload_field", v) }
func metricEncoding(v string) metrics.Tag { return metrics.StringTag("encoding", v) }
func metricOperation(v string) metrics.Tag { return metrics.StringTag("operation", v) }
func metricWorkflowType(v string) metrics.Tag { return metrics.StringTag("workflowType", v) }
func metricTaskQueue(v string) metrics.Tag { return metrics.StringTag("taskqueue", v) }
func logPayloadField(v string) tag.ZapTag { return tag.NewStringTag("payload_field", v) }
func logEncoding(v string) tag.ZapTag { return tag.NewStringTag("encoding", v) }
// PlainTextPayloadInterceptor logs and increments a metric whenever a
// frontend API call carries a payload with an unencrypted encoding.
// The request is always allowed through — this interceptor is observe-only.
//
// Covered APIs: StartWorkflowExecution, SignalWorkflowExecution,
// SignalWithStartWorkflowExecution, QueryWorkflow, UpdateWorkflowExecution,
// ExecuteMultiOperation (UpdateWithStart), RecordActivityTaskHeartbeat,
// CreateSchedule, UpdateSchedule (workflow input only),
// RespondWorkflowTaskCompleted, RespondWorkflowTaskFailed,
// RespondActivityTaskCompleted, RespondActivityTaskFailed.
type PlainTextPayloadInterceptor struct {
logger log.Logger
metricsHandler metrics.Handler
}
func NewPlainTextPayloadInterceptor(logger log.Logger, metricsHandler metrics.Handler) *PlainTextPayloadInterceptor {
return &PlainTextPayloadInterceptor{
logger: logger,
metricsHandler: metricsHandler,
}
}
func (i *PlainTextPayloadInterceptor) Intercept(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
i.check(req, api.MethodName(info.FullMethod))
return handler(ctx, req)
}
func (i *PlainTextPayloadInterceptor) check(req any, op string) {
switch r := req.(type) {
case *workflowservice.StartWorkflowExecutionRequest:
i.scanPayloads(r.Namespace, r.GetWorkflowType().GetName(), r.GetTaskQueue().GetName(), op, "input", r.Input)
case *workflowservice.SignalWithStartWorkflowExecutionRequest:
ns, wfType, tq := r.Namespace, r.GetWorkflowType().GetName(), r.GetTaskQueue().GetName()
i.scanPayloads(ns, wfType, tq, op, "input", r.Input)
i.scanPayloads(ns, wfType, tq, op, "signal_input", r.SignalInput)
case *workflowservice.SignalWorkflowExecutionRequest:
i.scanPayloads(r.Namespace, "", "", op, "input", r.Input)
case *workflowservice.QueryWorkflowRequest:
i.scanPayloads(r.Namespace, "", "", op, "query_args", r.GetQuery().GetQueryArgs())
case *workflowservice.UpdateWorkflowExecutionRequest:
i.scanPayloads(r.Namespace, "", "", op, "update_args", r.GetRequest().GetInput().GetArgs())
case *workflowservice.RespondActivityTaskCompletedRequest:
i.scanPayloads(r.Namespace, "", "", op, "result", r.Result)
case *workflowservice.RespondWorkflowTaskCompletedRequest:
i.scanCommands(r.Namespace, op, r.Commands)
case *workflowservice.CreateScheduleRequest:
i.scanScheduleAction(r.Namespace, op, r.GetSchedule().GetAction())
case *workflowservice.UpdateScheduleRequest:
i.scanScheduleAction(r.Namespace, op, r.GetSchedule().GetAction())
case *workflowservice.RecordActivityTaskHeartbeatRequest:
i.scanPayloads(r.Namespace, "", "", op, "details", r.Details)
case *workflowservice.ExecuteMultiOperationRequest:
// UpdateWithStart: each operation is a StartWorkflow or UpdateWorkflow —
// delegate to the existing cases so no payload logic is duplicated.
for _, multiOp := range r.Operations {
switch o := multiOp.Operation.(type) {
case *workflowservice.ExecuteMultiOperationRequest_Operation_StartWorkflow:
i.check(o.StartWorkflow, op)
case *workflowservice.ExecuteMultiOperationRequest_Operation_UpdateWorkflow:
i.check(o.UpdateWorkflow, op)
}
}
case *workflowservice.RespondWorkflowTaskFailedRequest:
i.scanFailure(r.Namespace, op, r.Failure)
case *workflowservice.RespondActivityTaskFailedRequest:
i.scanFailure(r.Namespace, op, r.Failure)
i.scanPayloads(r.Namespace, "", "", op, "last_heartbeat_details", r.LastHeartbeatDetails)
}
}
func (i *PlainTextPayloadInterceptor) scanCommands(ns, op string, cmds []*commandpb.Command) {
for _, cmd := range cmds {
location := cmd.GetCommandType().String() // e.g. "ScheduleActivityTask", "SignalExternalWorkflowExecution"
var wfType, tq string
var ps *commonpb.Payloads
switch a := cmd.Attributes.(type) {
case *commandpb.Command_ScheduleActivityTaskCommandAttributes:
ps = a.ScheduleActivityTaskCommandAttributes.Input
case *commandpb.Command_CompleteWorkflowExecutionCommandAttributes:
ps = a.CompleteWorkflowExecutionCommandAttributes.Result
case *commandpb.Command_CancelWorkflowExecutionCommandAttributes:
ps = a.CancelWorkflowExecutionCommandAttributes.Details
case *commandpb.Command_SignalExternalWorkflowExecutionCommandAttributes:
ps = a.SignalExternalWorkflowExecutionCommandAttributes.Input
case *commandpb.Command_StartChildWorkflowExecutionCommandAttributes:
attrs := a.StartChildWorkflowExecutionCommandAttributes
ps, wfType, tq = attrs.Input, attrs.GetWorkflowType().GetName(), attrs.GetTaskQueue().GetName()
case *commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes:
attrs := a.ContinueAsNewWorkflowExecutionCommandAttributes
ps, wfType = attrs.Input, attrs.GetWorkflowType().GetName()
default:
continue
}
i.scanPayloads(ns, wfType, tq, op, location, ps)
}
}
// scanScheduleAction checks the workflow input payload inside a ScheduleAction.
// Only the StartWorkflow action carries a payload — the schedule spec is ignored.
func (i *PlainTextPayloadInterceptor) scanScheduleAction(ns, op string, action *schedulepb.ScheduleAction) {
if action == nil {
return
}
sw, ok := action.Action.(*schedulepb.ScheduleAction_StartWorkflow)
if !ok || sw.StartWorkflow == nil {
return
}
wf := sw.StartWorkflow
i.scanPayloads(ns, wf.GetWorkflowType().GetName(), wf.GetTaskQueue().GetName(), op, "input", wf.Input)
}
// scanFailure walks a Failure and its cause chain, checking encoded_attributes
// and any failure-info-specific details payloads for unencrypted encodings.
func (i *PlainTextPayloadInterceptor) scanFailure(ns, op string, f *failurepb.Failure) {
if f == nil {
return
}
// encoded_attributes is the single Payload produced by the failure converter.
// If it is present and unencrypted, the failure converter is not encrypting.
i.scanPayload(ns, op, "encoded_attributes", f.EncodedAttributes)
// Failure-info-specific details payloads — derive location from the proto
// message descriptor name so it matches the canonical type name exactly.
switch fi := f.FailureInfo.(type) {
case *failurepb.Failure_ApplicationFailureInfo:
info := fi.ApplicationFailureInfo
loc := string(info.ProtoReflect().Descriptor().Name()) + ".details"
i.scanPayloads(ns, "", "", op, loc, info.GetDetails())
case *failurepb.Failure_CanceledFailureInfo:
info := fi.CanceledFailureInfo
loc := string(info.ProtoReflect().Descriptor().Name()) + ".details"
i.scanPayloads(ns, "", "", op, loc, info.GetDetails())
case *failurepb.Failure_TimeoutFailureInfo:
info := fi.TimeoutFailureInfo
loc := string(info.ProtoReflect().Descriptor().Name()) + ".last_heartbeat_details"
i.scanPayloads(ns, "", "", op, loc, info.GetLastHeartbeatDetails())
}
// Walk the cause chain.
i.scanFailure(ns, op, f.Cause)
}
// scanPayload checks a single Payload (used for Failure.encoded_attributes).
func (i *PlainTextPayloadInterceptor) scanPayload(ns, op, location string, p *commonpb.Payload) {
if p == nil {
return
}
i.scanPayloads(ns, "", "", op, location, &commonpb.Payloads{Payloads: []*commonpb.Payload{p}})
}
func (i *PlainTextPayloadInterceptor) scanPayloads(
ns, wfType, taskQueue, op, location string,
ps *commonpb.Payloads,
) {
if ps == nil {
return
}
for _, p := range ps.Payloads {
encoding := string(p.GetMetadata()["encoding"])
if !unencryptedEncodings[encoding] {
continue
}
// All label keys must be present on every series — Prometheus rejects
// series that share a metric name but have different label schemas.
// Use empty string for workflowType/taskqueue when not available.
// metricWorkflowType/metricTaskQueue use StringTag directly so that an
// absent value emits "" rather than Temporal's "_unknown_" sentinel.
metricTags := []metrics.Tag{
metrics.NamespaceTag(ns),
metricOperation(op),
metricPayloadField(location),
metricEncoding(encoding),
metricWorkflowType(wfType),
metricTaskQueue(taskQueue),
}
logTags := []tag.Tag{
tag.WorkflowNamespace(ns),
tag.Operation(op),
logPayloadField(location),
logEncoding(encoding),
}
if wfType != "" {
logTags = append(logTags, tag.WorkflowType(wfType))
}
if taskQueue != "" {
logTags = append(logTags, tag.WorkflowTaskQueueName(taskQueue))
}
i.metricsHandler.Counter(metricPlaintextPayload).Record(1, metricTags...)
i.logger.Warn("unencrypted payload detected", logTags...)
}
}