diff --git a/web/packages/shared/services/consts.ts b/web/packages/shared/services/consts.ts new file mode 100644 index 0000000000000..3a0c4dcd05ebe --- /dev/null +++ b/web/packages/shared/services/consts.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2022 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 const privateKeyEnablingPolicies = [ + 'hardware_key', + 'hardware_key_touch', +] as const; diff --git a/web/packages/shared/services/index.ts b/web/packages/shared/services/index.ts index c6063bd9efa40..61db3159abc73 100644 --- a/web/packages/shared/services/index.ts +++ b/web/packages/shared/services/index.ts @@ -13,4 +13,5 @@ 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 * from './consts'; export * from './types'; diff --git a/web/packages/shared/services/types.ts b/web/packages/shared/services/types.ts index 16e10239d658a..9d608e3a9abb3 100644 --- a/web/packages/shared/services/types.ts +++ b/web/packages/shared/services/types.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { privateKeyEnablingPolicies } from './consts'; + export type AuthProviderType = 'oidc' | 'saml' | 'github'; export type Auth2faType = 'otp' | 'off' | 'optional' | 'on' | 'webauthn'; @@ -42,3 +44,7 @@ export type AuthProvider = { type: AuthProviderType; url: string; }; + +export type PrivateKeyPolicy = + | 'none' + | typeof privateKeyEnablingPolicies[number]; diff --git a/web/packages/shared/utils/errorType.ts b/web/packages/shared/utils/errorType.ts index 674fa746bacd1..79e7aab24bc74 100644 --- a/web/packages/shared/utils/errorType.ts +++ b/web/packages/shared/utils/errorType.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +import { privateKeyEnablingPolicies } from 'shared/services'; + +export function isPrivateKeyRequiredError(err: Error) { + return privateKeyEnablingPolicies.some(p => err.message.includes(p)); +} + // getErrMessage first checks if the error is of type Error // before attempting to access the error message field. // Used with try catch blocks, where the error caught diff --git a/web/packages/teleport/src/Login/Login.test.tsx b/web/packages/teleport/src/Login/Login.test.tsx index 7002400342996..61ee3d1d1d1d2 100644 --- a/web/packages/teleport/src/Login/Login.test.tsx +++ b/web/packages/teleport/src/Login/Login.test.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { render, fireEvent, screen, waitFor } from 'design/utils/testing'; +import { privateKeyEnablingPolicies } from 'shared/services/consts'; import auth from 'teleport/services/auth/auth'; import history from 'teleport/services/history'; @@ -78,6 +79,41 @@ test('login with SSO', () => { ); }); +test('login with private key policy enabled through cluster wide', () => { + jest + .spyOn(cfg, 'getPrivateKeyPolicy') + .mockImplementation(() => 'hardware_key'); + + render(); + + expect(screen.queryByPlaceholderText(/username/i)).not.toBeInTheDocument(); + expect(screen.getByText(/login disabled/i)).toBeInTheDocument(); +}); + +test('login with private key policy enabled through role setting', async () => { + // Just needs any of these enabling keywords in error message + jest + .spyOn(auth, 'login') + .mockRejectedValue(new Error(privateKeyEnablingPolicies[0])); + + render(); + + // Fill form. + const username = screen.getByPlaceholderText(/username/i); + const password = screen.getByPlaceholderText(/password/i); + fireEvent.change(username, { target: { value: 'username' } }); + fireEvent.change(password, { target: { value: '123' } }); + + // Test logging in with private key error return renders private policy error. + fireEvent.click(screen.getByText('Sign In')); + await waitFor(() => { + expect(auth.login).toHaveBeenCalledWith('username', '123', ''); + }); + + expect(screen.queryByPlaceholderText(/username/i)).not.toBeInTheDocument(); + expect(screen.getByText(/login disabled/i)).toBeInTheDocument(); +}); + describe('test MOTD', () => { test('show motd only if motd is set', async () => { // default login form diff --git a/web/packages/teleport/src/Login/useLogin.ts b/web/packages/teleport/src/Login/useLogin.ts index f99d9a2b2dd89..8cc60b3a751bd 100644 --- a/web/packages/teleport/src/Login/useLogin.ts +++ b/web/packages/teleport/src/Login/useLogin.ts @@ -17,6 +17,7 @@ import { useState } from 'react'; import { useAttempt } from 'shared/hooks'; import { AuthProvider } from 'shared/services'; +import { isPrivateKeyRequiredError } from 'shared/utils/errorType'; import history from 'teleport/services/history'; import cfg from 'teleport/config'; @@ -24,6 +25,15 @@ import auth, { UserCredentials } from 'teleport/services/auth'; export default function useLogin() { const [attempt, attemptActions] = useAttempt({ isProcessing: false }); + // privateKeyPolicyEnabled can be enabled through cluster wide config, + // or through a role setting. + // Cluster wide config takes precedence and the user will not + // see a login form which prevents login attempts. + // Role setting requires the user to try a successful + // attempt at logging in to determine if private key policy was enabled. + const [privateKeyPolicyEnabled, setPrivateKeyPolicyEnabled] = useState( + cfg.getPrivateKeyPolicy() != 'none' + ); const authProviders = cfg.getAuthProviders(); const auth2faType = cfg.getAuth2faType(); @@ -48,6 +58,10 @@ export default function useLogin() { .login(email, password, token) .then(onSuccess) .catch(err => { + if (isPrivateKeyRequiredError(err)) { + setPrivateKeyPolicyEnabled(true); + return; + } attemptActions.error(err); }); } @@ -58,6 +72,10 @@ export default function useLogin() { .loginWithWebauthn(creds) .then(onSuccess) .catch(err => { + if (isPrivateKeyRequiredError(err)) { + setPrivateKeyPolicyEnabled(true); + return; + } attemptActions.error(err); }); } @@ -81,7 +99,7 @@ export default function useLogin() { clearAttempt: attemptActions.clear, isPasswordlessEnabled: cfg.isPasswordlessEnabled(), primaryAuthType: cfg.getPrimaryAuthType(), - privateKeyPolicyEnabled: false, + privateKeyPolicyEnabled, motd, showMotd, acknowledgeMotd, diff --git a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.test.tsx b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.test.tsx index 5fa612fa07c19..d2f2d7ec6267f 100644 --- a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.test.tsx +++ b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.test.tsx @@ -57,6 +57,14 @@ test('story.SuccessReset', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); +test('story.SuccessAndPrivateKeyEnabledRegister', () => { + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); +}); +test('story.SuccessAndPrivateKeyEnabledReset', () => { + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); +}); test('story.SuccessRegisterDashboard', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); diff --git a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx index 32220615b66c0..48138e7347ff5 100644 --- a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx +++ b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx @@ -137,6 +137,19 @@ export const SuccessReset = () => resetMode: true, }); +export const SuccessAndPrivateKeyEnabledRegister = () => + renderNewCredentials({ + success: true, + privateKeyPolicyEnabled: true, + }); + +export const SuccessAndPrivateKeyEnabledReset = () => + renderNewCredentials({ + success: true, + resetMode: true, + privateKeyPolicyEnabled: true, + }); + export const SuccessRegisterDashboard = () => renderNewCredentials({ success: true, diff --git a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.test.tsx b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.test.tsx index d425739e3348e..e788f685c7524 100644 --- a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.test.tsx +++ b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.test.tsx @@ -28,6 +28,7 @@ import { makeTestUserContext } from 'teleport/User/testHelpers/makeTestUserConte const attempt: Attempt = { status: '' }; const failedAttempt: Attempt = { status: 'failed' }; const processingAttempt: Attempt = { status: 'processing' }; +const successAttempt: Attempt = { status: 'success', statusText: 'hey' }; const resetToken: ResetToken = { tokenId: 'tokenId', @@ -79,9 +80,32 @@ test.each(nullCases)('renders $attempt as null', testCase => { expect(container).toBeEmptyDOMElement(); }); +test('renders Reset Complete for success and private key policy enabled during reset', () => { + const props = makeProps(); + props.fetchAttempt = successAttempt; + props.success = true; + props.privateKeyPolicyEnabled = true; + props.resetMode = true; + render(); + + expect(screen.getByText(/Reset Complete/i)).toBeInTheDocument(); +}); + +test('renders Registration Complete for success and private key policy enabled during registration', () => { + const props = makeProps(); + props.fetchAttempt = { status: 'success' }; + props.success = true; + props.privateKeyPolicyEnabled = true; + props.resetMode = false; + render(); + + expect(screen.getByText(/Registration Complete/i)).toBeInTheDocument(); +}); + test('renders Register Success on success', () => { const props = makeProps(); props.fetchAttempt = { status: 'success' }; + props.privateKeyPolicyEnabled = false; props.recoveryCodes = undefined; props.success = true; render(); diff --git a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx index 88c864b8dbb64..49a6f7ec6d958 100644 --- a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx +++ b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx @@ -22,6 +22,7 @@ import { OnboardCard } from 'design/Onboard/OnboardCard'; import { Box } from 'design'; import RecoveryCodes from 'teleport/components/RecoveryCodes'; +import { PrivateKeyLoginDisabledCard } from 'teleport/components/PrivateKeyPolicy'; import cfg from 'teleport/config'; import { loginFlows } from 'teleport/Welcome/NewCredentials/constants'; @@ -60,6 +61,7 @@ export function NewCredentials(props: NewCredentialsProps) { primaryAuthType, success, finishedRegister, + privateKeyPolicyEnabled, isDashboard, displayOnboardingQuestionnaire = false, setDisplayOnboardingQuestionnaire = false, @@ -84,6 +86,14 @@ export function NewCredentials(props: NewCredentialsProps) { return null; } + if (success && privateKeyPolicyEnabled) { + return ( + + ); + } + if ( success && !resetMode && diff --git a/web/packages/teleport/src/Welcome/NewCredentials/__snapshots__/NewCredentials.story.test.tsx.snap b/web/packages/teleport/src/Welcome/NewCredentials/__snapshots__/NewCredentials.story.test.tsx.snap index 30eb388b0aad8..ebaca56c218c1 100644 --- a/web/packages/teleport/src/Welcome/NewCredentials/__snapshots__/NewCredentials.story.test.tsx.snap +++ b/web/packages/teleport/src/Welcome/NewCredentials/__snapshots__/NewCredentials.story.test.tsx.snap @@ -3110,6 +3110,598 @@ exports[`story.PrimaryPasswordlessError 1`] = ` `; +exports[`story.SuccessAndPrivateKeyEnabledRegister 1`] = ` +.c6 { + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + box-sizing: border-box; + box-shadow: 0 1px 4px rgba(0,0,0,0.24); + margin: 0 0 24px 0; + min-height: 40px; + padding: 8px 16px; + overflow: auto; + word-break: break-word; + line-height: 1.5; + margin-top: 32px; + margin-bottom: 32px; + background: #FF6257; + color: #000000; +} + +.c6 a { + color: #FFFFFF; +} + +.c5 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 400; + font-size: 18px; + line-height: 32px; + margin: 0px; + padding-top: 24px; + color: #FFFFFF; + text-align: center; +} + +.c7 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 300; + font-size: 12px; + line-height: 24px; + margin: 0px; + margin-bottom: 8px; +} + +.c11 { + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; +} + +.c8 { + color: #009EFF; + font-weight: normal; + background: none; + text-decoration: underline; + text-transform: none; + color: #FFFFFF; +} + +.c12 { + color: #009EFF; + font-weight: normal; + background: none; + text-decoration: underline; + text-transform: none; +} + +.c1 { + box-sizing: border-box; + height: 100%; + display: flex; + justify-content: space-between; + flex-direction: column; +} + +.c2 { + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.c10 { + box-sizing: border-box; + display: flex; + justify-content: center; + width: 100%; + gap: 50px; +} + +.c9 { + padding-bottom: 24px; + width: 100%; + color: white; +} + +.c13 { + color: white; + text-decoration: none; +} + +.c13:hover, +.c13:active, +.c13:focus { + color: rgba(255,255,255,0.54); +} + +.c0 { + position: absolute; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + overflow: hidden; + z-index: -2; + background: url('file_stub'); + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; +} + +.c0::after { + content: ''; + top: 0; + left: 0; + bottom: 0; + right: 0; + position: absolute; + z-index: -1; + background-color: black; + opacity: 0.25; + backdrop-filter: blur(17.5px); +} + +.c3 { + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + margin: 24px 0; +} + +.c4 { + box-sizing: border-box; + box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px rgba(0,0,0,0.14),0px 1px 18px rgba(0,0,0,0.12); + border-radius: 8px; + background-color: #222C59; + width: 600px; + padding: 24px; + text-align: left; + margin: 16px auto 16px auto; + overflow-y: auto; +} + +@media screen and (max-width:800px) { + .c10 { + flex-direction: column-reverse; + text-align: center; + gap: 10px; + } +} + +@media screen and (max-width:800px) { + .c4 { + width: auto; + margin: 20px; + } +} + +@media screen and (max-height:760px) { + .c4 { + height: calc(100vh - 250px); + } +} + +
+
+
+
+ + + +
+
+
+ Registration Complete +
+
+ Web UI Login Disabled +
+
+ This Teleport Cluster requires that user + + + private keys + + + be stored on hardware authentication devices. Since these keys are not accessible by web browsers, Web UI login has been disabled. Please use + + + Teleport Connect + + + or + + + tsh + + + to log in. +
+
+
+ +
+
+`; + +exports[`story.SuccessAndPrivateKeyEnabledReset 1`] = ` +.c6 { + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + box-sizing: border-box; + box-shadow: 0 1px 4px rgba(0,0,0,0.24); + margin: 0 0 24px 0; + min-height: 40px; + padding: 8px 16px; + overflow: auto; + word-break: break-word; + line-height: 1.5; + margin-top: 32px; + margin-bottom: 32px; + background: #FF6257; + color: #000000; +} + +.c6 a { + color: #FFFFFF; +} + +.c5 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 400; + font-size: 18px; + line-height: 32px; + margin: 0px; + padding-top: 24px; + color: #FFFFFF; + text-align: center; +} + +.c7 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 300; + font-size: 12px; + line-height: 24px; + margin: 0px; + margin-bottom: 8px; +} + +.c11 { + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; +} + +.c8 { + color: #009EFF; + font-weight: normal; + background: none; + text-decoration: underline; + text-transform: none; + color: #FFFFFF; +} + +.c12 { + color: #009EFF; + font-weight: normal; + background: none; + text-decoration: underline; + text-transform: none; +} + +.c1 { + box-sizing: border-box; + height: 100%; + display: flex; + justify-content: space-between; + flex-direction: column; +} + +.c2 { + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.c10 { + box-sizing: border-box; + display: flex; + justify-content: center; + width: 100%; + gap: 50px; +} + +.c9 { + padding-bottom: 24px; + width: 100%; + color: white; +} + +.c13 { + color: white; + text-decoration: none; +} + +.c13:hover, +.c13:active, +.c13:focus { + color: rgba(255,255,255,0.54); +} + +.c0 { + position: absolute; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + overflow: hidden; + z-index: -2; + background: url('file_stub'); + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; +} + +.c0::after { + content: ''; + top: 0; + left: 0; + bottom: 0; + right: 0; + position: absolute; + z-index: -1; + background-color: black; + opacity: 0.25; + backdrop-filter: blur(17.5px); +} + +.c3 { + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + margin: 24px 0; +} + +.c4 { + box-sizing: border-box; + box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px rgba(0,0,0,0.14),0px 1px 18px rgba(0,0,0,0.12); + border-radius: 8px; + background-color: #222C59; + width: 600px; + padding: 24px; + text-align: left; + margin: 16px auto 16px auto; + overflow-y: auto; +} + +@media screen and (max-width:800px) { + .c10 { + flex-direction: column-reverse; + text-align: center; + gap: 10px; + } +} + +@media screen and (max-width:800px) { + .c4 { + width: auto; + margin: 20px; + } +} + +@media screen and (max-height:760px) { + .c4 { + height: calc(100vh - 250px); + } +} + +
+
+
+
+ + + +
+
+
+ Reset Complete +
+
+ Web UI Login Disabled +
+
+ This Teleport Cluster requires that user + + + private keys + + + be stored on hardware authentication devices. Since these keys are not accessible by web browsers, Web UI login has been disabled. Please use + + + Teleport Connect + + + or + + + tsh + + + to log in. +
+
+
+ +
+
+`; + exports[`story.SuccessRegister 1`] = ` .c9 { line-height: 1.5; diff --git a/web/packages/teleport/src/Welcome/useToken.ts b/web/packages/teleport/src/Welcome/useToken.ts index de7d970ace264..7bf579969bafe 100644 --- a/web/packages/teleport/src/Welcome/useToken.ts +++ b/web/packages/teleport/src/Welcome/useToken.ts @@ -32,6 +32,7 @@ export default function useToken(tokenId: string): UseTokenState { const [resetToken, setResetToken] = useState(); const [recoveryCodes, setRecoveryCodes] = useState(); const [success, setSuccess] = useState(false); // TODO rename + const [privateKeyPolicyEnabled, setPrivateKeyPolicyEnabled] = useState(false); const fetchAttempt = useAttempt(''); const submitAttempt = useAttempt(''); @@ -46,6 +47,9 @@ export default function useToken(tokenId: string): UseTokenState { }, []); function handleResponse(res: ChangedUserAuthn) { + if (res.privateKeyPolicyEnabled) { + setPrivateKeyPolicyEnabled(true); + } if (res.recovery.createdDate) { setRecoveryCodes(res.recovery); } else { @@ -105,6 +109,6 @@ export default function useToken(tokenId: string): UseTokenState { redirect, success, finishedRegister, - privateKeyPolicyEnabled: false, + privateKeyPolicyEnabled, }; } diff --git a/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx b/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx index 24d3c0ea59449..4a97005cfdb89 100644 --- a/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx +++ b/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx @@ -120,6 +120,10 @@ export const LocalWithSsoAndPwdless = () => { ); }; +export const PrivateKeyPolicyEnabled = () => ( + +); + export const LocalDisabledWithSso = () => { const ssoProvider = [ { name: 'github', type: 'oidc', url: '' } as const, diff --git a/web/packages/teleport/src/components/FormLogin/FormLogin.tsx b/web/packages/teleport/src/components/FormLogin/FormLogin.tsx index d66e248187b7c..0639d675690ef 100644 --- a/web/packages/teleport/src/components/FormLogin/FormLogin.tsx +++ b/web/packages/teleport/src/components/FormLogin/FormLogin.tsx @@ -44,6 +44,7 @@ import { import createMfaOptions, { MfaOption } from 'shared/utils/createMfaOptions'; import { StepSlider, StepComponentProps } from 'design/StepSlider'; +import { PrivateKeyLoginDisabledCard } from 'teleport/components/PrivateKeyPolicy'; import { UserCredentials } from 'teleport/services/auth'; import SSOButtonList from './SsoButtons'; @@ -54,7 +55,18 @@ export default function LoginForm(props: Props) { attempt, isLocalAuthEnabled = true, authProviders = [], + privateKeyPolicyEnabled, + isRecoveryEnabled, + onRecover, } = props; + if (privateKeyPolicyEnabled) { + return ( + + ); + } const ssoEnabled = authProviders?.length > 0; @@ -533,7 +545,7 @@ export type Props = { title?: string; isLocalAuthEnabled?: boolean; isPasswordlessEnabled: boolean; - privateKeyPolicyEnabled: boolean; + privateKeyPolicyEnabled?: boolean; authProviders?: AuthProvider[]; auth2faType?: Auth2faType; primaryAuthType: PrimaryAuthType; diff --git a/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.story.tsx b/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.story.tsx new file mode 100644 index 0000000000000..55131f2535f8e --- /dev/null +++ b/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.story.tsx @@ -0,0 +1,57 @@ +/** + * Copyright 2022 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 { + PrivateKeyLoginDisabledCard, + PrivateKeyAccessRequestDialogue, +} from './PrivateKeyPolicy'; + +export default { + title: 'Teleport/PrivateKeyPolicy', +}; + +export const CardDefault = () => ( + +); + +export const CardCloud = () => ( + null} + /> +); + +export const DialogueWithLocalAuth = () => ( + null} {...tshLoginProps} /> +); + +export const DialogueWithSso = () => ( + null} + {...tshLoginProps} + btnText="custom btn text" + authType="sso" + /> +); + +const tshLoginProps = { + username: 'llama', + authType: 'local' as any, + clusterId: 'cluster-id-1234', + accessRequestId: 'request-id-1234', +}; diff --git a/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.tsx b/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.tsx new file mode 100644 index 0000000000000..d88774c9a8690 --- /dev/null +++ b/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.tsx @@ -0,0 +1,150 @@ +/** + * Copyright 2022 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 { Text, Link, ButtonText, ButtonSecondary, Box } from 'design'; +import Dialog, { + DialogHeader, + DialogTitle, + DialogContent, + DialogFooter, +} from 'design/Dialog'; +import { Danger } from 'design/Alert'; + +import { OnboardCard } from 'design/Onboard/OnboardCard'; + +import { generateTshLoginCommand } from 'teleport/lib/util'; + +import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy'; + +import type { TshLoginCommand } from 'teleport/lib/util'; + +const LINK_HARDWARE_KEY_SUPPORT = + 'https://goteleport.com/docs/access-controls/guides/hardware-key-support/'; + +const LINK_TSH = + 'https://goteleport.com/docs/connect-your-client/tsh/#installing-tsh'; + +const LINK_CONNECT = + 'https://goteleport.com/docs/connect-your-client/teleport-connect/'; + +export const PrivateKeyLoginDisabledCard = ({ + title, + onRecover, +}: { + title: string; + // onRecover only applies to Teleport Cloud, + // and is called upon when user needs to recover + // lost password or two-factor device. + onRecover?: (isRecoverPassword: boolean) => void; +}) => ( + + + {title} + + Web UI Login Disabled + + This Teleport Cluster requires that user{' '} + + private keys + {' '} + be stored on hardware authentication devices. Since these keys are not + accessible by web browsers, Web UI login has been disabled. Please use{' '} + + Teleport Connect + {' '} + or{' '} + + tsh + {' '} + to log in. + + {onRecover && ( + + onRecover(true)} + style={{ padding: '0px', minHeight: 0 }} + mr={2} + > + Forgot Password? + + or{' '} + + Lost Two-Factor Device? + + + )} + +); + +export type PrivateKeyAccessRequest = TshLoginCommand & { + accessRequestId: string; +}; + +export function PrivateKeyAccessRequestDialogue({ + onClose, + btnText, + ...tshProps +}: PrivateKeyAccessRequest & { + btnText?: string; + onClose(): void; +}) { + return ( + ({ maxWidth: '500px', width: '100%' })} + onClose={close} + open={true} + > + + Private Key Policy + + + + This access requires use of hardware backed{' '} + + private keys + {' '} + which are not supported in the web. Please use{' '} + + tsh + {' '} + to login with the approved request ID or use{' '} + + Teleport Connect + + . + + + tsh login command with the requested access + + + + + {btnText || 'Okay'} + + + ); +} diff --git a/web/packages/teleport/src/components/PrivateKeyPolicy/index.ts b/web/packages/teleport/src/components/PrivateKeyPolicy/index.ts new file mode 100644 index 0000000000000..b0c63e65827d5 --- /dev/null +++ b/web/packages/teleport/src/components/PrivateKeyPolicy/index.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2022 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 { + PrivateKeyLoginDisabledCard, + PrivateKeyAccessRequestDialogue, +} from './PrivateKeyPolicy'; + +export type { PrivateKeyAccessRequest } from './PrivateKeyPolicy'; diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index e3201f230fb41..b4623812a8ffa 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -25,6 +25,7 @@ import type { AuthType, PreferredMfaType, PrimaryAuthType, + PrivateKeyPolicy, } from 'shared/services'; import type { SortType } from 'teleport/services/agents'; @@ -66,6 +67,7 @@ const cfg = { second_factor: 'off' as Auth2faType, authType: 'local' as AuthType, preferredLocalMfa: '' as PreferredMfaType, + privateKeyPolicy: 'none' as PrivateKeyPolicy, // motd is message of the day, displayed to users before login. motd: '', }, @@ -318,6 +320,10 @@ const cfg = { return cfg.auth.localAuthEnabled; }, + getPrivateKeyPolicy() { + return cfg.auth.privateKeyPolicy; + }, + isPasswordlessEnabled() { return cfg.auth.allowPasswordless; }, diff --git a/web/packages/teleport/src/services/auth/make.test.ts b/web/packages/teleport/src/services/auth/make.test.ts index 811d7de739333..76169bac61b14 100644 --- a/web/packages/teleport/src/services/auth/make.test.ts +++ b/web/packages/teleport/src/services/auth/make.test.ts @@ -19,6 +19,7 @@ import { makeChangedUserAuthn } from './make'; test('makeChangedUserAuthn with null', async () => { expect(makeChangedUserAuthn(null)).toStrictEqual({ recovery: { codes: [], createdDate: null }, + privateKeyPolicyEnabled: false, }); }); @@ -30,11 +31,13 @@ test('makeChangedUserAuthn with recovery codes', async () => { codes: ['llama', 'alpca'], created: date, }, + privateKeyPolicyEnabled: true, }) ).toStrictEqual({ recovery: { codes: ['llama', 'alpca'], createdDate: new Date('2022-10-25T00:30:18.162Z'), }, + privateKeyPolicyEnabled: true, }); }); diff --git a/web/packages/teleport/src/services/auth/make.ts b/web/packages/teleport/src/services/auth/make.ts index 91626a0682f68..020fee64e294c 100644 --- a/web/packages/teleport/src/services/auth/make.ts +++ b/web/packages/teleport/src/services/auth/make.ts @@ -24,6 +24,7 @@ export function makeChangedUserAuthn(json: any): ChangedUserAuthn { return { recovery: makeRecoveryCodes(json.recovery), + privateKeyPolicyEnabled: !!json.privateKeyPolicyEnabled, }; } diff --git a/web/packages/teleport/src/services/auth/types.ts b/web/packages/teleport/src/services/auth/types.ts index 679966848ca5d..0d246f76fcaa2 100644 --- a/web/packages/teleport/src/services/auth/types.ts +++ b/web/packages/teleport/src/services/auth/types.ts @@ -43,6 +43,7 @@ export type RecoveryCodes = { export type ChangedUserAuthn = { recovery: RecoveryCodes; + privateKeyPolicyEnabled?: boolean; }; export type NewCredentialRequest = {