mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -36,8 +36,13 @@ const (
|
||||
proxyTestAppNameOwner = "test-app-owner"
|
||||
proxyTestAppNameAuthenticated = "test-app-authenticated"
|
||||
proxyTestAppNamePublic = "test-app-public"
|
||||
proxyTestAppQuery = "query=true"
|
||||
proxyTestAppBody = "hello world from apps test"
|
||||
// nolint:gosec // Not a secret
|
||||
proxyTestAppNameAuthenticatedCORSPassthru = "test-app-authenticated-cors-passthru"
|
||||
proxyTestAppNamePublicCORSPassthru = "test-app-public-cors-passthru"
|
||||
proxyTestAppNameAuthenticatedCORSDefault = "test-app-authenticated-cors-default"
|
||||
proxyTestAppNamePublicCORSDefault = "test-app-public-cors-default"
|
||||
proxyTestAppQuery = "query=true"
|
||||
proxyTestAppBody = "hello world from apps test"
|
||||
|
||||
proxyTestSubdomainRaw = "*.test.coder.com"
|
||||
proxyTestSubdomain = "test.coder.com"
|
||||
@@ -60,6 +65,7 @@ type DeploymentOptions struct {
|
||||
noWorkspace bool
|
||||
port uint16
|
||||
headers http.Header
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
// Deployment is a license-agnostic deployment with all the fields that apps
|
||||
@@ -93,6 +99,9 @@ type App struct {
|
||||
// Prefix should have ---.
|
||||
Prefix string
|
||||
Query string
|
||||
|
||||
// Control the behavior of CORS handling.
|
||||
CORSBehavior codersdk.CORSBehavior
|
||||
}
|
||||
|
||||
// Details are the full test details returned from setupProxyTestWithFactory.
|
||||
@@ -109,12 +118,16 @@ type Details struct {
|
||||
AppPort uint16
|
||||
|
||||
Apps struct {
|
||||
Fake App
|
||||
Owner App
|
||||
Authenticated App
|
||||
Public App
|
||||
Port App
|
||||
PortHTTPS App
|
||||
Fake App
|
||||
Owner App
|
||||
Authenticated App
|
||||
Public App
|
||||
Port App
|
||||
PortHTTPS App
|
||||
PublicCORSPassthru App
|
||||
AuthenticatedCORSPassthru App
|
||||
PublicCORSDefault App
|
||||
AuthenticatedCORSDefault App
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +214,7 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De
|
||||
}
|
||||
|
||||
if opts.port == 0 {
|
||||
opts.port = appServer(t, opts.headers, opts.ServeHTTPS)
|
||||
opts.port = appServer(t, opts.headers, opts.ServeHTTPS, opts.handler)
|
||||
}
|
||||
workspace, agnt := createWorkspaceWithApps(t, deployment.SDKClient, deployment.FirstUser.OrganizationID, me, opts.port, opts.ServeHTTPS)
|
||||
|
||||
@@ -252,30 +265,64 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De
|
||||
AgentName: agnt.Name,
|
||||
AppSlugOrPort: strconv.Itoa(int(opts.port)) + "s",
|
||||
}
|
||||
details.Apps.PublicCORSPassthru = App{
|
||||
Username: me.Username,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: agnt.Name,
|
||||
AppSlugOrPort: proxyTestAppNamePublicCORSPassthru,
|
||||
CORSBehavior: codersdk.CORSBehaviorPassthru,
|
||||
Query: proxyTestAppQuery,
|
||||
}
|
||||
details.Apps.AuthenticatedCORSPassthru = App{
|
||||
Username: me.Username,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: agnt.Name,
|
||||
AppSlugOrPort: proxyTestAppNameAuthenticatedCORSPassthru,
|
||||
CORSBehavior: codersdk.CORSBehaviorPassthru,
|
||||
Query: proxyTestAppQuery,
|
||||
}
|
||||
details.Apps.PublicCORSDefault = App{
|
||||
Username: me.Username,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: agnt.Name,
|
||||
AppSlugOrPort: proxyTestAppNamePublicCORSDefault,
|
||||
Query: proxyTestAppQuery,
|
||||
}
|
||||
details.Apps.AuthenticatedCORSDefault = App{
|
||||
Username: me.Username,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: agnt.Name,
|
||||
AppSlugOrPort: proxyTestAppNameAuthenticatedCORSDefault,
|
||||
Query: proxyTestAppQuery,
|
||||
}
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
func appServer(t *testing.T, headers http.Header, isHTTPS bool) uint16 {
|
||||
server := httptest.NewUnstartedServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
_, 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)
|
||||
}
|
||||
func appServer(t *testing.T, headers http.Header, isHTTPS bool, handler http.Handler) uint16 {
|
||||
defaultHandler := http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
_, 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)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(proxyTestAppBody))
|
||||
},
|
||||
),
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(proxyTestAppBody))
|
||||
},
|
||||
)
|
||||
|
||||
if handler == nil {
|
||||
handler = defaultHandler
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(handler)
|
||||
|
||||
server.Config.ReadHeaderTimeout = time.Minute
|
||||
if isHTTPS {
|
||||
server.StartTLS()
|
||||
@@ -361,6 +408,36 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
Url: appURL,
|
||||
Subdomain: true,
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNamePublicCORSPassthru,
|
||||
DisplayName: proxyTestAppNamePublicCORSPassthru,
|
||||
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
||||
Url: appURL,
|
||||
Subdomain: true,
|
||||
// CorsBehavior: proto.AppCORSBehavior_PASSTHRU,
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNameAuthenticatedCORSPassthru,
|
||||
DisplayName: proxyTestAppNameAuthenticatedCORSPassthru,
|
||||
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
||||
Url: appURL,
|
||||
Subdomain: true,
|
||||
// CorsBehavior: proto.AppCORSBehavior_PASSTHRU,
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNamePublicCORSDefault,
|
||||
DisplayName: proxyTestAppNamePublicCORSDefault,
|
||||
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
||||
Url: appURL,
|
||||
Subdomain: true,
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNameAuthenticatedCORSDefault,
|
||||
DisplayName: proxyTestAppNameAuthenticatedCORSDefault,
|
||||
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
||||
Url: appURL,
|
||||
Subdomain: true,
|
||||
},
|
||||
}
|
||||
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
|
||||
Reference in New Issue
Block a user