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,
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

View File

@ -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.

View File

@ -348,6 +348,10 @@ oidc:
# URL pointing to the icon to use on the OpenID Connect login button.
# (default: <unset>, type: url)
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
# information before sending data to our servers. Please only disable telemetry
# 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": {
"type": "string"
},
"signups_disabled_text": {
"type": "string"
},
"user_role_field": {
"type": "string"
},

View File

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

View File

@ -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)))
}

View File

@ -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: `<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/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,
}
}

View File

@ -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",

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",
"scopes": ["string"],
"sign_in_text": "string",
"signups_disabled_text": "string",
"user_role_field": "string",
"user_role_mapping": {},
"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",
"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 | | |

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.
### --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
| | |

View File

@ -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.

5
go.mod
View File

@ -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

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/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=

View File

@ -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(),

View File

@ -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

View File

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