chore: Dynamic CSP connect-src to support terminals connecting to workspace proxies (#7352)

* chore: Expose proxy hostnames to csp header
This commit is contained in:
Steven Masley
2023-05-02 08:30:44 -05:00
committed by GitHub
parent 465fe8658d
commit a1db82582f
6 changed files with 220 additions and 117 deletions

View File

@ -250,6 +250,10 @@ func New(ctx context.Context, options *Options) (*API, error) {
// Force the initial loading of the cache. Do this in a go routine in case
// the calls to the workspace proxies hang and this takes some time.
go api.forceWorkspaceProxyHealthUpdate(ctx)
// Use proxy health to return the healthy workspace proxy hostnames.
f := api.ProxyHealth.ProxyHosts
api.AGPL.WorkspaceProxyHostsFn.Store(&f)
}
err = api.updateEntitlements(ctx)

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
@ -59,7 +60,9 @@ type ProxyHealth struct {
logger slog.Logger
client *http.Client
cache *atomic.Pointer[map[uuid.UUID]ProxyStatus]
// Cached values for quick access to the health of proxies.
cache *atomic.Pointer[map[uuid.UUID]ProxyStatus]
proxyHosts *atomic.Pointer[[]string]
// PromMetrics
healthCheckDuration prometheus.Histogram
@ -112,6 +115,7 @@ func New(opts *Options) (*ProxyHealth, error) {
logger: opts.Logger,
client: client,
cache: &atomic.Pointer[map[uuid.UUID]ProxyStatus]{},
proxyHosts: &atomic.Pointer[[]string]{},
healthCheckDuration: healthCheckDuration,
healthCheckResults: healthCheckResults,
}, nil
@ -133,12 +137,24 @@ func (p *ProxyHealth) Run(ctx context.Context) {
p.logger.Error(ctx, "proxy health check failed", slog.Error(err))
continue
}
// Store the statuses in the cache.
p.cache.Store(&statuses)
p.storeProxyHealth(statuses)
}
}
}
func (p *ProxyHealth) storeProxyHealth(statuses map[uuid.UUID]ProxyStatus) {
var proxyHosts []string
for _, s := range statuses {
if s.ProxyHost != "" {
proxyHosts = append(proxyHosts, s.ProxyHost)
}
}
// Store the statuses in the cache before any other quick values.
p.cache.Store(&statuses)
p.proxyHosts.Store(&proxyHosts)
}
// ForceUpdate runs a single health check and updates the cache. If the health
// check fails, the cache is not updated and an error is returned. This is useful
// to trigger an update when a proxy is created or deleted.
@ -148,8 +164,7 @@ func (p *ProxyHealth) ForceUpdate(ctx context.Context) error {
return err
}
// Store the statuses in the cache.
p.cache.Store(&statuses)
p.storeProxyHealth(statuses)
return nil
}
@ -168,12 +183,28 @@ type ProxyStatus struct {
// useful to know as it helps determine if the proxy checked has different values
// then the proxy in hand. AKA if the proxy was updated, and the status was for
// an older proxy.
Proxy database.WorkspaceProxy
Proxy database.WorkspaceProxy
// ProxyHost is the host:port of the proxy url. This is included in the status
// to make sure the proxy url is a valid URL. It also makes it easier to
// escalate errors if the url.Parse errors (should never happen).
ProxyHost string
Status Status
Report codersdk.ProxyHealthReport
CheckedAt time.Time
}
// ProxyHosts returns the host:port of all healthy proxies.
// This can be computed from HealthStatus, but is cached to avoid the
// caller needing to loop over all proxies to compute this on all
// static web requests.
func (p *ProxyHealth) ProxyHosts() []string {
ptr := p.proxyHosts.Load()
if ptr == nil {
return []string{}
}
return *ptr
}
// runOnce runs the health check for all workspace proxies. If there is an
// unexpected error, an error is returned. Expected errors will mark a proxy as
// unreachable.
@ -248,6 +279,7 @@ func (p *ProxyHealth) runOnce(ctx context.Context, now time.Time) (map[uuid.UUID
status.Status = Unhealthy
break
}
status.Status = Healthy
case err == nil && resp.StatusCode != http.StatusOK:
// Unhealthy as we did reach the proxy but it got an unexpected response.
@ -262,6 +294,15 @@ func (p *ProxyHealth) runOnce(ctx context.Context, now time.Time) (map[uuid.UUID
status.Status = Unknown
}
u, err := url.Parse(proxy.Url)
if err != nil {
// This should never happen. This would mean the proxy sent
// us an invalid url?
status.Report.Errors = append(status.Report.Errors, fmt.Sprintf("failed to parse proxy url: %s", err.Error()))
status.Status = Unhealthy
}
status.ProxyHost = u.Host
// Set the prometheus metric correctly.
switch status.Status {
case Healthy: