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 (
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 (
+
+ );
+}
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