diff --git a/cli/server.go b/cli/server.go index 1df5f49855909..a0bcf7896072b 100644 --- a/cli/server.go +++ b/cli/server.go @@ -179,6 +179,7 @@ func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*co UserRoleMapping: vals.OIDC.UserRoleMapping.Value, UserRolesDefault: vals.OIDC.UserRolesDefault.GetSlice(), SignInText: vals.OIDC.SignInText.String(), + SignupsDisabledText: vals.OIDC.SignupsDisabledText.String(), IconURL: vals.OIDC.IconURL.String(), IgnoreEmailVerified: vals.OIDC.IgnoreEmailVerified.Value(), }, nil diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 8d420da29b1e7..632a96a470354 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -426,6 +426,10 @@ OIDC OPTIONS: --oidc-icon-url url, $CODER_OIDC_ICON_URL URL pointing to the icon to use on the OpenID Connect login button. + --oidc-signups-disabled-text string, $CODER_OIDC_SIGNUPS_DISABLED_TEXT + The custom text to show on the error page informing about disabled + OIDC signups. Markdown format is supported. + PROVISIONING OPTIONS: Tune the behavior of the provisioner, which is responsible for creating, updating, and deleting workspace resources. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 1c9372fad62dd..dfc1d0ca150dc 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -348,6 +348,10 @@ oidc: # URL pointing to the icon to use on the OpenID Connect login button. # (default: , type: url) iconURL: + # The custom text to show on the error page informing about disabled OIDC signups. + # Markdown format is supported. + # (default: , type: string) + signupsDisabledText: "" # Telemetry is critical to our ability to improve Coder. We strip all personal # information before sending data to our servers. Please only disable telemetry # when required by your organization's security policy. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 4f7a7d3ebc38b..07ea323129e45 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10071,6 +10071,9 @@ const docTemplate = `{ "sign_in_text": { "type": "string" }, + "signups_disabled_text": { + "type": "string" + }, "user_role_field": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c7696a4157ab9..a8bd250085be2 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9052,6 +9052,9 @@ "sign_in_text": { "type": "string" }, + "signups_disabled_text": { + "type": "string" + }, "user_role_field": { "type": "string" }, diff --git a/coderd/parameter/plaintext.go b/coderd/parameter/renderer.go similarity index 84% rename from coderd/parameter/plaintext.go rename to coderd/parameter/renderer.go index bbee00d098c23..3767f63cd889c 100644 --- a/coderd/parameter/plaintext.go +++ b/coderd/parameter/renderer.go @@ -1,10 +1,14 @@ package parameter import ( + "bytes" "strings" "github.com/charmbracelet/glamour" "github.com/charmbracelet/glamour/ansi" + gomarkdown "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" "golang.org/x/xerrors" ) @@ -95,3 +99,13 @@ func Plaintext(markdown string) (string, error) { return strings.TrimSpace(output), nil } + +func HTML(markdown string) string { + p := parser.NewWithExtensions(parser.CommonExtensions) + doc := p.Parse([]byte(markdown)) + renderer := html.NewRenderer(html.RendererOptions{ + Flags: html.CommonFlags | html.SkipHTML, + }, + ) + return string(bytes.TrimSpace(gomarkdown.Render(doc, renderer))) +} diff --git a/coderd/parameter/plaintext_test.go b/coderd/parameter/renderer_test.go similarity index 54% rename from coderd/parameter/plaintext_test.go rename to coderd/parameter/renderer_test.go index 78945d9984e10..f0765a7a6eb14 100644 --- a/coderd/parameter/plaintext_test.go +++ b/coderd/parameter/renderer_test.go @@ -47,3 +47,45 @@ __This is bold text.__ require.Equal(t, nothingChanges, stripped) }) } + +func TestHTML(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected string + }{ + { + name: "Simple", + input: `**Coder** is in *early access* mode. To ~~register~~ request access, fill out [this form](https://internal.example.com). ***Thank you!***`, + expected: `

Coder is in early access mode. To register request access, fill out this form. Thank you!

`, + }, + { + name: "Tricky", + input: `**Cod*er** is in *early a**ccess** mode`, + expected: `

Cod*er is in *early access mode

`, + }, + { + name: "XSS", + input: `

Click here to get access!

?`, + expected: `

Click here to get access!?

`, + }, + { + name: "No Markdown tags", + input: "This is a simple description, so nothing changes.", + expected: "

This is a simple description, so nothing changes.

", + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + rendered := parameter.HTML(tt.input) + require.Equal(t, tt.expected, rendered) + }) + } +} diff --git a/coderd/userauth.go b/coderd/userauth.go index 37a0402fa5713..dbb01f12e31ad 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -31,6 +31,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/parameter" "github.com/coder/coder/v2/coderd/promoauth" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/userpassword" @@ -740,6 +741,8 @@ type OIDCConfig struct { SignInText string // IconURL points to the URL of an icon to display on the OIDC login button IconURL string + // SignupsDisabledText is the text do display on the static error page. + SignupsDisabledText string } func (cfg OIDCConfig) RoleSyncEnabled() bool { @@ -1252,6 +1255,8 @@ type httpError struct { msg string detail string renderStaticPage bool + + renderDetailMarkdown bool } func (e httpError) Write(rw http.ResponseWriter, r *http.Request) { @@ -1263,6 +1268,8 @@ func (e httpError) Write(rw http.ResponseWriter, r *http.Request) { Description: e.detail, RetryEnabled: false, DashboardURL: "/login", + + RenderDescriptionMarkdown: e.renderDetailMarkdown, }) return } @@ -1313,9 +1320,17 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C } if user.ID == uuid.Nil && !params.AllowSignups { + signupsDisabledText := "Please contact your Coder administrator to request access." + if api.OIDCConfig != nil && api.OIDCConfig.SignupsDisabledText != "" { + signupsDisabledText = parameter.HTML(api.OIDCConfig.SignupsDisabledText) + } return httpError{ - code: http.StatusForbidden, - msg: fmt.Sprintf("Signups are not allowed for login type %q", params.LoginType), + code: http.StatusForbidden, + msg: "Signups are disabled", + detail: signupsDisabledText, + renderStaticPage: true, + + renderDetailMarkdown: true, } } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index c02eea595924f..a47e72372b881 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -303,6 +303,7 @@ type OIDCConfig struct { UserRolesDefault clibase.StringArray `json:"user_roles_default" typescript:",notnull"` SignInText clibase.String `json:"sign_in_text" typescript:",notnull"` IconURL clibase.URL `json:"icon_url" typescript:",notnull"` + SignupsDisabledText clibase.String `json:"signups_disabled_text" typescript:",notnull"` } type TelemetryConfig struct { @@ -1266,6 +1267,15 @@ when required by your organization's security policy.`, Group: &deploymentGroupOIDC, YAML: "iconURL", }, + { + Name: "Signups disabled text", + Description: "The custom text to show on the error page informing about disabled OIDC signups. Markdown format is supported.", + Flag: "oidc-signups-disabled-text", + Env: "CODER_OIDC_SIGNUPS_DISABLED_TEXT", + Value: &c.OIDC.SignupsDisabledText, + Group: &deploymentGroupOIDC, + YAML: "signupsDisabledText", + }, // Telemetry settings { Name: "Telemetry Enable", diff --git a/docs/api/general.md b/docs/api/general.md index 1069a7d3328e5..764972919761d 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -298,6 +298,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "issuer_url": "string", "scopes": ["string"], "sign_in_text": "string", + "signups_disabled_text": "string", "user_role_field": "string", "user_role_mapping": {}, "user_roles_default": ["string"], diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 909158f2ebdc8..c650c625369d2 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2273,6 +2273,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "issuer_url": "string", "scopes": ["string"], "sign_in_text": "string", + "signups_disabled_text": "string", "user_role_field": "string", "user_role_mapping": {}, "user_roles_default": ["string"], @@ -2640,6 +2641,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "issuer_url": "string", "scopes": ["string"], "sign_in_text": "string", + "signups_disabled_text": "string", "user_role_field": "string", "user_role_mapping": {}, "user_roles_default": ["string"], @@ -3747,6 +3749,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "issuer_url": "string", "scopes": ["string"], "sign_in_text": "string", + "signups_disabled_text": "string", "user_role_field": "string", "user_role_mapping": {}, "user_roles_default": ["string"], @@ -3777,6 +3780,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `issuer_url` | string | false | | | | `scopes` | array of string | false | | | | `sign_in_text` | string | false | | | +| `signups_disabled_text` | string | false | | | | `user_role_field` | string | false | | | | `user_role_mapping` | object | false | | | | `user_roles_default` | array of string | false | | | diff --git a/docs/cli/server.md b/docs/cli/server.md index 2b649aa321847..f678041901ba5 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -897,6 +897,16 @@ Controls if the 'Secure' property is set on browser session cookies. The token expiry duration for browser sessions. Sessions may last longer if they are actively making requests, but this functionality can be disabled via --disable-session-expiry-refresh. +### --oidc-signups-disabled-text + +| | | +| ----------- | ---------------------------------------------- | +| Type | string | +| Environment | $CODER_OIDC_SIGNUPS_DISABLED_TEXT | +| YAML | oidc.signupsDisabledText | + +The custom text to show on the error page informing about disabled OIDC signups. Markdown format is supported. + ### --log-stackdriver | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 5129aaed52b07..8e9ccac868c1e 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -427,6 +427,10 @@ OIDC OPTIONS: --oidc-icon-url url, $CODER_OIDC_ICON_URL URL pointing to the icon to use on the OpenID Connect login button. + --oidc-signups-disabled-text string, $CODER_OIDC_SIGNUPS_DISABLED_TEXT + The custom text to show on the error page informing about disabled + OIDC signups. Markdown format is supported. + PROVISIONING OPTIONS: Tune the behavior of the provisioner, which is responsible for creating, updating, and deleting workspace resources. diff --git a/go.mod b/go.mod index bbdc6462f4495..a647298f69a8a 100644 --- a/go.mod +++ b/go.mod @@ -206,7 +206,10 @@ require ( require go.uber.org/mock v0.4.0 -require github.com/benbjohnson/clock v1.3.5 +require ( + github.com/benbjohnson/clock v1.3.5 + github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 +) require github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 // indirect diff --git a/go.sum b/go.sum index c74f064e40c20..9b44935426d7d 100644 --- a/go.sum +++ b/go.sum @@ -427,6 +427,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k= +github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v23.1.21+incompatible h1:bUqzx/MXCDxuS0hRJL2EfjyZL3uQrPbMocUa8zGqsTA= diff --git a/site/site.go b/site/site.go index ba6004fc3ce09..0476565521ac9 100644 --- a/site/site.go +++ b/site/site.go @@ -773,6 +773,8 @@ type ErrorPageData struct { RetryEnabled bool DashboardURL string Warnings []string + + RenderDescriptionMarkdown bool } // RenderStaticErrorPage renders the static error page. This is used by app @@ -781,12 +783,17 @@ type ErrorPageData struct { func RenderStaticErrorPage(rw http.ResponseWriter, r *http.Request, data ErrorPageData) { type outerData struct { Error ErrorPageData + + ErrorDescriptionHTML htmltemplate.HTML } rw.Header().Set("Content-Type", "text/html; charset=utf-8") rw.WriteHeader(data.Status) - err := errorTemplate.Execute(rw, outerData{Error: data}) + err := errorTemplate.Execute(rw, outerData{ + Error: data, + ErrorDescriptionHTML: htmltemplate.HTML(data.Description), //nolint:gosec // gosec thinks this is user-input, but it is from Coder deployment configuration. + }) if err != nil { httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ Message: "Failed to render error page: " + err.Error(), diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6858d7a890117..9d63f492c29b6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -752,6 +752,7 @@ export interface OIDCConfig { readonly user_roles_default: string[]; readonly sign_in_text: string; readonly icon_url: string; + readonly signups_disabled_text: string; } // From codersdk/organizations.go diff --git a/site/static/error.html b/site/static/error.html index 46c9a1b9e7f7a..a07c96b8ec5aa 100644 --- a/site/static/error.html +++ b/site/static/error.html @@ -167,8 +167,10 @@

{{- if not .Error.HideStatus }}{{ .Error.Status }} - {{end}}{{ .Error.Title }}

+ {{- if .Error.RenderDescriptionMarkdown }} {{ .ErrorDescriptionHTML }} {{ + else }}

{{ .Error.Description }}

- {{- if .Error.Warnings }} + {{ end }} {{- if .Error.Warnings }}