Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 56c2e94

Browse files
committed
Add tests for default CORS behavior
Signed-off-by: Danny Kopping <[email protected]>
1 parent 56c792a commit 56c2e94

File tree

2 files changed

+377
-35
lines changed

2 files changed

+377
-35
lines changed

coderd/workspaceapps/apptest/apptest.go

Lines changed: 310 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,7 +1388,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
13881388
forceURLTransport(t, client)
13891389

13901390
// Create workspace.
1391-
port := appServer(t, nil, false)
1391+
port := appServer(t, nil, false, nil)
13921392
workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port, false)
13931393

13941394
// Verify that the apps have the correct sharing levels set.
@@ -1399,10 +1399,12 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
13991399
agnt = workspaceBuild.Resources[0].Agents[0]
14001400
found := map[string]codersdk.WorkspaceAppSharingLevel{}
14011401
expected := map[string]codersdk.WorkspaceAppSharingLevel{
1402-
proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner,
1403-
proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner,
1404-
proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated,
1405-
proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic,
1402+
proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner,
1403+
proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner,
1404+
proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated,
1405+
proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic,
1406+
proxyTestAppNameAuthenticatedCORSDefault: codersdk.WorkspaceAppSharingLevelAuthenticated,
1407+
proxyTestAppNamePublicCORSDefault: codersdk.WorkspaceAppSharingLevelPublic,
14061408
}
14071409
for _, app := range agnt.Apps {
14081410
found[app.DisplayName] = app.SharingLevel
@@ -1559,6 +1561,9 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
15591561

15601562
// Unauthenticated user should not have any access.
15611563
verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, clientWithNoAuth, false, true)
1564+
1565+
// Unauthenticated user should not have any access, regardless of CORS behavior (using default).
1566+
verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticatedCORSDefault, clientWithNoAuth, false, true)
15621567
})
15631568

15641569
t.Run("LevelPublic", func(t *testing.T) {
@@ -1831,6 +1836,306 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
18311836
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
18321837
require.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
18331838
})
1839+
1840+
t.Run("WorkspaceApplicationCORS", func(t *testing.T) {
1841+
t.Parallel()
1842+
1843+
const external = "https://example.com"
1844+
1845+
unauthenticatedClient := func(t *testing.T, appDetails *Details) *codersdk.Client {
1846+
c := appDetails.AppClient(t)
1847+
c.SetSessionToken("")
1848+
return c
1849+
}
1850+
1851+
authenticatedClient := func(t *testing.T, appDetails *Details) *codersdk.Client {
1852+
uc, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
1853+
c := appDetails.AppClient(t)
1854+
c.SetSessionToken(uc.SessionToken())
1855+
return c
1856+
}
1857+
1858+
ownerClient := func(t *testing.T, appDetails *Details) *codersdk.Client {
1859+
return appDetails.SDKClient
1860+
}
1861+
1862+
ownSubdomain := func(details *Details, app App) string {
1863+
url := details.SubdomainAppURL(app)
1864+
return url.Scheme + "://" + url.Host
1865+
}
1866+
1867+
externalOrigin := func(*Details, App) string {
1868+
return external
1869+
}
1870+
1871+
tests := []struct {
1872+
name string
1873+
app func(details *Details) App
1874+
client func(t *testing.T, appDetails *Details) *codersdk.Client
1875+
httpMethod string
1876+
origin func(details *Details, app App) string
1877+
expectedStatusCode int
1878+
checkRequestHeaders func(t *testing.T, origin string, req http.Header)
1879+
checkResponseHeaders func(t *testing.T, origin string, resp http.Header)
1880+
}{
1881+
// Public
1882+
{
1883+
// The default behavior is to a accept preflight request if it matches the app's own subdomain.
1884+
name: "Default/Public/Preflight/Subdomain",
1885+
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
1886+
client: unauthenticatedClient,
1887+
httpMethod: http.MethodOptions,
1888+
origin: ownSubdomain,
1889+
expectedStatusCode: http.StatusOK,
1890+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
1891+
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
1892+
assert.Contains(t, resp.Get("Access-Control-Allow-Methods"), http.MethodGet)
1893+
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
1894+
assert.Equal(t, "X-Got-Host", resp.Get("Access-Control-Allow-Headers"))
1895+
},
1896+
},
1897+
{
1898+
// The default behavior is to reject a preflight request from origins other than the app's own subdomain.
1899+
name: "Default/Public/Preflight/External",
1900+
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
1901+
client: unauthenticatedClient,
1902+
httpMethod: http.MethodOptions,
1903+
origin: externalOrigin,
1904+
expectedStatusCode: http.StatusOK,
1905+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
1906+
// We don't add a valid Allow-Origin header for requests we won't proxy.
1907+
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
1908+
},
1909+
},
1910+
{
1911+
// An unauthenticated request to the app is allowed from its own subdomain.
1912+
name: "Default/Public/GET/Subdomain",
1913+
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
1914+
client: unauthenticatedClient,
1915+
origin: ownSubdomain,
1916+
httpMethod: http.MethodGet,
1917+
expectedStatusCode: http.StatusOK,
1918+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
1919+
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
1920+
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
1921+
// Added by the app handler.
1922+
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
1923+
},
1924+
},
1925+
{
1926+
// An unauthenticated request to the app is allowed from an external origin, but the CORS
1927+
// headers are not added to the response because it's not coming from a known origin.
1928+
name: "Default/Public/GET/External",
1929+
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
1930+
client: unauthenticatedClient,
1931+
origin: externalOrigin,
1932+
httpMethod: http.MethodGet,
1933+
expectedStatusCode: http.StatusOK,
1934+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
1935+
// We don't add a valid Allow-Origin header for requests we won't proxy.
1936+
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
1937+
},
1938+
},
1939+
{
1940+
// The owner can access their own apps from their own subdomain with valid CORS headers.
1941+
name: "Default/Public/GET/SubdomainOwner",
1942+
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
1943+
client: ownerClient,
1944+
origin: ownSubdomain,
1945+
httpMethod: http.MethodGet,
1946+
expectedStatusCode: http.StatusOK,
1947+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
1948+
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
1949+
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
1950+
// Added by the app handler.
1951+
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
1952+
},
1953+
},
1954+
{
1955+
// The owner can't access their own apps from an external origin with valid CORS headers.
1956+
name: "Default/Public/GET/ExternalOwner",
1957+
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
1958+
client: ownerClient,
1959+
origin: externalOrigin,
1960+
httpMethod: http.MethodGet,
1961+
expectedStatusCode: http.StatusOK,
1962+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
1963+
// We don't add a valid Allow-Origin header for requests we won't proxy.
1964+
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
1965+
},
1966+
},
1967+
{
1968+
// A request without an Origin header would be rejected by an actual browser since it lacks CORS headers,
1969+
// but we accept it since it's common for non-browser clients to not send the Origin header.
1970+
name: "Default/Public/GET/NoOrigin",
1971+
app: func(details *Details) App { return details.Apps.PublicCORSDefault },
1972+
client: unauthenticatedClient,
1973+
origin: func(*Details, App) string { return "" },
1974+
httpMethod: http.MethodGet,
1975+
expectedStatusCode: http.StatusOK,
1976+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
1977+
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
1978+
assert.Empty(t, resp.Get("Access-Control-Allow-Headers"))
1979+
assert.Empty(t, resp.Get("Access-Control-Allow-Credentials"))
1980+
// Added by the app handler.
1981+
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
1982+
},
1983+
},
1984+
// Authenticated
1985+
{
1986+
// Same behavior as Default/Public/Preflight/Subdomain.
1987+
name: "Default/Authenticated/Preflight/Subdomain",
1988+
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
1989+
client: authenticatedClient,
1990+
origin: ownSubdomain,
1991+
httpMethod: http.MethodOptions,
1992+
expectedStatusCode: http.StatusOK,
1993+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
1994+
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
1995+
assert.Contains(t, resp.Get("Access-Control-Allow-Methods"), http.MethodGet)
1996+
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
1997+
assert.Equal(t, "X-Got-Host", resp.Get("Access-Control-Allow-Headers"))
1998+
},
1999+
},
2000+
{
2001+
// Same behavior as Default/Public/Preflight/External.
2002+
name: "Default/Authenticated/Preflight/External",
2003+
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
2004+
client: authenticatedClient,
2005+
origin: externalOrigin,
2006+
httpMethod: http.MethodOptions,
2007+
expectedStatusCode: http.StatusOK,
2008+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
2009+
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
2010+
},
2011+
},
2012+
{
2013+
// An authenticated request to the app is allowed from its own subdomain.
2014+
name: "Default/Authenticated/GET/Subdomain",
2015+
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
2016+
client: authenticatedClient,
2017+
origin: ownSubdomain,
2018+
httpMethod: http.MethodGet,
2019+
expectedStatusCode: http.StatusOK,
2020+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
2021+
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
2022+
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
2023+
// Added by the app handler.
2024+
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
2025+
},
2026+
},
2027+
{
2028+
// An authenticated request to the app is allowed from an external origin.
2029+
// The origin doesn't match the app's own subdomain, so the CORS headers are not added.
2030+
name: "Default/Authenticated/GET/External",
2031+
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
2032+
client: authenticatedClient,
2033+
origin: externalOrigin,
2034+
httpMethod: http.MethodGet,
2035+
expectedStatusCode: http.StatusOK,
2036+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
2037+
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
2038+
assert.Empty(t, resp.Get("Access-Control-Allow-Headers"))
2039+
assert.Empty(t, resp.Get("Access-Control-Allow-Credentials"))
2040+
// Added by the app handler.
2041+
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
2042+
},
2043+
},
2044+
{
2045+
// Owners can access their own apps from their own subdomain with valid CORS headers.
2046+
name: "Default/Authenticated/GET/SubdomainOwner",
2047+
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
2048+
client: ownerClient,
2049+
origin: ownSubdomain,
2050+
httpMethod: http.MethodGet,
2051+
expectedStatusCode: http.StatusOK,
2052+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
2053+
assert.Equal(t, origin, resp.Get("Access-Control-Allow-Origin"))
2054+
assert.Equal(t, "true", resp.Get("Access-Control-Allow-Credentials"))
2055+
// Added by the app handler.
2056+
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
2057+
},
2058+
},
2059+
{
2060+
// Owners can't access their own apps from an external origin with valid CORS headers.
2061+
name: "Default/Owner/GET/ExternalOwner",
2062+
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
2063+
client: ownerClient,
2064+
origin: externalOrigin,
2065+
httpMethod: http.MethodGet,
2066+
expectedStatusCode: http.StatusOK,
2067+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
2068+
// We don't add a valid Allow-Origin header for requests we won't proxy.
2069+
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
2070+
},
2071+
},
2072+
{
2073+
// Same behavior as Default/Public/GET/NoOrigin.
2074+
name: "Default/Authenticated/GET/NoOrigin",
2075+
app: func(details *Details) App { return details.Apps.AuthenticatedCORSDefault },
2076+
client: authenticatedClient,
2077+
origin: func(*Details, App) string { return "" },
2078+
httpMethod: http.MethodGet,
2079+
expectedStatusCode: http.StatusOK,
2080+
checkResponseHeaders: func(t *testing.T, origin string, resp http.Header) {
2081+
assert.Empty(t, resp.Get("Access-Control-Allow-Origin"))
2082+
assert.Empty(t, resp.Get("Access-Control-Allow-Headers"))
2083+
assert.Empty(t, resp.Get("Access-Control-Allow-Credentials"))
2084+
// Added by the app handler.
2085+
assert.Equal(t, "simple", resp.Get("X-CORS-Handler"))
2086+
},
2087+
},
2088+
}
2089+
2090+
for _, tc := range tests {
2091+
t.Run(tc.name, func(t *testing.T) {
2092+
t.Parallel()
2093+
2094+
ctx := testutil.Context(t, testutil.WaitLong)
2095+
2096+
var reqHeaders http.Header
2097+
2098+
// Given: a workspace app
2099+
appDetails := setupProxyTest(t, &DeploymentOptions{
2100+
// Setup an HTTP handler which is the "app"; this handler conditionally responds
2101+
// to requests based on the CORS behavior
2102+
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2103+
_, err := r.Cookie(codersdk.SessionTokenCookie)
2104+
assert.ErrorIs(t, err, http.ErrNoCookie)
2105+
2106+
// Store the request headers for later assertions
2107+
reqHeaders = r.Header
2108+
w.Header().Set("X-CORS-Handler", "simple")
2109+
}),
2110+
})
2111+
2112+
// Given: a client
2113+
client := tc.client(t, appDetails)
2114+
path := appDetails.SubdomainAppURL(tc.app(appDetails)).String()
2115+
origin := tc.origin(appDetails, tc.app(appDetails))
2116+
2117+
// When: a preflight request is made to an app with a specified CORS behavior
2118+
resp, err := requestWithRetries(ctx, t, client, tc.httpMethod, path, nil, func(r *http.Request) {
2119+
// Mimic non-browser clients that don't send the Origin header.
2120+
if origin != "" {
2121+
r.Header.Set("Origin", origin)
2122+
}
2123+
r.Header.Set("Access-Control-Request-Method", "GET")
2124+
r.Header.Set("Access-Control-Request-Headers", "X-Got-Host")
2125+
})
2126+
require.NoError(t, err)
2127+
defer resp.Body.Close()
2128+
2129+
// Then: the request & response must match expectations
2130+
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
2131+
assert.NoError(t, err)
2132+
if tc.checkRequestHeaders != nil {
2133+
tc.checkRequestHeaders(t, origin, reqHeaders)
2134+
}
2135+
tc.checkResponseHeaders(t, origin, resp.Header)
2136+
})
2137+
}
2138+
})
18342139
}
18352140

18362141
type fakeStatsReporter struct {

0 commit comments

Comments
 (0)