feat: allow bypassing current CORS magic based on template config (#18706)

Solves https://github.com/coder/coder/issues/15096

This is a slight rework/refactor of the earlier PRs from @dannykopping
and @Emyrk:
- https://github.com/coder/coder/pull/15669
- https://github.com/coder/coder/pull/15684
- https://github.com/coder/coder/pull/17596

Rather than having a per-app CORS behaviour setting and additionally a
template level setting for ports, this PR adds a single template level
CORS behaviour setting that is then used by all apps/ports for
workspaces created from that template.

The main changes are in `proxy.go` and `request.go` to:
a) get the CORS behaviour setting from the template
b) have `HandleSubdomain` bypass the CORS middleware handler if the
selected behaviour is `passthru`
c) in `proxyWorkspaceApp`, do not modify the response if the selected
behaviour is `passthru`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for configuring CORS behavior ("simple" or "passthru")
at the template level for all shared ports.
* Introduced a new "CORS Behavior" setting in the template creation and
settings forms.
* API endpoints and responses now include the optional `cors_behavior`
property for templates.
* Workspace apps and proxy now honor the specified CORS behavior,
enabling conditional CORS middleware application.
* Enhanced workspace app tests with comprehensive scenarios covering
CORS behaviors and authentication states.

* **Bug Fixes**
  * None.

* **Documentation**
* Updated API and admin documentation to describe the new
`cors_behavior` property and its usage.
* Added examples and schema references for CORS behavior in relevant API
docs.

* **Tests**
* Extended automated tests to cover different CORS behavior scenarios
for templates and workspace apps.

* **Chores**
* Updated audit logging to track changes to the `cors_behavior` field on
templates.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Callum Styan <callumstyan@gmail.com>
This commit is contained in:
Callum Styan
2025-07-30 13:42:39 -07:00
committed by GitHub
parent 96e32d60a2
commit ffbfaf2a6f
36 changed files with 1149 additions and 108 deletions
+559 -5
View File
@@ -472,6 +472,409 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
})
})
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
}
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
behavior codersdk.CORSBehavior
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
{ // fails
// The default behavior is to accept preflight requests from the request origin if it matches the app's own subdomain.
name: "Default/Public/Preflight/Subdomain",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
behavior: codersdk.CORSBehaviorSimple,
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"))
},
},
{ // passes
// The default behavior is to reject preflight requests from origins other than the app's own subdomain.
name: "Default/Public/Preflight/External",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
behavior: codersdk.CORSBehaviorSimple,
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"))
},
},
{ // fails
// A request without an Origin header would be rejected by an actual browser since it lacks CORS headers.
name: "Default/Public/GET/NoOrigin",
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
behavior: codersdk.CORSBehaviorSimple,
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"))
},
},
{ // fails
// The passthru behavior will pass through the request headers to the upstream app.
name: "Passthru/Public/Preflight/Subdomain",
app: func(details *Details) App { return details.Apps.PublicCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
client: unauthenticatedClient,
origin: ownSubdomain,
httpMethod: http.MethodOptions,
expectedStatusCode: http.StatusOK,
checkRequestHeaders: func(t *testing.T, origin string, req http.Header) {
assert.Equal(t, origin, req.Get("Origin"))
assert.Equal(t, "GET", req.Get("Access-Control-Request-Method"))
},
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
assert.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
// Added by the app handler.
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
},
},
{ // fails
// Identical to the previous test, but the origin is different.
name: "Passthru/Public/PreflightOther",
app: func(details *Details) App { return details.Apps.PublicCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
client: unauthenticatedClient,
origin: externalOrigin,
httpMethod: http.MethodOptions,
expectedStatusCode: http.StatusOK,
checkRequestHeaders: func(t *testing.T, origin string, req http.Header) {
assert.Equal(t, origin, req.Get("Origin"))
assert.Equal(t, "GET", req.Get("Access-Control-Request-Method"))
assert.Equal(t, "X-Got-Host", req.Get("Access-Control-Request-Headers"))
},
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
assert.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
// Added by the app handler.
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
},
},
{
// A request without an Origin header would be rejected by an actual browser since it lacks CORS headers.
name: "Passthru/Public/GET/NoOrigin",
app: func(details *Details) App { return details.Apps.PublicCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
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, "passthru", 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 },
behavior: codersdk.CORSBehaviorSimple,
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 },
behavior: codersdk.CORSBehaviorSimple,
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 },
behavior: codersdk.CORSBehaviorSimple,
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 },
behavior: codersdk.CORSBehaviorSimple,
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"))
},
},
{
// The request is rejected because the client is unauthenticated.
name: "Passthru/Unauthenticated/Preflight/Subdomain",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
client: unauthenticatedClient,
origin: ownSubdomain,
httpMethod: http.MethodOptions,
expectedStatusCode: http.StatusSeeOther,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.NotEmpty(t, resp.Get("Location"))
},
},
{
// Same behavior as the above test, but the origin is different.
name: "Passthru/Unauthenticated/Preflight/External",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
client: unauthenticatedClient,
origin: externalOrigin,
httpMethod: http.MethodOptions,
expectedStatusCode: http.StatusSeeOther,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.NotEmpty(t, resp.Get("Location"))
},
},
{
// The request is rejected because the client is unauthenticated.
name: "Passthru/Unauthenticated/GET/Subdomain",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
client: unauthenticatedClient,
origin: ownSubdomain,
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusSeeOther,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.NotEmpty(t, resp.Get("Location"))
},
},
{
// Same behavior as the above test, but the origin is different.
name: "Passthru/Unauthenticated/GET/External",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
client: unauthenticatedClient,
origin: externalOrigin,
httpMethod: http.MethodGet,
expectedStatusCode: http.StatusSeeOther,
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
assert.NotEmpty(t, resp.Get("Location"))
},
},
{
// The request is allowed because the client is authenticated.
name: "Passthru/Authenticated/Preflight/Subdomain",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
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.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
// Added by the app handler.
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
},
},
{
// Same behavior as the above test, but the origin is different.
name: "Passthru/Authenticated/Preflight/External",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
client: authenticatedClient,
origin: externalOrigin,
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.Equal(t, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
// Added by the app handler.
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
},
},
{
// The request is allowed because the client is authenticated.
name: "Passthru/Authenticated/GET/Subdomain",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
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, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
// Added by the app handler.
assert.Equal(t, "passthru", resp.Get("X-CORS-Handler"))
},
},
{
// Same behavior as the above test, but the origin is different.
name: "Passthru/Authenticated/GET/External",
app: func(details *Details) App { return details.Apps.AuthenticatedCORSPassthru },
behavior: codersdk.CORSBehaviorPassthru,
client: authenticatedClient,
origin: externalOrigin,
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, http.MethodGet, resp.Get("Access-Control-Allow-Methods"))
// Added by the app handler.
assert.Equal(t, "passthru", 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
// Setup an HTTP handler which is the "app"; this handler conditionally responds
// to requests based on the CORS behavior
appDetails := setupProxyTest(t, &DeploymentOptions{
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
switch tc.behavior {
case codersdk.CORSBehaviorPassthru:
w.Header().Set("X-CORS-Handler", "passthru")
// Only allow GET and OPTIONS requests
if r.Method != http.MethodGet && r.Method != http.MethodOptions {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// If the Origin header is present, add the CORS headers.
if origin := r.Header.Get("Origin"); origin != "" {
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", http.MethodGet)
}
w.WriteHeader(http.StatusOK)
case codersdk.CORSBehaviorSimple:
w.Header().Set("X-CORS-Handler", "simple")
}
}),
})
// Update the template CORS behavior.
b := tc.behavior
template, err := appDetails.SDKClient.UpdateTemplateMeta(ctx, appDetails.Workspace.TemplateID, codersdk.UpdateTemplateMeta{
CORSBehavior: &b,
})
require.NoError(t, err)
require.Equal(t, tc.behavior, template.CORSBehavior)
// Given: a client and a workspace app
client := tc.client(t, appDetails)
path := appDetails.SubdomainAppURL(tc.app(appDetails)).String()
origin := tc.origin(appDetails, tc.app(appDetails))
fmt.Println("method: ", tc.httpMethod)
// 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)
})
}
})
t.Run("WorkspaceApplicationAuth", func(t *testing.T) {
t.Parallel()
@@ -1340,6 +1743,153 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
})
})
t.Run("CORS", func(t *testing.T) {
t.Parallel()
// 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"},
}
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 {
c := appDetails.AppClient(t) // <-- Use same server as others
c.SetSessionToken(appDetails.SDKClient.SessionToken()) // But with owner auth
return c
}
tests := []struct {
name string
shareLevel codersdk.WorkspaceAgentPortShareLevel
behavior codersdk.CORSBehavior
client func(t *testing.T, appDetails *Details) *codersdk.Client
expectedStatusCode int
expectedCORSHeaders bool
}{
// Public
{
name: "Default/Public",
shareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
behavior: codersdk.CORSBehaviorSimple,
expectedCORSHeaders: false,
client: unauthenticatedClient,
expectedStatusCode: http.StatusOK,
},
{ // fails
name: "Passthru/Public",
shareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
behavior: codersdk.CORSBehaviorPassthru,
expectedCORSHeaders: true,
client: unauthenticatedClient,
expectedStatusCode: http.StatusOK,
},
// Authenticated
{
name: "Default/Authenticated",
shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated,
behavior: codersdk.CORSBehaviorSimple,
expectedCORSHeaders: false,
client: authenticatedClient,
expectedStatusCode: http.StatusOK,
},
{
name: "Passthru/Authenticated",
shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated,
behavior: codersdk.CORSBehaviorPassthru,
expectedCORSHeaders: true,
client: authenticatedClient,
expectedStatusCode: http.StatusOK,
},
{
// The CORS behavior will not affect unauthenticated requests.
// The request will be redirected to the login page.
name: "Passthru/Unauthenticated",
shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated,
behavior: codersdk.CORSBehaviorPassthru,
expectedCORSHeaders: false,
client: unauthenticatedClient,
expectedStatusCode: http.StatusSeeOther,
},
// Owner
{
name: "Default/Owner",
shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, // Owner is not a valid share level for ports.
behavior: codersdk.CORSBehaviorSimple,
expectedCORSHeaders: false,
client: ownerClient,
expectedStatusCode: http.StatusOK,
},
{ // fails
name: "Passthru/Owner",
shareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated, // Owner is not a valid share level for ports.
behavior: codersdk.CORSBehaviorPassthru,
expectedCORSHeaders: true,
client: ownerClient,
expectedStatusCode: http.StatusOK,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
appDetails := setupProxyTest(t, &DeploymentOptions{
headers: testHeaders,
})
port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32)
require.NoError(t, err)
// Update the template CORS behavior.
b := tc.behavior
template, err := appDetails.SDKClient.UpdateTemplateMeta(ctx, appDetails.Workspace.TemplateID, codersdk.UpdateTemplateMeta{
CORSBehavior: &b,
})
require.NoError(t, err)
require.Equal(t, tc.behavior, template.CORSBehavior)
// Set the port we have to be shared.
_, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
AgentName: proxyTestAgentName,
Port: int32(port),
ShareLevel: tc.shareLevel,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)
client := tc.client(t, appDetails)
resp, err := requestWithRetries(ctx, t, client, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, tc.expectedStatusCode, resp.StatusCode)
if tc.expectedCORSHeaders {
require.Equal(t, testHeaders.Get("Access-Control-Allow-Origin"), resp.Header.Get("Access-Control-Allow-Origin"), "allow origin did not match")
require.Equal(t, testHeaders.Get("Access-Control-Allow-Methods"), resp.Header.Get("Access-Control-Allow-Methods"), "allow methods did not match")
} else {
require.Empty(t, resp.Header.Get("Access-Control-Allow-Origin"))
require.Empty(t, resp.Header.Get("Access-Control-Allow-Methods"))
}
})
}
})
t.Run("AppSharing", func(t *testing.T) {
t.Parallel()
@@ -1386,7 +1936,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.
@@ -1397,10 +1947,14 @@ 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,
proxyTestAppNameAuthenticatedCORSPassthru: codersdk.WorkspaceAppSharingLevelAuthenticated,
proxyTestAppNamePublicCORSPassthru: codersdk.WorkspaceAppSharingLevelPublic,
proxyTestAppNameAuthenticatedCORSDefault: codersdk.WorkspaceAppSharingLevelAuthenticated,
proxyTestAppNamePublicCORSDefault: codersdk.WorkspaceAppSharingLevelPublic,
}
for _, app := range agnt.Apps {
found[app.DisplayName] = app.SharingLevel