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