import { object, array, string, number, enums, boolean, literal, optional, dynamic, nullable, defaulted, union, size, create, assert, define, } from "superstruct"; import { Database } from "all.db"; import crypto from "node:crypto"; import { ipv4, ipv6 } from "cidr-block"; import path from "node:path"; import { EventEmitter } from "node:events"; class ObjectModel { #modelName; #modelStruct; #modelStore; #dynRef = {}; #hooks = new EventEmitter(); constructor(settings) { this.#modelName = settings.name; this.#modelStruct = settings.model; this.#modelStore = new Database({ dataPath: path.join(process.cwd(), 'data/database', `${settings.name}.json`) }); if (settings.hooks) { Object.entries(settings.hooks).forEach(([eventName, eventFunction]) => { this.on(eventName, eventFunction); }) } this.#dynRef.refOne = dynamic((objectId, context) => { let permittedObjIds = Object.values(this.getAll()).map(item => item.id); return enums(permittedObjIds); }); this.#dynRef.refMany = dynamic((objectId, context) => { let permittedObjIds = Object.values(this.getAll()).map(item => item.id); return array( enums(permittedObjIds) ); }); if (settings.dynRef instanceof Object) { this.#dynRef = Object.assign(this.#dynRef, settings.dynRef) } } complete(data) { try { let result = create(data, this.#modelStruct); return result; } catch (error) { throw new Error(`error completing ${this.#modelName}: ` + error.message); } } validate(data) { try { assert(data, this.#modelStruct); return data; } catch (error) { throw new Error(`error validating ${this.#modelName}: ` + error.message); } } getAll() { return this.#modelStore.getAll(); } getById(objectId) { return this.#modelStore.get(objectId); } create(objectData) { // try { const newData = this.validate( this.complete(objectData) ); if (this.#modelStore.exists(newData.id)) { throw new Error(`failed create ${this.#modelName} with ID ${newData.id}. already existing in database.`); } this.#hooks.emit('beforeCreate', null, newData); this.#modelStore.set(newData.id, newData); this.#hooks.emit('afterCreate', newData); return newData; // } catch (error) { // throw error; // } } update(objectData) { // try { const newData = this.validate( this.complete(objectData) ); const oldData = this.getById(newData.id) if (!oldData) { throw new Error(`failed update ${this.#modelName} with ID ${newData.id}. not existing in database.`); } this.#hooks.emit('beforeUpdate', oldData, newData); this.#modelStore.set(newData.id, newData); this.#hooks.emit('afterUpdate', oldData, newData); return newData; // } catch (error) { // throw error; // } } delete(objectId) { // try { const oldData = this.getById(objectId); if (!oldData) { throw new Error(`failed delete ${this.#modelName} with ID ${newData.id}. not existing in database.`); } this.#hooks.emit('beforeDelete', oldData, null); this.#modelStore.delete(oldData.id); this.#hooks.emit('afterDelete', oldData, null); return oldData; // } catch (error) { // throw error; // } } has(objectId) { return this.#modelStore.has(objectId); } get dynRef() { return this.#dynRef; } on(eventName, eventFunction) { this.#hooks.on(eventName, eventFunction); } } // - --- --- --- --- --- --- --- --- --- --- --- --- // // Custom Checks // // - --- --- --- --- --- --- --- --- --- --- --- --- const ipv4Address = define('ipv4Address', (value) => { return ipv4.isValidAddress(value); }); const ipv4Cidr = define('ipv4Cidr', (value) => { console.log(value); console.log(ipv4.isValidCIDR(value)); return ipv4.isValidCIDR(value); }); // - --- --- --- --- --- --- --- --- --- --- --- --- // // Base Models // // - --- --- --- --- --- --- --- --- --- --- --- --- // Authorisation Provider Model export const authProvider = new ObjectModel({ name: 'authProvider', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), name: string(), login_text: string(), type: enums(["oauth2"]), settings: dynamic((value, context) => { switch (value.type) { case "oauth2": return object({ client_id: string(), client_secret: string(), authorize_url: string(), token_url: string(), user_info_url: string(), redirect_url: string(), scope: array(string()) }); } return object({}); }) }) }); // User Model export const user = new ObjectModel({ name: 'user', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), external_id: string(), provider: string(), displayName: string(), email: string() }) }); // User Group Model export const userGroup = new ObjectModel({ name: 'userGroup', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), name: string(), comment: defaulted(string(), ''), members: user.dynRef.refMany }) }); // - --- --- --- --- --- --- --- --- --- --- --- --- // // Wireguard Models // // - --- --- --- --- --- --- --- --- --- --- --- --- // Wireguard Interface Model export const wireguardInterface = new ObjectModel({ name: 'wireguardInterface', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), name: size(string(), 1, 128), comment: defaulted(size(string(), 0, 256), ''), enabled: defaulted(boolean(), true), privateKey: string(), publicKey: string(), ifName: string(), ifAddress: ipv4Cidr, listenPort: number(), endpoint: string(), dnsServer: nullable(ipv4Address) }) }); // Wireguard Peer Model export const wireguardPeer = new ObjectModel({ name: 'wireguardPeer', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), name: size(string(), 1, 128), comment: defaulted(size(string(), 0, 256), ''), interface: wireguardInterface.dynRef.refOne, enabled: defaulted(boolean(), true), privateKey: string(), publicKey: string(), presharedKey: string(), ifAddress: ipv4Cidr, keepalive: size(defaulted(number(), 30), 1, 300), }) }); // - --- --- --- --- --- --- --- --- --- --- --- --- // // CMDB Models // // - --- --- --- --- --- --- --- --- --- --- --- --- // Address Object Model export const addressObject = new ObjectModel({ name: 'addressObject', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), name: size(string(), 1, 128), comment: defaulted(size(string(), 0, 256), ''), elements: array(union([ object({ type: literal('ipmask'), ipmask: ipv4Address }), object({ type: literal('fqdn'), fqdn: string(), result: nullable(defaulted(string(), null)) }), object({ type: literal('peer'), peer: wireguardPeer.dynRef.refOne }) ])), members: dynamic((memberList, context) => { // allow many values from list of Peer IDs // - exclude objects that contain members (loop prevention) // - exclude self (loop prevention) let permittedAdressObjects = Object.values(addressObject.getAll()) .filter(object => object.members.length == 0) .filter(object => object.id != context.id) .map(item => item.id); // return array from here to reduce Database reads return array(enums(permittedAdressObjects)); }), }) }); // Service Object Model export const serviceObject = new ObjectModel({ name: 'serviceObject', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), name: size(string(), 1, 128), comment: defaulted(size(string(), 0, 256), ''), elements: array(union([ object({ type: literal('tcp'), port: number() }), object({ type: literal('udp'), port: number() }), object({ type: literal('icmp'), icmpType: enums([ "echo-reply", "destination-unreachable", "source-quench", "redirect", "echo-request", "time-exceeded", "parameter-problem", "timestamp-request", "timestamp-reply", "info-request", "info-reply", "address-mask-request", "address-mask-reply", "router-advertisement", "router-solicitation" ]) }), object({ type: literal('esp'), port: number() }) ])) }) }); // - --- --- --- --- --- --- --- --- --- --- --- --- // // Firewall Policy Models // // - --- --- --- --- --- --- --- --- --- --- --- --- export const accessPolicy = new ObjectModel({ name: 'accessPolicy', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), name: size(string(), 1, 128), comment: defaulted(size(string(), 0, 256), ''), enabled: defaulted(boolean(), true), srcAddr: addressObject.dynRef.refMany, dstAddr: addressObject.dynRef.refMany, services: serviceObject.dynRef.refMany, }) }); export const natPolicy = new ObjectModel({ name: 'natPolicy', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), name: size(string(), 1, 128), comment: defaulted(size(string(), 0, 256), ''), enabled: defaulted(boolean(), true), natType: enums(['snat', 'dnat', ' masquerade']), srcAddress: string(), dstAddress: string(), dnatPort: optional(number()), // Ziel Port für DNAT }) }); // - --- --- --- --- --- --- --- --- --- --- --- --- // // Access Control Models // // - --- --- --- --- --- --- --- --- --- --- --- --- export const configToken = new ObjectModel({ name: 'configToken', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), token: defaulted(string(), () => { return crypto.randomBytes(32).toString('hex'); }), peer: string(), name: string() }) }); export const apiToken = new ObjectModel({ name: 'apiToken', model: object({ id: defaulted(string(), () => { return crypto.randomUUID(); }), token: defaulted(string(), () => { return crypto.randomBytes(32).toString('hex'); }), name: string() }), hooks: { beforeCreate: (oldData, newData) => { console.log("before API Token created"); }, afterCreate: (oldData, newData) => { console.log("after API Token created"); } } }); export default { wireguardInterface, wireguardPeer, addressObject, authProvider, user, userGroup, accessPolicy, natPolicy, configToken, apiToken }