mirror of
https://github.com/temporalio/temporal.git
synced 2026-08-31 02:51:51 -07:00
## What changed? This PR implements the server-side plumbing for Principal Attribution - attaching a server-computed, immutable caller identity (Principal) to workflow history events. * Extended the Authorizer Result struct to include a Principal struct * Added the authorization interceptor logic to propagate Principal via gRPC metadata headers when enabled via dynamic config (frontend.enablePrincipalPropagation) * The default authorizer sets principal based on sub JWT claim * On the history service side, Principal is read from context headers and stamped onto every history event * Principal is set per-operation at two entry points: UpdateWorkflowWithNew (existing workflows) and NewWorkflowWithSignal (new workflows) * Added Principal headers to the Nexus disallowed operation headers blocklist to prevent leaking internal identity to external endpoints ## Why? The existing identity field on history events is client-supplied and spoofable. Principal Attribution provides a trusted, server-computed identity derived from authentication context (e.g. JWT claims, mTLS certificates), enabling reliable audit trails for workflow operations. ## How did you test it? - [X] built - [X] run locally and tested manually - [X] covered by existing tests - [X] added new unit test(s) I ran a simple test with local JWT server (PR here: https://github.com/temporalio/temporal/pull/9647). There a test client sends a signal to a running workflow using a separate JWT identity, then waits for the workflow to complete. Here is the workflow history: | eventId | eventType | principal | |---------|-----------|-----------| | 1 | WORKFLOW_EXECUTION_STARTED | jwt/workflow-starter | | 2 | WORKFLOW_TASK_SCHEDULED | jwt/workflow-starter | | 3 | WORKFLOW_TASK_STARTED | jwt/workflow-starter | | 4 | WORKFLOW_TASK_COMPLETED | jwt/workflow-starter | | 5 | WORKFLOW_EXECUTION_SIGNALED | jwt/signal-sender | | 6 | WORKFLOW_TASK_SCHEDULED | jwt/signal-sender | | 7 | WORKFLOW_TASK_STARTED | jwt/workflow-starter | | 8 | WORKFLOW_TASK_COMPLETED | jwt/workflow-starter | | 9 | ACTIVITY_TASK_SCHEDULED | jwt/workflow-starter | | 10 | ACTIVITY_TASK_STARTED | jwt/workflow-starter | | 11 | ACTIVITY_TASK_COMPLETED | jwt/workflow-starter | | 12 | WORKFLOW_TASK_SCHEDULED | jwt/workflow-starter | | 13 | WORKFLOW_TASK_STARTED | jwt/workflow-starter | | 14 | WORKFLOW_TASK_COMPLETED | jwt/workflow-starter | | 15 | WORKFLOW_EXECUTION_COMPLETED | jwt/workflow-starter | ## Potential risks - Feature-gated: Principal propagation is behind frontend.enablePrincipalPropagation dynamic config (default off), so no impact unless explicitly enabled. - Nexus blocklist addition: Adding principal-type/principal-name to the Nexus disallowed headers blocklist could reject operations that happen to use these header names
79 lines
2.1 KiB
Go
79 lines
2.1 KiB
Go
//go:generate mockgen -package $GOPACKAGE -source $GOFILE -destination authorizer_mock.go
|
|
|
|
package authorization
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
commonpb "go.temporal.io/api/common/v1"
|
|
"go.temporal.io/server/common/config"
|
|
)
|
|
|
|
const (
|
|
// DecisionDeny means auth decision is deny
|
|
DecisionDeny Decision = iota + 1
|
|
// DecisionAllow means auth decision is allow
|
|
DecisionAllow
|
|
)
|
|
|
|
// @@@SNIPSTART temporal-common-authorization-authorizer-calltarget
|
|
// CallTarget is contains information for Authorizer to make a decision.
|
|
// It can be extended to include resources like WorkflowType and TaskQueue
|
|
type CallTarget struct {
|
|
// APIName must be the full API function name.
|
|
// Example: "/temporal.api.workflowservice.v1.WorkflowService/StartWorkflowExecution".
|
|
APIName string
|
|
// If a Namespace is not being targeted this be set to an empty string.
|
|
Namespace string
|
|
// The nexus endpoint name being targeted (if any).
|
|
NexusEndpointName string
|
|
// Request contains a deserialized copy of the API request object
|
|
Request any
|
|
}
|
|
|
|
// @@@SNIPEND
|
|
|
|
type (
|
|
// Result is result from authority.
|
|
Result struct {
|
|
Decision Decision
|
|
// Reason may contain a message explaining the value of the Decision field.
|
|
Reason string
|
|
// Principal is the server-computed identity of the caller. Can be nil when not computed.
|
|
Principal *commonpb.Principal
|
|
}
|
|
|
|
// Decision is enum type for auth decision
|
|
Decision int
|
|
)
|
|
|
|
// @@@SNIPSTART temporal-common-authorization-authorizer-interface
|
|
// Authorizer is an interface for implementing authorization logic
|
|
type Authorizer interface {
|
|
Authorize(ctx context.Context, caller *Claims, target *CallTarget) (Result, error)
|
|
}
|
|
|
|
// @@@SNIPEND
|
|
|
|
type hasNamespace interface {
|
|
GetNamespace() string
|
|
}
|
|
|
|
func GetAuthorizerFromConfig(config *config.Authorization) (Authorizer, error) {
|
|
|
|
switch strings.ToLower(config.Authorizer) {
|
|
case "":
|
|
return NewNoopAuthorizer(), nil
|
|
case "default":
|
|
return NewDefaultAuthorizer(), nil
|
|
}
|
|
return nil, fmt.Errorf("unknown authorizer: %s", config.Authorizer)
|
|
}
|
|
|
|
func IsNoopAuthorizer(authorizer Authorizer) bool {
|
|
_, ok := authorizer.(*noopAuthorizer)
|
|
return ok
|
|
}
|