diff --git a/api/client/webclient/webconfig.go b/api/client/webclient/webconfig.go index 5cf48727a46c3..cdedc2abd2281 100644 --- a/api/client/webclient/webconfig.go +++ b/api/client/webclient/webconfig.go @@ -107,4 +107,6 @@ type WebConfigAuthSettings struct { LocalConnectorName string `json:"localConnectorName,omitempty"` // PrivateKeyPolicy is the configured private key policy for the cluster. PrivateKeyPolicy keys.PrivateKeyPolicy `json:"privateKeyPolicy,omitempty"` + // MOTD is message of the day. MOTD is displayed to users before login. + MOTD string `json:"motd"` } diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index e7f7f9f49f726..4ec2993ab7854 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -1378,6 +1378,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou PreferredLocalMFA: cap.GetPreferredLocalMFA(), LocalConnectorName: localConnectorName, PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), + MOTD: cap.GetMessageOfTheDay(), } } diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 045f217825b13..19260a121fa65 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -4223,6 +4223,7 @@ func TestGetWebConfig(t *testing.T) { env := newWebPack(t, 1) // Set auth preference with passwordless. + const MOTD = "Welcome to cluster, your activity will be recorded." ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{ Type: constants.Local, SecondFactor: constants.SecondFactorOptional, @@ -4230,6 +4231,7 @@ func TestGetWebConfig(t *testing.T) { Webauthn: &types.Webauthn{ RPID: "localhost", }, + MessageOfTheDay: MOTD, }) require.NoError(t, err) err = env.server.Auth().SetAuthPreference(ctx, ap) @@ -4263,6 +4265,7 @@ func TestGetWebConfig(t *testing.T) { PreferredLocalMFA: constants.SecondFactorWebauthn, LocalConnectorName: constants.PasswordlessConnector, PrivateKeyPolicy: keys.PrivateKeyPolicyNone, + MOTD: MOTD, }, CanJoinSessions: true, ProxyClusterName: env.server.ClusterName(), diff --git a/web/packages/teleport/src/Login/Login.story.tsx b/web/packages/teleport/src/Login/Login.story.tsx index 26873668bde2a..9f12549316e90 100644 --- a/web/packages/teleport/src/Login/Login.story.tsx +++ b/web/packages/teleport/src/Login/Login.story.tsx @@ -52,4 +52,7 @@ const sample: State = { isPasswordlessEnabled: false, primaryAuthType: 'local', privateKeyPolicyEnabled: false, + motd: '', + showMotd: false, + acknowledgeMotd: () => null, }; diff --git a/web/packages/teleport/src/Login/Login.test.tsx b/web/packages/teleport/src/Login/Login.test.tsx index f3ea802acd0fc..8102775cb9653 100644 --- a/web/packages/teleport/src/Login/Login.test.tsx +++ b/web/packages/teleport/src/Login/Login.test.tsx @@ -113,3 +113,42 @@ test('login with private key policy enabled through role setting', async () => { expect(screen.queryByPlaceholderText(/username/i)).not.toBeInTheDocument(); expect(screen.getByText(/login disabled/i)).toBeInTheDocument(); }); + +test('show motd only if motd is set', async () => { + // default login form + const { unmount } = render(); + expect(screen.getByPlaceholderText(/username/i)).toBeInTheDocument(); + expect( + screen.queryByText('Welcome to cluster, your activity will be recorded.') + ).not.toBeInTheDocument(); + unmount(); + + // now set motd + jest + .spyOn(cfg, 'getMotd') + .mockImplementation( + () => 'Welcome to cluster, your activity will be recorded.' + ); + + render(); + + expect( + screen.getByText('Welcome to cluster, your activity will be recorded.') + ).toBeInTheDocument(); + expect(screen.queryByPlaceholderText(/username/i)).not.toBeInTheDocument(); +}); + +test('show login form after modt acknowledge', async () => { + jest + .spyOn(cfg, 'getMotd') + .mockImplementation( + () => 'Welcome to cluster, your activity will be recorded.' + ); + render(); + expect( + screen.getByText('Welcome to cluster, your activity will be recorded.') + ).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Acknowledge')); + expect(screen.getByPlaceholderText(/username/i)).toBeInTheDocument(); +}); diff --git a/web/packages/teleport/src/Login/Login.tsx b/web/packages/teleport/src/Login/Login.tsx index ec1ef14fc47a0..5a35110cce0ec 100644 --- a/web/packages/teleport/src/Login/Login.tsx +++ b/web/packages/teleport/src/Login/Login.tsx @@ -22,6 +22,7 @@ import FormLogin from 'teleport/components/FormLogin'; import Logo from 'teleport/components/LogoHero'; import useLogin, { State } from './useLogin'; +import Motd from './Motd'; export default function Container() { const state = useLogin(); @@ -41,25 +42,32 @@ export function Login({ isPasswordlessEnabled, primaryAuthType, privateKeyPolicyEnabled, + motd, + showMotd, + acknowledgeMotd, }: State) { return ( <> - + {showMotd ? ( + + ) : ( + + )} ); } diff --git a/web/packages/teleport/src/Login/Motd/Motd.tsx b/web/packages/teleport/src/Login/Motd/Motd.tsx new file mode 100644 index 0000000000000..ad26965f71764 --- /dev/null +++ b/web/packages/teleport/src/Login/Motd/Motd.tsx @@ -0,0 +1,38 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { Card, Box, Text, ButtonPrimary } from 'design'; + +export function Motd({ message, onClick }: Props) { + return ( + + + + {message} + + + Acknowledge + + + + ); +} + +type Props = { + message: string; + onClick(): void; +}; diff --git a/web/packages/teleport/src/Login/Motd/index.ts b/web/packages/teleport/src/Login/Motd/index.ts new file mode 100644 index 0000000000000..811eedbe483a5 --- /dev/null +++ b/web/packages/teleport/src/Login/Motd/index.ts @@ -0,0 +1,17 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export { Motd as default } from './Motd'; diff --git a/web/packages/teleport/src/Login/useLogin.ts b/web/packages/teleport/src/Login/useLogin.ts index cbf24265c6757..5ce89419d3be4 100644 --- a/web/packages/teleport/src/Login/useLogin.ts +++ b/web/packages/teleport/src/Login/useLogin.ts @@ -38,6 +38,12 @@ export default function useLogin() { const authProviders = cfg.getAuthProviders(); const auth2faType = cfg.getAuth2faType(); const isLocalAuthEnabled = cfg.getLocalAuthFlag(); + const motd = cfg.getMotd(); + const [showMotd, setShowMotd] = useState(!!motd); + + function acknowledgeMotd() { + setShowMotd(false); + } function onLogin(email, password, token) { attemptActions.start(); @@ -87,6 +93,9 @@ export default function useLogin() { isPasswordlessEnabled: cfg.isPasswordlessEnabled(), primaryAuthType: cfg.getPrimaryAuthType(), privateKeyPolicyEnabled, + motd, + showMotd, + acknowledgeMotd, }; } diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index cc1dc08fdb139..9c52c8d3ba645 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -66,6 +66,8 @@ const cfg = { authType: 'local' as AuthType, preferredLocalMfa: '' as PreferredMfaType, privateKeyPolicy: 'none' as PrivateKeyPolicy, + // motd is message of the day, displayed to users before login. + motd: '', }, proxyCluster: 'localhost', @@ -280,6 +282,10 @@ const cfg = { return cfg.auth ? cfg.auth.preferredLocalMfa : null; }, + getMotd() { + return cfg.auth.motd; + }, + getLocalAuthFlag() { return cfg.auth.localAuthEnabled; },