mirror of https://github.com/coder/coder.git
chore: CORs option for yarn dev server (#7630)
* chore: Yarn dev servers require CORs headers for external proxies Adds a flag to set CORs headers to `*` for yarn dev servers
This commit is contained in:
parent
1f4f0efed6
commit
5d711fc95a
|
@ -7252,6 +7252,9 @@ const docTemplate = `{
|
||||||
"codersdk.DangerousConfig": {
|
"codersdk.DangerousConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"allow_all_cors": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"allow_path_app_sharing": {
|
"allow_path_app_sharing": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
@ -6454,6 +6454,9 @@
|
||||||
"codersdk.DangerousConfig": {
|
"codersdk.DangerousConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"allow_all_cors": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"allow_path_app_sharing": {
|
"allow_path_app_sharing": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
@ -393,8 +393,10 @@ func New(options *Options) *API {
|
||||||
|
|
||||||
derpHandler := derphttp.Handler(api.DERPServer)
|
derpHandler := derphttp.Handler(api.DERPServer)
|
||||||
derpHandler, api.derpCloseFunc = tailnet.WithWebsocketSupport(api.DERPServer, derpHandler)
|
derpHandler, api.derpCloseFunc = tailnet.WithWebsocketSupport(api.DERPServer, derpHandler)
|
||||||
|
cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value())
|
||||||
|
|
||||||
r.Use(
|
r.Use(
|
||||||
|
cors,
|
||||||
httpmw.Recover(api.Logger),
|
httpmw.Recover(api.Logger),
|
||||||
tracing.StatusWriterMiddleware,
|
tracing.StatusWriterMiddleware,
|
||||||
tracing.Middleware(api.TracerProvider),
|
tracing.Middleware(api.TracerProvider),
|
||||||
|
@ -799,6 +801,10 @@ func New(options *Options) *API {
|
||||||
// Add CSP headers to all static assets and pages. CSP headers only affect
|
// Add CSP headers to all static assets and pages. CSP headers only affect
|
||||||
// browsers, so these don't make sense on api routes.
|
// browsers, so these don't make sense on api routes.
|
||||||
cspMW := httpmw.CSPHeaders(func() []string {
|
cspMW := httpmw.CSPHeaders(func() []string {
|
||||||
|
if api.DeploymentValues.Dangerous.AllowAllCors {
|
||||||
|
// In this mode, allow all external requests
|
||||||
|
return []string{"*"}
|
||||||
|
}
|
||||||
if f := api.WorkspaceProxyHostsFn.Load(); f != nil {
|
if f := api.WorkspaceProxyHostsFn.Load(); f != nil {
|
||||||
return (*f)()
|
return (*f)()
|
||||||
}
|
}
|
||||||
|
@ -813,7 +819,7 @@ func New(options *Options) *API {
|
||||||
// This is the only route we add before all the middleware.
|
// This is the only route we add before all the middleware.
|
||||||
// We want to time the latency of the request, so any middleware will
|
// We want to time the latency of the request, so any middleware will
|
||||||
// interfere with that timing.
|
// interfere with that timing.
|
||||||
rootRouter.Get("/latency-check", LatencyCheck(api.AccessURL))
|
rootRouter.Get("/latency-check", cors(LatencyCheck(options.DeploymentValues.Dangerous.AllowAllCors.Value(), api.AccessURL)).ServeHTTP)
|
||||||
rootRouter.Mount("/", r)
|
rootRouter.Mount("/", r)
|
||||||
api.RootHandler = rootRouter
|
api.RootHandler = rootRouter
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package httpmw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/cors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:revive
|
||||||
|
func Cors(allowAll bool, origins ...string) func(next http.Handler) http.Handler {
|
||||||
|
if len(origins) == 0 {
|
||||||
|
// The default behavior is '*', so putting the empty string defaults to
|
||||||
|
// the secure behavior of blocking CORs requests.
|
||||||
|
origins = []string{""}
|
||||||
|
}
|
||||||
|
if allowAll {
|
||||||
|
origins = []string{"*"}
|
||||||
|
}
|
||||||
|
return cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: origins,
|
||||||
|
// We only need GET for latency requests
|
||||||
|
AllowedMethods: []string{http.MethodOptions, http.MethodGet},
|
||||||
|
AllowedHeaders: []string{"Accept", "Content-Type", "X-LATENCY-CHECK", "X-CSRF-TOKEN"},
|
||||||
|
// Do not send any cookies
|
||||||
|
AllowCredentials: false,
|
||||||
|
})
|
||||||
|
}
|
|
@ -103,6 +103,11 @@ func CSPHeaders(websocketHosts func() []string) func(next http.Handler) http.Han
|
||||||
extraConnect := websocketHosts()
|
extraConnect := websocketHosts()
|
||||||
if len(extraConnect) > 0 {
|
if len(extraConnect) > 0 {
|
||||||
for _, extraHost := range extraConnect {
|
for _, extraHost := range extraConnect {
|
||||||
|
if extraHost == "*" {
|
||||||
|
// '*' means all
|
||||||
|
cspSrcs.Append(cspDirectiveConnectSrc, "*")
|
||||||
|
continue
|
||||||
|
}
|
||||||
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost))
|
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("wss://%[1]s ws://%[1]s", extraHost))
|
||||||
// We also require this to make http/https requests to the workspace proxy for latency checking.
|
// We also require this to make http/https requests to the workspace proxy for latency checking.
|
||||||
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("https://%[1]s http://%[1]s", extraHost))
|
cspSrcs.Append(cspDirectiveConnectSrc, fmt.Sprintf("https://%[1]s http://%[1]s", extraHost))
|
||||||
|
|
|
@ -6,7 +6,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LatencyCheck(allowedOrigins ...*url.URL) http.HandlerFunc {
|
// LatencyCheck is an endpoint for the web ui to measure latency with.
|
||||||
|
// allowAll allows any Origin to get timing information. The allowAll should
|
||||||
|
// only be set in dev modes.
|
||||||
|
//
|
||||||
|
//nolint:revive
|
||||||
|
func LatencyCheck(allowAll bool, allowedOrigins ...*url.URL) http.HandlerFunc {
|
||||||
allowed := make([]string, 0, len(allowedOrigins))
|
allowed := make([]string, 0, len(allowedOrigins))
|
||||||
for _, origin := range allowedOrigins {
|
for _, origin := range allowedOrigins {
|
||||||
// Allow the origin without a path
|
// Allow the origin without a path
|
||||||
|
@ -14,6 +19,9 @@ func LatencyCheck(allowedOrigins ...*url.URL) http.HandlerFunc {
|
||||||
tmp.Path = ""
|
tmp.Path = ""
|
||||||
allowed = append(allowed, strings.TrimSuffix(origin.String(), "/"))
|
allowed = append(allowed, strings.TrimSuffix(origin.String(), "/"))
|
||||||
}
|
}
|
||||||
|
if allowAll {
|
||||||
|
allowed = append(allowed, "*")
|
||||||
|
}
|
||||||
origins := strings.Join(allowed, ",")
|
origins := strings.Join(allowed, ",")
|
||||||
return func(rw http.ResponseWriter, r *http.Request) {
|
return func(rw http.ResponseWriter, r *http.Request) {
|
||||||
// Allowing timing information to be shared. This allows the browser
|
// Allowing timing information to be shared. This allows the browser
|
||||||
|
|
|
@ -330,6 +330,7 @@ type LoggingConfig struct {
|
||||||
type DangerousConfig struct {
|
type DangerousConfig struct {
|
||||||
AllowPathAppSharing clibase.Bool `json:"allow_path_app_sharing" typescript:",notnull"`
|
AllowPathAppSharing clibase.Bool `json:"allow_path_app_sharing" typescript:",notnull"`
|
||||||
AllowPathAppSiteOwnerAccess clibase.Bool `json:"allow_path_app_site_owner_access" typescript:",notnull"`
|
AllowPathAppSiteOwnerAccess clibase.Bool `json:"allow_path_app_site_owner_access" typescript:",notnull"`
|
||||||
|
AllowAllCors clibase.Bool `json:"allow_all_cors" typescript:",notnull"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -1167,6 +1168,16 @@ when required by your organization's security policy.`,
|
||||||
Annotations: clibase.Annotations{}.Mark(annotationExternalProxies, "true"),
|
Annotations: clibase.Annotations{}.Mark(annotationExternalProxies, "true"),
|
||||||
},
|
},
|
||||||
// ☢️ Dangerous settings
|
// ☢️ Dangerous settings
|
||||||
|
{
|
||||||
|
Name: "DANGEROUS: Allow all CORs requests",
|
||||||
|
Description: "For security reasons, CORs requests are blocked. If external requests are required, setting this to true will set all cors headers as '*'. This should never be used in production.",
|
||||||
|
Flag: "dangerous-allow-cors-requests",
|
||||||
|
Env: "CODER_DANGEROUS_ALLOW_CORS_REQUESTS",
|
||||||
|
Hidden: true, // Hidden, should only be used by yarn dev server
|
||||||
|
Value: &c.Dangerous.AllowAllCors,
|
||||||
|
Group: &deploymentGroupDangerous,
|
||||||
|
Annotations: clibase.Annotations{}.Mark(annotationExternalProxies, "true"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "DANGEROUS: Allow Path App Sharing",
|
Name: "DANGEROUS: Allow Path App Sharing",
|
||||||
Description: "Allow workspace apps that are not served from subdomains to be shared. Path-based app sharing is DISABLED by default for security purposes. Path-based apps can make requests to the Coder API and pose a security risk when the workspace serves malicious JavaScript. Path-based apps can be disabled entirely with --disable-path-apps for further security.",
|
Description: "Allow workspace apps that are not served from subdomains to be shared. Path-based app sharing is DISABLED by default for security purposes. Path-based apps can make requests to the Coder API and pose a security risk when the workspace serves malicious JavaScript. Path-based apps can be disabled entirely with --disable-path-apps for further security.",
|
||||||
|
|
|
@ -161,6 +161,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
||||||
"sshconfigOptions": ["string"]
|
"sshconfigOptions": ["string"]
|
||||||
},
|
},
|
||||||
"dangerous": {
|
"dangerous": {
|
||||||
|
"allow_all_cors": true,
|
||||||
"allow_path_app_sharing": true,
|
"allow_path_app_sharing": true,
|
||||||
"allow_path_app_site_owner_access": true
|
"allow_path_app_site_owner_access": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -1800,6 +1800,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"allow_all_cors": true,
|
||||||
"allow_path_app_sharing": true,
|
"allow_path_app_sharing": true,
|
||||||
"allow_path_app_site_owner_access": true
|
"allow_path_app_site_owner_access": true
|
||||||
}
|
}
|
||||||
|
@ -1809,6 +1810,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
||||||
|
|
||||||
| Name | Type | Required | Restrictions | Description |
|
| Name | Type | Required | Restrictions | Description |
|
||||||
| ---------------------------------- | ------- | -------- | ------------ | ----------- |
|
| ---------------------------------- | ------- | -------- | ------------ | ----------- |
|
||||||
|
| `allow_all_cors` | boolean | false | | |
|
||||||
| `allow_path_app_sharing` | boolean | false | | |
|
| `allow_path_app_sharing` | boolean | false | | |
|
||||||
| `allow_path_app_site_owner_access` | boolean | false | | |
|
| `allow_path_app_site_owner_access` | boolean | false | | |
|
||||||
|
|
||||||
|
@ -1857,6 +1859,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
||||||
"sshconfigOptions": ["string"]
|
"sshconfigOptions": ["string"]
|
||||||
},
|
},
|
||||||
"dangerous": {
|
"dangerous": {
|
||||||
|
"allow_all_cors": true,
|
||||||
"allow_path_app_sharing": true,
|
"allow_path_app_sharing": true,
|
||||||
"allow_path_app_site_owner_access": true
|
"allow_path_app_site_owner_access": true
|
||||||
},
|
},
|
||||||
|
@ -2201,6 +2204,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
||||||
"sshconfigOptions": ["string"]
|
"sshconfigOptions": ["string"]
|
||||||
},
|
},
|
||||||
"dangerous": {
|
"dangerous": {
|
||||||
|
"allow_all_cors": true,
|
||||||
"allow_path_app_sharing": true,
|
"allow_path_app_sharing": true,
|
||||||
"allow_path_app_site_owner_access": true
|
"allow_path_app_site_owner_access": true
|
||||||
},
|
},
|
||||||
|
|
|
@ -245,6 +245,7 @@ func (*RootCmd) proxyServer() *clibase.Cmd {
|
||||||
SecureAuthCookie: cfg.SecureAuthCookie.Value(),
|
SecureAuthCookie: cfg.SecureAuthCookie.Value(),
|
||||||
DisablePathApps: cfg.DisablePathApps.Value(),
|
DisablePathApps: cfg.DisablePathApps.Value(),
|
||||||
ProxySessionToken: proxySessionToken.Value(),
|
ProxySessionToken: proxySessionToken.Value(),
|
||||||
|
AllowAllCors: cfg.Dangerous.AllowAllCors.Value(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("create workspace proxy: %w", err)
|
return xerrors.Errorf("create workspace proxy: %w", err)
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/cors"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
@ -61,6 +60,10 @@ type Options struct {
|
||||||
DisablePathApps bool
|
DisablePathApps bool
|
||||||
|
|
||||||
ProxySessionToken string
|
ProxySessionToken string
|
||||||
|
// AllowAllCors will set all CORs headers to '*'.
|
||||||
|
// By default, CORs is set to accept external requests
|
||||||
|
// from the dashboardURL. This should only be used in development.
|
||||||
|
AllowAllCors bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) Validate() error {
|
func (o *Options) Validate() error {
|
||||||
|
@ -189,18 +192,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
|
||||||
|
|
||||||
// The primary coderd dashboard needs to make some GET requests to
|
// The primary coderd dashboard needs to make some GET requests to
|
||||||
// the workspace proxies to check latency.
|
// the workspace proxies to check latency.
|
||||||
corsMW := cors.Handler(cors.Options{
|
corsMW := httpmw.Cors(opts.AllowAllCors, opts.DashboardURL.String())
|
||||||
AllowedOrigins: []string{
|
|
||||||
// Allow the dashboard to make requests to the proxy for latency
|
|
||||||
// checks.
|
|
||||||
opts.DashboardURL.String(),
|
|
||||||
},
|
|
||||||
// Only allow GET requests for latency checks.
|
|
||||||
AllowedMethods: []string{http.MethodOptions, http.MethodGet},
|
|
||||||
AllowedHeaders: []string{"Accept", "Content-Type", "X-LATENCY-CHECK", "X-CSRF-TOKEN"},
|
|
||||||
// Do not send any cookies
|
|
||||||
AllowCredentials: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
apiRateLimiter := httpmw.RateLimit(opts.APIRateLimit, time.Minute)
|
apiRateLimiter := httpmw.RateLimit(opts.APIRateLimit, time.Minute)
|
||||||
|
@ -266,7 +258,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
|
||||||
// See coderd/coderd.go for why we need this.
|
// See coderd/coderd.go for why we need this.
|
||||||
rootRouter := chi.NewRouter()
|
rootRouter := chi.NewRouter()
|
||||||
// Make sure to add the cors middleware to the latency check route.
|
// Make sure to add the cors middleware to the latency check route.
|
||||||
rootRouter.Get("/latency-check", corsMW(coderd.LatencyCheck(s.DashboardURL, s.AppServer.AccessURL)).ServeHTTP)
|
rootRouter.Get("/latency-check", corsMW(coderd.LatencyCheck(opts.AllowAllCors, s.DashboardURL, s.AppServer.AccessURL)).ServeHTTP)
|
||||||
rootRouter.Mount("/", r)
|
rootRouter.Mount("/", r)
|
||||||
s.Handler = rootRouter
|
s.Handler = rootRouter
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,7 @@ fatal() {
|
||||||
trap 'fatal "Script encountered an error"' ERR
|
trap 'fatal "Script encountered an error"' ERR
|
||||||
|
|
||||||
cdroot
|
cdroot
|
||||||
start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --experiments "*" "$@"
|
start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --dangerous-allow-cors-requests=true --experiments "*" "$@"
|
||||||
|
|
||||||
echo '== Waiting for Coder to become ready'
|
echo '== Waiting for Coder to become ready'
|
||||||
# Start the timeout in the background so interrupting this script
|
# Start the timeout in the background so interrupting this script
|
||||||
|
@ -185,7 +185,7 @@ fatal() {
|
||||||
# Create the proxy
|
# Create the proxy
|
||||||
proxy_session_token=$("${CODER_DEV_SHIM}" wsproxy create --name=local-proxy --display-name="Local Proxy" --icon="/emojis/1f4bb.png" --only-token)
|
proxy_session_token=$("${CODER_DEV_SHIM}" wsproxy create --name=local-proxy --display-name="Local Proxy" --icon="/emojis/1f4bb.png" --only-token)
|
||||||
# Start the proxy
|
# Start the proxy
|
||||||
start_cmd PROXY "" "${CODER_DEV_SHIM}" wsproxy server --http-address=localhost:3010 --proxy-session-token="${proxy_session_token}" --primary-access-url=http://localhost:3000
|
start_cmd PROXY "" "${CODER_DEV_SHIM}" wsproxy server --dangerous-allow-cors-requests=true --http-address=localhost:3010 --proxy-session-token="${proxy_session_token}" --primary-access-url=http://localhost:3000
|
||||||
) || echo "Failed to create workspace proxy. No workspace proxy created."
|
) || echo "Failed to create workspace proxy. No workspace proxy created."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
@ -313,6 +313,7 @@ export interface DERPServerConfig {
|
||||||
export interface DangerousConfig {
|
export interface DangerousConfig {
|
||||||
readonly allow_path_app_sharing: boolean
|
readonly allow_path_app_sharing: boolean
|
||||||
readonly allow_path_app_site_owner_access: boolean
|
readonly allow_path_app_site_owner_access: boolean
|
||||||
|
readonly allow_all_cors: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
|
|
Loading…
Reference in New Issue