2022-06-04 20:13:37 +00:00
|
|
|
package coderd_test
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"net"
|
2023-04-17 19:57:21 +00:00
|
|
|
"net/http"
|
2022-09-13 17:31:33 +00:00
|
|
|
"net/url"
|
2022-06-04 20:13:37 +00:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2023-08-18 18:55:43 +00:00
|
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
|
|
"github.com/coder/coder/v2/coderd/httpmw"
|
|
|
|
"github.com/coder/coder/v2/coderd/workspaceapps"
|
|
|
|
"github.com/coder/coder/v2/coderd/workspaceapps/apptest"
|
|
|
|
"github.com/coder/coder/v2/codersdk"
|
|
|
|
"github.com/coder/coder/v2/testutil"
|
2024-03-15 16:24:38 +00:00
|
|
|
"github.com/coder/serpent"
|
2022-06-04 20:13:37 +00:00
|
|
|
)
|
|
|
|
|
2022-09-22 22:30:32 +00:00
|
|
|
func TestGetAppHost(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2022-12-15 18:43:00 +00:00
|
|
|
cases := []struct {
|
|
|
|
name string
|
|
|
|
accessURL string
|
|
|
|
appHostname string
|
|
|
|
expected string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "OK",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
expected: "*.test.coder.com",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "None",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "",
|
|
|
|
expected: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "OKWithPort",
|
|
|
|
accessURL: "https://test.coder.com:8443",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
expected: "*.test.coder.com:8443",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "OKWithSuffix",
|
|
|
|
accessURL: "https://test.coder.com:8443",
|
|
|
|
appHostname: "*--suffix.test.coder.com",
|
|
|
|
expected: "*--suffix.test.coder.com:8443",
|
|
|
|
},
|
|
|
|
}
|
2022-09-22 22:30:32 +00:00
|
|
|
for _, c := range cases {
|
|
|
|
c := c
|
2022-12-15 18:43:00 +00:00
|
|
|
t.Run(c.name, func(t *testing.T) {
|
2022-09-22 22:30:32 +00:00
|
|
|
t.Parallel()
|
2022-12-15 18:43:00 +00:00
|
|
|
|
|
|
|
accessURL, err := url.Parse(c.accessURL)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2022-09-22 22:30:32 +00:00
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
2022-12-15 18:43:00 +00:00
|
|
|
AccessURL: accessURL,
|
|
|
|
AppHostname: c.appHostname,
|
2022-09-22 22:30:32 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// Should not leak to unauthenticated users.
|
2023-01-29 21:47:24 +00:00
|
|
|
host, err := client.AppHost(ctx)
|
2022-09-22 22:30:32 +00:00
|
|
|
require.Error(t, err)
|
|
|
|
require.Equal(t, "", host.Host)
|
|
|
|
|
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
2023-01-29 21:47:24 +00:00
|
|
|
host, err = client.AppHost(ctx)
|
2022-09-22 22:30:32 +00:00
|
|
|
require.NoError(t, err)
|
2022-12-15 18:43:00 +00:00
|
|
|
require.Equal(t, c.expected, host.Host)
|
2022-09-22 22:30:32 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-17 19:57:21 +00:00
|
|
|
func TestWorkspaceApplicationAuth(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
cases := []struct {
|
|
|
|
name string
|
|
|
|
accessURL string
|
|
|
|
appHostname string
|
|
|
|
proxyURL string
|
|
|
|
proxyAppHostname string
|
|
|
|
|
|
|
|
redirectURI string
|
|
|
|
expectRedirect string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "OK",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
proxyURL: "https://proxy.test.coder.com",
|
|
|
|
proxyAppHostname: "*.proxy.test.coder.com",
|
|
|
|
redirectURI: "https://something.test.coder.com",
|
|
|
|
expectRedirect: "https://something.test.coder.com",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "ProxyPathOK",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
proxyURL: "https://proxy.test.coder.com",
|
|
|
|
proxyAppHostname: "*.proxy.test.coder.com",
|
|
|
|
redirectURI: "https://proxy.test.coder.com/path",
|
|
|
|
expectRedirect: "https://proxy.test.coder.com/path",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "ProxySubdomainOK",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
proxyURL: "https://proxy.test.coder.com",
|
|
|
|
proxyAppHostname: "*.proxy.test.coder.com",
|
|
|
|
redirectURI: "https://something.proxy.test.coder.com/path?yeah=true",
|
|
|
|
expectRedirect: "https://something.proxy.test.coder.com/path?yeah=true",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "ProxySubdomainSuffixOK",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
proxyURL: "https://proxy.test.coder.com",
|
|
|
|
proxyAppHostname: "*--suffix.proxy.test.coder.com",
|
|
|
|
redirectURI: "https://something--suffix.proxy.test.coder.com/",
|
|
|
|
expectRedirect: "https://something--suffix.proxy.test.coder.com/",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "NormalizeSchemePrimaryAppHostname",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
proxyURL: "https://proxy.test.coder.com",
|
|
|
|
proxyAppHostname: "*.proxy.test.coder.com",
|
|
|
|
redirectURI: "http://x.test.coder.com",
|
|
|
|
expectRedirect: "https://x.test.coder.com",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "NormalizeSchemeProxyAppHostname",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
proxyURL: "https://proxy.test.coder.com",
|
|
|
|
proxyAppHostname: "*.proxy.test.coder.com",
|
|
|
|
redirectURI: "http://x.proxy.test.coder.com",
|
|
|
|
expectRedirect: "https://x.proxy.test.coder.com",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "NoneError",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
proxyURL: "https://proxy.test.coder.com",
|
|
|
|
proxyAppHostname: "*.proxy.test.coder.com",
|
|
|
|
redirectURI: "",
|
|
|
|
expectRedirect: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "PrimaryAccessURLError",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
proxyURL: "https://proxy.test.coder.com",
|
|
|
|
proxyAppHostname: "*.proxy.test.coder.com",
|
|
|
|
redirectURI: "https://test.coder.com/",
|
|
|
|
expectRedirect: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "OtherError",
|
|
|
|
accessURL: "https://test.coder.com",
|
|
|
|
appHostname: "*.test.coder.com",
|
|
|
|
proxyURL: "https://proxy.test.coder.com",
|
|
|
|
proxyAppHostname: "*.proxy.test.coder.com",
|
|
|
|
redirectURI: "https://example.com/",
|
|
|
|
expectRedirect: "",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, c := range cases {
|
|
|
|
c := c
|
|
|
|
t.Run(c.name, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
db, pubsub := dbtestutil.NewDB(t)
|
|
|
|
|
|
|
|
accessURL, err := url.Parse(c.accessURL)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
|
|
Database: db,
|
|
|
|
Pubsub: pubsub,
|
|
|
|
AccessURL: accessURL,
|
|
|
|
AppHostname: c.appHostname,
|
|
|
|
})
|
|
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
|
|
|
|
// Disable redirects.
|
|
|
|
client.HTTPClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
|
|
|
|
return http.ErrUseLastResponse
|
|
|
|
}
|
|
|
|
|
|
|
|
_, _ = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{
|
|
|
|
Url: c.proxyURL,
|
|
|
|
WildcardHostname: c.proxyAppHostname,
|
|
|
|
})
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
resp, err := client.Request(ctx, http.MethodGet, "/api/v2/applications/auth-redirect", nil, func(req *http.Request) {
|
|
|
|
q := req.URL.Query()
|
|
|
|
q.Set("redirect_uri", c.redirectURI)
|
|
|
|
req.URL.RawQuery = q.Encode()
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusSeeOther {
|
|
|
|
err = codersdk.ReadBodyAsError(resp)
|
|
|
|
if c.expectRedirect == "" {
|
|
|
|
require.Error(t, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if c.expectRedirect == "" {
|
|
|
|
t.Fatal("expected a failure but got a success")
|
|
|
|
}
|
|
|
|
|
|
|
|
loc, err := resp.Location()
|
|
|
|
require.NoError(t, err)
|
|
|
|
q := loc.Query()
|
|
|
|
|
|
|
|
// Verify the API key is set.
|
|
|
|
encryptedAPIKey := loc.Query().Get(workspaceapps.SubdomainProxyAPIKeyParam)
|
|
|
|
require.NotEmpty(t, encryptedAPIKey, "no API key was set in the query parameters")
|
|
|
|
|
|
|
|
// Strip the API key from the actual redirect URI and compare.
|
|
|
|
q.Del(workspaceapps.SubdomainProxyAPIKeyParam)
|
|
|
|
loc.RawQuery = q.Encode()
|
|
|
|
require.Equal(t, c.expectRedirect, loc.String())
|
|
|
|
|
|
|
|
// The decrypted key is verified in the apptest test suite.
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-06 21:35:27 +00:00
|
|
|
func TestWorkspaceApps(t *testing.T) {
|
2022-09-13 17:31:33 +00:00
|
|
|
t.Parallel()
|
2023-01-18 22:56:14 +00:00
|
|
|
|
2023-05-15 00:37:00 +00:00
|
|
|
apptest.Run(t, true, func(t *testing.T, opts *apptest.DeploymentOptions) *apptest.Deployment {
|
2023-03-07 21:10:01 +00:00
|
|
|
deploymentValues := coderdtest.DeploymentValues(t)
|
2024-03-15 16:24:38 +00:00
|
|
|
deploymentValues.DisablePathApps = serpent.Bool(opts.DisablePathApps)
|
|
|
|
deploymentValues.Dangerous.AllowPathAppSharing = serpent.Bool(opts.DangerousAllowPathAppSharing)
|
|
|
|
deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = serpent.Bool(opts.DangerousAllowPathAppSiteOwnerAccess)
|
2024-02-13 14:31:20 +00:00
|
|
|
deploymentValues.Experiments = append(deploymentValues.Experiments, string(codersdk.ExperimentSharedPorts))
|
2022-12-07 12:55:02 +00:00
|
|
|
|
2023-04-17 19:57:21 +00:00
|
|
|
if opts.DisableSubdomainApps {
|
|
|
|
opts.AppHost = ""
|
|
|
|
}
|
|
|
|
|
2024-01-16 14:06:39 +00:00
|
|
|
flushStatsCollectorCh := make(chan chan<- struct{}, 1)
|
|
|
|
opts.StatsCollectorOptions.Flush = flushStatsCollectorCh
|
|
|
|
flushStats := func() {
|
|
|
|
flushStatsCollectorDone := make(chan struct{}, 1)
|
|
|
|
flushStatsCollectorCh <- flushStatsCollectorDone
|
|
|
|
<-flushStatsCollectorDone
|
|
|
|
}
|
2022-12-07 12:55:02 +00:00
|
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
2023-04-06 21:35:27 +00:00
|
|
|
DeploymentValues: deploymentValues,
|
|
|
|
AppHostname: opts.AppHost,
|
|
|
|
IncludeProvisionerDaemon: true,
|
2022-12-07 12:55:02 +00:00
|
|
|
RealIPConfig: &httpmw.RealIPConfig{
|
|
|
|
TrustedOrigins: []*net.IPNet{{
|
|
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
|
|
Mask: net.CIDRMask(8, 32),
|
|
|
|
}},
|
|
|
|
TrustedHeaders: []string{
|
|
|
|
"CF-Connecting-IP",
|
|
|
|
},
|
|
|
|
},
|
2023-08-16 12:22:00 +00:00
|
|
|
WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions,
|
2022-12-07 12:55:02 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
|
|
|
2023-04-06 21:35:27 +00:00
|
|
|
return &apptest.Deployment{
|
2023-05-15 00:37:00 +00:00
|
|
|
Options: opts,
|
|
|
|
SDKClient: client,
|
|
|
|
FirstUser: user,
|
|
|
|
PathAppBaseURL: client.URL,
|
2024-01-16 14:06:39 +00:00
|
|
|
FlushStats: flushStats,
|
2022-12-07 12:55:02 +00:00
|
|
|
}
|
2023-01-18 22:56:14 +00:00
|
|
|
})
|
|
|
|
}
|