Skip to content

Commit b5571a6

Browse files
fix: [M3-9917] - Internal Akamai Employee Firewall Policy banner logic (linode#12166)
* save progress * clean up logic * unit test * add comment * Added changeset: Issue preventing Internal Akamai Employees from creating Linodes using VLAN interfaces --------- Co-authored-by: Banks Nussman <[email protected]>
1 parent 5bd3774 commit b5571a6

File tree

6 files changed

+198
-68
lines changed

6 files changed

+198
-68
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Fixed
3+
---
4+
5+
Issue preventing Internal Akamai Employees from creating Linodes using VLAN interfaces ([#12166](https://github.com/linode/manager/pull/12166))

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

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes';
1010

1111
import { ApiAwarenessModal } from './ApiAwarenessModal/ApiAwarenessModal';
1212
import {
13+
getDoesEmployeeNeedToAssignFirewall,
1314
getLinodeCreatePayload,
1415
useLinodeCreateQueryParams,
1516
} from './utilities';
@@ -29,34 +30,18 @@ export const Actions = () => {
2930
globalGrantType: 'add_linodes',
3031
});
3132

32-
const [
33-
legacyFirewallId,
34-
firstLinodeInterfaceFirewallId,
35-
firstLinodeInterfaceType,
36-
interfaceGeneration,
37-
] = useWatch({
38-
name: [
39-
'firewall_id',
40-
'linodeInterfaces.0.firewall_id',
41-
'linodeInterfaces.0.purpose',
42-
'interface_generation',
43-
],
33+
const [legacyFirewallId, linodeInterfaces, interfaceGeneration] = useWatch({
4434
control,
35+
name: ['firewall_id', 'linodeInterfaces', 'interface_generation'],
4536
});
4637

47-
const firewallId =
48-
interfaceGeneration === 'linode'
49-
? firstLinodeInterfaceFirewallId
50-
: legacyFirewallId;
51-
52-
const userNeedsToTakeActionAboutInternalFirewallPolicy =
53-
interfaceGeneration === 'linode' && firstLinodeInterfaceType === 'vlan'
54-
? false
55-
: 'firewallOverride' in formState.errors && !firewallId;
56-
57-
const disableSubmitButton =
58-
isLinodeCreateRestricted ||
59-
userNeedsToTakeActionAboutInternalFirewallPolicy;
38+
const userNeedsToAssignFirewall =
39+
'firewallOverride' in formState.errors &&
40+
getDoesEmployeeNeedToAssignFirewall(
41+
legacyFirewallId,
42+
linodeInterfaces,
43+
interfaceGeneration
44+
);
6045

6146
const onOpenAPIAwareness = async () => {
6247
sendApiAwarenessClickEvent('Button', 'View Code Snippets');
@@ -80,7 +65,7 @@ export const Actions = () => {
8065
</Button>
8166
<Button
8267
buttonType="primary"
83-
disabled={disableSubmitButton}
68+
disabled={isLinodeCreateRestricted || userNeedsToAssignFirewall}
8469
loading={formState.isSubmitting}
8570
type="submit"
8671
>

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

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { useController, useFormContext, useWatch } from 'react-hook-form';
55
import { AkamaiBanner } from 'src/components/AkamaiBanner/AkamaiBanner';
66
import { useFlags } from 'src/hooks/useFlags';
77

8-
import type { LinodeCreateFormValues } from './utilities';
8+
import {
9+
getDoesEmployeeNeedToAssignFirewall,
10+
type LinodeCreateFormValues,
11+
} from './utilities';
912

1013
export const FirewallAuthorization = () => {
1114
const flags = useFlags();
@@ -17,34 +20,18 @@ export const FirewallAuthorization = () => {
1720
name: 'firewallOverride',
1821
});
1922

20-
const [
21-
legacyFirewallId,
22-
firstLinodeInterfaceFirewallId,
23-
firstLinodeInterfaceType,
24-
interfaceGeneration,
25-
] = useWatch({
26-
name: [
27-
'firewall_id',
28-
'linodeInterfaces.0.firewall_id',
29-
'linodeInterfaces.0.purpose',
30-
'interface_generation',
31-
],
23+
const [legacyFirewallId, linodeInterfaces, interfaceGeneration] = useWatch({
3224
control,
25+
name: ['firewall_id', 'linodeInterfaces', 'interface_generation'],
3326
});
3427

35-
// Special case ❗️
36-
// VLAN interfaces do not support Firewalls, so we hide this notice
37-
// if that's what the user selects.
38-
if (firstLinodeInterfaceType === 'vlan' && interfaceGeneration === 'linode') {
39-
return null;
40-
}
41-
42-
const firewallId =
43-
interfaceGeneration === 'linode'
44-
? firstLinodeInterfaceFirewallId
45-
: legacyFirewallId;
28+
const userNeedsToAssignFirewall = getDoesEmployeeNeedToAssignFirewall(
29+
legacyFirewallId,
30+
linodeInterfaces,
31+
interfaceGeneration
32+
);
4633

47-
if (firewallId || !(fieldState.isDirty || fieldState.error)) {
34+
if (!userNeedsToAssignFirewall || !(fieldState.isDirty || fieldState.error)) {
4835
return null;
4936
}
5037

packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import {
1111
CreateLinodeFromStackScriptSchema,
1212
CreateLinodeSchema,
1313
} from './schemas';
14-
import { getInterfacesPayload } from './utilities';
14+
import {
15+
getDoesEmployeeNeedToAssignFirewall,
16+
getInterfacesPayload,
17+
} from './utilities';
1518

1619
import type {
1720
LinodeCreateFormContext,
@@ -87,22 +90,24 @@ export const getLinodeCreateResolver = (
8790
}
8891
}
8992

90-
// If we're dealing with an employee account and they did not bypass
91-
// the firewall banner....
92-
if (context?.secureVMNoticesEnabled && !values.firewallOverride) {
93-
// Get the selected Firewall ID depending on what Interface Generation is selected
94-
const firewallId =
95-
values.interface_generation === 'linode'
96-
? values.linodeInterfaces[0].firewall_id
97-
: values.firewall_id;
98-
99-
if (!firewallId) {
100-
(errors as FieldErrors<LinodeCreateFormValues>)['firewallOverride'] = {
101-
// This message does not get surfaced, see FirewallAuthorization.tsx
102-
message: 'You must select a Firewall or bypass the Firewall policy.',
103-
type: 'validate',
104-
};
105-
}
93+
// If
94+
// - we're dealing with an employee account
95+
// - and the employee did not bypass/override the Firewall warning
96+
// - and their networking configuration "requires" a firewall
97+
if (
98+
context?.secureVMNoticesEnabled &&
99+
!values.firewallOverride &&
100+
getDoesEmployeeNeedToAssignFirewall(
101+
values.firewall_id,
102+
values.linodeInterfaces,
103+
values.interface_generation
104+
)
105+
) {
106+
(errors as FieldErrors<LinodeCreateFormValues>)['firewallOverride'] = {
107+
// This message does not get surfaced, but triggers an error so that FirewallAuthorization.tsx renders
108+
message: 'You must select a Firewall or bypass the Firewall policy.',
109+
type: 'validate',
110+
};
106111
}
107112

108113
if (errors) {

packages/manager/src/features/Linodes/LinodeCreate/utilities.test.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99

1010
import {
1111
getDefaultInterfaceGenerationFromAccountSetting,
12+
getDoesEmployeeNeedToAssignFirewall,
1213
getInterfacesPayload,
1314
getIsValidLinodeLabelCharacter,
1415
getLinodeCreatePayload,
@@ -437,3 +438,121 @@ describe('getDefaultInterfaceGenerationFromAccountSetting', () => {
437438
).toBe('linode');
438439
});
439440
});
441+
442+
describe('getDoesEmployeeNeedToAssignFirewall', () => {
443+
describe('Legacy Interfaces', () => {
444+
it('returns true when no firewall is selected', () => {
445+
expect(
446+
getDoesEmployeeNeedToAssignFirewall(null, [], 'legacy_config')
447+
).toBe(true);
448+
});
449+
it('returns false when a firewall is selected', () => {
450+
expect(getDoesEmployeeNeedToAssignFirewall(5, [], 'legacy_config')).toBe(
451+
false
452+
);
453+
});
454+
});
455+
456+
describe('Linode Interfaces', () => {
457+
it('should return true for a public Linode Interface without a firewall selected', () => {
458+
expect(
459+
getDoesEmployeeNeedToAssignFirewall(
460+
null,
461+
[
462+
{
463+
public: {},
464+
purpose: 'public',
465+
vpc: null,
466+
default_route: null,
467+
vlan: null,
468+
},
469+
],
470+
'linode'
471+
)
472+
).toBe(true);
473+
});
474+
475+
it('should return false for a public Linode Interface with a firewall selected', () => {
476+
expect(
477+
getDoesEmployeeNeedToAssignFirewall(
478+
null,
479+
[
480+
{
481+
public: {},
482+
purpose: 'public',
483+
vpc: null,
484+
firewall_id: 5,
485+
default_route: null,
486+
vlan: null,
487+
},
488+
],
489+
'linode'
490+
)
491+
).toBe(false);
492+
});
493+
494+
it('should return true for VPC Linode Interface without a firewall selected', () => {
495+
expect(
496+
getDoesEmployeeNeedToAssignFirewall(
497+
5,
498+
[
499+
{
500+
vpc: { vpc_id: 1, subnet_id: 2 },
501+
purpose: 'vpc',
502+
public: null,
503+
firewall_id: null,
504+
default_route: null,
505+
vlan: null,
506+
},
507+
],
508+
'linode'
509+
)
510+
).toBe(true);
511+
});
512+
513+
it('should return false when only a VLAN Linode Interface is selected', () => {
514+
expect(
515+
getDoesEmployeeNeedToAssignFirewall(
516+
5,
517+
[
518+
{
519+
vpc: null,
520+
purpose: 'vlan',
521+
public: null,
522+
default_route: null,
523+
vlan: { vlan_label: 'my-vlan-1' },
524+
},
525+
],
526+
'linode'
527+
)
528+
).toBe(false);
529+
});
530+
531+
// Currently, the Linode Create flow only allows one Linode interface, but we built it to support
532+
// many interfaces if we ever want to allow it.
533+
it('should require a firewall when a Public and VLAN Linode Interface are selected', () => {
534+
expect(
535+
getDoesEmployeeNeedToAssignFirewall(
536+
5,
537+
[
538+
{
539+
purpose: 'public',
540+
public: {},
541+
vpc: null,
542+
vlan: null,
543+
default_route: null,
544+
},
545+
{
546+
vpc: null,
547+
purpose: 'vlan',
548+
public: null,
549+
default_route: null,
550+
vlan: { vlan_label: 'my-vlan-1' },
551+
},
552+
],
553+
'linode'
554+
)
555+
).toBe(true);
556+
});
557+
});
558+
});

packages/manager/src/features/Linodes/LinodeCreate/utilities.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,3 +693,32 @@ export const getDefaultInterfaceGenerationFromAccountSetting = (
693693
}
694694
return undefined;
695695
};
696+
697+
/**
698+
* getDoesEmployeeNeedToAssignFirewall
699+
*
700+
* @returns
701+
* `true` if an internal Akamai employee should be creating their Linode
702+
* with a Firewall given the current network Configuration.
703+
*
704+
* `false` if the user has satisified the Firewall requirment or
705+
* their network configuration does not require a Firewall.
706+
*/
707+
export const getDoesEmployeeNeedToAssignFirewall = (
708+
legacyFirewallId: LinodeCreateFormValues['firewall_id'],
709+
linodeInterfaces: LinodeCreateFormValues['linodeInterfaces'],
710+
interfaceGeneration: LinodeCreateFormValues['interface_generation']
711+
) => {
712+
if (interfaceGeneration === 'linode') {
713+
// VLAN Linode interfaces do not support Firewalls, so we don't consider them.
714+
const interfacesThatMayHaveInternetConnectivity = linodeInterfaces.filter(
715+
(i) => i.purpose !== 'vlan'
716+
);
717+
718+
return !interfacesThatMayHaveInternetConnectivity.every(
719+
(i) => i.firewall_id
720+
);
721+
}
722+
723+
return !legacyFirewallId;
724+
};

0 commit comments

Comments
 (0)