From 74f27719b8d2ae382b4ace5087a3ac3715a4e71d Mon Sep 17 00:00:00 2001 From: Kayla Washburn-Love Date: Thu, 25 Apr 2024 16:36:51 -0600 Subject: [PATCH] feat: specify a custom "terms of service" link (#13068) --- cli/testdata/coder_server_--help.golden | 4 +++ cli/testdata/server-config.yaml.golden | 4 +++ coderd/apidoc/docs.go | 6 ++++ coderd/apidoc/swagger.json | 6 ++++ coderd/userauth.go | 1 + codersdk/deployment.go | 9 ++++++ codersdk/users.go | 7 +++-- docs/api/general.md | 1 + docs/api/schemas.md | 17 +++++++---- docs/api/users.md | 3 +- docs/cli/server.md | 10 +++++++ .../cli/testdata/coder_server_--help.golden | 4 +++ site/src/api/typesGenerated.ts | 2 ++ .../pages/LoginPage/LoginPageView.stories.tsx | 12 ++++++++ site/src/pages/LoginPage/LoginPageView.tsx | 19 ++++++++++++- site/src/pages/LoginPage/SignInForm.tsx | 2 +- .../pages/LoginPage/TermsOfServiceLink.tsx | 28 +++++++++++++++++++ .../SecurityPage/SingleSignOnSection.tsx | 5 +--- site/src/testHelpers/entities.ts | 7 +++++ 19 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 site/src/pages/LoginPage/TermsOfServiceLink.tsx diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index f7ba3b2f80..6d8f866c11 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -60,6 +60,10 @@ OPTIONS: --support-links struct[[]codersdk.LinkConfig], $CODER_SUPPORT_LINKS Support links to display in the top right drop down menu. + --terms-of-service-url string, $CODER_TERMS_OF_SERVICE_URL + A URL to an external Terms of Service that must be accepted by users + when logging in. + --update-check bool, $CODER_UPDATE_CHECK (default: false) Periodically check for new releases of Coder and inform the owner. The check is performed once per day. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index f70d8b8825..4366dbc4d9 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -414,6 +414,10 @@ inMemoryDatabase: false # Type of auth to use when connecting to postgres. # (default: password, type: enum[password\|awsiamrds]) pgAuth: password +# A URL to an external Terms of Service that must be accepted by users when +# logging in. +# (default: , type: string) +termsOfServiceURL: "" # The algorithm to use for generating ssh keys. Accepted values are "ed25519", # "ecdsa", or "rsa4096". # (default: ed25519, type: string) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 60ed16632c..a87ee28027 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8446,6 +8446,9 @@ const docTemplate = `{ }, "password": { "$ref": "#/definitions/codersdk.AuthMethod" + }, + "terms_of_service_url": { + "type": "string" } } }, @@ -9408,6 +9411,9 @@ const docTemplate = `{ "telemetry": { "$ref": "#/definitions/codersdk.TelemetryConfig" }, + "terms_of_service_url": { + "type": "string" + }, "tls": { "$ref": "#/definitions/codersdk.TLSConfig" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 785450be71..7d03086d55 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7515,6 +7515,9 @@ }, "password": { "$ref": "#/definitions/codersdk.AuthMethod" + }, + "terms_of_service_url": { + "type": "string" } } }, @@ -8413,6 +8416,9 @@ "telemetry": { "$ref": "#/definitions/codersdk.TelemetryConfig" }, + "terms_of_service_url": { + "type": "string" + }, "tls": { "$ref": "#/definitions/codersdk.TLSConfig" }, diff --git a/coderd/userauth.go b/coderd/userauth.go index eda4dd60ab..3f341db65b 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -472,6 +472,7 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) { } httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AuthMethods{ + TermsOfServiceURL: api.DeploymentValues.TermsOfServiceURL.Value(), Password: codersdk.AuthMethod{ Enabled: !api.DeploymentValues.DisablePasswordAuth.Value(), }, diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 34eaa4edd4..d2da0c3183 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -200,6 +200,7 @@ type DeploymentValues struct { AllowWorkspaceRenames serpent.Bool `json:"allow_workspace_renames,omitempty" typescript:",notnull"` Healthcheck HealthcheckConfig `json:"healthcheck,omitempty" typescript:",notnull"` CLIUpgradeMessage serpent.String `json:"cli_upgrade_message,omitempty" typescript:",notnull"` + TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"` Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"` WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"` @@ -1683,6 +1684,14 @@ when required by your organization's security policy.`, YAML: "secureAuthCookie", Annotations: serpent.Annotations{}.Mark(annotationExternalProxies, "true"), }, + { + Name: "Terms of Service URL", + Description: "A URL to an external Terms of Service that must be accepted by users when logging in.", + Flag: "terms-of-service-url", + Env: "CODER_TERMS_OF_SERVICE_URL", + YAML: "termsOfServiceURL", + Value: &c.TermsOfServiceURL, + }, { Name: "Strict-Transport-Security", Description: "Controls if the 'Strict-Transport-Security' header is set on all static file responses. " + diff --git a/codersdk/users.go b/codersdk/users.go index 9fe6d8eb65..7eb7604fc5 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -209,9 +209,10 @@ type CreateOrganizationRequest struct { // AuthMethods contains authentication method information like whether they are enabled or not or custom text, etc. type AuthMethods struct { - Password AuthMethod `json:"password"` - Github AuthMethod `json:"github"` - OIDC OIDCAuthMethod `json:"oidc"` + TermsOfServiceURL string `json:"terms_of_service_url,omitempty"` + Password AuthMethod `json:"password"` + Github AuthMethod `json:"github"` + OIDC OIDCAuthMethod `json:"oidc"` } type AuthMethod struct { diff --git a/docs/api/general.md b/docs/api/general.md index 330c41a335..95b3933a9a 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -377,6 +377,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "user": {} } }, + "terms_of_service_url": "string", "tls": { "address": { "host": "string", diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 1d00ac18c3..9f2a74752a 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1040,17 +1040,19 @@ }, "password": { "enabled": true - } + }, + "terms_of_service_url": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ---------- | -------------------------------------------------- | -------- | ------------ | ----------- | -| `github` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | -| `oidc` | [codersdk.OIDCAuthMethod](#codersdkoidcauthmethod) | false | | | -| `password` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | +| Name | Type | Required | Restrictions | Description | +| ---------------------- | -------------------------------------------------- | -------- | ------------ | ----------- | +| `github` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | +| `oidc` | [codersdk.OIDCAuthMethod](#codersdkoidcauthmethod) | false | | | +| `password` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | | +| `terms_of_service_url` | string | false | | | ## codersdk.AuthorizationCheck @@ -2102,6 +2104,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "user": {} } }, + "terms_of_service_url": "string", "tls": { "address": { "host": "string", @@ -2474,6 +2477,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "user": {} } }, + "terms_of_service_url": "string", "tls": { "address": { "host": "string", @@ -2562,6 +2566,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `support` | [codersdk.SupportConfig](#codersdksupportconfig) | false | | | | `swagger` | [codersdk.SwaggerConfig](#codersdkswaggerconfig) | false | | | | `telemetry` | [codersdk.TelemetryConfig](#codersdktelemetryconfig) | false | | | +| `terms_of_service_url` | string | false | | | | `tls` | [codersdk.TLSConfig](#codersdktlsconfig) | false | | | | `trace` | [codersdk.TraceConfig](#codersdktraceconfig) | false | | | | `update_check` | boolean | false | | | diff --git a/docs/api/users.md b/docs/api/users.md index a057376b38..c9910bf66c 100644 --- a/docs/api/users.md +++ b/docs/api/users.md @@ -157,7 +157,8 @@ curl -X GET http://coder-server:8080/api/v2/users/authmethods \ }, "password": { "enabled": true - } + }, + "terms_of_service_url": "string" } ``` diff --git a/docs/cli/server.md b/docs/cli/server.md index 2a793c6faf..a7c32c2d78 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -928,6 +928,16 @@ Type of auth to use when connecting to postgres. Controls if the 'Secure' property is set on browser session cookies. +### --terms-of-service-url + +| | | +| ----------- | ---------------------------------------- | +| Type | string | +| Environment | $CODER_TERMS_OF_SERVICE_URL | +| YAML | termsOfServiceURL | + +A URL to an external Terms of Service that must be accepted by users when logging in. + ### --strict-transport-security | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 50dfa3bdd4..4d4576d6d5 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -61,6 +61,10 @@ OPTIONS: --support-links struct[[]codersdk.LinkConfig], $CODER_SUPPORT_LINKS Support links to display in the top right drop down menu. + --terms-of-service-url string, $CODER_TERMS_OF_SERVICE_URL + A URL to an external Terms of Service that must be accepted by users + when logging in. + --update-check bool, $CODER_UPDATE_CHECK (default: false) Periodically check for new releases of Coder and inform the owner. The check is performed once per day. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 13971a0345..45062e910f 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -124,6 +124,7 @@ export interface AuthMethod { // From codersdk/users.go export interface AuthMethods { + readonly terms_of_service_url?: string; readonly password: AuthMethod; readonly github: AuthMethod; readonly oidc: OIDCAuthMethod; @@ -445,6 +446,7 @@ export interface DeploymentValues { readonly allow_workspace_renames?: boolean; readonly healthcheck?: HealthcheckConfig; readonly cli_upgrade_message?: string; + readonly terms_of_service_url?: string; readonly config?: string; readonly write_config?: boolean; readonly address?: string; diff --git a/site/src/pages/LoginPage/LoginPageView.stories.tsx b/site/src/pages/LoginPage/LoginPageView.stories.tsx index 48ea6e31d7..d52316ff30 100644 --- a/site/src/pages/LoginPage/LoginPageView.stories.tsx +++ b/site/src/pages/LoginPage/LoginPageView.stories.tsx @@ -3,6 +3,8 @@ import { MockAuthMethodsAll, MockAuthMethodsExternal, MockAuthMethodsPasswordOnly, + MockAuthMethodsPasswordTermsOfService, + MockBuildInfo, mockApiError, } from "testHelpers/entities"; import { LoginPageView } from "./LoginPageView"; @@ -10,6 +12,9 @@ import { LoginPageView } from "./LoginPageView"; const meta: Meta = { title: "pages/LoginPage", component: LoginPageView, + args: { + buildInfo: MockBuildInfo, + }, }; export default meta; @@ -33,6 +38,12 @@ export const WithAllAuthMethods: Story = { }, }; +export const WithTermsOfService: Story = { + args: { + authMethods: MockAuthMethodsPasswordTermsOfService, + }, +}; + export const AuthError: Story = { args: { error: mockApiError({ @@ -53,6 +64,7 @@ export const ExternalAuthError: Story = { export const LoadingAuthMethods: Story = { args: { + isLoading: true, authMethods: undefined, }, }; diff --git a/site/src/pages/LoginPage/LoginPageView.tsx b/site/src/pages/LoginPage/LoginPageView.tsx index cd585f3f1d..c2c369b745 100644 --- a/site/src/pages/LoginPage/LoginPageView.tsx +++ b/site/src/pages/LoginPage/LoginPageView.tsx @@ -1,5 +1,6 @@ import type { Interpolation, Theme } from "@emotion/react"; -import type { FC } from "react"; +import Button from "@mui/material/Button"; +import { type FC, useState } from "react"; import { useLocation } from "react-router-dom"; import type { AuthMethods, BuildInfoResponse } from "api/typesGenerated"; import { CoderIcon } from "components/Icons/CoderIcon"; @@ -7,6 +8,7 @@ import { Loader } from "components/Loader/Loader"; import { getApplicationName, getLogoURL } from "utils/appearance"; import { retrieveRedirect } from "utils/redirect"; import { SignInForm } from "./SignInForm"; +import { TermsOfServiceLink } from "./TermsOfServiceLink"; export interface LoginPageViewProps { authMethods: AuthMethods | undefined; @@ -49,12 +51,21 @@ export const LoginPageView: FC = ({ ); + const [tosAccepted, setTosAccepted] = useState(false); + const tosAcceptanceRequired = + authMethods?.terms_of_service_url && !tosAccepted; + return (
{applicationLogo} {isLoading ? ( + ) : tosAcceptanceRequired ? ( + <> + + + ) : ( = ({ Copyright © {new Date().getFullYear()} Coder Technologies, Inc.
{buildInfo?.version}
+ {tosAccepted && ( + + )}
diff --git a/site/src/pages/LoginPage/SignInForm.tsx b/site/src/pages/LoginPage/SignInForm.tsx index 4efc0fbf7a..40930fd023 100644 --- a/site/src/pages/LoginPage/SignInForm.tsx +++ b/site/src/pages/LoginPage/SignInForm.tsx @@ -110,7 +110,7 @@ export const SignInForm: FC = ({ {passwordEnabled && oAuthEnabled && (
-
Or
+
or
)} diff --git a/site/src/pages/LoginPage/TermsOfServiceLink.tsx b/site/src/pages/LoginPage/TermsOfServiceLink.tsx new file mode 100644 index 0000000000..6a2659b17b --- /dev/null +++ b/site/src/pages/LoginPage/TermsOfServiceLink.tsx @@ -0,0 +1,28 @@ +import LaunchIcon from "@mui/icons-material/LaunchOutlined"; +import Link from "@mui/material/Link"; +import type { FC } from "react"; + +interface TermsOfServiceLinkProps { + className?: string; + url?: string; +} + +export const TermsOfServiceLink: FC = ({ + className, + url, +}) => { + return ( +
+ By continuing, you agree to the{" "} + + Terms of Service  + + +
+ ); +}; diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx index 6d743594a9..f2c14dcd45 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SingleSignOnSection.tsx @@ -136,10 +136,7 @@ export const SingleSignOnSection: FC = ({ }) => { const theme = useTheme(); - const authList = Object.values( - authMethods, - ) as (typeof authMethods)[keyof typeof authMethods][]; - const noSsoEnabled = !authList.some((method) => method.enabled); + const noSsoEnabled = !authMethods.github.enabled && !authMethods.oidc.enabled; return ( <> diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 3d9f837ab3..6604faf96e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1373,6 +1373,13 @@ export const MockAuthMethodsPasswordOnly: TypesGen.AuthMethods = { oidc: { enabled: false, signInText: "", iconUrl: "" }, }; +export const MockAuthMethodsPasswordTermsOfService: TypesGen.AuthMethods = { + terms_of_service_url: "https://www.youtube.com/watch?v=C2f37Vb2NAE", + password: { enabled: true }, + github: { enabled: false }, + oidc: { enabled: false, signInText: "", iconUrl: "" }, +}; + export const MockAuthMethodsExternal: TypesGen.AuthMethods = { password: { enabled: false }, github: { enabled: true },