mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-18 08:18:43 +02:00
Add validation
This commit is contained in:
parent
4a6da91faf
commit
42434ca832
2 changed files with 138 additions and 22 deletions
|
@ -480,7 +480,7 @@ async function checkRules(
|
||||||
return rule.action == "ACCEPT";
|
return rule.action == "ACCEPT";
|
||||||
} else if (path && rule.match == "PATH") {
|
} else if (path && rule.match == "PATH") {
|
||||||
// rule.value is a regex, match on the path and see if it matches
|
// rule.value is a regex, match on the path and see if it matches
|
||||||
const re = new RegExp(rule.value);
|
const re = urlGlobToRegex(rule.value);
|
||||||
if (re.test(path)) {
|
if (re.test(path)) {
|
||||||
return rule.action == "ACCEPT";
|
return rule.action == "ACCEPT";
|
||||||
}
|
}
|
||||||
|
@ -489,3 +489,19 @@ async function checkRules(
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function urlGlobToRegex(pattern: string): RegExp {
|
||||||
|
// Remove leading slash if present (we'll add it to the regex pattern)
|
||||||
|
pattern = pattern.startsWith('/') ? pattern.slice(1) : pattern;
|
||||||
|
|
||||||
|
// Escape special regex characters except *
|
||||||
|
const escapedPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
|
// Replace * with regex pattern for any valid URL segment characters
|
||||||
|
const regexPattern = escapedPattern.replace(/\*/g, '[a-zA-Z0-9_-]+');
|
||||||
|
|
||||||
|
// Create the final pattern that:
|
||||||
|
// 1. Optionally matches leading slash
|
||||||
|
// 2. Matches the entire string
|
||||||
|
return new RegExp(`^/?${regexPattern}$`);
|
||||||
|
}
|
|
@ -93,9 +93,9 @@ export default function ResourceRules(props: {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRules = async () => {
|
const fetchRules = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get<AxiosResponse<ListResourceRulesResponse>>(
|
const res = await api.get<
|
||||||
`/resource/${params.resourceId}/rules`
|
AxiosResponse<ListResourceRulesResponse>
|
||||||
);
|
>(`/resource/${params.resourceId}/rules`);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setRules(res.data.data.rules);
|
setRules(res.data.data.rules);
|
||||||
}
|
}
|
||||||
|
@ -133,6 +133,25 @@ export default function ResourceRules(props: {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.match === "CIDR" && !isValidCIDR(data.value)) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Invalid CIDR",
|
||||||
|
description: "Please enter a valid CIDR value"
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Invalid URL path",
|
||||||
|
description: "Please enter a valid URL path value"
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newRule: LocalRule = {
|
const newRule: LocalRule = {
|
||||||
...data,
|
...data,
|
||||||
ruleId: new Date().getTime(),
|
ruleId: new Date().getTime(),
|
||||||
|
@ -171,11 +190,27 @@ export default function ResourceRules(props: {
|
||||||
value: rule.value
|
value: rule.value
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Invalid CIDR",
|
||||||
|
description: "Please enter a valid CIDR value"
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rule.match === "PATH" && !isValidUrlGlobPattern(rule.value)) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Invalid URL path",
|
||||||
|
description: "Please enter a valid URL path value"
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (rule.new) {
|
if (rule.new) {
|
||||||
await api.put(
|
await api.put(`/resource/${params.resourceId}/rule`, data);
|
||||||
`/resource/${params.resourceId}/rule`,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
} else if (rule.updated) {
|
} else if (rule.updated) {
|
||||||
await api.post(
|
await api.post(
|
||||||
`/resource/${params.resourceId}/rule/${rule.ruleId}`,
|
`/resource/${params.resourceId}/rule/${rule.ruleId}`,
|
||||||
|
@ -190,7 +225,9 @@ export default function ResourceRules(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setRules(rules.map(rule => ({ ...rule, new: false, updated: false })));
|
setRules(
|
||||||
|
rules.map((rule) => ({ ...rule, new: false, updated: false }))
|
||||||
|
);
|
||||||
setRulesToRemove([]);
|
setRulesToRemove([]);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
|
@ -322,7 +359,9 @@ export default function ResourceRules(props: {
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onValueChange={field.onChange}
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
@ -350,7 +389,9 @@ export default function ResourceRules(props: {
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onValueChange={field.onChange}
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
@ -380,7 +421,8 @@ export default function ResourceRules(props: {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Enter CIDR or path value based on match type
|
Enter CIDR or path value based
|
||||||
|
on match type
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
@ -401,7 +443,8 @@ export default function ResourceRules(props: {
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef.header,
|
header.column
|
||||||
|
.columnDef.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
@ -413,14 +456,17 @@ export default function ResourceRules(props: {
|
||||||
{table.getRowModel().rows?.length ? (
|
{table.getRowModel().rows?.length ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row
|
||||||
<TableCell key={cell.id}>
|
.getVisibleCells()
|
||||||
{flexRender(
|
.map((cell) => (
|
||||||
cell.column.columnDef.cell,
|
<TableCell key={cell.id}>
|
||||||
cell.getContext()
|
{flexRender(
|
||||||
)}
|
cell.column
|
||||||
</TableCell>
|
.columnDef.cell,
|
||||||
))}
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
@ -449,4 +495,58 @@ export default function ResourceRules(props: {
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidCIDR(cidr: string): boolean {
|
||||||
|
// Match CIDR pattern (e.g., "192.168.0.0/24")
|
||||||
|
const cidrPattern =
|
||||||
|
/^([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
||||||
|
|
||||||
|
if (!cidrPattern.test(cidr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate IP address part
|
||||||
|
const ipPart = cidr.split("/")[0];
|
||||||
|
const octets = ipPart.split(".");
|
||||||
|
|
||||||
|
return octets.every((octet) => {
|
||||||
|
const num = parseInt(octet, 10);
|
||||||
|
return num >= 0 && num <= 255;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidUrlGlobPattern(pattern: string): boolean {
|
||||||
|
// Remove leading slash if present
|
||||||
|
pattern = pattern.startsWith('/') ? pattern.slice(1) : pattern;
|
||||||
|
|
||||||
|
// Empty string is not valid
|
||||||
|
if (!pattern) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split path into segments
|
||||||
|
const segments = pattern.split('/');
|
||||||
|
|
||||||
|
// Check each segment
|
||||||
|
for (let i = 0; i < segments.length; i++) {
|
||||||
|
const segment = segments[i];
|
||||||
|
|
||||||
|
// Empty segments are not allowed (double slashes)
|
||||||
|
if (!segment && i !== segments.length - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If segment contains *, it must be exactly *
|
||||||
|
if (segment.includes('*') && segment !== '*') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid characters
|
||||||
|
if (!/^[a-zA-Z0-9_*-]*$/.test(segment)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue