From ad8e0db1729518f32f85ad68598d250800ef1143 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 1 Feb 2024 18:01:25 +0100 Subject: [PATCH] feat: add custom error message on signups disabled page (#11959) --- cli/server.go | 1 + cli/testdata/coder_server_--help.golden | 4 ++ cli/testdata/server-config.yaml.golden | 4 ++ coderd/apidoc/docs.go | 3 ++ coderd/apidoc/swagger.json | 3 ++ .../parameter/{plaintext.go => renderer.go} | 14 +++++++ .../{plaintext_test.go => renderer_test.go} | 42 +++++++++++++++++++ coderd/userauth.go | 19 ++++++++- codersdk/deployment.go | 10 +++++ docs/api/general.md | 1 + docs/api/schemas.md | 4 ++ docs/cli/server.md | 10 +++++ .../cli/testdata/coder_server_--help.golden | 4 ++ go.mod | 5 ++- go.sum | 2 + site/site.go | 9 +++- site/src/api/typesGenerated.ts | 1 + site/static/error.html | 4 +- 18 files changed, 135 insertions(+), 5 deletions(-) rename coderd/parameter/{plaintext.go => renderer.go} (84%) rename coderd/parameter/{plaintext_test.go => renderer_test.go} (54%) diff --git a/cli/server.go b/cli/server.go index fe53c5a093..520f58454d 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 8d420da29b..632a96a470 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 1c9372fad6..dfc1d0ca15 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 7ee176f411..04988bf485 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 69f0479cb5..1facd37ad4 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 bbee00d098..3767f63cd8 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 78945d9984..f0765a7a6e 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 37a0402fa5..dbb01f12e3 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 c4744e5448..6bc115ae5b 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 1069a7d332..7649729197 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 c20453ae6b..04b3a1ce6d 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 2b649aa321..f678041901 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 5129aaed52..8e9ccac868 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 14a80121cc..ef41dc95e9 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 8d5623161d..44bf3cd700 100644 --- a/go.sum +++ b/go.sum @@ -417,6 +417,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 ba6004fc3c..0476565521 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 6858d7a890..9d63f492c2 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 46c9a1b9e7..a07c96b8ec 100644 --- a/site/static/error.html +++ b/site/static/error.html @@ -167,8 +167,10 @@ running). */}} {{- if not .Error.HideStatus }}{{ .Error.Status }} - {{end}}{{ .Error.Title }} + {{- if .Error.RenderDescriptionMarkdown }} {{ .ErrorDescriptionHTML }} {{ + else }}

{{ .Error.Description }}

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