Added IPAM IpNetwork Module

This commit is contained in:
Luca Haid
2024-05-10 23:07:21 +02:00
parent c2e5796661
commit f57d57b46d
4 changed files with 438 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<?php
/**
* @property mixed|null $name
*/
class IpNetwork extends mfBaseModel
{
}

View File

@@ -0,0 +1,184 @@
<?php
class IpNetworkController extends mfBaseController {
private User $me;
protected function init(): void {
$me = new User();
$me->loadMe();
$this->layout()->set("me", $me);
$this->me = $me;
if (!$this->me->isAdmin()) {
$this->redirect("dashboard");
}
}
protected function indexAction(): void {
$JSGlobals = ["BASE_URL" => self::getUrl("IpNetwork"),
"DASHBOARD_URL" => self::getUrl("Dashboard"),
"MFAPPNAME" => MFAPPNAME_SLUG,
"PAGE_TITLE" => "IPAM",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "IPAM", "href" => self::getUrl("IpNetwork")]
],
"IPNETWORK_API_URL" => self::getUrl("IpNetwork/api"),
];
$this->layout()->set("vueViewName", "IpNetwork");
$this->layout()->set("JSGlobals", $JSGlobals);
$this->layout()->setTemplate("VueViews/Vue");
}
protected function apiAction() {
$do = $this->request->do;
if (!$this->me->isAdmin()) {
$this->redirect("dashboard");
}
switch ($do) {
case "get":
$return = $this->get();
break;
case "getById":
$return = $this->getById();
break;
case "create":
$return = $this->create();
break;
case "update":
$return = $this->update();
break;
case "delete":
$return = $this->delete();
break;
default:
$return = false;
break;
}
if (!$return) {
$return = [
"status" => "error",
"message" => "Invalid request."
];
}
header('Content-Type: application/json');
die(json_encode($return));
}
private function aggregateChildren($network, &$childrenCount) {
$children = IpNetworkModel::getChildren($network->id);
foreach ($children as $child) {
$childrenCount++;
if ($child->cidr !== 32) {
$this->aggregateChildren($child, $childrenCount);
}
}
}
private function get(): array {
$json = json_decode(file_get_contents('php://input'), true);
$filters = $json['filters'] ?? [];
$order = $json['order'] ?? [];
$page = $json['pagination']['page'] ?? 1;
$perPage = $json['pagination']['per_page'] ?? 10;
$orderChildren = false;
if ($order['key'] === 'network_address_str') {
$order['key'] = 'network_address';
} else if ($order['key'] === 'children') {
$orderChildren = $order['order'];
$order = null;
}
$networks = IpNetworkModel::getIpNetworks($filters, null, $perPage * $page - $perPage, $order);
$total_rows = IpNetworkModel::countIpNetworks([
"parent_network_id" => $filters['parent_network_id'] ?? ''
]);
$processedNetworks = [];
foreach ($networks as $network) {
$childrenCount = 0;
$this->aggregateChildren($network, $childrenCount);
$from = $filters['children']['from'] ?? null;
$to = $filters['children']['to'] ?? null;
if (($from !== null && $childrenCount < $from) || ($to !== null && $childrenCount > $to)) {
continue;
}
$network->children = $childrenCount;
$processedNetworks[] = $network;
}
if ($orderChildren) {
usort($processedNetworks, function ($a, $b) use ($orderChildren) {
if ($orderChildren === 'asc') {
return $a->children <=> $b->children;
} else {
return $b->children <=> $a->children;
}
});
}
return [
"rows" => array_slice($processedNetworks, $perPage * $page - $perPage, $perPage),
"pagination" => [
"page" => $page,
"total_pages" => ceil(count($processedNetworks) / $perPage),
"filtered_available" => count($processedNetworks),
"per_page" => $perPage,
"total_rows" => intval($total_rows)
]
];
}
private function getById(): array {
$json = json_decode(file_get_contents('php://input'), true);
$network = IpNetworkModel::getById($json['id']);
if ($network === null) {
return [
"status" => "error",
"message" => "Network not found."
];
}
return [
"status" => "success",
"network" => $network
];
}
private function create(): array {
$json = json_decode(file_get_contents('php://input'), true);
try {
IpNetworkModel::createIpNetwork($json);
return [
"status" => "success",
"message" => "IP Network created."
];
} catch (Exception $e) {
return [
"status" => "error",
"message" => $e->getMessage()
];
}
}
}

View File

@@ -0,0 +1,212 @@
<?php
class IpNetworkModel {
public $id;
public $network_address;
public $cidr;
public $parent_network_id;
public $status;
public $network_address_str;
public $name;
public $description;
public $create;
public $edit;
public $location;
public function __construct($data = []) {
foreach ($data as $field => $value) {
if (property_exists(get_called_class(), $field)) {
$this->$field = $value;
}
}
}
public static function getIpNetworks($filters, $limit = null, $offset = 0, $order = null): array {
$db = FronkDB::singleton();
$sql = "SELECT *, CONCAT(INET_NTOA(network_address), '/', cidr) AS network_address_str FROM `IpNetwork` WHERE 1 ";
$sql .= isset($filters['network_address_str']) ? " AND CONCAT(INET_NTOA(network_address), '/', cidr) LIKE '%" . $filters['network_address_str'] . "%'" : "";
$sql .= self::getSqlFilter($filters);
$sql .= $order === null || $order['key'] === null ? " ORDER BY `network_address` ASC" : " ORDER BY `" . $order['key'] . "` " . $order['order'];
$sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset;
$result = $db->query($sql);
$rows = [];
while ($row = $result->fetch_assoc()) {
$rows[] = new IpNetworkModel($row);
}
return $rows;
}
public static function getSqlFilter($filters): string {
$sql = isset($filters['name']) ? Helper::generateFilterCondition($filters['name'], 'name') : "";
$sql .= isset($filters['description']) ? Helper::generateFilterCondition($filters['description'], 'description') : "";
$sql .= isset($filters['location']) ? Helper::generateFilterCondition($filters['location'], 'location') : "";
$sql .= isset($filters['status']) ? Helper::generateFilterCondition($filters['status'], 'status') : "";
$sql .= empty($filters['parent_network_id']) ? " AND `parent_network_id` IS NULL" : " AND `parent_network_id` = " . $filters['parent_network_id'];
return $sql;
}
public static function countIpNetworks($filters) {
$db = FronkDB::singleton();
$sql = "SELECT COUNT(*) as `total_rows` FROM `IpNetwork` WHERE 1 " . self::getSqlFilter($filters);
$result = $db->query($sql);
return $result->fetch_assoc()['total_rows'];
}
public static function countChildren($id) {
$db = FronkDB::singleton();
$sql = "SELECT COUNT(*) as `total_rows` FROM `IpNetwork` WHERE `parent_network_id` = $id";
$result = $db->query($sql);
return $result->fetch_assoc()['total_rows'];
}
public static function getChildren($id): array {
$db = FronkDB::singleton();
$sql = "SELECT * FROM `IpNetwork` WHERE `parent_network_id` = $id";
$result = $db->query($sql);
$rows = [];
while ($row = $result->fetch_assoc()) {
$rows[] = new IpNetworkModel($row);
}
return $rows;
}
/**
* @throws Exception
*/
public static function createIpNetwork($data): void {
$db = FronkDB::singleton();
$network_address = $data['network_address'];
$cidr = $data['cidr'];
$parent_network_id = $data['parent_network_id'] ?? 'NULL';
$status = $data['status'];
$name = $data['name'];
$description = $data['description'];
$location = $data['location'];
// Convert network address to integer
$network_address_int = ip2long($network_address);
// Define query to check for overlapping networks
$check_sql = "
SELECT `id`, `network_address`, `cidr`
FROM `IpNetwork`
WHERE (
(INET_ATON('$network_address') & ~((1 << (32 - `cidr`)) - 1)) = `network_address`
OR
(`network_address` & ~((1 << (32 - $cidr)) - 1)) = INET_ATON('$network_address')
)";
// die($check_sql);
$result = $db->query($check_sql);
$parentFound = false;
$parentFoundId = null;
while ($row = $result->fetch_assoc()) {
$existing_network_address_int = $row['network_address'];
$existing_cidr = $row['cidr'];
// Check if the new network is within an existing network
if ($network_address_int >= $existing_network_address_int &&
$network_address_int < ($existing_network_address_int + pow(2, (32 - $existing_cidr)))) {
if ($cidr <= $existing_cidr) {
// The new network is larger or equal, which is invalid
if ($cidr == 32) {
throw new Exception("Address $network_address/32 already exists");
} else {
throw new Exception("Network $network_address/$cidr conflicts with existing network " . long2ip($existing_network_address_int) . "/$existing_cidr");
}
}
// Check if the new network is a correct subnetwork
if ($parent_network_id != 'NULL') {
$result->data_seek(0);
while ($row = $result->fetch_assoc()) {
if ($row['id'] == $parent_network_id) {
$parentFoundId = $row['id'];
$parentFound = true;
break;
}
}
if (!$parentFound) {
throw new Exception("Parent network ID $parent_network_id does not match the actual parent network ID {$row['id']}");
}
// New check for conflicts with child networks
$check_child_sql = "
SELECT `id`, `network_address`, `cidr`
FROM `IpNetwork`
WHERE `parent_network_id` = $parent_network_id
AND (
INET_ATON('$network_address') BETWEEN `network_address`
AND (`network_address` + POW(2, (32 - `cidr`)) - 1)
OR
`network_address` BETWEEN INET_ATON('$network_address')
AND (INET_ATON('$network_address') + POW(2, (32 - $cidr)) - 1)
)";
;
$child_result = $db->query($check_child_sql);
while ($child_row = $child_result->fetch_assoc()) {
$existing_child_network_address_int = $child_row['network_address'];
$existing_child_cidr = $child_row['cidr'];
// Check if the new network overlaps any existing child networks
if ($network_address_int < $existing_child_network_address_int &&
($network_address_int + pow(2, (32 - $cidr))) > $existing_child_network_address_int) {
throw new Exception("Network $network_address/$cidr conflicts with child network " . long2ip($existing_child_network_address_int) . "/$existing_child_cidr");
}
}
} else {
// If no parent ID provided and the new network is within an existing network, throw an error
throw new Exception("Network $network_address/$cidr must be a child of " . long2ip($existing_network_address_int) . "/$existing_cidr.");
}
// For CIDR 32, check if it already exists but also check if $parent_network_id is same as $parentFoundId but if $parentFoundId is null, throw an error
if ($cidr == 32 && $parent_network_id != 'NULL' && $parent_network_id != $parentFoundId) {
throw new Exception("CIDR 32 address $network_address already exists within " . long2ip($existing_network_address_int) . "/$existing_cidr");
}
}
// Check if the new network overlaps any existing networks
if ($network_address_int < $existing_network_address_int &&
($network_address_int + pow(2, (32 - $cidr))) > $existing_network_address_int) {
throw new Exception("Network $network_address/$cidr overlaps with existing network " . long2ip($existing_network_address_int) . "/$existing_cidr");
}
}
if (!$parentFound && $parent_network_id === 'NULL' && intval($cidr) >= 32) {
throw new Exception("Root Networks cannot be single IPs");
}
// Proceed with insertion if no conflicts are found
$sql = "INSERT INTO `IpNetwork` (`network_address`, `cidr`, `parent_network_id`, `status`, `name`, `description`, `location`, `create`, `edit`)
VALUES (INET_ATON('$network_address'), $cidr, $parent_network_id, '$status', '$name', '$description', '$location', UNIX_TIMESTAMP(), UNIX_TIMESTAMP())";
$result = $db->query($sql);
if (!$result) {
throw new Exception("Failed to insert network");
}
}
public static function getById($id) {
$db = FronkDB::singleton();
$sql = "SELECT *, INET_NTOA(network_address) as network_address_str FROM `IpNetwork` WHERE `id` = $id";
$result = $db->query($sql);
$row = $result->fetch_assoc();
return $row ? new IpNetworkModel($row) : null;
}
}

View File

@@ -0,0 +1,33 @@
<?php /** @noinspection ALL */
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddIpNetwork extends AbstractMigration {
public function up(): void {
if ($this->getEnvironment() == "thetool") {
// IpNetwork Table
$ipNetworkTable = $this->table("IpNetwork", ["signed" => true]);
$ipNetworkTable->addColumn("network_address", "integer", ["null" => false, "signed" => false])
->addColumn("cidr", "integer", ["null" => false])
->addColumn("parent_network_id", "integer", ["null" => true, "signed" => true])
->addColumn("status", "enum", ["values" => ["active", "inactive", "reserved"], "null" => false])
->addColumn("name", "string", ["null" => true, "limit" => 100])
->addColumn("description", "text", ["null" => true])
->addColumn("location", "string", ["null" => true, "limit" => 255])
->addColumn("create", "integer", ["null" => true, "signed" => false])
->addColumn("edit", "integer", ["null" => true, "signed" => false])
->addForeignKey("parent_network_id", "IpNetwork", "id", ["delete" => "SET_NULL", "update" => "NO_ACTION"])
->addIndex(["parent_network_id"]);
$ipNetworkTable->save();
}
}
public function down(): void {
if ($this->getEnvironment() == "thetool") {
$this->table("IpNetwork")->drop()->save();
}
}
}