diff --git a/packages/shared/services/consts.ts b/packages/shared/services/consts.ts new file mode 100644 index 000000000..3a0c4dcd0 --- /dev/null +++ b/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/packages/shared/services/index.ts b/packages/shared/services/index.ts index c6d3d5a11..61db3159a 100644 --- a/packages/shared/services/index.ts +++ b/packages/shared/services/index.ts @@ -13,5 +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/packages/shared/services/types.ts b/packages/shared/services/types.ts index 16e10239d..9d608e3a9 100644 --- a/packages/shared/services/types.ts +++ b/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/packages/shared/utils/errorType.ts b/packages/shared/utils/errorType.ts new file mode 100644 index 000000000..5a07e0208 --- /dev/null +++ b/packages/shared/utils/errorType.ts @@ -0,0 +1,21 @@ +/** + * 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 { privateKeyEnablingPolicies } from 'shared/services'; + +export function isPrivateKeyRequiredError(err: Error) { + return privateKeyEnablingPolicies.some(p => err.message.includes(p)); +} diff --git a/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx b/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx index ea03a56e1..6b2b9afed 100644 --- a/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx +++ b/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx @@ -26,6 +26,7 @@ import { Option } from 'shared/components/Select'; import TextSelectCopy from 'teleport/components/TextSelectCopy'; import useTeleport from 'teleport/useTeleport'; +import { generateTshLoginCommand } from 'teleport/lib/util'; import { Header, @@ -71,12 +72,6 @@ export function TestConnection({ () => userOpts[0] || { value: username, label: username } ); - const { hostname, port } = window.document.location; - const host = `${hostname}:${port || '443'}`; - const authSpec = - authType === 'local' ? `--auth=${authType} --user=${username} ` : ''; - const tshLoginCmd = `tsh login --proxy=${host} ${authSpec}${clusterId}`; - let $diagnosisStateComponent; if (attempt.status === 'processing') { $diagnosisStateComponent = ( @@ -263,7 +258,14 @@ export function TestConnection({ Log into your Teleport cluster - + Log into your Kubernetes cluster diff --git a/packages/teleport/src/Kubes/ConnectDialog/ConnectDialog.tsx b/packages/teleport/src/Kubes/ConnectDialog/ConnectDialog.tsx index dbbde3b56..c50ac867d 100644 --- a/packages/teleport/src/Kubes/ConnectDialog/ConnectDialog.tsx +++ b/packages/teleport/src/Kubes/ConnectDialog/ConnectDialog.tsx @@ -25,6 +25,7 @@ import { Text, Box, ButtonSecondary } from 'design'; import TextSelectCopy from 'teleport/components/TextSelectCopy'; import { AuthType } from 'teleport/services/user'; +import { generateTshLoginCommand } from 'teleport/lib/util'; function ConnectDialog(props: Props) { const { @@ -35,15 +36,6 @@ function ConnectDialog(props: Props) { clusterId, accessRequestId, } = props; - const { hostname, port } = window.document.location; - const host = `${hostname}:${port || '443'}`; - const authSpec = - authType === 'local' ? `--auth=${authType} --user=${username} ` : ''; - const text = `tsh login --proxy=${host} ${authSpec}${clusterId}`; - - const requestIdFlag = accessRequestId - ? ` --request-id=${accessRequestId}` - : ''; return ( {' - Login to Teleport'} - + diff --git a/packages/teleport/src/Login/Login.story.tsx b/packages/teleport/src/Login/Login.story.tsx index 0c8bd804d..26873668b 100644 --- a/packages/teleport/src/Login/Login.story.tsx +++ b/packages/teleport/src/Login/Login.story.tsx @@ -51,4 +51,5 @@ const sample: State = { clearAttempt: () => null, isPasswordlessEnabled: false, primaryAuthType: 'local', + privateKeyPolicyEnabled: false, }; diff --git a/packages/teleport/src/Login/Login.test.tsx b/packages/teleport/src/Login/Login.test.tsx index 2010d5147..fa13fe6d2 100644 --- a/packages/teleport/src/Login/Login.test.tsx +++ b/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'; @@ -24,9 +25,9 @@ import cfg from 'teleport/config'; import Login from './Login'; beforeEach(() => { + jest.restoreAllMocks(); jest.spyOn(history, 'push').mockImplementation(); jest.spyOn(history, 'getRedirectParam').mockImplementation(() => '/'); - jest.resetAllMocks(); }); test('basic rendering', () => { @@ -77,3 +78,38 @@ test('login with SSO', () => { true ); }); + +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(); +}); diff --git a/packages/teleport/src/Login/Login.tsx b/packages/teleport/src/Login/Login.tsx index 6b1e241be..7aa7f5eee 100644 --- a/packages/teleport/src/Login/Login.tsx +++ b/packages/teleport/src/Login/Login.tsx @@ -40,6 +40,7 @@ export function Login({ clearAttempt, isPasswordlessEnabled, primaryAuthType, + privateKeyPolicyEnabled, }: State) { return ( <> @@ -57,6 +58,7 @@ export function Login({ clearAttempt={clearAttempt} isPasswordlessEnabled={isPasswordlessEnabled} primaryAuthType={primaryAuthType} + privateKeyPolicyEnabled={privateKeyPolicyEnabled} /> ); diff --git a/packages/teleport/src/Login/useLogin.ts b/packages/teleport/src/Login/useLogin.ts index 81c590d5d..cbf24265c 100644 --- a/packages/teleport/src/Login/useLogin.ts +++ b/packages/teleport/src/Login/useLogin.ts @@ -14,9 +14,10 @@ * limitations under the License. */ +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,16 @@ 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(); const isLocalAuthEnabled = cfg.getLocalAuthFlag(); @@ -34,6 +45,10 @@ export default function useLogin() { .login(email, password, token) .then(onSuccess) .catch(err => { + if (isPrivateKeyRequiredError(err)) { + setPrivateKeyPolicyEnabled(true); + return; + } attemptActions.error(err); }); } @@ -44,6 +59,10 @@ export default function useLogin() { .loginWithWebauthn(creds) .then(onSuccess) .catch(err => { + if (isPrivateKeyRequiredError(err)) { + setPrivateKeyPolicyEnabled(true); + return; + } attemptActions.error(err); }); } @@ -67,6 +86,7 @@ export default function useLogin() { clearAttempt: attemptActions.clear, isPasswordlessEnabled: cfg.isPasswordlessEnabled(), primaryAuthType: cfg.getPrimaryAuthType(), + privateKeyPolicyEnabled, }; } diff --git a/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx b/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx index 924d11470..4b1fa486d 100644 --- a/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx +++ b/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx @@ -141,6 +141,17 @@ export const SuccessRegister = () => ( export const SuccessReset = () => ( ); +export const SuccessAndPrivateKeyEnabledRegister = () => ( + +); +export const SuccessAndPrivateKeyEnabledReset = () => ( + +); function CardWrapper({ children }) { return ( @@ -175,6 +186,7 @@ const props: Props = { success: false, finishedRegister: () => null, recoveryCodes: null, + privateKeyPolicyEnabled: false, resetToken: { user: 'john@example.com', tokenId: 'test123', diff --git a/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx b/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx index f60288770..ecf43d1e1 100644 --- a/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx +++ b/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx @@ -21,6 +21,7 @@ import { PrimaryAuthType } from 'shared/services'; import { StepSlider, NewFlow, StepComponentProps } from 'design/StepSlider'; import RecoveryCodes from 'teleport/components/RecoveryCodes'; +import { PrivateKeyLoginDisabledCard } from 'teleport/components/PrivateKeyPolicy'; import useToken, { State } from '../useToken'; @@ -54,6 +55,7 @@ export function NewCredentials(props: State & Props) { primaryAuthType, success, finishedRegister, + privateKeyPolicyEnabled, } = props; if (fetchAttempt.status === 'failed') { @@ -64,6 +66,14 @@ export function NewCredentials(props: State & Props) { return null; } + if (success && privateKeyPolicyEnabled) { + return ( + + ); + } + if (success) { return ; } diff --git a/packages/teleport/src/Welcome/useToken.ts b/packages/teleport/src/Welcome/useToken.ts index a4a155a3a..f4162c838 100644 --- a/packages/teleport/src/Welcome/useToken.ts +++ b/packages/teleport/src/Welcome/useToken.ts @@ -19,12 +19,18 @@ import useAttempt from 'shared/hooks/useAttemptNext'; import cfg from 'teleport/config'; import history from 'teleport/services/history'; -import auth, { RecoveryCodes, ResetToken } from 'teleport/services/auth'; +import auth, { + ChangedUserAuthn, + RecoveryCodes, + ResetToken, +} from 'teleport/services/auth'; export default function useToken(tokenId: string) { 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(''); const auth2faType = cfg.getAuth2faType(); @@ -37,17 +43,22 @@ export default function useToken(tokenId: string) { ); }, []); + function handleResponse(res: ChangedUserAuthn) { + if (res.privateKeyPolicyEnabled) { + setPrivateKeyPolicyEnabled(true); + } + if (res.recovery.createdDate) { + setRecoveryCodes(res.recovery); + } else { + finishedRegister(); + } + } + function onSubmit(password: string, otpCode = '', deviceName = '') { submitAttempt.setAttempt({ status: 'processing' }); auth .resetPassword({ tokenId, password, otpCode, deviceName }) - .then(recoveryCodes => { - if (recoveryCodes.createdDate) { - setRecoveryCodes(recoveryCodes); - } else { - finishedRegister(); - } - }) + .then(handleResponse) .catch(submitAttempt.handleError); } @@ -55,13 +66,7 @@ export default function useToken(tokenId: string) { submitAttempt.setAttempt({ status: 'processing' }); auth .resetPasswordWithWebauthn({ tokenId, password, deviceName }) - .then(recoveryCodes => { - if (recoveryCodes.createdDate) { - setRecoveryCodes(recoveryCodes); - } else { - finishedRegister(); - } - }) + .then(handleResponse) .catch(submitAttempt.handleError); } @@ -91,6 +96,7 @@ export default function useToken(tokenId: string) { redirect, success, finishedRegister, + privateKeyPolicyEnabled, }; } diff --git a/packages/teleport/src/components/FormLogin/FormLogin.story.tsx b/packages/teleport/src/components/FormLogin/FormLogin.story.tsx index e39041e5a..65279cbcd 100644 --- a/packages/teleport/src/components/FormLogin/FormLogin.story.tsx +++ b/packages/teleport/src/components/FormLogin/FormLogin.story.tsx @@ -34,6 +34,7 @@ const props: Props = { auth2faType: 'off', primaryAuthType: 'local', isPasswordlessEnabled: false, + privateKeyPolicyEnabled: false, }; export default { @@ -106,6 +107,10 @@ export const LocalWithSsoAndPwdless = () => { ); }; +export const PrivateKeyPolicyEnabled = () => ( + +); + export const LocalDisabledWithSso = () => { const ssoProvider = [ { name: 'github', type: 'oidc', url: '' } as const, diff --git a/packages/teleport/src/components/FormLogin/FormLogin.test.tsx b/packages/teleport/src/components/FormLogin/FormLogin.test.tsx index 9c9d65983..568e7cc75 100644 --- a/packages/teleport/src/components/FormLogin/FormLogin.test.tsx +++ b/packages/teleport/src/components/FormLogin/FormLogin.test.tsx @@ -195,4 +195,5 @@ const props: Props = { onLoginWithWebauthn: null, isPasswordlessEnabled: false, primaryAuthType: 'local', + privateKeyPolicyEnabled: false, }; diff --git a/packages/teleport/src/components/FormLogin/FormLogin.tsx b/packages/teleport/src/components/FormLogin/FormLogin.tsx index 8e8180c65..23937fab7 100644 --- a/packages/teleport/src/components/FormLogin/FormLogin.tsx +++ b/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; @@ -503,6 +515,7 @@ export type Props = { title?: string; isLocalAuthEnabled?: boolean; isPasswordlessEnabled: boolean; + privateKeyPolicyEnabled: boolean; authProviders?: AuthProvider[]; auth2faType?: Auth2faType; primaryAuthType: PrimaryAuthType; diff --git a/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.story.tsx b/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.story.tsx new file mode 100644 index 000000000..55131f253 --- /dev/null +++ b/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/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.tsx b/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.tsx new file mode 100644 index 000000000..3f9add2c6 --- /dev/null +++ b/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.tsx @@ -0,0 +1,144 @@ +/** + * 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 { Card, Text, Link, ButtonText, ButtonSecondary, Box } from 'design'; +import Dialog, { + DialogHeader, + DialogTitle, + DialogContent, + DialogFooter, +} from 'design/Dialog'; +import { Danger } from 'design/Alert'; + +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/packages/teleport/src/components/PrivateKeyPolicy/index.ts b/packages/teleport/src/components/PrivateKeyPolicy/index.ts new file mode 100644 index 000000000..b0c63e658 --- /dev/null +++ b/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/packages/teleport/src/config.ts b/packages/teleport/src/config.ts index 4ae267f90..39734e39b 100644 --- a/packages/teleport/src/config.ts +++ b/packages/teleport/src/config.ts @@ -17,18 +17,18 @@ limitations under the License. import { generatePath } from 'react-router'; import { merge } from 'lodash'; -import { +import generateResourcePath from './generateResourcePath'; + +import type { AuthProvider, Auth2faType, AuthType, PrimaryAuthType, PreferredMfaType, + PrivateKeyPolicy, } from 'shared/services'; - -import { SortType } from 'teleport/services/agents'; -import { RecordingType } from 'teleport/services/recordings'; - -import generateResourcePath from './generateResourcePath'; +import type { SortType } from 'teleport/services/agents'; +import type { RecordingType } from 'teleport/services/recordings'; const cfg = { isEnterprise: false, @@ -51,6 +51,7 @@ const cfg = { second_factor: 'off' as Auth2faType, authType: 'local' as AuthType, preferredLocalMfa: '' as PreferredMfaType, + privateKeyPolicy: 'none' as PrivateKeyPolicy, }, proxyCluster: 'localhost', @@ -220,6 +221,10 @@ const cfg = { return cfg.auth.localAuthEnabled; }, + getPrivateKeyPolicy() { + return cfg.auth.privateKeyPolicy; + }, + isPasswordlessEnabled() { return cfg.auth.allowPasswordless; }, diff --git a/packages/teleport/src/lib/util.test.ts b/packages/teleport/src/lib/util.test.ts new file mode 100644 index 000000000..6f78dd135 --- /dev/null +++ b/packages/teleport/src/lib/util.test.ts @@ -0,0 +1,63 @@ +/** + * 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 { generateTshLoginCommand } from './util'; + +let windowSpy; + +beforeEach(() => { + windowSpy = jest.spyOn(window, 'window', 'get'); +}); + +afterEach(() => { + windowSpy.mockRestore(); +}); + +test('with all params defined', () => { + windowSpy.mockImplementation(() => ({ + location: { + hostname: 'my-cluster', + port: '1234', + }, + })); + + expect( + generateTshLoginCommand({ + accessRequestId: 'ar-1234', + username: 'llama', + authType: 'local', + clusterId: 'cluster-1234', + }) + ).toBe( + 'tsh login --proxy=my-cluster:1234 --auth=local --user=llama cluster-1234 --request-id=ar-1234' + ); +}); + +test('no port and access request id', () => { + windowSpy.mockImplementation(() => ({ + location: { + hostname: 'my-cluster', + }, + })); + + expect( + generateTshLoginCommand({ + username: 'llama', + authType: 'sso', + clusterId: 'cluster-1234', + }) + ).toBe('tsh login --proxy=my-cluster:443 cluster-1234'); +}); diff --git a/packages/teleport/src/lib/util.ts b/packages/teleport/src/lib/util.ts index fb6ae71ca..ea16ee5a3 100644 --- a/packages/teleport/src/lib/util.ts +++ b/packages/teleport/src/lib/util.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { AuthType } from 'teleport/services/user'; + export const openNewTab = (url: string) => { const element = document.createElement('a'); element.setAttribute('href', `${url}`); @@ -44,3 +46,26 @@ export async function Sha256Digest( const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string return hashHex; } + +export type TshLoginCommand = { + accessRequestId?: string; + username: string; + authType: AuthType; + clusterId: string; +}; + +export function generateTshLoginCommand({ + authType, + clusterId, + username, + accessRequestId, +}: TshLoginCommand) { + const { hostname, port } = window.location; + const host = `${hostname}:${port || '443'}`; + const authSpec = + authType === 'local' ? `--auth=${authType} --user=${username} ` : ''; + + const requestId = accessRequestId ? ` --request-id=${accessRequestId}` : ''; + + return `tsh login --proxy=${host} ${authSpec}${clusterId}${requestId}`; +} diff --git a/packages/teleport/src/services/auth/auth.ts b/packages/teleport/src/services/auth/auth.ts index 9e088b5e2..1eb6751b0 100644 --- a/packages/teleport/src/services/auth/auth.ts +++ b/packages/teleport/src/services/auth/auth.ts @@ -19,7 +19,7 @@ import cfg from 'teleport/config'; import { DeviceType, DeviceUsage } from 'teleport/services/mfa'; import makePasswordToken from './makePasswordToken'; -import { makeRecoveryCodes } from './makeRecoveryCodes'; +import { makeChangedUserAuthn } from './make'; import { makeMfaAuthenticateChallenge, makeMfaRegistrationChallenge, @@ -145,7 +145,7 @@ const auth = { return api.put(cfg.getPasswordTokenUrl(), request); }) - .then(makeRecoveryCodes); + .then(makeChangedUserAuthn); }, resetPassword(req: NewCredentialRequest) { @@ -156,7 +156,9 @@ const auth = { deviceName: req.deviceName, }; - return api.put(cfg.getPasswordTokenUrl(), request).then(makeRecoveryCodes); + return api + .put(cfg.getPasswordTokenUrl(), request) + .then(makeChangedUserAuthn); }, changePassword(oldPass: string, newPass: string, token: string) { diff --git a/packages/teleport/src/services/auth/index.ts b/packages/teleport/src/services/auth/index.ts index f477ed3dd..25c53331e 100644 --- a/packages/teleport/src/services/auth/index.ts +++ b/packages/teleport/src/services/auth/index.ts @@ -17,6 +17,6 @@ limitations under the License. import service from './auth'; export * from './makeMfa'; -export * from './makeRecoveryCodes'; +export * from './make'; export * from './types'; export default service; diff --git a/packages/teleport/src/services/auth/make.test.ts b/packages/teleport/src/services/auth/make.test.ts new file mode 100644 index 000000000..76169bac6 --- /dev/null +++ b/packages/teleport/src/services/auth/make.test.ts @@ -0,0 +1,43 @@ +/** + * 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 { makeChangedUserAuthn } from './make'; + +test('makeChangedUserAuthn with null', async () => { + expect(makeChangedUserAuthn(null)).toStrictEqual({ + recovery: { codes: [], createdDate: null }, + privateKeyPolicyEnabled: false, + }); +}); + +test('makeChangedUserAuthn with recovery codes', async () => { + const date = '2022-10-25T00:30:18.162Z'; + expect( + makeChangedUserAuthn({ + recovery: { + 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/packages/teleport/src/services/auth/makeRecoveryCodes.ts b/packages/teleport/src/services/auth/make.ts similarity index 66% rename from packages/teleport/src/services/auth/makeRecoveryCodes.ts rename to packages/teleport/src/services/auth/make.ts index 1421e8fff..020fee64e 100644 --- a/packages/teleport/src/services/auth/makeRecoveryCodes.ts +++ b/packages/teleport/src/services/auth/make.ts @@ -14,12 +14,21 @@ * limitations under the License. */ -import { RecoveryCodes } from './types'; +import { ChangedUserAuthn, RecoveryCodes } from './types'; -// makeRecoveryCodes makes the response from a successful user reset or invite. +// makeChangedUserAuthn makes the response from a successful user reset or invite. // Only teleport cloud and users with valid emails as username will receive // recovery codes. -export function makeRecoveryCodes(json): RecoveryCodes { +export function makeChangedUserAuthn(json: any): ChangedUserAuthn { + json = json || {}; + + return { + recovery: makeRecoveryCodes(json.recovery), + privateKeyPolicyEnabled: !!json.privateKeyPolicyEnabled, + }; +} + +export function makeRecoveryCodes(json: any): RecoveryCodes { json = json || {}; return { diff --git a/packages/teleport/src/services/auth/types.ts b/packages/teleport/src/services/auth/types.ts index 79c622cc9..b63119e34 100644 --- a/packages/teleport/src/services/auth/types.ts +++ b/packages/teleport/src/services/auth/types.ts @@ -40,6 +40,11 @@ export type RecoveryCodes = { createdDate: Date; }; +export type ChangedUserAuthn = { + recovery: RecoveryCodes; + privateKeyPolicyEnabled?: boolean; +}; + export type NewCredentialRequest = { tokenId: string; password?: string; diff --git a/packages/webapps.e b/packages/webapps.e index 74d3be697..d39d4c8cc 160000 --- a/packages/webapps.e +++ b/packages/webapps.e @@ -1 +1 @@ -Subproject commit 74d3be697bfcc31b4c69ffe024083902c516be55 +Subproject commit d39d4c8ccaf7b20149bde658a386c9ab13d3c0c0