feat: add custom error message on signups disabled page (#11959)

This commit is contained in:
Marcin Tojek 2024-02-01 18:01:25 +01:00 committed by GitHub
parent e070a55142
commit ad8e0db172
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 135 additions and 5 deletions

View File

@ -179,6 +179,7 @@ func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*co
UserRoleMapping: vals.OIDC.UserRoleMapping.Value, UserRoleMapping: vals.OIDC.UserRoleMapping.Value,
UserRolesDefault: vals.OIDC.UserRolesDefault.GetSlice(), UserRolesDefault: vals.OIDC.UserRolesDefault.GetSlice(),
SignInText: vals.OIDC.SignInText.String(), SignInText: vals.OIDC.SignInText.String(),
SignupsDisabledText: vals.OIDC.SignupsDisabledText.String(),
IconURL: vals.OIDC.IconURL.String(), IconURL: vals.OIDC.IconURL.String(),
IgnoreEmailVerified: vals.OIDC.IgnoreEmailVerified.Value(), IgnoreEmailVerified: vals.OIDC.IgnoreEmailVerified.Value(),
}, nil }, nil

View File

@ -426,6 +426,10 @@ OIDC OPTIONS:
--oidc-icon-url url, $CODER_OIDC_ICON_URL --oidc-icon-url url, $CODER_OIDC_ICON_URL
URL pointing to the icon to use on the OpenID Connect login button. 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: PROVISIONING OPTIONS:
Tune the behavior of the provisioner, which is responsible for creating, Tune the behavior of the provisioner, which is responsible for creating,
updating, and deleting workspace resources. updating, and deleting workspace resources.

View File

@ -348,6 +348,10 @@ oidc:
# URL pointing to the icon to use on the OpenID Connect login button. # URL pointing to the icon to use on the OpenID Connect login button.
# (default: <unset>, type: url) # (default: <unset>, type: url)
iconURL: iconURL:
# The custom text to show on the error page informing about disabled OIDC signups.
# Markdown format is supported.
# (default: <unset>, type: string)
signupsDisabledText: ""
# Telemetry is critical to our ability to improve Coder. We strip all personal # Telemetry is critical to our ability to improve Coder. We strip all personal
# information before sending data to our servers. Please only disable telemetry # information before sending data to our servers. Please only disable telemetry
# when required by your organization's security policy. # when required by your organization's security policy.

3
coderd/apidoc/docs.go generated
View File

@ -10071,6 +10071,9 @@ const docTemplate = `{
"sign_in_text": { "sign_in_text": {
"type": "string" "type": "string"
}, },
"signups_disabled_text": {
"type": "string"
},
"user_role_field": { "user_role_field": {
"type": "string" "type": "string"
}, },

View File

@ -9052,6 +9052,9 @@
"sign_in_text": { "sign_in_text": {
"type": "string" "type": "string"
}, },
"signups_disabled_text": {
"type": "string"
},
"user_role_field": { "user_role_field": {
"type": "string" "type": "string"
}, },

View File

@ -1,10 +1,14 @@
package parameter package parameter
import ( import (
"bytes"
"strings" "strings"
"github.com/charmbracelet/glamour" "github.com/charmbracelet/glamour"
"github.com/charmbracelet/glamour/ansi" "github.com/charmbracelet/glamour/ansi"
gomarkdown "github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"golang.org/x/xerrors" "golang.org/x/xerrors"
) )
@ -95,3 +99,13 @@ func Plaintext(markdown string) (string, error) {
return strings.TrimSpace(output), nil 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)))
}

View File

@ -47,3 +47,45 @@ __This is bold text.__
require.Equal(t, nothingChanges, stripped) 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: `<p><strong>Coder</strong> is in <em>early access</em> mode. To <del>register</del> request access, fill out <a href="https://internal.example.com">this form</a>. <strong><em>Thank you!</em></strong></p>`,
},
{
name: "Tricky",
input: `**Cod*er** is in *early a**ccess** <img src="foobar">mode`,
expected: `<p><strong>Cod*er</strong> is in *early a<strong>ccess</strong> mode</p>`,
},
{
name: "XSS",
input: `<p onclick="alert(\"omghax\")">Click here to get access!</p>?`,
expected: `<p>Click here to get access!?</p>`,
},
{
name: "No Markdown tags",
input: "This is a simple description, so nothing changes.",
expected: "<p>This is a simple description, so nothing changes.</p>",
},
}
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)
})
}
}

View File

@ -31,6 +31,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw" "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/promoauth"
"github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/userpassword" "github.com/coder/coder/v2/coderd/userpassword"
@ -740,6 +741,8 @@ type OIDCConfig struct {
SignInText string SignInText string
// IconURL points to the URL of an icon to display on the OIDC login button // IconURL points to the URL of an icon to display on the OIDC login button
IconURL string IconURL string
// SignupsDisabledText is the text do display on the static error page.
SignupsDisabledText string
} }
func (cfg OIDCConfig) RoleSyncEnabled() bool { func (cfg OIDCConfig) RoleSyncEnabled() bool {
@ -1252,6 +1255,8 @@ type httpError struct {
msg string msg string
detail string detail string
renderStaticPage bool renderStaticPage bool
renderDetailMarkdown bool
} }
func (e httpError) Write(rw http.ResponseWriter, r *http.Request) { 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, Description: e.detail,
RetryEnabled: false, RetryEnabled: false,
DashboardURL: "/login", DashboardURL: "/login",
RenderDescriptionMarkdown: e.renderDetailMarkdown,
}) })
return return
} }
@ -1313,9 +1320,17 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
} }
if user.ID == uuid.Nil && !params.AllowSignups { 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{ return httpError{
code: http.StatusForbidden, code: http.StatusForbidden,
msg: fmt.Sprintf("Signups are not allowed for login type %q", params.LoginType), msg: "Signups are disabled",
detail: signupsDisabledText,
renderStaticPage: true,
renderDetailMarkdown: true,
} }
} }

View File

@ -303,6 +303,7 @@ type OIDCConfig struct {
UserRolesDefault clibase.StringArray `json:"user_roles_default" typescript:",notnull"` UserRolesDefault clibase.StringArray `json:"user_roles_default" typescript:",notnull"`
SignInText clibase.String `json:"sign_in_text" typescript:",notnull"` SignInText clibase.String `json:"sign_in_text" typescript:",notnull"`
IconURL clibase.URL `json:"icon_url" typescript:",notnull"` IconURL clibase.URL `json:"icon_url" typescript:",notnull"`
SignupsDisabledText clibase.String `json:"signups_disabled_text" typescript:",notnull"`
} }
type TelemetryConfig struct { type TelemetryConfig struct {
@ -1266,6 +1267,15 @@ when required by your organization's security policy.`,
Group: &deploymentGroupOIDC, Group: &deploymentGroupOIDC,
YAML: "iconURL", 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 // Telemetry settings
{ {
Name: "Telemetry Enable", Name: "Telemetry Enable",

1
docs/api/general.md generated
View File

@ -298,6 +298,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"issuer_url": "string", "issuer_url": "string",
"scopes": ["string"], "scopes": ["string"],
"sign_in_text": "string", "sign_in_text": "string",
"signups_disabled_text": "string",
"user_role_field": "string", "user_role_field": "string",
"user_role_mapping": {}, "user_role_mapping": {},
"user_roles_default": ["string"], "user_roles_default": ["string"],

4
docs/api/schemas.md generated
View File

@ -2273,6 +2273,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"issuer_url": "string", "issuer_url": "string",
"scopes": ["string"], "scopes": ["string"],
"sign_in_text": "string", "sign_in_text": "string",
"signups_disabled_text": "string",
"user_role_field": "string", "user_role_field": "string",
"user_role_mapping": {}, "user_role_mapping": {},
"user_roles_default": ["string"], "user_roles_default": ["string"],
@ -2640,6 +2641,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"issuer_url": "string", "issuer_url": "string",
"scopes": ["string"], "scopes": ["string"],
"sign_in_text": "string", "sign_in_text": "string",
"signups_disabled_text": "string",
"user_role_field": "string", "user_role_field": "string",
"user_role_mapping": {}, "user_role_mapping": {},
"user_roles_default": ["string"], "user_roles_default": ["string"],
@ -3747,6 +3749,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"issuer_url": "string", "issuer_url": "string",
"scopes": ["string"], "scopes": ["string"],
"sign_in_text": "string", "sign_in_text": "string",
"signups_disabled_text": "string",
"user_role_field": "string", "user_role_field": "string",
"user_role_mapping": {}, "user_role_mapping": {},
"user_roles_default": ["string"], "user_roles_default": ["string"],
@ -3777,6 +3780,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `issuer_url` | string | false | | | | `issuer_url` | string | false | | |
| `scopes` | array of string | false | | | | `scopes` | array of string | false | | |
| `sign_in_text` | string | false | | | | `sign_in_text` | string | false | | |
| `signups_disabled_text` | string | false | | |
| `user_role_field` | string | false | | | | `user_role_field` | string | false | | |
| `user_role_mapping` | object | false | | | | `user_role_mapping` | object | false | | |
| `user_roles_default` | array of string | false | | | | `user_roles_default` | array of string | false | | |

10
docs/cli/server.md generated
View File

@ -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. 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 | <code>string</code> |
| Environment | <code>$CODER_OIDC_SIGNUPS_DISABLED_TEXT</code> |
| YAML | <code>oidc.signupsDisabledText</code> |
The custom text to show on the error page informing about disabled OIDC signups. Markdown format is supported.
### --log-stackdriver ### --log-stackdriver
| | | | | |

View File

@ -427,6 +427,10 @@ OIDC OPTIONS:
--oidc-icon-url url, $CODER_OIDC_ICON_URL --oidc-icon-url url, $CODER_OIDC_ICON_URL
URL pointing to the icon to use on the OpenID Connect login button. 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: PROVISIONING OPTIONS:
Tune the behavior of the provisioner, which is responsible for creating, Tune the behavior of the provisioner, which is responsible for creating,
updating, and deleting workspace resources. updating, and deleting workspace resources.

5
go.mod
View File

@ -206,7 +206,10 @@ require (
require go.uber.org/mock v0.4.0 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 require github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 // indirect

2
go.sum
View File

@ -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/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 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/flatbuffers v23.1.21+incompatible h1:bUqzx/MXCDxuS0hRJL2EfjyZL3uQrPbMocUa8zGqsTA= github.com/google/flatbuffers v23.1.21+incompatible h1:bUqzx/MXCDxuS0hRJL2EfjyZL3uQrPbMocUa8zGqsTA=

View File

@ -773,6 +773,8 @@ type ErrorPageData struct {
RetryEnabled bool RetryEnabled bool
DashboardURL string DashboardURL string
Warnings []string Warnings []string
RenderDescriptionMarkdown bool
} }
// RenderStaticErrorPage renders the static error page. This is used by app // 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) { func RenderStaticErrorPage(rw http.ResponseWriter, r *http.Request, data ErrorPageData) {
type outerData struct { type outerData struct {
Error ErrorPageData Error ErrorPageData
ErrorDescriptionHTML htmltemplate.HTML
} }
rw.Header().Set("Content-Type", "text/html; charset=utf-8") rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(data.Status) 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 { if err != nil {
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to render error page: " + err.Error(), Message: "Failed to render error page: " + err.Error(),

View File

@ -752,6 +752,7 @@ export interface OIDCConfig {
readonly user_roles_default: string[]; readonly user_roles_default: string[];
readonly sign_in_text: string; readonly sign_in_text: string;
readonly icon_url: string; readonly icon_url: string;
readonly signups_disabled_text: string;
} }
// From codersdk/organizations.go // From codersdk/organizations.go

View File

@ -167,8 +167,10 @@ running). */}}
{{- if not .Error.HideStatus }}{{ .Error.Status }} - {{end}}{{ {{- if not .Error.HideStatus }}{{ .Error.Status }} - {{end}}{{
.Error.Title }} .Error.Title }}
</h1> </h1>
{{- if .Error.RenderDescriptionMarkdown }} {{ .ErrorDescriptionHTML }} {{
else }}
<p>{{ .Error.Description }}</p> <p>{{ .Error.Description }}</p>
{{- if .Error.Warnings }} {{ end }} {{- if .Error.Warnings }}
<div class="warning"> <div class="warning">
<div class="warning-title"> <div class="warning-title">
<svg <svg