diff --git a/integration/control_plane_test.go b/integration/control_plane_test.go index 875d357e1..3b2b4e927 100644 --- a/integration/control_plane_test.go +++ b/integration/control_plane_test.go @@ -8,6 +8,8 @@ import ( "time" "github.com/stretchr/testify/assert" + + "github.com/pomerium/pomerium/integration/internal/flows" ) func TestDashboard(t *testing.T) { @@ -15,6 +17,28 @@ func TestDashboard(t *testing.T) { ctx, clearTimeout := context.WithTimeout(ctx, time.Second*30) defer clearTimeout() + t.Run("admin impersonate", func(t *testing.T) { + client := testcluster.NewHTTPClient() + + res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"), + flows.WithEmail("bob@dogs.test"), flows.WithGroups("user")) + if !assert.NoError(t, err) { + return + } + + req, err := http.NewRequestWithContext(ctx, "GET", "https://httpdetails.localhost.pomerium.io/.pomerium/admin/impersonate", nil) + if err != nil { + t.Fatal(err) + } + + res, err = client.Do(req) + if !assert.NoError(t, err, "unexpected http error") { + return + } + defer res.Body.Close() + + assertDeniedAccess(t, res) + }) t.Run("user dashboard", func(t *testing.T) { client := testcluster.NewHTTPClient() diff --git a/internal/controlplane/xds_listeners_test.go b/internal/controlplane/xds_listeners_test.go index 314a0b117..d070de34e 100644 --- a/internal/controlplane/xds_listeners_test.go +++ b/internal/controlplane/xds_listeners_test.go @@ -94,7 +94,7 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) { "domains": ["example.com"], "routes": [ { - "name": "pomerium-protected-path-/.pomerium/jwt", + "name": "pomerium-path-/.pomerium/jwt", "match": { "path": "/.pomerium/jwt" }, @@ -132,6 +132,24 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) { } } }, + { + "name": "pomerium-path-/.pomerium/admin", + "match": { + "path": "/.pomerium/admin" + }, + "route": { + "cluster": "pomerium-control-plane-http" + } + }, + { + "name": "pomerium-prefix-/.pomerium/admin/", + "match": { + "prefix": "/.pomerium/admin/" + }, + "route": { + "cluster": "pomerium-control-plane-http" + } + }, { "name": "pomerium-path-/.pomerium", "match": { @@ -214,7 +232,7 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) { "domains": ["*"], "routes": [ { - "name": "pomerium-protected-path-/.pomerium/jwt", + "name": "pomerium-path-/.pomerium/jwt", "match": { "path": "/.pomerium/jwt" }, @@ -252,6 +270,24 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) { } } }, + { + "name": "pomerium-path-/.pomerium/admin", + "match": { + "path": "/.pomerium/admin" + }, + "route": { + "cluster": "pomerium-control-plane-http" + } + }, + { + "name": "pomerium-prefix-/.pomerium/admin/", + "match": { + "prefix": "/.pomerium/admin/" + }, + "route": { + "cluster": "pomerium-control-plane-http" + } + }, { "name": "pomerium-path-/.pomerium", "match": { diff --git a/internal/controlplane/xds_routes.go b/internal/controlplane/xds_routes.go index 12a58f8b8..a6b066335 100644 --- a/internal/controlplane/xds_routes.go +++ b/internal/controlplane/xds_routes.go @@ -49,22 +49,24 @@ func buildGRPCRoutes() []*envoy_config_route_v3.Route { func buildPomeriumHTTPRoutes(options *config.Options, domain string) []*envoy_config_route_v3.Route { routes := []*envoy_config_route_v3.Route{ // enable ext_authz - buildControlPlaneProtectedPathRoute("/.pomerium/jwt"), + buildControlPlanePathRoute("/.pomerium/jwt", true), // disable ext_authz and passthrough to proxy handlers - buildControlPlanePathRoute("/ping"), - buildControlPlanePathRoute("/healthz"), - buildControlPlanePathRoute("/.pomerium"), - buildControlPlanePrefixRoute("/.pomerium/"), - buildControlPlanePathRoute("/.well-known/pomerium"), - buildControlPlanePrefixRoute("/.well-known/pomerium/"), + buildControlPlanePathRoute("/ping", false), + buildControlPlanePathRoute("/healthz", false), + buildControlPlanePathRoute("/.pomerium/admin", true), + buildControlPlanePrefixRoute("/.pomerium/admin/", true), + buildControlPlanePathRoute("/.pomerium", false), + buildControlPlanePrefixRoute("/.pomerium/", false), + buildControlPlanePathRoute("/.well-known/pomerium", false), + buildControlPlanePrefixRoute("/.well-known/pomerium/", false), } // per #837, only add robots.txt if there are no unauthenticated routes if !hasPublicPolicyMatchingURL(options, mustParseURL("https://"+domain+"/robots.txt")) { - routes = append(routes, buildControlPlanePathRoute("/robots.txt")) + routes = append(routes, buildControlPlanePathRoute("/robots.txt", false)) } // if we're handling authentication, add the oauth2 callback url if config.IsAuthenticate(options.Services) && hostMatchesDomain(options.GetAuthenticateURL(), domain) { - routes = append(routes, buildControlPlanePathRoute(options.AuthenticateCallbackPath)) + routes = append(routes, buildControlPlanePathRoute(options.AuthenticateCallbackPath, false)) } // if we're the proxy and this is the forward-auth url if config.IsProxy(options.Services) && options.ForwardAuthURL != nil && hostMatchesDomain(options.GetForwardAuthURL(), domain) { @@ -97,22 +99,6 @@ func buildControlPlaneProtectedPrefixRoute(prefix string) *envoy_config_route_v3 } } -func buildControlPlaneProtectedPathRoute(path string) *envoy_config_route_v3.Route { - return &envoy_config_route_v3.Route{ - Name: "pomerium-protected-path-" + path, - Match: &envoy_config_route_v3.RouteMatch{ - PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{Path: path}, - }, - Action: &envoy_config_route_v3.Route_Route{ - Route: &envoy_config_route_v3.RouteAction{ - ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{ - Cluster: httpCluster, - }, - }, - }, - } -} - func buildControlPlanePathAndQueryRoute(path string, queryparams []string) *envoy_config_route_v3.Route { var queryParameterMatchers []*envoy_config_route_v3.QueryParameterMatcher for _, q := range queryparams { @@ -142,8 +128,8 @@ func buildControlPlanePathAndQueryRoute(path string, queryparams []string) *envo } } -func buildControlPlanePathRoute(path string) *envoy_config_route_v3.Route { - return &envoy_config_route_v3.Route{ +func buildControlPlanePathRoute(path string, protected bool) *envoy_config_route_v3.Route { + r := &envoy_config_route_v3.Route{ Name: "pomerium-path-" + path, Match: &envoy_config_route_v3.RouteMatch{ PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{Path: path}, @@ -155,14 +141,17 @@ func buildControlPlanePathRoute(path string) *envoy_config_route_v3.Route { }, }, }, - TypedPerFilterConfig: map[string]*any.Any{ - "envoy.filters.http.ext_authz": disableExtAuthz, - }, } + if !protected { + r.TypedPerFilterConfig = map[string]*any.Any{ + "envoy.filters.http.ext_authz": disableExtAuthz, + } + } + return r } -func buildControlPlanePrefixRoute(prefix string) *envoy_config_route_v3.Route { - return &envoy_config_route_v3.Route{ +func buildControlPlanePrefixRoute(prefix string, protected bool) *envoy_config_route_v3.Route { + r := &envoy_config_route_v3.Route{ Name: "pomerium-prefix-" + prefix, Match: &envoy_config_route_v3.RouteMatch{ PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: prefix}, @@ -174,10 +163,13 @@ func buildControlPlanePrefixRoute(prefix string) *envoy_config_route_v3.Route { }, }, }, - TypedPerFilterConfig: map[string]*any.Any{ - "envoy.filters.http.ext_authz": disableExtAuthz, - }, } + if !protected { + r.TypedPerFilterConfig = map[string]*any.Any{ + "envoy.filters.http.ext_authz": disableExtAuthz, + } + } + return r } var getPolicyName = func(policy *config.Policy) string { diff --git a/internal/controlplane/xds_routes_test.go b/internal/controlplane/xds_routes_test.go index 5978e83ea..8aafe1456 100644 --- a/internal/controlplane/xds_routes_test.go +++ b/internal/controlplane/xds_routes_test.go @@ -42,33 +42,28 @@ func Test_buildGRPCRoutes(t *testing.T) { } func Test_buildPomeriumHTTPRoutes(t *testing.T) { - routeString := func(typ, name string) string { - return `{ + routeString := func(typ, name string, protected bool) string { + str := `{ "name": "pomerium-` + typ + `-` + name + `", "match": { "` + typ + `": "` + name + `" }, "route": { "cluster": "pomerium-control-plane-http" - }, + } + ` + if !protected { + str += `, "typedPerFilterConfig": { "envoy.filters.http.ext_authz": { "@type": "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute", "disabled": true } } - }` - } - protectedRouteString := func(typ, name string) string { - return `{ - "name": "pomerium-protected-` + typ + `-` + name + `", - "match": { - "` + typ + `": "` + name + `" - }, - "route": { - "cluster": "pomerium-control-plane-http" - } - }` + ` + } + str += "}" + return str } t.Run("authenticate", func(t *testing.T) { options := &config.Options{ @@ -80,15 +75,17 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) { routes := buildPomeriumHTTPRoutes(options, "authenticate.example.com") testutil.AssertProtoJSONEqual(t, `[ - `+protectedRouteString("path", "/.pomerium/jwt")+`, - `+routeString("path", "/ping")+`, - `+routeString("path", "/healthz")+`, - `+routeString("path", "/.pomerium")+`, - `+routeString("prefix", "/.pomerium/")+`, - `+routeString("path", "/.well-known/pomerium")+`, - `+routeString("prefix", "/.well-known/pomerium/")+`, - `+routeString("path", "/robots.txt")+`, - `+routeString("path", "/oauth2/callback")+` + `+routeString("path", "/.pomerium/jwt", true)+`, + `+routeString("path", "/ping", false)+`, + `+routeString("path", "/healthz", false)+`, + `+routeString("path", "/.pomerium/admin", true)+`, + `+routeString("prefix", "/.pomerium/admin/", true)+`, + `+routeString("path", "/.pomerium", false)+`, + `+routeString("prefix", "/.pomerium/", false)+`, + `+routeString("path", "/.well-known/pomerium", false)+`, + `+routeString("prefix", "/.well-known/pomerium/", false)+`, + `+routeString("path", "/robots.txt", false)+`, + `+routeString("path", "/oauth2/callback", false)+` ]`, routes) }) @@ -107,14 +104,16 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) { routes := buildPomeriumHTTPRoutes(options, "from.example.com") testutil.AssertProtoJSONEqual(t, `[ - `+protectedRouteString("path", "/.pomerium/jwt")+`, - `+routeString("path", "/ping")+`, - `+routeString("path", "/healthz")+`, - `+routeString("path", "/.pomerium")+`, - `+routeString("prefix", "/.pomerium/")+`, - `+routeString("path", "/.well-known/pomerium")+`, - `+routeString("prefix", "/.well-known/pomerium/")+`, - `+routeString("path", "/robots.txt")+` + `+routeString("path", "/.pomerium/jwt", true)+`, + `+routeString("path", "/ping", false)+`, + `+routeString("path", "/healthz", false)+`, + `+routeString("path", "/.pomerium/admin", true)+`, + `+routeString("prefix", "/.pomerium/admin/", true)+`, + `+routeString("path", "/.pomerium", false)+`, + `+routeString("prefix", "/.pomerium/", false)+`, + `+routeString("path", "/.well-known/pomerium", false)+`, + `+routeString("prefix", "/.well-known/pomerium/", false)+`, + `+routeString("path", "/robots.txt", false)+` ]`, routes) }) @@ -134,19 +133,21 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) { routes := buildPomeriumHTTPRoutes(options, "from.example.com") testutil.AssertProtoJSONEqual(t, `[ - `+protectedRouteString("path", "/.pomerium/jwt")+`, - `+routeString("path", "/ping")+`, - `+routeString("path", "/healthz")+`, - `+routeString("path", "/.pomerium")+`, - `+routeString("prefix", "/.pomerium/")+`, - `+routeString("path", "/.well-known/pomerium")+`, - `+routeString("prefix", "/.well-known/pomerium/")+` + `+routeString("path", "/.pomerium/jwt", true)+`, + `+routeString("path", "/ping", false)+`, + `+routeString("path", "/healthz", false)+`, + `+routeString("path", "/.pomerium/admin", true)+`, + `+routeString("prefix", "/.pomerium/admin/", true)+`, + `+routeString("path", "/.pomerium", false)+`, + `+routeString("prefix", "/.pomerium/", false)+`, + `+routeString("path", "/.well-known/pomerium", false)+`, + `+routeString("prefix", "/.well-known/pomerium/", false)+` ]`, routes) }) } func Test_buildControlPlanePathRoute(t *testing.T) { - route := buildControlPlanePathRoute("/hello/world") + route := buildControlPlanePathRoute("/hello/world", false) testutil.AssertProtoJSONEqual(t, ` { "name": "pomerium-path-/hello/world", @@ -167,7 +168,7 @@ func Test_buildControlPlanePathRoute(t *testing.T) { } func Test_buildControlPlanePrefixRoute(t *testing.T) { - route := buildControlPlanePrefixRoute("/hello/world/") + route := buildControlPlanePrefixRoute("/hello/world/", false) testutil.AssertProtoJSONEqual(t, ` { "name": "pomerium-prefix-/hello/world/",