@@ -1388,7 +1388,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
1388
1388
forceURLTransport (t , client )
1389
1389
1390
1390
// Create workspace.
1391
- port := appServer (t , nil , false )
1391
+ port := appServer (t , nil , false , nil )
1392
1392
workspace , _ = createWorkspaceWithApps (t , client , user .OrganizationIDs [0 ], user , port , false )
1393
1393
1394
1394
// Verify that the apps have the correct sharing levels set.
@@ -1399,10 +1399,12 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
1399
1399
agnt = workspaceBuild .Resources [0 ].Agents [0 ]
1400
1400
found := map [string ]codersdk.WorkspaceAppSharingLevel {}
1401
1401
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 ,
1406
1408
}
1407
1409
for _ , app := range agnt .Apps {
1408
1410
found [app .DisplayName ] = app .SharingLevel
@@ -1559,6 +1561,9 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
1559
1561
1560
1562
// Unauthenticated user should not have any access.
1561
1563
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 )
1562
1567
})
1563
1568
1564
1569
t .Run ("LevelPublic" , func (t * testing.T ) {
@@ -1831,6 +1836,306 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
1831
1836
require .Equal (t , http .StatusBadRequest , resp .StatusCode )
1832
1837
require .Equal (t , "text/html; charset=utf-8" , resp .Header .Get ("Content-Type" ))
1833
1838
})
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
+ })
1834
2139
}
1835
2140
1836
2141
type fakeStatsReporter struct {
0 commit comments