Skip to content

chore: add tests for default CORS behavior #15685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ scripts/ci-report/testdata/.gen-golden: $(wildcard scripts/ci-report/testdata/*)
done

test:
$(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./...
$(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=10 ./enterprise
.PHONY: test

# sqlc-cloud-is-setup will fail if no SQLc auth token is set. Use this as a
Expand Down
319 changes: 314 additions & 5 deletions coderd/workspaceapps/apptest/apptest.go
Original file line number Diff line number Diff line change
Expand Up @@ -1388,7 +1388,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
forceURLTransport(t, client)

// Create workspace.
port := appServer(t, nil, false)
port := appServer(t, nil, false, nil)
workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port, false)

// Verify that the apps have the correct sharing levels set.
Expand All @@ -1399,10 +1399,12 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
agnt = workspaceBuild.Resources[0].Agents[0]
found := map[string]codersdk.WorkspaceAppSharingLevel{}
expected := map[string]codersdk.WorkspaceAppSharingLevel{
proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner,
proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner,
proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated,
proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic,
proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner,
proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner,
proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated,
proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic,
proxyTestAppNameAuthenticatedCORSDefault: codersdk.WorkspaceAppSharingLevelAuthenticated,
proxyTestAppNamePublicCORSDefault: codersdk.WorkspaceAppSharingLevelPublic,
}
for _, app := range agnt.Apps {
found[app.DisplayName] = app.SharingLevel
Expand Down Expand Up @@ -1559,6 +1561,9 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {

// Unauthenticated user should not have any access.
verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, clientWithNoAuth, false, true)

// Unauthenticated user should not have any access, regardless of CORS behavior (using default).
verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticatedCORSDefault, clientWithNoAuth, false, true)
})

t.Run("LevelPublic", func(t *testing.T) {
Expand Down Expand Up @@ -1831,6 +1836,310 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
require.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
})

t.Run("WorkspaceApplicationCORS", func(t *testing.T) {
t.Parallel()

const external = "https://example.com"

unauthenticatedClient := func(t *testing.T, appDetails *Details) *codersdk.Client {
c := appDetails.AppClient(t)
c.SetSessionToken("")
return c
}

authenticatedClient := func(t *testing.T, appDetails *Details) *codersdk.Client {
uc, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
c := appDetails.AppClient(t)
c.SetSessionToken(uc.SessionToken())
return c
}

ownerClient := func(t *testing.T, appDetails *Details) *codersdk.Client {
return appDetails.SDKClient
}

ownSubdomain := func(details *Details, app App) string {
url := details.SubdomainAppURL(app)
return url.Scheme + "://" + url.Host
}

externalOrigin := func(*Details, App) string {
return external
}

tests := []struct {
name string
app func(details *Details) App
client func(t *testing.T, appDetails *Details) *codersdk.Client
httpMethod string
origin func(details *Details, app App) string
expectedStatusCode int
checkRequestHeaders func(t *testing.T, origin string, req http.Header)
checkResponseHeaders func(t *testing.T, origin string, resp http.Header)
}{
// Public
{
// The default behavior is to a accept preflight request if it matches the app's own subdomain.
name: "Default/Public/Preflight/Subdomain",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
client: unauthenticatedClient,
httpMethod: http.MethodOptions,
origin: ownSubdomain,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
assert.Contains(t, resp.Get("Access-Control-Allow-Methods"), http.MethodGet)
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
assert.Equal(t, "X-Got-Host", resp.Get("Access-Control-Allow-Headers"))
},
},
{
// The default behavior is to reject a preflight request from origins other than the app's own subdomain.
name: "Default/Public/Preflight/External",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
client: unauthenticatedClient,
httpMethod: http.MethodOptions,
origin: externalOrigin,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
// We don't add a valid Allow-Origin header for requests we won't proxy.
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
},
},
{
// An unauthenticated request to the app is allowed from its own subdomain.
name: "Default/Public/GET/Subdomain",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
client: unauthenticatedClient,
origin: ownSubdomain,
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
// Added by the app handler.
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
},
},
{
// An unauthenticated request to the app is allowed from an external origin, but the CORS
// headers are not added to the response because it's not coming from a known origin.
name: "Default/Public/GET/External",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
client: unauthenticatedClient,
origin: externalOrigin,
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
// We don't add a valid Allow-Origin header for requests we won't proxy.
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
},
},
{
// The owner can access their own apps from their own subdomain with valid CORS headers.
name: "Default/Public/GET/SubdomainOwner",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
client: ownerClient,
origin: ownSubdomain,
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
// Added by the app handler.
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
},
},
{
// The owner can't access their own apps from an external origin with valid CORS headers.
name: "Default/Public/GET/ExternalOwner",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
client: ownerClient,
origin: externalOrigin,
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
// We don't add a valid Allow-Origin header for requests we won't proxy.
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
},
},
{
// A request without an Origin header would be rejected by an actual browser since it lacks CORS headers,
// but we accept it since it's common for non-browser clients to not send the Origin header.
name: "Default/Public/GET/NoOrigin",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
client: unauthenticatedClient,
origin: func(*Details, App) string { return "" },
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
assert.Empty(t, resp.Get("Access-Control-Allow-Headers"))
assert.Empty(t, resp.Get("Access-Control-Allow-Credentials"))
// Added by the app handler.
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
},
},
// Authenticated
// {
// // Same behavior as Default/Public/Preflight/Subdomain.
// name: "Default/Authenticated/Preflight/Subdomain",
// app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
// client: authenticatedClient,
// origin: ownSubdomain,
// httpMethod: http.MethodOptions,
// expectedStatusCode: http.StatusOK,
// checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
// assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
// assert.Contains(t, resp.Get("Access-Control-Allow-Methods"), http.MethodGet)
// assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
// assert.Equal(t, "X-Got-Host", resp.Get("Access-Control-Allow-Headers"))
// },
// },
{
// Same behavior as Default/Public/Preflight/External.
name: "Default/Authenticated/Preflight/External",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
client: authenticatedClient,
origin: externalOrigin,
httpMethod: http.MethodOptions,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
},
},
{
// An authenticated request to the app is allowed from its own subdomain.
name: "Default/Authenticated/GET/Subdomain",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
client: authenticatedClient,
origin: ownSubdomain,
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
// Added by the app handler.
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
},
},
{
// An authenticated request to the app is allowed from an external origin.
// The origin doesn't match the app's own subdomain, so the CORS headers are not added.
name: "Default/Authenticated/GET/External",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
client: authenticatedClient,
origin: externalOrigin,
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
assert.Empty(t, resp.Get("Access-Control-Allow-Headers"))
assert.Empty(t, resp.Get("Access-Control-Allow-Credentials"))
// Added by the app handler.
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
},
},
// {
// // Owners can access their own apps from their own subdomain with valid CORS headers.
// name: "Default/Authenticated/GET/SubdomainOwner",
// app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
// client: ownerClient,
// origin: ownSubdomain,
// httpMethod: http.MethodGet,
// expectedStatusCode: http.StatusOK,
// checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
// assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
// assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
// // Added by the app handler.
// assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
// },
// },
{
// Owners can't access their own apps from an external origin with valid CORS headers.
name: "Default/Owner/GET/ExternalOwner",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
client: ownerClient,
origin: externalOrigin,
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
// We don't add a valid Allow-Origin header for requests we won't proxy.
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
},
},
{
// Same behavior as Default/Public/GET/NoOrigin.
name: "Default/Authenticated/GET/NoOrigin",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
client: authenticatedClient,
origin: func(*Details, App) string { return "" },
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusOK,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
assert.Empty(t, resp.Get("Access-Control-Allow-Headers"))
assert.Empty(t, resp.Get("Access-Control-Allow-Credentials"))
// Added by the app handler.
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitLong)

var reqHeaders http.Header

// Given: a workspace app
appDetails := setupProxyTest(t, &DeploymentOptions{
// Setup an HTTP handler which is the "app"; this handler conditionally responds
// to requests based on the CORS behavior
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := r.Cookie(codersdk.SessionTokenCookie)
assert.ErrorIs(t, err, http.ErrNoCookie)

// Store the request headers for later assertions
reqHeaders = r.Header
w.Header().Set("X-CORS-Handler", "simple")
}),
})

if appDetails.BlockDirect {
t.Skip("skipping because BlockDirect is true")
}

// Given: a client
client := tc.client(t, appDetails)
path := appDetails.SubdomainAppURL(tc.app(appDetails)).String()
origin := tc.origin(appDetails, tc.app(appDetails))

// When: a preflight request is made to an app with a specified CORS behavior
resp, err := requestWithRetries(ctx, t, client, tc.httpMethod, path, nil, func(r *http.Request) {
// Mimic non-browser clients that don't send the Origin header.
if origin != "" {
r.Header.Set("Origin", origin)
}
r.Header.Set("Access-Control-Request-Method", "GET")
r.Header.Set("Access-Control-Request-Headers", "X-Got-Host")
})
require.NoError(t, err)
defer resp.Body.Close()

// Then: the request & response must match expectations
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
assert.NoError(t, err)
if tc.checkRequestHeaders != nil {
tc.checkRequestHeaders(t, origin, reqHeaders)
}
tc.checkResponseHeaders(t, origin, resp.Header)
})
}
})
}

type fakeStatsReporter struct {
Expand Down
Loading
Loading