forked from linode/manager
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvpcs.schema.ts
291 lines (262 loc) · 8.35 KB
/
vpcs.schema.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import ipaddr from 'ipaddr.js';
import { array, lazy, object, string } from 'yup';
const LABEL_MESSAGE = 'Label must be between 1 and 64 characters.';
const LABEL_REQUIRED = 'Label is required.';
const LABEL_REQUIREMENTS =
'Label must include only ASCII letters, numbers, and dashes.';
const labelTestDetails = {
testName: 'no two dashes in a row',
testMessage: 'Label must not contain two dashes in a row.',
};
export const IP_EITHER_BOTH_NOT_NEITHER =
'A subnet must have either IPv4 or IPv6, or both, but not neither.';
// @TODO VPC IPv6 - remove below constant when IPv6 is in GA
const TEMPORARY_IPV4_REQUIRED_MESSAGE = 'A subnet must have an IPv4 range.';
export const determineIPType = (ip: string) => {
try {
let addr;
const [, mask] = ip.split('/');
if (mask) {
const parsed = ipaddr.parseCIDR(ip);
addr = parsed[0];
} else {
addr = ipaddr.parse(ip);
}
return addr.kind();
} catch (e) {
return undefined;
}
};
/**
* VPC-related IP validation that handles for single IPv4 and IPv6 addresses as well as
* IPv4 ranges in CIDR format and IPv6 ranges with prefix lengths.
* @param { value } - the IP address string to be validated
* @param { shouldHaveIPMask } - a boolean indicating whether the value should have a mask (e.g., /32) or not
* @param { mustBeIPMask } - a boolean indicating whether the value MUST be an IP mask/prefix length or not
* @param { isIPv6Subnet } - a boolean indicating whether the IPv6 value is for a subnet
*/
export const vpcsValidateIP = ({
value,
shouldHaveIPMask,
mustBeIPMask,
isIPv6Subnet,
}: {
value: string | undefined | null;
shouldHaveIPMask: boolean;
mustBeIPMask: boolean;
isIPv6Subnet?: boolean;
}): boolean => {
if (!value) {
return false;
}
const [, mask] = value.trim().split('/');
/*
// 1. If the test specifies the value must be a mask, and the value is not, fail the test.
// 2. If the value is a mask, ensure it is a valid IPv6 mask. For MVP: the IPv6 property of
// the Subnet /POST object is just the prefix length (CIDR mask), 64-125.
// subnetMaskFromPrefixLength returns null for invalid prefix lengths
*/
if (mustBeIPMask) {
// Check that the value equals just the /mask. This is to prevent, for example,
// something like 2600:3c00::f03c:92ff:feeb:98f9/64 from falsely passing this check.
const valueIsMaskOnly = value === `/${mask}`;
return !mask
? false
: ipaddr.IPv6.subnetMaskFromPrefixLength(Number(mask)) !== null &&
valueIsMaskOnly &&
Number(mask) >= 64 &&
Number(mask) <= 125;
}
try {
const type = determineIPType(value);
const isIPv4 = type === 'ipv4';
const isIPv6 = type === 'ipv6';
if (!isIPv4 && !isIPv6) {
return false;
}
// Do protocol-specific checks
if (isIPv4) {
if (shouldHaveIPMask) {
ipaddr.IPv4.parseCIDR(value);
} else {
ipaddr.IPv4.isValid(value);
ipaddr.IPv4.parse(value); // Parse again to prompt test failure if it has a mask but should not.
}
}
if (isIPv6) {
// @TODO NB-VPC: update the IPv6 prefix if required for NB-VPC integration
// VPCs must be assigned an IPv6 prefix of /52, /48, or /44
const invalidVPCIPv6Prefix = !['52', '48', '44'].includes(mask);
if (!isIPv6Subnet && invalidVPCIPv6Prefix) {
return false;
}
// VPC subnets must be assigned an IPv6 prefix of 52-62
const invalidVPCIPv6SubnetPrefix = +mask < 52 || +mask > 62;
if (isIPv6Subnet && invalidVPCIPv6SubnetPrefix) {
return false;
}
if (shouldHaveIPMask) {
ipaddr.IPv6.parseCIDR(value);
} else {
ipaddr.IPv6.isValid(value);
ipaddr.IPv6.parse(value); // Parse again to prompt test failure if it has a mask but should not.
}
}
return true;
} catch (err) {
return false;
}
};
const labelValidation = string()
.test(
labelTestDetails.testName,
labelTestDetails.testMessage,
(value) => !value?.includes('--')
)
.min(1, LABEL_MESSAGE)
.max(64, LABEL_MESSAGE)
.matches(/^[a-zA-Z0-9-]*$/, LABEL_REQUIREMENTS);
export const updateVPCSchema = object({
label: labelValidation,
description: string(),
});
const VPCIPv6Schema = object({
range: string()
.optional()
.test({
name: 'IPv6 prefix length',
message: 'Must be the prefix length 52, 48, or 44 of the IP, e.g. /52',
test: (value) => {
if (value && value.length > 0) {
vpcsValidateIP({
value,
shouldHaveIPMask: true,
mustBeIPMask: false,
});
}
},
}),
});
const VPCIPv6SubnetSchema = object({
range: string()
.required()
.test({
name: 'IPv6 prefix length',
message: 'Must be the prefix length (52-62) of the IP, e.g. /52',
test: (value) => {
if (value && value !== 'auto' && value.length > 0) {
vpcsValidateIP({
value,
shouldHaveIPMask: true,
mustBeIPMask: false,
isIPv6Subnet: true,
});
}
},
}),
});
// @TODO VPC IPv6: Delete this when IPv6 is in GA
export const createSubnetSchemaIPv4 = object({
label: labelValidation.required(LABEL_REQUIRED),
ipv4: string().when('ipv6', {
is: (value: unknown) =>
value === '' || value === null || value === undefined,
then: (schema) =>
schema.required(TEMPORARY_IPV4_REQUIRED_MESSAGE).test({
name: 'IPv4 CIDR format',
message: 'The IPv4 range must be in CIDR format.',
test: (value) =>
vpcsValidateIP({
value,
shouldHaveIPMask: true,
mustBeIPMask: false,
}),
}),
otherwise: (schema) =>
lazy((value: string | undefined) => {
switch (typeof value) {
case 'undefined':
return schema.notRequired().nullable();
case 'string':
return schema.notRequired().test({
name: 'IPv4 CIDR format',
message: 'The IPv4 range must be in CIDR format.',
test: (value) =>
vpcsValidateIP({
value,
shouldHaveIPMask: true,
mustBeIPMask: false,
}),
});
default:
return schema.notRequired().nullable();
}
}),
}),
});
export const createSubnetSchemaWithIPv6 = object().shape(
{
label: labelValidation.required(LABEL_REQUIRED),
ipv4: string().when('ipv6', {
is: (value: unknown) =>
value === '' || value === null || value === undefined,
then: (schema) =>
schema.required(IP_EITHER_BOTH_NOT_NEITHER).test({
name: 'IPv4 CIDR format',
message: 'The IPv4 range must be in CIDR format.',
test: (value) =>
vpcsValidateIP({
value,
shouldHaveIPMask: true,
mustBeIPMask: false,
}),
}),
otherwise: (schema) =>
lazy((value: string | undefined) => {
switch (typeof value) {
case 'undefined':
return schema.notRequired().nullable();
case 'string':
return schema.notRequired().test({
name: 'IPv4 CIDR format',
message: 'The IPv4 range must be in CIDR format.',
test: (value) =>
vpcsValidateIP({
value,
shouldHaveIPMask: true,
mustBeIPMask: false,
}),
});
default:
return schema.notRequired().nullable();
}
}),
}),
ipv6: array()
.of(VPCIPv6SubnetSchema)
.when('ipv4', {
is: (value: unknown) =>
value === '' || value === null || value === undefined,
then: (schema) => schema.required(IP_EITHER_BOTH_NOT_NEITHER),
}),
},
[
['ipv6', 'ipv4'],
['ipv4', 'ipv6'],
]
);
const createVPCIPv6Schema = VPCIPv6Schema.concat(
object({
allocation_class: string().optional(),
})
);
export const createVPCSchema = object({
label: labelValidation.required(LABEL_REQUIRED),
description: string(),
region: string().required('Region is required'),
subnets: array().of(createSubnetSchemaIPv4),
ipv6: array().of(createVPCIPv6Schema).max(1).optional(),
});
export const modifySubnetSchema = object({
label: labelValidation.required(LABEL_REQUIRED),
});