Skip to content

feat: bypass built-in CORS handling for workspace apps #15669

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 12 commits into from
Prev Previous commit
Next Next commit
Minor fixes
Signed-off-by: Danny Kopping <danny@coder.com>
  • Loading branch information
dannykopping committed Nov 28, 2024
commit 873c3e45f5162374474f6a13af6f0e854a995e83
3 changes: 1 addition & 2 deletions coderd/httpmw/cors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ func TestWorkspaceAppCors(t *testing.T) {
r.Header.Set("Access-Control-Request-Method", method)
}

// TODO: signed token provider
handler := httpmw.WorkspaceAppCors(nil, regex, test.app)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
handler := httpmw.WorkspaceAppCors(regex, test.app)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent)
}))

Expand Down
1 change: 0 additions & 1 deletion coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1988,7 +1988,6 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
sharingLevel = database.AppSharingLevelPublic
}

// TODO: consider backwards-compat where proto might not contain this field
var corsBehavior database.AppCORSBehavior
switch app.CorsBehavior {
case sdkproto.AppCORSBehavior_PASSTHRU:
Expand Down
88 changes: 74 additions & 14 deletions coderd/workspaceapps/apptest/apptest.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,12 +475,20 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
t.Run("CORS", func(t *testing.T) {
t.Parallel()

t.Run("AuthenticatedPassthruProtected", func(t *testing.T) {
// Set up test headers that should be returned by the app
testHeaders := http.Header{
"Access-Control-Allow-Origin": []string{"*"},
"Access-Control-Allow-Methods": []string{"GET, POST, OPTIONS"},
}

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

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

appDetails := setupProxyTest(t, nil)
appDetails := setupProxyTest(t, &DeploymentOptions{
headers: testHeaders,
})

// Given: an unauthenticated client
client := appDetails.AppClient(t)
Expand All @@ -491,7 +499,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.NoError(t, err)
defer resp.Body.Close()

// Then: the request is redirected to the primary access URL because even though CORS is passthru,
// Then: the request is redirected to login because even though CORS is passthru,
// the request must still be authenticated first
require.Equal(t, http.StatusSeeOther, resp.StatusCode)
gotLocation, err := resp.Location()
Expand All @@ -505,7 +513,9 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {

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

appDetails := setupProxyTest(t, nil)
appDetails := setupProxyTest(t, &DeploymentOptions{
headers: testHeaders,
})

userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
userAppClient := appDetails.AppClient(t)
Expand All @@ -516,6 +526,65 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)

// Check CORS headers are passed through
require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, testHeaders.Get("Access-Control-Allow-Credentials"), resp.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods"))
})

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

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

appDetails := setupProxyTest(t, &DeploymentOptions{
headers: testHeaders,
})

// Given: an unauthenticated client
client := appDetails.AppClient(t)
client.SetSessionToken("")

// When: a request is made to a public app with passthru CORS behavior
resp, err := requestWithRetries(ctx, t, client, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.PublicCORSPassthru).String(), nil)
require.NoError(t, err)
defer resp.Body.Close()

// Then: the request succeeds because the app is public
require.Equal(t, http.StatusOK, resp.StatusCode)

// Check CORS headers are passed through
require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, testHeaders.Get("Access-Control-Allow-Credentials"), resp.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods"))
})

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

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

appDetails := setupProxyTest(t, &DeploymentOptions{
headers: testHeaders,
})

userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
userAppClient := appDetails.AppClient(t)
userAppClient.SetSessionToken(userClient.SessionToken())

// Given: an authenticated client accessing a public app with passthru CORS behavior
resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.PublicCORSPassthru).String(), nil)
require.NoError(t, err)
defer resp.Body.Close()

// Then: the request succeeds because the app is public
require.Equal(t, http.StatusOK, resp.StatusCode)

// Check CORS headers are passed through
require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin"))
require.Equal(t, testHeaders.Get("Access-Control-Allow-Credentials"), resp.Header.Get("Access-Control-Allow-Credentials"))
require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods"))
})
})

Expand Down Expand Up @@ -1842,7 +1911,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
})

// See above test for original implementation.
t.Run("CORSHeadersConditionalStrip", func(t *testing.T) {
t.Run("CORSHeadersConditionallyStripped", func(t *testing.T) {
t.Parallel()

// Set a bunch of headers which may or may not be stripped, depending on the CORS behavior.
Expand All @@ -1854,15 +1923,6 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
"Access-Control-Allow-Credentials": []string{"true"},
"Access-Control-Allow-Methods": []string{"PUT"},
"Access-Control-Allow-Headers": []string{"X-Foobar"},
"Vary": []string{
"Origin",
"origin",
"Access-Control-Request-Headers",
"access-Control-request-Headers",
"Access-Control-Request-Methods",
"ACCESS-CONTROL-REQUEST-METHODS",
"X-Foobar",
},
}

appDetails := setupProxyTest(t, &DeploymentOptions{
Expand Down
2 changes: 1 addition & 1 deletion coderd/workspaceapps/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR
)
//nolint:nestif
if portUintErr == nil {
// TODO: handle this branch
// TODO: handle CORS passthru for port sharing use-case.
appCORSBehavior = database.AppCorsBehaviorSimple

protocol := "http"
Expand Down
5 changes: 2 additions & 3 deletions provisioner/terraform/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,12 +435,11 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s

var corsBehavior proto.AppCORSBehavior
switch strings.ToLower(attrs.CORSBehavior) {
case "simple":
corsBehavior = proto.AppCORSBehavior_SIMPLE
case "passthru":
corsBehavior = proto.AppCORSBehavior_PASSTHRU
default:
return nil, xerrors.Errorf("invalid app CORS behavior %q", attrs.CORSBehavior)
corsBehavior = proto.AppCORSBehavior_SIMPLE
logger.Debug(ctx, "CORS behavior not set, defaulting to 'simple'")
}

for _, agents := range resourceAgents {
Expand Down