feat: allow prefixes at the beginning of subdomain app hostnames (#10150)

This commit is contained in:
Dean Sheather
2023-10-10 20:02:39 +00:00
committed by GitHub
parent f48bc33e00
commit e7d9b8d858
15 changed files with 306 additions and 6 deletions
+113
View File
@@ -19,6 +19,7 @@ import (
"testing"
"time"
"github.com/go-jose/go-jose/v3"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -552,6 +553,118 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
})
})
t.Run("WorkspaceAppsProxySubdomainHostnamePrefix/OK", func(t *testing.T) {
t.Parallel()
appDetails := setupProxyTest(t, nil)
// Try to load the owner app with a prefix.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
prefixedOwnerApp := appDetails.Apps.Owner
prefixedOwnerApp.Prefix = "some---prefix---"
u := appDetails.SubdomainAppURL(prefixedOwnerApp)
require.Contains(t, u.Host, prefixedOwnerApp.Prefix)
resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
require.NoError(t, err)
_ = resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, resp.Header.Get("X-Got-Host"), u.Host)
// Parse the returned signed token to verify that it contains the
// prefix.
var appTokenCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == codersdk.SignedAppTokenCookie {
appTokenCookie = c
break
}
}
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
// Parse the JWT without verifying it (since we can't access the key
// from this test).
object, err := jose.ParseSigned(appTokenCookie.Value)
require.NoError(t, err)
require.Len(t, object.Signatures, 1)
// Parse the payload.
var tok workspaceapps.SignedToken
//nolint:gosec
err = json.Unmarshal(object.UnsafePayloadWithoutVerification(), &tok)
require.NoError(t, err)
// Verify the prefix is in the token.
require.Equal(t, prefixedOwnerApp.Prefix, tok.Request.Prefix)
// Ensure the signed app token cookie is valid by making a request with
// it with no session token.
appTokenClient := appDetails.AppClient(t)
appTokenClient.SetSessionToken("")
appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
require.NoError(t, err)
appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})
resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
require.NoError(t, err)
_ = resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, resp.Header.Get("X-Got-Host"), u.Host)
})
t.Run("WorkspaceAppsProxySubdomainHostnamePrefix/Different", func(t *testing.T) {
t.Parallel()
appDetails := setupProxyTest(t, nil)
// Try to load the owner app with a prefix.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
prefixedOwnerApp := appDetails.Apps.Owner
t.Log(appDetails.SubdomainAppURL(prefixedOwnerApp))
prefixedOwnerApp.Prefix = "some---prefix---"
t.Log(appDetails.SubdomainAppURL(prefixedOwnerApp))
u := appDetails.SubdomainAppURL(prefixedOwnerApp)
require.Contains(t, u.Host, prefixedOwnerApp.Prefix)
resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
require.NoError(t, err)
_ = resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
// Find the cookie.
var appTokenCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == codersdk.SignedAppTokenCookie {
appTokenCookie = c
break
}
}
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
// Ensure the signed app token cookie is valid only for the given prefix
// by making a request with it with no session token.
appTokenClient := appDetails.AppClient(t)
appTokenClient.SetSessionToken("")
appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
require.NoError(t, err)
appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})
prefixedOwnerApp.Prefix = "different---"
u = appDetails.SubdomainAppURL(prefixedOwnerApp)
require.Contains(t, u.Host, prefixedOwnerApp.Prefix)
resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
require.NoError(t, err)
_ = resp.Body.Close()
require.NotEqual(t, http.StatusOK, resp.StatusCode)
})
// This test ensures that the subdomain handler does nothing if
// --app-hostname is not set by the admin.
t.Run("WorkspaceAppsProxySubdomainPassthrough", func(t *testing.T) {
+19 -1
View File
@@ -25,6 +25,7 @@ import (
"github.com/coder/coder/v2/coderd/workspaceapps"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/testutil"
@@ -88,7 +89,9 @@ type App struct {
AgentName string
AppSlugOrPort string
Query string
// Prefix should have ---.
Prefix string
Query string
}
// Details are the full test details returned from setupProxyTestWithFactory.
@@ -143,6 +146,7 @@ func (d *Details) PathAppURL(app App) *url.URL {
// SubdomainAppURL returns the URL for the given subdomain app.
func (d *Details) SubdomainAppURL(app App) *url.URL {
appHost := httpapi.ApplicationURL{
Prefix: app.Prefix,
AppSlugOrPort: app.AppSlugOrPort,
AgentName: app.AgentName,
WorkspaceName: app.WorkspaceName,
@@ -252,6 +256,7 @@ func appServer(t *testing.T, headers http.Header, isHTTPS bool) uint16 {
_, err := r.Cookie(codersdk.SessionTokenCookie)
assert.ErrorIs(t, err, http.ErrNoCookie)
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
w.Header().Set("X-Got-Host", r.Host)
for name, values := range headers {
for _, value := range values {
w.Header().Add(name, value)
@@ -290,6 +295,17 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
scheme = "https"
}
// Workspace name needs to be short to avoid hitting 62 char hostname
// segment limit.
workspaceName, err := cryptorand.String(6)
require.NoError(t, err)
workspaceName = "ws-" + workspaceName
workspaceMutators = append([]func(*codersdk.CreateWorkspaceRequest){
func(req *codersdk.CreateWorkspaceRequest) {
req.Name = workspaceName
},
}, workspaceMutators...)
appURL := fmt.Sprintf("%s://127.0.0.1:%d?%s", scheme, port, proxyTestAppQuery)
protoApps := []*proto.App{
{
@@ -354,6 +370,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
require.True(t, app.Subdomain)
appURL := httpapi.ApplicationURL{
Prefix: "",
// findProtoApp is needed as the order of apps returned from PG database
// is not guaranteed.
AppSlugOrPort: findProtoApp(t, protoApps, app.Slug).Slug,
@@ -382,6 +399,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
require.NoError(t, err)
appHost := httpapi.ApplicationURL{
Prefix: "",
AppSlugOrPort: "{{port}}",
AgentName: proxyTestAgentName,
WorkspaceName: workspace.Name,