Skip to content

Commit f62fd99

Browse files
authored
upcoming: [UIE-10333] - Allow Creating Secure Linodes without Root Password (#13516)
* upcoming: [UIE-10333] - Allow Creating Secure Linodes without Root Password * Add unit tests for useIsPasswordLessLinodesEnabled * fixing margin issues * update validation error message * co-pilot PR feedback * Added changeset: Allow Creating Secure Linodes without Root Password * remove error from root password when ssh keys are added * update the heading font size * add custom error message for invalid authentication * remove root_pass/ssh keys when it is empty for passwordless linodes * fix root password error when image is not selected * trigger root_pass error only when field is touched
1 parent 5902fbb commit f62fd99

24 files changed

Lines changed: 555 additions & 112 deletions

File tree

packages/api-v4/src/linodes/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,11 @@ export interface CreateLinodeRequest {
653653
* @default false
654654
*/
655655
backups_enabled?: boolean | null;
656+
/**
657+
* When deploying from an Image, this field is optional, otherwise it is ignored.
658+
* This is used to set the boot size for the newly-created Linode.
659+
*/
660+
boot_size?: null | number;
656661
/**
657662
* If it is deployed from an Image or a Backup and you wish it to remain offline after deployment, set this to false.
658663
*
@@ -694,6 +699,11 @@ export interface CreateLinodeRequest {
694699
* Must be empty if Linode is configured to use new Linode Interfaces.
695700
*/
696701
ipv4?: string[];
702+
/**
703+
* When deploying from an Image, this field is optional, otherwise it is ignored.
704+
* This is used to set the kernel type for the newly-created Linode.
705+
*/
706+
kernel?: null | string;
697707
/**
698708
* The Linode's label is for display purposes only.
699709
* If no label is provided for a Linode, a default will be assigned.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Allow Creating Secure Linodes without Root Password ([#13516](https://github.com/linode/manager/pull/13516))

packages/manager/src/dev-tools/FeatureFlagTool.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const options: { flag: keyof Flags; label: string }[] = [
4949
{ flag: 'objMultiCluster', label: 'OBJ Multi-Cluster' },
5050
{ flag: 'objectStorageGen2', label: 'OBJ Gen2' },
5151
{ flag: 'objectStorageGlobalQuotas', label: 'OBJ Global Quotas' },
52+
{ flag: 'passwordlessLinodes', label: 'PasswordLess Linodes' },
5253
{
5354
flag: 'placementGroupPolicyUpdate',
5455
label: 'Placement Group Policy Update',

packages/manager/src/featureFlags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export interface Flags {
275275
objectStorageGlobalQuotas: boolean;
276276
objMultiCluster: boolean;
277277
objSummaryPage: boolean;
278+
passwordlessLinodes: boolean;
278279
placementGroupPolicyUpdate: boolean;
279280
privateImageSharing: boolean;
280281
productInformationBanners: ProductInformationBannerFlag[];
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Notice, Typography } from '@linode/ui';
2+
import React from 'react';
3+
import { Controller, useFormContext } from 'react-hook-form';
4+
5+
import { Skeleton } from 'src/components/Skeleton';
6+
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
7+
import { useIsPasswordLessLinodesEnabled } from 'src/utilities/linodes';
8+
9+
import type { CreateLinodeRequest } from '@linode/api-v4';
10+
11+
const PasswordInput = React.lazy(() =>
12+
import('src/components/PasswordInput/PasswordInput').then((module) => ({
13+
default: module.PasswordInput,
14+
}))
15+
);
16+
17+
export const Password = () => {
18+
const { control, formState } = useFormContext<CreateLinodeRequest>();
19+
const { data: permissions } = usePermissions('account', ['create_linode']);
20+
const { isPasswordLessLinodesEnabled } = useIsPasswordLessLinodesEnabled();
21+
22+
return (
23+
<>
24+
{isPasswordLessLinodesEnabled && (
25+
<>
26+
<Typography mb={2} variant="h3">
27+
Authentication Method
28+
</Typography>
29+
{formState.errors.root_pass?.message && (
30+
<Notice text={formState.errors.root_pass.message} variant="error" />
31+
)}
32+
</>
33+
)}
34+
<React.Suspense
35+
fallback={
36+
<Skeleton
37+
sx={(theme) => ({ height: '89px', maxWidth: theme.inputMaxWidth })}
38+
/>
39+
}
40+
>
41+
<Controller
42+
control={control}
43+
name="root_pass"
44+
render={({ field, fieldState }) => (
45+
<PasswordInput
46+
autoComplete="off"
47+
disabled={!permissions.create_linode}
48+
errorText={
49+
!isPasswordLessLinodesEnabled
50+
? fieldState.error?.message
51+
: undefined
52+
}
53+
id="linode-password"
54+
label="Root Password"
55+
name="password"
56+
noMarginTop
57+
onBlur={field.onBlur}
58+
onChange={field.onChange}
59+
placeholder="Enter a password."
60+
value={field.value ?? ''}
61+
/>
62+
)}
63+
/>
64+
</React.Suspense>
65+
</>
66+
);
67+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import { Controller, useFormContext } from 'react-hook-form';
3+
4+
import { UserSSHKeyPanel } from 'src/components/AccessPanel/UserSSHKeyPanel';
5+
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
6+
import { useIsPasswordLessLinodesEnabled } from 'src/utilities/linodes';
7+
8+
import type { CreateLinodeRequest } from '@linode/api-v4';
9+
10+
export const SSHKeys = () => {
11+
const { control, trigger } = useFormContext<CreateLinodeRequest>();
12+
const { data: permissions } = usePermissions('account', ['create_linode']);
13+
const { isPasswordLessLinodesEnabled } = useIsPasswordLessLinodesEnabled();
14+
15+
return (
16+
<Controller
17+
control={control}
18+
name="authorized_users"
19+
render={({ field }) => (
20+
<UserSSHKeyPanel
21+
authorizedUsers={field.value ?? []}
22+
disabled={!permissions.create_linode}
23+
headingVariant={isPasswordLessLinodesEnabled ? 'h3' : 'h2'}
24+
setAuthorizedUsers={(values) => {
25+
field.onChange(values);
26+
if (isPasswordLessLinodesEnabled) {
27+
trigger('root_pass');
28+
}
29+
}}
30+
/>
31+
)}
32+
/>
33+
);
34+
};

packages/manager/src/features/Linodes/LinodeCreate/Security.test.tsx renamed to packages/manager/src/features/Linodes/LinodeCreate/Security/Security.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313

1414
import { Security } from './Security';
1515

16-
import type { LinodeCreateFormValues } from './utilities';
16+
import type { LinodeCreateFormValues } from '../utilities';
1717

1818
const queryMocks = vi.hoisted(() => ({
1919
useNavigate: vi.fn(),

packages/manager/src/features/Linodes/LinodeCreate/Security.tsx renamed to packages/manager/src/features/Linodes/LinodeCreate/Security/Security.tsx

Lines changed: 18 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Divider, Paper, Typography } from '@linode/ui';
33
import React from 'react';
44
import { Controller, useFormContext, useWatch } from 'react-hook-form';
55

6-
import { UserSSHKeyPanel } from 'src/components/AccessPanel/UserSSHKeyPanel';
76
import {
87
DISK_ENCRYPTION_DEFAULT_DISTRIBUTED_INSTANCES,
98
DISK_ENCRYPTION_DISTRIBUTED_DESCRIPTION,
@@ -13,16 +12,12 @@ import {
1312
import { Encryption } from 'src/components/Encryption/Encryption';
1413
import { useIsDiskEncryptionFeatureEnabled } from 'src/components/Encryption/utils';
1514
import { getIsDistributedRegion } from 'src/components/RegionSelect/RegionSelect.utils';
16-
import { Skeleton } from 'src/components/Skeleton';
17-
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
15+
import { useIsPasswordLessLinodesEnabled } from 'src/utilities/linodes';
1816

19-
import type { CreateLinodeRequest } from '@linode/api-v4';
17+
import { Password } from './Password';
18+
import { SSHKeys } from './SSHKeys';
2019

21-
const PasswordInput = React.lazy(() =>
22-
import('src/components/PasswordInput/PasswordInput').then((module) => ({
23-
default: module.PasswordInput,
24-
}))
25-
);
20+
import type { CreateLinodeRequest } from '@linode/api-v4';
2621

2722
export const Security = () => {
2823
const { control } = useFormContext<CreateLinodeRequest>();
@@ -45,52 +40,26 @@ export const Security = () => {
4540
selectedRegion?.id ?? ''
4641
);
4742

48-
const { data: permissions } = usePermissions('account', ['create_linode']);
43+
const { isPasswordLessLinodesEnabled } = useIsPasswordLessLinodesEnabled();
4944

5045
return (
5146
<Paper>
5247
<Typography sx={{ mb: 2 }} variant="h2">
5348
Security
5449
</Typography>
55-
<React.Suspense
56-
fallback={
57-
<Skeleton
58-
sx={(theme) => ({ height: '89px', maxWidth: theme.inputMaxWidth })}
59-
/>
60-
}
61-
>
62-
<Controller
63-
control={control}
64-
name="root_pass"
65-
render={({ field, fieldState }) => (
66-
<PasswordInput
67-
autoComplete="off"
68-
disabled={!permissions.create_linode}
69-
errorText={fieldState.error?.message}
70-
id="linode-password"
71-
label="Root Password"
72-
name="password"
73-
noMarginTop
74-
onBlur={field.onBlur}
75-
onChange={field.onChange}
76-
placeholder="Enter a password."
77-
value={field.value ?? ''}
78-
/>
79-
)}
80-
/>
81-
</React.Suspense>
82-
<Divider spacingBottom={20} spacingTop={24} />
83-
<Controller
84-
control={control}
85-
name="authorized_users"
86-
render={({ field }) => (
87-
<UserSSHKeyPanel
88-
authorizedUsers={field.value ?? []}
89-
disabled={!permissions.create_linode}
90-
setAuthorizedUsers={field.onChange}
91-
/>
92-
)}
93-
/>
50+
{!isPasswordLessLinodesEnabled ? (
51+
<>
52+
<Password />
53+
<Divider spacingBottom={20} spacingTop={24} />
54+
<SSHKeys />
55+
</>
56+
) : (
57+
<>
58+
<SSHKeys />
59+
<Divider spacingBottom={20} spacingTop={24} />
60+
<Password />
61+
</>
62+
)}
9463
{isDiskEncryptionFeatureEnabled && (
9564
<>
9665
<Divider spacingBottom={20} spacingTop={24} />

packages/manager/src/features/Linodes/LinodeCreate/Tabs/OperatingSystems.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useController, useFormContext, useWatch } from 'react-hook-form';
66

77
import { ImageSelect } from 'src/components/ImageSelect/ImageSelect';
88
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
9+
import { useIsPasswordLessLinodesEnabled } from 'src/utilities/linodes';
910

1011
import { Region } from '../Region';
1112
import { getGeneratedLinodeLabel } from '../utilities';
@@ -17,9 +18,14 @@ export const OperatingSystems = () => {
1718
const {
1819
formState: {
1920
dirtyFields: { label: isLabelFieldDirty },
21+
touchedFields: {
22+
authorized_users: isAuthorizedUsersFieldTouched,
23+
root_pass: isRootPassFieldTouched,
24+
},
2025
},
2126
getValues,
2227
setValue,
28+
trigger,
2329
} = useFormContext<LinodeCreateFormValues>();
2430

2531
const queryClient = useQueryClient();
@@ -31,6 +37,7 @@ export const OperatingSystems = () => {
3137
const regionId = useWatch<LinodeCreateFormValues, 'region'>({
3238
name: 'region',
3339
});
40+
const { isPasswordLessLinodesEnabled } = useIsPasswordLessLinodesEnabled();
3441

3542
const { data: region } = useRegionQuery(regionId);
3643

@@ -39,6 +46,13 @@ export const OperatingSystems = () => {
3946
const onChange = async (image: Image | null) => {
4047
field.onChange(image?.id ?? null);
4148

49+
if (
50+
isRootPassFieldTouched ||
51+
(isPasswordLessLinodesEnabled && isAuthorizedUsersFieldTouched)
52+
) {
53+
trigger('root_pass');
54+
}
55+
4256
if (!isLabelFieldDirty) {
4357
const label = await getGeneratedLinodeLabel({
4458
queryClient,

packages/manager/src/features/Linodes/LinodeCreate/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
import {
4646
useIsLinodeCloneFirewallEnabled,
4747
useIsLinodeInterfacesEnabled,
48+
useIsPasswordLessLinodesEnabled,
4849
} from 'src/utilities/linodes';
4950
import { sanitizeHTML } from 'src/utilities/sanitizeHTML';
5051

@@ -60,7 +61,7 @@ import { Networking } from './Networking/Networking';
6061
import { transformLegacyInterfaceErrorsToLinodeInterfaceErrors } from './Networking/utilities';
6162
import { Plan } from './Plan';
6263
import { getLinodeCreateResolver } from './resolvers';
63-
import { Security } from './Security';
64+
import { Security } from './Security/Security';
6465
import { SMTP } from './SMTP';
6566
import { Summary } from './Summary/Summary';
6667
import { UserData } from './UserData/UserData';
@@ -69,6 +70,7 @@ import {
6970
defaultValues,
7071
EMPTY_ACLP_ALERTS,
7172
getLinodeCreatePayload,
73+
transformPasswordLessCreateErrors,
7274
useHandleLinodeCreateAnalyticsFormError,
7375
} from './utilities';
7476
import { VLAN } from './VLAN/VLAN';
@@ -86,6 +88,7 @@ export const LinodeCreate = () => {
8688
});
8789
const { secureVMNoticesEnabled } = useSecureVMNoticesEnabled();
8890
const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled();
91+
const { isPasswordLessLinodesEnabled } = useIsPasswordLessLinodesEnabled();
8992
const { data: profile } = useProfile();
9093
const { isLinodeCloneFirewallEnabled } = useIsLinodeCloneFirewallEnabled();
9194
const { isVMHostMaintenanceEnabled } = useVMHostMaintenanceEnabled();
@@ -103,7 +106,12 @@ export const LinodeCreate = () => {
103106
const { isDualStackEnabled } = useVPCDualStack();
104107

105108
const form = useForm<LinodeCreateFormValues, LinodeCreateFormContext>({
106-
context: { isLinodeInterfacesEnabled, profile, secureVMNoticesEnabled },
109+
context: {
110+
isPasswordLessLinodesEnabled,
111+
isLinodeInterfacesEnabled,
112+
profile,
113+
secureVMNoticesEnabled,
114+
},
107115
defaultValues: () =>
108116
defaultValues(linodeCreateType, search, queryClient, {
109117
isLinodeInterfacesEnabled,
@@ -185,6 +193,7 @@ export const LinodeCreate = () => {
185193
isShowingNewNetworkingUI: isLinodeInterfacesEnabled,
186194
isAclpAlertsEnabled: aclpServices?.linode?.alerts?.enabled,
187195
isAclpAlertsMode: isAclpAlertsModeCreateFlow,
196+
isPasswordLessLinodesEnabled,
188197
});
189198

190199
try {
@@ -227,6 +236,9 @@ export const LinodeCreate = () => {
227236
if (isLinodeInterfacesEnabled) {
228237
transformLegacyInterfaceErrorsToLinodeInterfaceErrors(errors);
229238
}
239+
if (isPasswordLessLinodesEnabled) {
240+
transformPasswordLessCreateErrors(errors);
241+
}
230242
for (const error of errors) {
231243
if (error.field) {
232244
form.setError(error.field, { message: error.reason });

0 commit comments

Comments
 (0)