mirror of https://github.com/coder/coder.git
fix(security)!: path-based app sharing changes (#5772)
This commit disables path-based app sharing by default. It is possible for a workspace app on a path (not a subdomain) to make API requests to the Coder API. When accessing your own workspace, this is not much of a problem. When accessing a shared workspace app, the workspace owner could include malicious javascript in the page that makes requests to the Coder API on behalf of the visitor. This vulnerability does not affect subdomain apps. - Disables path-based app sharing by default. Previous behavior can be restored using the `--dangerous-allow-path-app-sharing` flag which is not recommended. - Disables users with the site "owner" role from accessing path-based apps from workspaces they do not own. Previous behavior can be restored using the `--dangerous-allow-path-app-site-owner-access` flag which is not recommended. - Adds a flag `--disable-path-apps` which can be used by security-conscious admins to disable all path-based apps across the entire deployment. This check is enforced at app-access time, not at template-ingest time.
This commit is contained in:
parent
b42e2ae81f
commit
0374af23b2
|
@ -500,6 +500,26 @@ func newConfig() *codersdk.DeploymentConfig {
|
|||
Default: "",
|
||||
},
|
||||
},
|
||||
Dangerous: &codersdk.DangerousConfig{
|
||||
AllowPathAppSharing: &codersdk.DeploymentConfigField[bool]{
|
||||
Name: "DANGEROUS: Allow Path App Sharing",
|
||||
Usage: "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.",
|
||||
Flag: "dangerous-allow-path-app-sharing",
|
||||
Default: false,
|
||||
},
|
||||
AllowPathAppSiteOwnerAccess: &codersdk.DeploymentConfigField[bool]{
|
||||
Name: "DANGEROUS: Allow Site Owners to Access Path Apps",
|
||||
Usage: "Allow site-owners to access workspace apps from workspaces they do not own. Owners cannot access path-based apps they do not own by default. 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.",
|
||||
Flag: "dangerous-allow-path-app-site-owner-access",
|
||||
Default: false,
|
||||
},
|
||||
},
|
||||
DisablePathApps: &codersdk.DeploymentConfigField[bool]{
|
||||
Name: "Disable Path Apps",
|
||||
Usage: "Disable workspace apps that are not served from subdomains. Path-based apps can make requests to the Coder API and pose a security risk when the workspace serves malicious JavaScript. This is recommended for security purposes if a --wildcard-access-url is configured.",
|
||||
Flag: "disable-path-apps",
|
||||
Default: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,28 @@ Flags:
|
|||
with systemd.
|
||||
Consumes $CODER_CACHE_DIRECTORY (default
|
||||
"/tmp/coder-cli-test-cache")
|
||||
--dangerous-allow-path-app-sharing 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.
|
||||
Consumes
|
||||
$CODER_DANGEROUS_ALLOW_PATH_APP_SHARING
|
||||
--dangerous-allow-path-app-site-owner-access Allow site-owners to access workspace
|
||||
apps from workspaces they do not own.
|
||||
Owners cannot access path-based apps they
|
||||
do not own by default. 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.
|
||||
Consumes
|
||||
$CODER_DANGEROUS_ALLOW_PATH_APP_SITE_OWNER_ACCESS
|
||||
--dangerous-disable-rate-limits Disables all rate limits. This is not
|
||||
recommended in production.
|
||||
Consumes $CODER_RATE_LIMIT_DISABLE_ALL
|
||||
|
@ -61,6 +83,14 @@ Flags:
|
|||
Consumes
|
||||
$CODER_DERP_SERVER_STUN_ADDRESSES
|
||||
(default [stun.l.google.com:19302])
|
||||
--disable-path-apps Disable workspace apps that are not
|
||||
served from subdomains. Path-based apps
|
||||
can make requests to the Coder API and
|
||||
pose a security risk when the workspace
|
||||
serves malicious JavaScript. This is
|
||||
recommended for security purposes if a
|
||||
--wildcard-access-url is configured.
|
||||
Consumes $CODER_DISABLE_PATH_APPS
|
||||
--experiments strings Enable one or more experiments. These are
|
||||
not ready for production. Separate
|
||||
multiple experiments with commas, or
|
||||
|
|
|
@ -5732,6 +5732,17 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DangerousConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow_path_app_sharing": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
|
||||
},
|
||||
"allow_path_app_site_owner_access": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DeploymentConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -5764,9 +5775,15 @@ const docTemplate = `{
|
|||
"cache_directory": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
|
||||
},
|
||||
"dangerous": {
|
||||
"$ref": "#/definitions/codersdk.DangerousConfig"
|
||||
},
|
||||
"derp": {
|
||||
"$ref": "#/definitions/codersdk.DERP"
|
||||
},
|
||||
"disable_path_apps": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
|
||||
},
|
||||
"experimental": {
|
||||
"description": "DEPRECATED: Use Experiments instead.",
|
||||
"allOf": [
|
||||
|
|
|
@ -5081,6 +5081,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DangerousConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow_path_app_sharing": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
|
||||
},
|
||||
"allow_path_app_site_owner_access": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DeploymentConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -5113,9 +5124,15 @@
|
|||
"cache_directory": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
|
||||
},
|
||||
"dangerous": {
|
||||
"$ref": "#/definitions/codersdk.DangerousConfig"
|
||||
},
|
||||
"derp": {
|
||||
"$ref": "#/definitions/codersdk.DERP"
|
||||
},
|
||||
"disable_path_apps": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
|
||||
},
|
||||
"experimental": {
|
||||
"description": "DEPRECATED: Use Experiments instead.",
|
||||
"allOf": [
|
||||
|
|
|
@ -65,6 +65,13 @@ var nonCanonicalHeaders = map[string]string{
|
|||
"Sec-Websocket-Version": "Sec-WebSocket-Version",
|
||||
}
|
||||
|
||||
type workspaceAppAccessMethod string
|
||||
|
||||
const (
|
||||
workspaceAppAccessMethodPath workspaceAppAccessMethod = "path"
|
||||
workspaceAppAccessMethodSubdomain workspaceAppAccessMethod = "subdomain"
|
||||
)
|
||||
|
||||
// @Summary Get applications host
|
||||
// @ID get-applications-host
|
||||
// @Security CoderSessionToken
|
||||
|
@ -89,6 +96,17 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
|||
workspace := httpmw.WorkspaceParam(r)
|
||||
agent := httpmw.WorkspaceAgentParam(r)
|
||||
|
||||
if api.DeploymentConfig.DisablePathApps.Value {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusUnauthorized,
|
||||
Title: "Unauthorized",
|
||||
Description: "Path-based applications are disabled on this Coder deployment by the administrator.",
|
||||
RetryEnabled: false,
|
||||
DashboardURL: api.AccessURL.String(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// We do not support port proxying on paths, so lookup the app by slug.
|
||||
appSlug := chi.URLParam(r, "workspaceapp")
|
||||
app, ok := api.lookupWorkspaceApp(rw, r, agent.ID, appSlug)
|
||||
|
@ -100,7 +118,7 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
|||
if app.SharingLevel != "" {
|
||||
appSharingLevel = app.SharingLevel
|
||||
}
|
||||
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel)
|
||||
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspaceAppAccessMethodPath, workspace, appSharingLevel)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -127,11 +145,12 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
|
||||
api.proxyWorkspaceApplication(proxyApplication{
|
||||
Workspace: workspace,
|
||||
Agent: agent,
|
||||
App: &app,
|
||||
Port: 0,
|
||||
Path: chiPath,
|
||||
AccessMethod: workspaceAppAccessMethodPath,
|
||||
Workspace: workspace,
|
||||
Agent: agent,
|
||||
App: &app,
|
||||
Port: 0,
|
||||
Path: chiPath,
|
||||
}, rw, r)
|
||||
}
|
||||
|
||||
|
@ -238,11 +257,12 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht
|
|||
}
|
||||
|
||||
api.proxyWorkspaceApplication(proxyApplication{
|
||||
Workspace: workspace,
|
||||
Agent: agent,
|
||||
App: workspaceAppPtr,
|
||||
Port: app.Port,
|
||||
Path: r.URL.Path,
|
||||
AccessMethod: workspaceAppAccessMethodSubdomain,
|
||||
Workspace: workspace,
|
||||
Agent: agent,
|
||||
App: workspaceAppPtr,
|
||||
Port: app.Port,
|
||||
Path: r.URL.Path,
|
||||
}, rw, r)
|
||||
})).ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
|
@ -411,9 +431,25 @@ func (api *API) lookupWorkspaceApp(rw http.ResponseWriter, r *http.Request, agen
|
|||
return app, true
|
||||
}
|
||||
|
||||
func (api *API) authorizeWorkspaceApp(r *http.Request, sharingLevel database.AppSharingLevel, workspace database.Workspace) (bool, error) {
|
||||
//nolint:revive
|
||||
func (api *API) authorizeWorkspaceApp(r *http.Request, accessMethod workspaceAppAccessMethod, sharingLevel database.AppSharingLevel, workspace database.Workspace) (bool, error) {
|
||||
ctx := r.Context()
|
||||
|
||||
if accessMethod == "" {
|
||||
accessMethod = workspaceAppAccessMethodPath
|
||||
}
|
||||
isPathApp := accessMethod == workspaceAppAccessMethodPath
|
||||
|
||||
// If path-based app sharing is disabled (which is the default), we can
|
||||
// force the sharing level to be "owner" so that the user can only access
|
||||
// their own apps.
|
||||
//
|
||||
// Site owners are blocked from accessing path-based apps unless the
|
||||
// Dangerous.AllowPathAppSiteOwnerAccess flag is enabled in the check below.
|
||||
if isPathApp && !api.DeploymentConfig.Dangerous.AllowPathAppSharing.Value {
|
||||
sharingLevel = database.AppSharingLevelOwner
|
||||
}
|
||||
|
||||
// Short circuit if not authenticated.
|
||||
roles, ok := httpmw.UserAuthorizationOptional(r)
|
||||
if !ok {
|
||||
|
@ -422,6 +458,21 @@ func (api *API) authorizeWorkspaceApp(r *http.Request, sharingLevel database.App
|
|||
return sharingLevel == database.AppSharingLevelPublic, nil
|
||||
}
|
||||
|
||||
// Block anyone from accessing workspaces they don't own in path-based apps
|
||||
// unless the admin disables this security feature. This blocks site-owners
|
||||
// from accessing any apps from any user's workspaces.
|
||||
//
|
||||
// When the Dangerous.AllowPathAppSharing flag is not enabled, the sharing
|
||||
// level will be forced to "owner", so this check will always be true for
|
||||
// workspaces owned by different users.
|
||||
if isPathApp &&
|
||||
sharingLevel == database.AppSharingLevelOwner &&
|
||||
workspace.OwnerID != roles.ID &&
|
||||
!api.DeploymentConfig.Dangerous.AllowPathAppSiteOwnerAccess.Value {
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Do a standard RBAC check. This accounts for share level "owner" and any
|
||||
// other RBAC rules that may be in place.
|
||||
//
|
||||
|
@ -463,8 +514,8 @@ func (api *API) authorizeWorkspaceApp(r *http.Request, sharingLevel database.App
|
|||
// for a given app share level in the given workspace. The user's authorization
|
||||
// status is returned. If a server error occurs, a HTML error page is rendered
|
||||
// and false is returned so the caller can return early.
|
||||
func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, appSharingLevel database.AppSharingLevel) (authed bool, ok bool) {
|
||||
ok, err := api.authorizeWorkspaceApp(r, appSharingLevel, workspace)
|
||||
func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, accessMethod workspaceAppAccessMethod, workspace database.Workspace, appSharingLevel database.AppSharingLevel) (authed bool, ok bool) {
|
||||
ok, err := api.authorizeWorkspaceApp(r, accessMethod, appSharingLevel, workspace)
|
||||
if err != nil {
|
||||
api.Logger.Error(r.Context(), "authorize workspace app", slog.Error(err))
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
|
@ -484,8 +535,8 @@ func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Re
|
|||
// for a given app share level in the given workspace. If the user is not
|
||||
// authorized or a server error occurs, a discrete HTML error page is rendered
|
||||
// and false is returned so the caller can return early.
|
||||
func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, appSharingLevel database.AppSharingLevel) bool {
|
||||
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel)
|
||||
func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, accessMethod workspaceAppAccessMethod, workspace database.Workspace, appSharingLevel database.AppSharingLevel) bool {
|
||||
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, accessMethod, workspace, appSharingLevel)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
@ -502,7 +553,7 @@ func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Re
|
|||
// they will be redirected to the route below. If the user does have a session
|
||||
// key but insufficient permissions a static error page will be rendered.
|
||||
func (api *API) verifyWorkspaceApplicationSubdomainAuth(rw http.ResponseWriter, r *http.Request, host string, workspace database.Workspace, appSharingLevel database.AppSharingLevel) bool {
|
||||
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel)
|
||||
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspaceAppAccessMethodSubdomain, workspace, appSharingLevel)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
@ -731,8 +782,9 @@ func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request
|
|||
|
||||
// proxyApplication are the required fields to proxy a workspace application.
|
||||
type proxyApplication struct {
|
||||
Workspace database.Workspace
|
||||
Agent database.WorkspaceAgent
|
||||
AccessMethod workspaceAppAccessMethod
|
||||
Workspace database.Workspace
|
||||
Agent database.WorkspaceAgent
|
||||
|
||||
// Either App or Port must be set, but not both.
|
||||
App *database.WorkspaceApp
|
||||
|
@ -752,7 +804,7 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
|
|||
if proxyApp.App != nil && proxyApp.App.SharingLevel != "" {
|
||||
sharingLevel = proxyApp.App.SharingLevel
|
||||
}
|
||||
if !api.checkWorkspaceApplicationAuth(rw, r, proxyApp.Workspace, sharingLevel) {
|
||||
if !api.checkWorkspaceApplicationAuth(rw, r, proxyApp.AccessMethod, proxyApp.Workspace, sharingLevel) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -108,10 +108,26 @@ func TestGetAppHost(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type setupProxyTestOpts struct {
|
||||
AppHost string
|
||||
DisablePathApps bool
|
||||
DangerousAllowPathAppSharing bool
|
||||
DangerousAllowPathAppSiteOwnerAccess bool
|
||||
|
||||
NoWorkspace bool
|
||||
}
|
||||
|
||||
// setupProxyTest creates a workspace with an agent and some apps. It returns a
|
||||
// codersdk client, the first user, the workspace, and the port number the test
|
||||
// listener is running on.
|
||||
func setupProxyTest(t *testing.T, customAppHost ...string) (*codersdk.Client, codersdk.CreateFirstUserResponse, codersdk.Workspace, uint16) {
|
||||
func setupProxyTest(t *testing.T, opts *setupProxyTestOpts) (*codersdk.Client, codersdk.CreateFirstUserResponse, *codersdk.Workspace, uint16) {
|
||||
if opts == nil {
|
||||
opts = &setupProxyTestOpts{}
|
||||
}
|
||||
if opts.AppHost == "" {
|
||||
opts.AppHost = proxyTestSubdomainRaw
|
||||
}
|
||||
|
||||
// #nosec
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
require.NoError(t, err)
|
||||
|
@ -133,13 +149,14 @@ func setupProxyTest(t *testing.T, customAppHost ...string) (*codersdk.Client, co
|
|||
tcpAddr, ok := ln.Addr().(*net.TCPAddr)
|
||||
require.True(t, ok)
|
||||
|
||||
appHost := proxyTestSubdomainRaw
|
||||
if len(customAppHost) > 0 {
|
||||
appHost = customAppHost[0]
|
||||
}
|
||||
deploymentConfig := coderdtest.DeploymentConfig(t)
|
||||
deploymentConfig.DisablePathApps.Value = opts.DisablePathApps
|
||||
deploymentConfig.Dangerous.AllowPathAppSharing.Value = opts.DangerousAllowPathAppSharing
|
||||
deploymentConfig.Dangerous.AllowPathAppSiteOwnerAccess.Value = opts.DangerousAllowPathAppSiteOwnerAccess
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
AppHostname: appHost,
|
||||
DeploymentConfig: deploymentConfig,
|
||||
AppHostname: opts.AppHost,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AgentStatsRefreshInterval: time.Millisecond * 100,
|
||||
MetricsCacheRefreshInterval: time.Millisecond * 100,
|
||||
|
@ -156,23 +173,18 @@ func setupProxyTest(t *testing.T, customAppHost ...string) (*codersdk.Client, co
|
|||
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
workspace := createWorkspaceWithApps(t, client, user.OrganizationID, appHost, uint16(tcpAddr.Port))
|
||||
var workspace *codersdk.Workspace
|
||||
if !opts.NoWorkspace {
|
||||
ws := createWorkspaceWithApps(t, client, user.OrganizationID, opts.AppHost, uint16(tcpAddr.Port))
|
||||
workspace = &ws
|
||||
}
|
||||
|
||||
// Configure the HTTP client to not follow redirects and to route all
|
||||
// requests regardless of hostname to the coderd test server.
|
||||
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
transport := defaultTransport.Clone()
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, network, client.URL.Host)
|
||||
}
|
||||
client.HTTPClient.Transport = transport
|
||||
t.Cleanup(func() {
|
||||
transport.CloseIdleConnections()
|
||||
})
|
||||
forceURLTransport(t, client)
|
||||
|
||||
return client, user, workspace, uint16(tcpAddr.Port)
|
||||
}
|
||||
|
@ -234,6 +246,12 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
|||
workspace := coderdtest.CreateWorkspace(t, client, orgID, template.ID, workspaceMutators...)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
|
||||
defer cancel()
|
||||
|
||||
user, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
|
||||
agentClient := codersdk.New(client.URL)
|
||||
agentClient.SetSessionToken(authToken)
|
||||
if appHost != "" {
|
||||
|
@ -243,7 +261,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
|||
"http://{{port}}--%s--%s--%s%s",
|
||||
proxyTestAgentName,
|
||||
workspace.Name,
|
||||
"testuser",
|
||||
user.Username,
|
||||
strings.ReplaceAll(appHost, "*", ""),
|
||||
)
|
||||
if client.URL.Port() != "" {
|
||||
|
@ -265,7 +283,34 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
|||
|
||||
func TestWorkspaceAppsProxyPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
client, firstUser, workspace, _ := setupProxyTest(t)
|
||||
client, firstUser, workspace, _ := setupProxyTest(t, nil)
|
||||
|
||||
t.Run("Disabled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deploymentConfig := coderdtest.DeploymentConfig(t)
|
||||
deploymentConfig.DisablePathApps.Value = true
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
DeploymentConfig: deploymentConfig,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AgentStatsRefreshInterval: time.Millisecond * 100,
|
||||
MetricsCacheRefreshInterval: time.Millisecond * 100,
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
workspace := createWorkspaceWithApps(t, client, user.OrganizationID, "", 0)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := requestWithRetries(ctx, t, client, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s", workspace.Name, proxyTestAppNameOwner), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(body), "Path-based applications are disabled")
|
||||
})
|
||||
|
||||
t.Run("LoginWithoutAuth", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -384,7 +429,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) {
|
|||
t.Run("End-to-End", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, firstUser, workspace, _ := setupProxyTest(t)
|
||||
client, firstUser, workspace, _ := setupProxyTest(t, nil)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
@ -511,7 +556,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) {
|
|||
t.Run("VerifyRedirectURI", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, _, _ := setupProxyTest(t)
|
||||
client, _, _, _ := setupProxyTest(t, nil)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
|
@ -655,7 +700,7 @@ func TestWorkspaceAppsProxySubdomainBlocked(t *testing.T) {
|
|||
|
||||
func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
client, firstUser, _, port := setupProxyTest(t)
|
||||
client, firstUser, _, port := setupProxyTest(t, nil)
|
||||
|
||||
// proxyURL generates a URL for the proxy subdomain. The default path is a
|
||||
// slash.
|
||||
|
@ -829,7 +874,9 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
|||
t.Run("SuffixWildcardOK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, _, _ := setupProxyTest(t, "*-suffix.test.coder.com")
|
||||
client, _, _, _ := setupProxyTest(t, &setupProxyTestOpts{
|
||||
AppHost: "*-suffix.test.coder.com",
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
@ -849,7 +896,9 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
|||
t.Run("SuffixWildcardNotMatch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, _, _ := setupProxyTest(t, "*-suffix.test.coder.com")
|
||||
client, _, _, _ := setupProxyTest(t, &setupProxyTestOpts{
|
||||
AppHost: "*-suffix.test.coder.com",
|
||||
})
|
||||
|
||||
t.Run("NoSuffix", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
|
@ -991,7 +1040,7 @@ func TestAppSubdomainLogout(t *testing.T) {
|
|||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, _, _ := setupProxyTest(t)
|
||||
client, _, _, _ := setupProxyTest(t, nil)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
@ -1085,18 +1134,54 @@ func TestAppSubdomainLogout(t *testing.T) {
|
|||
func TestAppSharing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
setup := func(t *testing.T) (workspace codersdk.Workspace, agnt codersdk.WorkspaceAgent, user codersdk.User, client *codersdk.Client, clientInOtherOrg *codersdk.Client, clientWithNoAuth *codersdk.Client) {
|
||||
setup := func(t *testing.T, allowPathAppSharing, allowSiteOwnerAccess bool) (workspace codersdk.Workspace, agnt codersdk.WorkspaceAgent, user codersdk.User, ownerClient *codersdk.Client, client *codersdk.Client, clientInOtherOrg *codersdk.Client, clientWithNoAuth *codersdk.Client) {
|
||||
//nolint:gosec
|
||||
const password = "password"
|
||||
|
||||
client, _, workspace, _ = setupProxyTest(t)
|
||||
var port uint16
|
||||
ownerClient, _, _, port = setupProxyTest(t, &setupProxyTestOpts{
|
||||
NoWorkspace: true,
|
||||
DangerousAllowPathAppSharing: allowPathAppSharing,
|
||||
DangerousAllowPathAppSiteOwnerAccess: allowSiteOwnerAccess,
|
||||
})
|
||||
forceURLTransport(t, ownerClient)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
user, err := client.User(ctx, codersdk.Me)
|
||||
ownerUser, err := ownerClient.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a template-admin user in the same org. We don't use an owner
|
||||
// since they have access to everything.
|
||||
user, err = ownerClient.CreateUser(ctx, codersdk.CreateUserRequest{
|
||||
Email: "user@coder.com",
|
||||
Username: "user",
|
||||
Password: password,
|
||||
OrganizationID: ownerUser.OrganizationIDs[0],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ownerClient.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
|
||||
Roles: []string{"template-admin", "member"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
client = codersdk.New(ownerClient.URL)
|
||||
loginRes, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
Email: user.Email,
|
||||
Password: password,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client.SetSessionToken(loginRes.SessionToken)
|
||||
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
forceURLTransport(t, client)
|
||||
|
||||
// Create workspace.
|
||||
workspace = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], proxyTestSubdomainRaw, port)
|
||||
|
||||
// Verify that the apps have the correct sharing levels set.
|
||||
workspaceBuild, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
|
@ -1114,11 +1199,11 @@ func TestAppSharing(t *testing.T) {
|
|||
require.Equal(t, expected, found, "apps have incorrect sharing levels")
|
||||
|
||||
// Create a user in a different org.
|
||||
otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||
otherOrg, err := ownerClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||
Name: "a-different-org",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
userInOtherOrg, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
|
||||
userInOtherOrg, err := ownerClient.CreateUser(ctx, codersdk.CreateUserRequest{
|
||||
Email: "no-template-access@coder.com",
|
||||
Username: "no-template-access",
|
||||
Password: password,
|
||||
|
@ -1127,7 +1212,7 @@ func TestAppSharing(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
clientInOtherOrg = codersdk.New(client.URL)
|
||||
loginRes, err := clientInOtherOrg.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
loginRes, err = clientInOtherOrg.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
Email: userInOtherOrg.Email,
|
||||
Password: password,
|
||||
})
|
||||
|
@ -1136,17 +1221,19 @@ func TestAppSharing(t *testing.T) {
|
|||
clientInOtherOrg.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
forceURLTransport(t, clientInOtherOrg)
|
||||
|
||||
// Create an unauthenticated codersdk client.
|
||||
clientWithNoAuth = codersdk.New(client.URL)
|
||||
clientWithNoAuth.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
forceURLTransport(t, clientWithNoAuth)
|
||||
|
||||
return workspace, agnt, user, client, clientInOtherOrg, clientWithNoAuth
|
||||
return workspace, agnt, user, ownerClient, client, clientInOtherOrg, clientWithNoAuth
|
||||
}
|
||||
|
||||
verifyAccess := func(t *testing.T, username, workspaceName, agentName, appName string, client *codersdk.Client, shouldHaveAccess, shouldRedirectToLogin bool) {
|
||||
verifyAccess := func(t *testing.T, isPathApp bool, username, workspaceName, agentName, appName string, client *codersdk.Client, shouldHaveAccess, shouldRedirectToLogin bool) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
|
@ -1164,6 +1251,7 @@ func TestAppSharing(t *testing.T) {
|
|||
scopedClient := codersdk.New(client.URL)
|
||||
scopedClient.SetSessionToken(token.Key)
|
||||
scopedClient.HTTPClient.CheckRedirect = client.HTTPClient.CheckRedirect
|
||||
scopedClient.HTTPClient.Transport = client.HTTPClient.Transport
|
||||
|
||||
clients = append(clients, scopedClient)
|
||||
}
|
||||
|
@ -1171,21 +1259,38 @@ func TestAppSharing(t *testing.T) {
|
|||
for i, client := range clients {
|
||||
msg := fmt.Sprintf("client %d", i)
|
||||
|
||||
appPath := fmt.Sprintf("/@%s/%s.%s/apps/%s/?%s", username, workspaceName, agentName, appName, proxyTestAppQuery)
|
||||
res, err := requestWithRetries(ctx, t, client, http.MethodGet, appPath, nil)
|
||||
u := fmt.Sprintf("/@%s/%s.%s/apps/%s/?%s", username, workspaceName, agentName, appName, proxyTestAppQuery)
|
||||
if !isPathApp {
|
||||
subdomain := httpapi.ApplicationURL{
|
||||
AppSlug: appName,
|
||||
AgentName: agentName,
|
||||
WorkspaceName: workspaceName,
|
||||
Username: username,
|
||||
}.String()
|
||||
|
||||
hostname := strings.Replace(proxyTestSubdomainRaw, "*", subdomain, 1)
|
||||
u = fmt.Sprintf("http://%s/?%s", hostname, proxyTestAppQuery)
|
||||
}
|
||||
|
||||
res, err := requestWithRetries(ctx, t, client, http.MethodGet, u, nil)
|
||||
require.NoError(t, err, msg)
|
||||
|
||||
dump, err := httputil.DumpResponse(res, true)
|
||||
res.Body.Close()
|
||||
_ = res.Body.Close()
|
||||
require.NoError(t, err, msg)
|
||||
t.Logf("response dump: %s", dump)
|
||||
// t.Logf("response dump: %s", dump)
|
||||
|
||||
if !shouldHaveAccess {
|
||||
if shouldRedirectToLogin {
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, res.StatusCode, "should not have access, expected temporary redirect. "+msg)
|
||||
location, err := res.Location()
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, "/login", location.Path, "should not have access, expected redirect to /login. "+msg)
|
||||
|
||||
expectedPath := "/login"
|
||||
if !isPathApp {
|
||||
expectedPath = "/api/v2/applications/auth-redirect"
|
||||
}
|
||||
assert.Equal(t, expectedPath, location.Path, "should not have access, expected redirect to applicable login endpoint. "+msg)
|
||||
} else {
|
||||
// If the user doesn't have access we return 404 to avoid
|
||||
// leaking information about the existence of the app.
|
||||
|
@ -1200,50 +1305,99 @@ func TestAppSharing(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
t.Run("Level", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testLevels := func(t *testing.T, isPathApp, pathAppSharingEnabled, siteOwnerPathAppAccessEnabled bool) {
|
||||
workspace, agnt, user, ownerClient, client, clientInOtherOrg, clientWithNoAuth := setup(t, pathAppSharingEnabled, siteOwnerPathAppAccessEnabled)
|
||||
|
||||
workspace, agent, user, client, clientInOtherOrg, clientWithNoAuth := setup(t)
|
||||
allowedUnlessSharingDisabled := !isPathApp || pathAppSharingEnabled
|
||||
siteOwnerCanAccess := !isPathApp || siteOwnerPathAppAccessEnabled
|
||||
siteOwnerCanAccessShared := siteOwnerCanAccess || pathAppSharingEnabled
|
||||
|
||||
t.Run("Owner", func(t *testing.T) {
|
||||
deploymentConfig, err := ownerClient.DeploymentConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, pathAppSharingEnabled, deploymentConfig.Dangerous.AllowPathAppSharing.Value)
|
||||
assert.Equal(t, siteOwnerPathAppAccessEnabled, deploymentConfig.Dangerous.AllowPathAppSiteOwnerAccess.Value)
|
||||
|
||||
t.Run("LevelOwner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Site owner should be able to access all workspaces if
|
||||
// enabled.
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameOwner, ownerClient, siteOwnerCanAccess, false)
|
||||
|
||||
// Owner should be able to access their own workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, client, true, false)
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameOwner, client, true, false)
|
||||
|
||||
// Authenticated users should not have access to a workspace that
|
||||
// they do not own.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientInOtherOrg, false, false)
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameOwner, clientInOtherOrg, false, false)
|
||||
|
||||
// Unauthenticated user should not have any access.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientWithNoAuth, false, true)
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameOwner, clientWithNoAuth, false, true)
|
||||
})
|
||||
|
||||
t.Run("Authenticated", func(t *testing.T) {
|
||||
t.Run("LevelAuthenticated", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Site owner should be able to access all workspaces if
|
||||
// enabled.
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, ownerClient, siteOwnerCanAccessShared, false)
|
||||
|
||||
// Owner should be able to access their own workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, client, true, false)
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, client, true, false)
|
||||
|
||||
// Authenticated users should be able to access the workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientInOtherOrg, true, false)
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, clientInOtherOrg, allowedUnlessSharingDisabled, false)
|
||||
|
||||
// Unauthenticated user should not have any access.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientWithNoAuth, false, true)
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, clientWithNoAuth, false, true)
|
||||
})
|
||||
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
t.Run("LevelPublic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Site owner should be able to access all workspaces if
|
||||
// enabled.
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublic, ownerClient, siteOwnerCanAccessShared, false)
|
||||
|
||||
// Owner should be able to access their own workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, client, true, false)
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublic, client, true, false)
|
||||
|
||||
// Authenticated users should be able to access the workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientInOtherOrg, true, false)
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublic, clientInOtherOrg, allowedUnlessSharingDisabled, false)
|
||||
|
||||
// Unauthenticated user should be able to access the workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientWithNoAuth, true, false)
|
||||
verifyAccess(t, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublic, clientWithNoAuth, allowedUnlessSharingDisabled, !allowedUnlessSharingDisabled)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testLevels(t, true, false, false)
|
||||
})
|
||||
|
||||
t.Run("AppSharingEnabled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testLevels(t, true, true, false)
|
||||
})
|
||||
|
||||
t.Run("SiteOwnerAccessEnabled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testLevels(t, true, false, true)
|
||||
})
|
||||
|
||||
t.Run("BothEnabled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testLevels(t, true, false, true)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Subdomain", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testLevels(t, false, false, false)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1438,3 +1592,18 @@ func TestWorkspaceAppsNonCanonicalHeaders(t *testing.T) {
|
|||
require.Equal(t, secWebSocketKey, resp.Header.Get("Sec-WebSocket-Key"))
|
||||
})
|
||||
}
|
||||
|
||||
// forceURLTransport forces the client to route all requests to the client's
|
||||
// configured URL host regardless of hostname.
|
||||
func forceURLTransport(t *testing.T, client *codersdk.Client) {
|
||||
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
transport := defaultTransport.Clone()
|
||||
transport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, network, client.URL.Host)
|
||||
}
|
||||
client.HTTPClient.Transport = transport
|
||||
t.Cleanup(func() {
|
||||
transport.CloseIdleConnections()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,10 +11,8 @@ import (
|
|||
|
||||
// DeploymentConfig is the central configuration for the coder server.
|
||||
type DeploymentConfig struct {
|
||||
AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"`
|
||||
WildcardAccessURL *DeploymentConfigField[string] `json:"wildcard_access_url" typescript:",notnull"`
|
||||
// DEPRECATED: Use HTTPAddress or TLS.Address instead.
|
||||
Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"`
|
||||
AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"`
|
||||
WildcardAccessURL *DeploymentConfigField[string] `json:"wildcard_access_url" typescript:",notnull"`
|
||||
HTTPAddress *DeploymentConfigField[string] `json:"http_address" typescript:",notnull"`
|
||||
AutobuildPollInterval *DeploymentConfigField[time.Duration] `json:"autobuild_poll_interval" typescript:",notnull"`
|
||||
DERP *DERP `json:"derp" typescript:",notnull"`
|
||||
|
@ -46,7 +44,11 @@ type DeploymentConfig struct {
|
|||
MaxTokenLifetime *DeploymentConfigField[time.Duration] `json:"max_token_lifetime" typescript:",notnull"`
|
||||
Swagger *SwaggerConfig `json:"swagger" typescript:",notnull"`
|
||||
Logging *LoggingConfig `json:"logging" typescript:",notnull"`
|
||||
Dangerous *DangerousConfig `json:"dangerous" typescript:",notnull"`
|
||||
DisablePathApps *DeploymentConfigField[bool] `json:"disable_path_apps" typescript:",notnull"`
|
||||
|
||||
// DEPRECATED: Use HTTPAddress or TLS.Address instead.
|
||||
Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"`
|
||||
// DEPRECATED: Use Experiments instead.
|
||||
Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"`
|
||||
}
|
||||
|
@ -165,6 +167,11 @@ type LoggingConfig struct {
|
|||
Stackdriver *DeploymentConfigField[string] `json:"stackdriver" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type DangerousConfig struct {
|
||||
AllowPathAppSharing *DeploymentConfigField[bool] `json:"allow_path_app_sharing" typescript:",notnull"`
|
||||
AllowPathAppSiteOwnerAccess *DeploymentConfigField[bool] `json:"allow_path_app_site_owner_access" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type Flaggable interface {
|
||||
string | time.Duration | bool | int | []string | []GitAuthConfig
|
||||
}
|
||||
|
|
|
@ -171,6 +171,30 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \
|
|||
"usage": "string",
|
||||
"value": "string"
|
||||
},
|
||||
"dangerous": {
|
||||
"allow_path_app_sharing": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": true
|
||||
},
|
||||
"allow_path_app_site_owner_access": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
"derp": {
|
||||
"config": {
|
||||
"path": {
|
||||
|
@ -265,6 +289,17 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \
|
|||
}
|
||||
}
|
||||
},
|
||||
"disable_path_apps": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": true
|
||||
},
|
||||
"experimental": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
|
|
|
@ -1159,6 +1159,42 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
| `relay_url` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
| `stun_addresses` | [codersdk.DeploymentConfigField-array_string](#codersdkdeploymentconfigfield-array_string) | false | | |
|
||||
|
||||
## codersdk.DangerousConfig
|
||||
|
||||
```json
|
||||
{
|
||||
"allow_path_app_sharing": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": true
|
||||
},
|
||||
"allow_path_app_site_owner_access": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------------------------- | -------------------------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `allow_path_app_sharing` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | |
|
||||
| `allow_path_app_site_owner_access` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | |
|
||||
|
||||
## codersdk.DeploymentConfig
|
||||
|
||||
```json
|
||||
|
@ -1251,6 +1287,30 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
"usage": "string",
|
||||
"value": "string"
|
||||
},
|
||||
"dangerous": {
|
||||
"allow_path_app_sharing": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": true
|
||||
},
|
||||
"allow_path_app_site_owner_access": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
"derp": {
|
||||
"config": {
|
||||
"path": {
|
||||
|
@ -1345,6 +1405,17 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
}
|
||||
}
|
||||
},
|
||||
"disable_path_apps": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": true
|
||||
},
|
||||
"experimental": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
|
@ -2068,7 +2139,9 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
| `autobuild_poll_interval` | [codersdk.DeploymentConfigField-time_Duration](#codersdkdeploymentconfigfield-time_duration) | false | | |
|
||||
| `browser_only` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | |
|
||||
| `cache_directory` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
| `dangerous` | [codersdk.DangerousConfig](#codersdkdangerousconfig) | false | | |
|
||||
| `derp` | [codersdk.DERP](#codersdkderp) | false | | |
|
||||
| `disable_path_apps` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | |
|
||||
| `experimental` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | Experimental Use Experiments instead. |
|
||||
| `experiments` | [codersdk.DeploymentConfigField-array_string](#codersdkdeploymentconfigfield-array_string) | false | | |
|
||||
| `gitauth` | [codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig](#codersdkdeploymentconfigfield-array_codersdk_gitauthconfig) | false | | |
|
||||
|
|
|
@ -280,11 +280,16 @@ export interface DERPServerConfig {
|
|||
readonly relay_url: DeploymentConfigField<string>
|
||||
}
|
||||
|
||||
// From codersdk/deploymentconfig.go
|
||||
export interface DangerousConfig {
|
||||
readonly allow_path_app_sharing: DeploymentConfigField<boolean>
|
||||
readonly allow_path_app_site_owner_access: DeploymentConfigField<boolean>
|
||||
}
|
||||
|
||||
// From codersdk/deploymentconfig.go
|
||||
export interface DeploymentConfig {
|
||||
readonly access_url: DeploymentConfigField<string>
|
||||
readonly wildcard_access_url: DeploymentConfigField<string>
|
||||
readonly address: DeploymentConfigField<string>
|
||||
readonly http_address: DeploymentConfigField<string>
|
||||
readonly autobuild_poll_interval: DeploymentConfigField<number>
|
||||
readonly derp: DERP
|
||||
|
@ -316,6 +321,9 @@ export interface DeploymentConfig {
|
|||
readonly max_token_lifetime: DeploymentConfigField<number>
|
||||
readonly swagger: SwaggerConfig
|
||||
readonly logging: LoggingConfig
|
||||
readonly dangerous: DangerousConfig
|
||||
readonly disable_path_apps: DeploymentConfigField<boolean>
|
||||
readonly address: DeploymentConfigField<string>
|
||||
readonly experimental: DeploymentConfigField<boolean>
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue