From 5be322e2ef8d9c656d3677a7f13a7328f7041506 Mon Sep 17 00:00:00 2001 From: Caleb Doxsey Date: Thu, 1 Jun 2023 16:00:02 -0600 Subject: [PATCH] config: add support for $pomerium.id_token and $pomerium.access_token in set_request_headers (#4219) * config: add support for $pomerium.id_token and $pomerium.access_token in set_request_headers * lint * Update authorize/evaluator/headers_evaluator_test.go Co-authored-by: Denis Mishin * fix spelling --------- Co-authored-by: Denis Mishin --- authorize/evaluator/headers_evaluator.go | 18 ++-- authorize/evaluator/headers_evaluator_test.go | 48 +++++++++ authorize/evaluator/opa/policy/headers.rego | 102 +++++++----------- config/envoyconfig/routes.go | 1 - config/envoyconfig/routes_test.go | 7 -- config/policy.go | 5 + 6 files changed, 104 insertions(+), 77 deletions(-) diff --git a/authorize/evaluator/headers_evaluator.go b/authorize/evaluator/headers_evaluator.go index afb56248e..8b6dbccf3 100644 --- a/authorize/evaluator/headers_evaluator.go +++ b/authorize/evaluator/headers_evaluator.go @@ -17,14 +17,15 @@ import ( // HeadersRequest is the input to the headers.rego script. type HeadersRequest struct { - EnableGoogleCloudServerlessAuthentication bool `json:"enable_google_cloud_serverless_authentication"` - EnableRoutingKey bool `json:"enable_routing_key"` - Issuer string `json:"issuer"` - KubernetesServiceAccountToken string `json:"kubernetes_service_account_token"` - ToAudience string `json:"to_audience"` - Session RequestSession `json:"session"` - PassAccessToken bool `json:"pass_access_token"` - PassIDToken bool `json:"pass_id_token"` + EnableGoogleCloudServerlessAuthentication bool `json:"enable_google_cloud_serverless_authentication"` + EnableRoutingKey bool `json:"enable_routing_key"` + Issuer string `json:"issuer"` + KubernetesServiceAccountToken string `json:"kubernetes_service_account_token"` + ToAudience string `json:"to_audience"` + Session RequestSession `json:"session"` + PassAccessToken bool `json:"pass_access_token"` + PassIDToken bool `json:"pass_id_token"` + SetRequestHeaders map[string]string `json:"set_request_headers"` } // NewHeadersRequestFromPolicy creates a new HeadersRequest from a policy. @@ -41,6 +42,7 @@ func NewHeadersRequestFromPolicy(policy *config.Policy, hostname string) *Header } input.PassAccessToken = policy.GetSetAuthorizationHeader() == configpb.Route_ACCESS_TOKEN input.PassIDToken = policy.GetSetAuthorizationHeader() == configpb.Route_ID_TOKEN + input.SetRequestHeaders = policy.SetRequestHeaders } return input } diff --git a/authorize/evaluator/headers_evaluator_test.go b/authorize/evaluator/headers_evaluator_test.go index 5891d50b0..c4228a23d 100644 --- a/authorize/evaluator/headers_evaluator_test.go +++ b/authorize/evaluator/headers_evaluator_test.go @@ -139,4 +139,52 @@ func TestHeadersEvaluator(t *testing.T) { assert.Equal(t, "Bearer ID_TOKEN", output.Headers.Get("Authorization")) }) + + t.Run("set_request_headers", func(t *testing.T) { + output, err := eval(t, + []proto.Message{ + &session.Session{Id: "s1", IdToken: &session.IDToken{ + Raw: "ID_TOKEN", + }, OauthToken: &session.OAuthToken{ + AccessToken: "ACCESS_TOKEN", + }}, + }, + &HeadersRequest{ + Issuer: "from.example.com", + ToAudience: "to.example.com", + Session: RequestSession{ID: "s1"}, + SetRequestHeaders: map[string]string{ + "X-Custom-Header": "CUSTOM_VALUE", + "X-ID-Token": "$pomerium.id_token", + "X-Access-Token": "$pomerium.access_token", + }, + }) + require.NoError(t, err) + + assert.Equal(t, "CUSTOM_VALUE", output.Headers.Get("X-Custom-Header")) + assert.Equal(t, "ID_TOKEN", output.Headers.Get("X-ID-Token")) + assert.Equal(t, "ACCESS_TOKEN", output.Headers.Get("X-Access-Token")) + }) + + t.Run("set_request_headers original behavior", func(t *testing.T) { + output, err := eval(t, + []proto.Message{ + &session.Session{Id: "s1", IdToken: &session.IDToken{ + Raw: "ID_TOKEN", + }, OauthToken: &session.OAuthToken{ + AccessToken: "ACCESS_TOKEN", + }}, + }, + &HeadersRequest{ + Issuer: "from.example.com", + ToAudience: "to.example.com", + Session: RequestSession{ID: "s1"}, + SetRequestHeaders: map[string]string{ + "Authorization": "Bearer $pomerium.id_token", + }, + }) + require.NoError(t, err) + + assert.Equal(t, "Bearer ID_TOKEN", output.Headers.Get("Authorization")) + }) } diff --git a/authorize/evaluator/opa/policy/headers.rego b/authorize/evaluator/opa/policy/headers.rego index 4b0590ab4..661e5f505 100644 --- a/authorize/evaluator/opa/policy/headers.rego +++ b/authorize/evaluator/opa/policy/headers.rego @@ -10,6 +10,7 @@ package pomerium.headers # to_audience: string # pass_access_token: boolean # pass_id_token: boolean +# set_request_headers: map[string]string # # data: # jwt_claim_headers: map[string]string @@ -46,30 +47,22 @@ session = v { v = get_databroker_record("type.googleapis.com/session.Session", input.session.id) v != null object.get(v, "impersonate_session_id", "") == "" -} else = {} { - true -} +} else = {} user = u { u = get_databroker_record("type.googleapis.com/user.User", session.user_id) u != null -} else = {} { - true -} +} else = {} directory_user = du { du = get_databroker_record("pomerium.io/DirectoryUser", session.user_id) du != null -} else = {} { - true -} +} else = {} group_ids = gs { gs = directory_user.group_ids gs != null -} else = [] { - true -} +} else = [] groups := array.concat(group_ids, array.concat(get_databroker_group_names(group_ids), get_databroker_group_emails(group_ids))) @@ -81,29 +74,21 @@ jwt_headers = { jwt_payload_aud = v { v := input.issuer -} else = "" { - true -} +} else = "" jwt_payload_iss = v { v := input.issuer -} else = "" { - true -} +} else = "" jwt_payload_jti = v { v = session.id -} else = "" { - true -} +} else = "" jwt_payload_exp = v { v = min([five_minutes, round(session.expires_at.seconds)]) } else = v { v = five_minutes -} else = null { - true -} +} else = null jwt_payload_iat = v { # sessions store the issued_at on the id_token @@ -111,29 +96,21 @@ jwt_payload_iat = v { } else = v { # service accounts store the issued at directly v = round(session.issued_at.seconds) -} else = null { - true -} +} else = null jwt_payload_sub = v { v = session.user_id -} else = "" { - true -} +} else = "" jwt_payload_user = v { v = session.user_id -} else = "" { - true -} +} else = "" jwt_payload_email = v { v = directory_user.email } else = v { v = user.email -} else = "" { - true -} +} else = "" jwt_payload_groups = v { v = array.concat(group_ids, get_databroker_group_names(group_ids)) @@ -141,17 +118,13 @@ jwt_payload_groups = v { } else = v { v = session.claims.groups v != null -} else = [] { - true -} +} else = [] jwt_payload_name = v { v = get_header_string_value(session.claims.name) } else = v { v = get_header_string_value(user.claims.name) -} else = "" { - true -} +} else = "" # the session id is always set to the input session id, even if impersonating jwt_payload_sid := input.session.id @@ -204,43 +177,49 @@ kubernetes_headers = h { ["Impersonate-User", jwt_payload_email], ["Impersonate-Group", get_header_string_value(jwt_payload_groups)], ] -} else = [] { - true -} +} else = [] google_cloud_serverless_authentication_service_account = s { s := data.google_cloud_serverless_authentication_service_account -} else = "" { - true -} +} else = "" google_cloud_serverless_headers = h { input.enable_google_cloud_serverless_authentication h := get_google_cloud_serverless_headers(google_cloud_serverless_authentication_service_account, input.to_audience) -} else = {} { - true -} +} else = {} routing_key_headers = h { input.enable_routing_key h := [["x-pomerium-routing-key", crypto.sha256(input.session.id)]] -} else = [] { - true -} +} else = [] pass_access_token_headers = h { input.pass_access_token h := [["Authorization", concat(" ", ["Bearer", session.oauth_token.access_token])]] -} else = [] { - true -} +} else = [] pass_id_token_headers = h { input.pass_id_token h := [["Authorization", concat(" ", ["Bearer", session.id_token.raw])]] -} else = [] { - true -} +} else = [] + +session_id_token = v { + v := session.id_token.raw +} else = "" + +session_access_token = v { + v := session.oauth_token.access_token +} else = "" + +set_request_headers = h { + h := [[header_name, header_value] | + some header_name + v1 := input.set_request_headers[header_name] + v2 := replace(v1, "$pomerium.id_token", session_id_token) + v3 := replace(v2, "$pomerium.access_token", session_access_token) + header_value := v3 + ] +} else = [] identity_headers := {key: values | h1 := [["x-pomerium-jwt-assertion", signed_jwt]] @@ -263,8 +242,9 @@ identity_headers := {key: values | h5 := routing_key_headers h6 := pass_access_token_headers h7 := pass_id_token_headers + h8 := set_request_headers - h := array.concat(array.concat(array.concat(array.concat(array.concat(array.concat(h1, h2), h3), h4), h5), h6), h7) + h := array.concat(array.concat(array.concat(array.concat(array.concat(array.concat(array.concat(h1, h2), h3), h4), h5), h6), h7), h8) some i [key, v1] := h[i] diff --git a/config/envoyconfig/routes.go b/config/envoyconfig/routes.go index 555dbff7f..abef68d02 100644 --- a/config/envoyconfig/routes.go +++ b/config/envoyconfig/routes.go @@ -277,7 +277,6 @@ func (b *Builder) buildRouteForPolicyAndMatch( Name: name, Match: match, Metadata: &envoy_config_core_v3.Metadata{}, - RequestHeadersToAdd: toEnvoyHeaders(policy.SetRequestHeaders), RequestHeadersToRemove: getRequestHeadersToRemove(cfg.Options, policy), ResponseHeadersToAdd: toEnvoyHeaders(cfg.Options.GetSetResponseHeadersForPolicy(policy)), } diff --git a/config/envoyconfig/routes_test.go b/config/envoyconfig/routes_test.go index b59f2fb76..dc9c9ce0f 100644 --- a/config/envoyconfig/routes_test.go +++ b/config/envoyconfig/routes_test.go @@ -609,13 +609,6 @@ func Test_buildPolicyRoutes(t *testing.T) { { "enabled": false, "upgradeType": "spdy/3.1"} ] }, - "requestHeadersToAdd": [{ - "appendAction": "OVERWRITE_IF_EXISTS_OR_ADD", - "header": { - "key": "HEADER-KEY", - "value": "HEADER-VALUE" - } - }], "requestHeadersToRemove": [ "x-pomerium-reproxy-policy", "x-pomerium-reproxy-policy-hmac" diff --git a/config/policy.go b/config/policy.go index 9258cb523..73618068f 100644 --- a/config/policy.go +++ b/config/policy.go @@ -542,6 +542,11 @@ func (p *Policy) Validate() error { return fmt.Errorf("config: invalid policy set_authorization_header: %v", p.SetAuthorizationHeader) } + if p.SetAuthorizationHeader != "" { + log.Warn(context.Background()).Msg("config: set_authorization_header is deprecated, " + + "use $pomerium.id_token or $pomerium.access_token in set_request_headers instead") + } + return nil }