config: support redirect actions (#1776)

* add route redirect options

* add xds support for redirect

* add test

* handle nil destinations

* remove unchanged statik files

* remove unchanged statik files

* update docs

* Update docs/reference/settings.yaml

Co-authored-by: Travis Groth <travisgroth@users.noreply.github.com>

Co-authored-by: Travis Groth <travisgroth@users.noreply.github.com>
This commit is contained in:
Caleb Doxsey 2021-01-14 16:18:27 -07:00 committed by GitHub
parent 6466efddd5
commit c99994bed8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 968 additions and 527 deletions

View file

@ -25,6 +25,10 @@ import (
type Policy struct {
From string `mapstructure:"from" yaml:"from"`
To string `mapstructure:"to" yaml:"to"`
// Redirect is used for a redirect action instead of `To`
Redirect *PolicyRedirect `mapstructure:"redirect" yaml:"redirect"`
// Identity related policy
AllowedUsers []string `mapstructure:"allowed_users" yaml:"allowed_users,omitempty" json:"allowed_users,omitempty"`
AllowedGroups []string `mapstructure:"allowed_groups" yaml:"allowed_groups,omitempty" json:"allowed_groups,omitempty"`
@ -147,6 +151,18 @@ type SubPolicy struct {
Rego []string `mapstructure:"rego" yaml:"rego" json:"rego,omitempty"`
}
// PolicyRedirect is a route redirect action.
type PolicyRedirect struct {
HTTPSRedirect *bool `mapstructure:"https_redirect" yaml:"https_redirect,omitempty" json:"https_redirect,omitempty"`
SchemeRedirect *string `mapstructure:"scheme_redirect" yaml:"scheme_redirect,omitempty" json:"scheme_redirect,omitempty"`
HostRedirect *string `mapstructure:"host_redirect" yaml:"host_redirect,omitempty" json:"host_redirect,omitempty"`
PortRedirect *uint32 `mapstructure:"port_redirect" yaml:"port_redirect,omitempty" json:"port_redirect,omitempty"`
PathRedirect *string `mapstructure:"path_redirect" yaml:"path_redirect,omitempty" json:"path_redirect,omitempty"`
PrefixRewrite *string `mapstructure:"prefix_rewrite" yaml:"prefix_rewrite,omitempty" json:"prefix_rewrite,omitempty"`
ResponseCode *int32 `mapstructure:"response_code" yaml:"response_code,omitempty" json:"response_code,omitempty"`
StripQuery *bool `mapstructure:"strip_query" yaml:"strip_query,omitempty" json:"strip_query,omitempty"`
}
// NewPolicyFromProto creates a new Policy from a protobuf policy config route.
func NewPolicyFromProto(pb *configpb.Route) (*Policy, error) {
timeout, _ := ptypes.Duration(pb.GetTimeout())
@ -183,6 +199,18 @@ func NewPolicyFromProto(pb *configpb.Route) (*Policy, error) {
PassIdentityHeaders: pb.GetPassIdentityHeaders(),
KubernetesServiceAccountToken: pb.GetKubernetesServiceAccountToken(),
}
if pb.Redirect != nil {
p.Redirect = &PolicyRedirect{
HTTPSRedirect: pb.Redirect.HttpsRedirect,
SchemeRedirect: pb.Redirect.SchemeRedirect,
HostRedirect: pb.Redirect.HostRedirect,
PortRedirect: pb.Redirect.PortRedirect,
PathRedirect: pb.Redirect.PathRedirect,
PrefixRewrite: pb.Redirect.PrefixRewrite,
ResponseCode: pb.Redirect.ResponseCode,
StripQuery: pb.Redirect.StripQuery,
}
}
for _, sp := range pb.GetPolicies() {
p.SubPolicies = append(p.SubPolicies, SubPolicy{
ID: sp.GetId(),
@ -212,7 +240,7 @@ func (p *Policy) ToProto() *configpb.Route {
Rego: sp.Rego,
})
}
return &configpb.Route{
pb := &configpb.Route{
Name: fmt.Sprint(p.RouteID()),
From: p.From,
To: p.To,
@ -246,6 +274,19 @@ func (p *Policy) ToProto() *configpb.Route {
KubernetesServiceAccountToken: p.KubernetesServiceAccountToken,
Policies: sps,
}
if p.Redirect != nil {
pb.Redirect = &configpb.RouteRedirect{
HttpsRedirect: p.Redirect.HTTPSRedirect,
SchemeRedirect: p.Redirect.SchemeRedirect,
HostRedirect: p.Redirect.HostRedirect,
PortRedirect: p.Redirect.PortRedirect,
PathRedirect: p.Redirect.PathRedirect,
PrefixRewrite: p.Redirect.PrefixRewrite,
ResponseCode: p.Redirect.ResponseCode,
StripQuery: p.Redirect.StripQuery,
}
}
return pb
}
// Validate checks the validity of a policy.
@ -264,9 +305,15 @@ func (p *Policy) Validate() error {
p.Source = &StringURL{source}
p.Destination, err = urlutil.ParseAndValidateURL(p.To)
if err != nil {
return fmt.Errorf("config: policy bad destination url %w", err)
switch {
case p.To != "":
p.Destination, err = urlutil.ParseAndValidateURL(p.To)
if err != nil {
return fmt.Errorf("config: policy bad destination url %w", err)
}
case p.Redirect != nil:
default:
return fmt.Errorf("config: policy must have either a `to` or `redirect`")
}
// Only allow public access if no other whitelists are in place

View file

@ -1221,11 +1221,33 @@ Remove Request Headers allows you to remove given request headers. This can be u
```
### Redirect
- `yaml`/`json` setting: 'redirect'
- Type: object
- Optional
- Example: `{ "host_redirect": "example.com" }`
`Redirect` is used to redirect incoming requests to a new URL. The `redirect` field is an object with several optional,
options:
- `https_redirect` (boolean): the incoming scheme will be swapped with "https".
- `scheme_redirect` (string): the incoming scheme will be swapped with the given value.
- `host_redirect` (string): the incoming host will be swapped with the given value.
- `port_redirect` (integer): the incoming port will be swapped with the given value.
- `path_redirect` (string): the incoming path portion of the URL will be swapped with the given value.
- `prefix_rewrite` (string): the incoming matched prefix will be swapped with the given value.
- `regex_rewrite_pattern`, `regex_rewrite_substitution` (string): the incoming matched regex will be swapped with this value.
- `response_code` (integer): the response code to use for the redirect. Defaults to 301.
- `strip_query` (boolean): indicates that during redirection, the query portion of the URL will be removed. Defaults to false.
Either `redirect` or `to` must be set.
### To
- `yaml`/`json` setting: `to`
- Type: `URL` (must contain a scheme and hostname)
- Schemes: `http`, `https`, `tcp`
- Required
- Optional
- Example: `http://verify` , `https://192.1.20.12:8080`, `http://neverssl.com`, `https://verify.pomerium.com/anything/`
`To` is the destination of a proxied request. It can be an internal resource, or an external resource.
@ -1254,6 +1276,8 @@ While the rule:
All requests to `https://verify.corp.example.com/*` will be forwarded to `https://verify.pomerium.com/anything/*`. That means accessing to `https://verify.corp.example.com` will be forwarded to `https://verify.pomerium.com/anything/`. That said, if your application does not handle trailing slash, the request will end up with 404 not found.
Either `redirect` or `to` must be set.
:::

View file

@ -1345,13 +1345,34 @@ settings:
- X-Email
- X-Username
```
- name: "Redirect"
keys: ["redirect"]
attributes: |
- `yaml`/`json` setting: 'redirect'
- Type: object
- Optional
- Example: `{ "host_redirect": "example.com" }`
doc: |
`Redirect` is used to redirect incoming requests to a new URL. The `redirect` field is an object with several possible
options:
- `https_redirect` (boolean): the incoming scheme will be swapped with "https".
- `scheme_redirect` (string): the incoming scheme will be swapped with the given value.
- `host_redirect` (string): the incoming host will be swapped with the given value.
- `port_redirect` (integer): the incoming port will be swapped with the given value.
- `path_redirect` (string): the incoming path portion of the URL will be swapped with the given value.
- `prefix_rewrite` (string): the incoming matched prefix will be swapped with the given value.
- `response_code` (integer): the response code to use for the redirect. Defaults to 301.
- `strip_query` (boolean): indicates that during redirection, the query portion of the URL will be removed. Defaults to false.
Either `redirect` or `to` must be set.
- name: "To"
keys: ["to"]
attributes: |
- `yaml`/`json` setting: `to`
- Type: `URL` (must contain a scheme and hostname)
- Schemes: `http`, `https`, `tcp`
- Required
- Optional
- Example: `http://verify` , `https://192.1.20.12:8080`, `http://neverssl.com`, `https://verify.pomerium.com/anything/`
doc: |
`To` is the destination of a proxied request. It can be an internal resource, or an external resource.
@ -1380,6 +1401,8 @@ settings:
All requests to `https://verify.corp.example.com/*` will be forwarded to `https://verify.pomerium.com/anything/*`. That means accessing to `https://verify.corp.example.com` will be forwarded to `https://verify.pomerium.com/anything/`. That said, if your application does not handle trailing slash, the request will end up with 404 not found.
Either `redirect` or `to` must be set.
:::
- name: "TLS Skip Verification"
keys: ["tls_skip_verify"]

View file

@ -43,7 +43,9 @@ func (srv *Server) buildClusters(options *config.Options) []*envoy_config_cluste
if config.IsProxy(options.Services) {
for i := range options.Policies {
policy := options.Policies[i]
clusters = append(clusters, buildPolicyCluster(options, &policy))
if policy.Destination != nil {
clusters = append(clusters, buildPolicyCluster(options, &policy))
}
}
}
@ -114,7 +116,7 @@ func buildInternalTransportSocket(options *config.Options, endpoint *url.URL) *e
}
func buildPolicyTransportSocket(policy *config.Policy) *envoy_config_core_v3.TransportSocket {
if policy.Destination.Scheme != "https" {
if policy.Destination == nil || policy.Destination.Scheme != "https" {
return nil
}
@ -154,6 +156,10 @@ func buildPolicyTransportSocket(policy *config.Policy) *envoy_config_core_v3.Tra
}
func buildPolicyValidationContext(policy *config.Policy) *envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext {
if policy.Destination == nil {
return nil
}
sni := policy.Destination.Hostname()
if policy.TLSServerName != "" {
sni = policy.TLSServerName
@ -196,6 +202,10 @@ func buildCluster(
forceHTTP2 bool,
dnsLookupFamily envoy_config_cluster_v3.Cluster_DnsLookupFamily,
) *envoy_config_cluster_v3.Cluster {
if endpoint == nil {
return nil
}
defaultPort := 80
if transportSocket != nil && transportSocket.Name == "tls" {
defaultPort = 443

View file

@ -188,47 +188,10 @@ func buildPolicyRoutes(options *config.Options, domain string) []*envoy_config_r
}
match := mkRouteMatch(&policy)
clusterName := getPolicyName(&policy)
requestHeadersToAdd := toEnvoyHeaders(policy.SetRequestHeaders)
requestHeadersToRemove := getRequestHeadersToRemove(options, &policy)
routeTimeout := getRouteTimeout(options, &policy)
idleTimeout := getRouteIdleTimeout(&policy)
prefixRewrite, regexRewrite := getRewriteOptions(&policy)
upgradeConfigs := []*envoy_config_route_v3.RouteAction_UpgradeConfig{
{
UpgradeType: "websocket",
Enabled: &wrappers.BoolValue{Value: policy.AllowWebsockets},
},
{
UpgradeType: "spdy/3.1",
Enabled: &wrappers.BoolValue{Value: policy.AllowSPDY},
},
}
if urlutil.IsTCP(policy.Source.URL) {
upgradeConfigs = append(upgradeConfigs, &envoy_config_route_v3.RouteAction_UpgradeConfig{
UpgradeType: "CONNECT",
Enabled: &wrappers.BoolValue{Value: true},
ConnectConfig: &envoy_config_route_v3.RouteAction_UpgradeConfig_ConnectConfig{},
})
}
routeAction := &envoy_config_route_v3.RouteAction{
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
Cluster: clusterName,
},
UpgradeConfigs: upgradeConfigs,
HostRewriteSpecifier: &envoy_config_route_v3.RouteAction_AutoHostRewrite{
AutoHostRewrite: &wrappers.BoolValue{Value: !policy.PreserveHostHeader},
},
Timeout: routeTimeout,
IdleTimeout: idleTimeout,
PrefixRewrite: prefixRewrite,
RegexRewrite: regexRewrite,
}
setHostRewriteOptions(&policy, routeAction)
routes = append(routes, &envoy_config_route_v3.Route{
envoyRoute := &envoy_config_route_v3.Route{
Name: fmt.Sprintf("policy-%d", i),
Match: match,
Metadata: &envoy_config_core_v3.Metadata{
@ -254,15 +217,99 @@ func buildPolicyRoutes(options *config.Options, domain string) []*envoy_config_r
},
},
},
Action: &envoy_config_route_v3.Route_Route{Route: routeAction},
RequestHeadersToAdd: requestHeadersToAdd,
RequestHeadersToRemove: requestHeadersToRemove,
ResponseHeadersToAdd: responseHeadersToAdd,
})
}
if policy.Redirect != nil {
envoyRoute.Action = &envoy_config_route_v3.Route_Redirect{
Redirect: buildPolicyRouteRedirectAction(policy.Redirect),
}
} else {
envoyRoute.Action = &envoy_config_route_v3.Route_Route{Route: buildPolicyRouteRouteAction(options, &policy)}
}
routes = append(routes, envoyRoute)
}
return routes
}
func buildPolicyRouteRedirectAction(r *config.PolicyRedirect) *envoy_config_route_v3.RedirectAction {
action := &envoy_config_route_v3.RedirectAction{}
switch {
case r.HTTPSRedirect != nil:
action.SchemeRewriteSpecifier = &envoy_config_route_v3.RedirectAction_HttpsRedirect{
HttpsRedirect: *r.HTTPSRedirect,
}
case r.SchemeRedirect != nil:
action.SchemeRewriteSpecifier = &envoy_config_route_v3.RedirectAction_SchemeRedirect{
SchemeRedirect: *r.SchemeRedirect,
}
}
if r.HostRedirect != nil {
action.HostRedirect = *r.HostRedirect
}
if r.PortRedirect != nil {
action.PortRedirect = *r.PortRedirect
}
switch {
case r.PathRedirect != nil:
action.PathRewriteSpecifier = &envoy_config_route_v3.RedirectAction_PathRedirect{
PathRedirect: *r.PathRedirect,
}
case r.PrefixRewrite != nil:
action.PathRewriteSpecifier = &envoy_config_route_v3.RedirectAction_PrefixRewrite{
PrefixRewrite: *r.PrefixRewrite,
}
}
if r.ResponseCode != nil {
action.ResponseCode = envoy_config_route_v3.RedirectAction_RedirectResponseCode(*r.ResponseCode)
}
if r.StripQuery != nil {
action.StripQuery = *r.StripQuery
}
return action
}
func buildPolicyRouteRouteAction(options *config.Options, policy *config.Policy) *envoy_config_route_v3.RouteAction {
clusterName := getPolicyName(policy)
routeTimeout := getRouteTimeout(options, policy)
idleTimeout := getRouteIdleTimeout(policy)
prefixRewrite, regexRewrite := getRewriteOptions(policy)
upgradeConfigs := []*envoy_config_route_v3.RouteAction_UpgradeConfig{
{
UpgradeType: "websocket",
Enabled: &wrappers.BoolValue{Value: policy.AllowWebsockets},
},
{
UpgradeType: "spdy/3.1",
Enabled: &wrappers.BoolValue{Value: policy.AllowSPDY},
},
}
if urlutil.IsTCP(policy.Source.URL) {
upgradeConfigs = append(upgradeConfigs, &envoy_config_route_v3.RouteAction_UpgradeConfig{
UpgradeType: "CONNECT",
Enabled: &wrappers.BoolValue{Value: true},
ConnectConfig: &envoy_config_route_v3.RouteAction_UpgradeConfig_ConnectConfig{},
})
}
action := &envoy_config_route_v3.RouteAction{
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
Cluster: clusterName,
},
UpgradeConfigs: upgradeConfigs,
HostRewriteSpecifier: &envoy_config_route_v3.RouteAction_AutoHostRewrite{
AutoHostRewrite: &wrappers.BoolValue{Value: !policy.PreserveHostHeader},
},
Timeout: routeTimeout,
IdleTimeout: idleTimeout,
PrefixRewrite: prefixRewrite,
RegexRewrite: regexRewrite,
}
setHostRewriteOptions(policy, action)
return action
}
func mkEnvoyHeader(k, v string) *envoy_config_core_v3.HeaderValueOption {
return &envoy_config_core_v3.HeaderValueOption{
Header: &envoy_config_core_v3.HeaderValue{

View file

@ -5,6 +5,10 @@ import (
"testing"
"time"
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/testutil"
)
@ -816,3 +820,87 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) {
]
`, routes)
}
func Test_buildPolicyRouteRedirectAction(t *testing.T) {
t.Run("HTTPSRedirect", func(t *testing.T) {
action := buildPolicyRouteRedirectAction(&config.PolicyRedirect{
HTTPSRedirect: proto.Bool(true),
})
assert.Equal(t, &envoy_config_route_v3.RedirectAction{
SchemeRewriteSpecifier: &envoy_config_route_v3.RedirectAction_HttpsRedirect{
HttpsRedirect: true,
},
}, action)
action = buildPolicyRouteRedirectAction(&config.PolicyRedirect{
HTTPSRedirect: proto.Bool(false),
})
assert.Equal(t, &envoy_config_route_v3.RedirectAction{
SchemeRewriteSpecifier: &envoy_config_route_v3.RedirectAction_HttpsRedirect{
HttpsRedirect: false,
},
}, action)
})
t.Run("SchemeRedirect", func(t *testing.T) {
action := buildPolicyRouteRedirectAction(&config.PolicyRedirect{
SchemeRedirect: proto.String("https"),
})
assert.Equal(t, &envoy_config_route_v3.RedirectAction{
SchemeRewriteSpecifier: &envoy_config_route_v3.RedirectAction_SchemeRedirect{
SchemeRedirect: "https",
},
}, action)
})
t.Run("HostRedirect", func(t *testing.T) {
action := buildPolicyRouteRedirectAction(&config.PolicyRedirect{
HostRedirect: proto.String("HOST"),
})
assert.Equal(t, &envoy_config_route_v3.RedirectAction{
HostRedirect: "HOST",
}, action)
})
t.Run("PortRedirect", func(t *testing.T) {
action := buildPolicyRouteRedirectAction(&config.PolicyRedirect{
PortRedirect: proto.Uint32(1234),
})
assert.Equal(t, &envoy_config_route_v3.RedirectAction{
PortRedirect: 1234,
}, action)
})
t.Run("PathRedirect", func(t *testing.T) {
action := buildPolicyRouteRedirectAction(&config.PolicyRedirect{
PathRedirect: proto.String("PATH"),
})
assert.Equal(t, &envoy_config_route_v3.RedirectAction{
PathRewriteSpecifier: &envoy_config_route_v3.RedirectAction_PathRedirect{
PathRedirect: "PATH",
},
}, action)
})
t.Run("PrefixRewrite", func(t *testing.T) {
action := buildPolicyRouteRedirectAction(&config.PolicyRedirect{
PrefixRewrite: proto.String("PREFIX_REWRITE"),
})
assert.Equal(t, &envoy_config_route_v3.RedirectAction{
PathRewriteSpecifier: &envoy_config_route_v3.RedirectAction_PrefixRewrite{
PrefixRewrite: "PREFIX_REWRITE",
},
}, action)
})
t.Run("ResponseCode", func(t *testing.T) {
action := buildPolicyRouteRedirectAction(&config.PolicyRedirect{
ResponseCode: proto.Int32(301),
})
assert.Equal(t, &envoy_config_route_v3.RedirectAction{
ResponseCode: 301,
}, action)
})
t.Run("StripQuery", func(t *testing.T) {
action := buildPolicyRouteRedirectAction(&config.PolicyRedirect{
StripQuery: proto.Bool(true),
})
assert.Equal(t, &envoy_config_route_v3.RedirectAction{
StripQuery: true,
}, action)
})
}

File diff suppressed because it is too large Load diff

View file

@ -12,12 +12,25 @@ message Config {
Settings settings = 3;
}
message RouteRedirect {
optional bool https_redirect = 1;
optional string scheme_redirect = 2;
optional string host_redirect = 3;
optional uint32 port_redirect = 4;
optional string path_redirect = 5;
optional string prefix_rewrite = 6;
optional int32 response_code = 7;
optional bool strip_query = 8;
}
message Route {
string name = 1;
string from = 2;
string to = 3;
RouteRedirect redirect = 34;
repeated string allowed_users = 4 [ deprecated = true ];
repeated string allowed_groups = 5 [ deprecated = true ];
repeated string allowed_domains = 6 [ deprecated = true ];