mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-09-24 19:40:23 +01:00
Adds integration tests for per path access lists, fixes ipv6 jsv,
and enforces host-wide access list when location access list is not set
This commit is contained in:
@@ -42,9 +42,11 @@ const getProxyHostsUsingAccessListInLocations = async (accessListId) => {
|
||||
[`%"access_list_id":${accessListId}%`],
|
||||
);
|
||||
}
|
||||
// knex raw() returns [rows, metadata] for MySQL
|
||||
const rows = Array.isArray(result) && Array.isArray(result[0]) ? result[0] : result;
|
||||
return rows || [];
|
||||
// knex raw() returns [rows, metadata] for MySQL, { rows } for Postgres and rows for SQLite
|
||||
if (!Array.isArray(result)) {
|
||||
return result?.rows || [];
|
||||
}
|
||||
return (Array.isArray(result[0]) ? result[0] : result) || [];
|
||||
};
|
||||
|
||||
const internalAccessList = {
|
||||
@@ -233,26 +235,6 @@ const internalAccessList = {
|
||||
.whereIn("id", locationHostIds)
|
||||
.allowGraph(proxyHostModel.defaultAllowGraph)
|
||||
.withGraphFetched("[owner, certificate, access_list.[clients,items]]");
|
||||
for (const host of locationHosts) {
|
||||
// Fetch access lists for locations
|
||||
if (host.locations?.length) {
|
||||
for (let i = 0; i < host.locations.length; i++) {
|
||||
const loc = host.locations[i];
|
||||
if (loc.access_list_id && loc.access_list_id > 0) {
|
||||
const locAccessList = await accessListModel
|
||||
.query()
|
||||
.allowGraph("[clients,items]")
|
||||
.where("is_deleted", 0)
|
||||
.andWhere("id", loc.access_list_id)
|
||||
.withGraphFetched("[clients,items]")
|
||||
.first();
|
||||
if (locAccessList) {
|
||||
host.locations[i].access_list = locAccessList;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await internalNginx.bulkGenerateConfigs("proxy_host", locationHosts);
|
||||
}
|
||||
}
|
||||
@@ -338,50 +320,38 @@ const internalAccessList = {
|
||||
});
|
||||
|
||||
// 2. update any proxy hosts that were using it (ignoring permissions)
|
||||
if (row.proxy_hosts) {
|
||||
const affectedHostIds = new Set((row.proxy_hosts || []).map((h) => h.id));
|
||||
if (affectedHostIds.size) {
|
||||
await proxyHostModel.query().where("access_list_id", "=", row.id).patch({ access_list_id: 0 });
|
||||
|
||||
// 3. reconfigure those hosts, then reload nginx
|
||||
// set the access_list_id to zero for these items
|
||||
row.proxy_hosts.map((_val, idx) => {
|
||||
row.proxy_hosts[idx].access_list_id = 0;
|
||||
return true;
|
||||
});
|
||||
|
||||
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
|
||||
}
|
||||
|
||||
// Also handle proxy hosts that reference this access list in their locations JSON
|
||||
// Also clear it from any proxy host locations using it, these will then inherit the host's access list
|
||||
const locationHostRows = await getProxyHostsUsingAccessListInLocations(row.id);
|
||||
if (locationHostRows?.length) {
|
||||
const locationHostIds = locationHostRows.map((r) => r.id).filter((id) => {
|
||||
return !row.proxy_hosts?.find((h) => h.id === id);
|
||||
});
|
||||
if (locationHostIds.length) {
|
||||
// Clear the access_list_id in locations JSON for these hosts
|
||||
for (const hostId of locationHostIds) {
|
||||
const host = await proxyHostModel.query().where("id", hostId).first();
|
||||
if (host?.locations) {
|
||||
const updatedLocations = host.locations.map((loc) => {
|
||||
if (loc.access_list_id === row.id) {
|
||||
return { ...loc, access_list_id: 0 };
|
||||
}
|
||||
return loc;
|
||||
});
|
||||
await proxyHostModel.query().where("id", hostId).patch({ locations: updatedLocations });
|
||||
for (const { id: hostId } of locationHostRows) {
|
||||
const host = await proxyHostModel.query().where("id", hostId).first();
|
||||
if (host?.locations?.some((loc) => loc.access_list_id === row.id)) {
|
||||
const updatedLocations = host.locations.map((loc) => {
|
||||
if (loc.access_list_id === row.id) {
|
||||
return { ...loc, access_list_id: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// Re-fetch and regenerate configs
|
||||
const locationHosts = await proxyHostModel.query()
|
||||
.where("is_deleted", 0)
|
||||
.whereIn("id", locationHostIds)
|
||||
.allowGraph(proxyHostModel.defaultExpand)
|
||||
.withGraphFetched("[owner, certificate, access_list.[clients,items]]");
|
||||
await internalNginx.bulkGenerateConfigs("proxy_host", locationHosts);
|
||||
return loc;
|
||||
});
|
||||
await proxyHostModel.query().where("id", hostId).patch({ locations: updatedLocations });
|
||||
affectedHostIds.add(hostId);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. reconfigure those hosts from fresh rows, then reload nginx
|
||||
if (affectedHostIds.size) {
|
||||
const affectedHosts = await proxyHostModel
|
||||
.query()
|
||||
.where("is_deleted", 0)
|
||||
.whereIn("id", [...affectedHostIds])
|
||||
.allowGraph(proxyHostModel.defaultAllowGraph)
|
||||
.withGraphFetched("[owner, certificate, access_list.[clients,items]]");
|
||||
await internalNginx.bulkGenerateConfigs("proxy_host", affectedHosts);
|
||||
}
|
||||
|
||||
await internalNginx.reload();
|
||||
|
||||
// delete the htpasswd file
|
||||
|
||||
@@ -6,6 +6,7 @@ import _ from "lodash";
|
||||
import errs from "../lib/error.js";
|
||||
import utils from "../lib/utils.js";
|
||||
import { debug, nginx as logger } from "../logger.js";
|
||||
import accessListModel from "../models/access_list.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -168,6 +169,24 @@ const internalNginx = {
|
||||
host.locations[i],
|
||||
);
|
||||
|
||||
// A location with its own access list overrides the host's,
|
||||
// otherwise it inherits the host's access list
|
||||
let locationAccessList = null;
|
||||
if (locationCopy.access_list_id > 0 && locationCopy.access_list_id !== host.access_list?.id) {
|
||||
locationAccessList = await accessListModel
|
||||
.query()
|
||||
.where("is_deleted", 0)
|
||||
.andWhere("id", locationCopy.access_list_id)
|
||||
.withGraphFetched("[clients,items]")
|
||||
.first();
|
||||
}
|
||||
if (locationAccessList) {
|
||||
locationCopy.access_list = locationAccessList;
|
||||
} else {
|
||||
locationCopy.access_list_id = host.access_list_id;
|
||||
locationCopy.access_list = host.access_list;
|
||||
}
|
||||
|
||||
if (locationCopy.forward_host.indexOf("/") > -1) {
|
||||
const splitted = locationCopy.forward_host.split("/");
|
||||
|
||||
@@ -179,7 +198,9 @@ const internalNginx = {
|
||||
}
|
||||
};
|
||||
|
||||
locationRendering().then(() => resolve(renderedLocations));
|
||||
locationRendering()
|
||||
.then(() => resolve(renderedLocations))
|
||||
.catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -271,6 +292,9 @@ const internalNginx = {
|
||||
debug(logger, `Could not write ${filename}:`, err.message);
|
||||
reject(new errs.ConfigurationError(err.message));
|
||||
});
|
||||
}).catch((err) => {
|
||||
debug(logger, `Could not render locations for ${filename}:`, err.message);
|
||||
reject(new errs.ConfigurationError(err.message));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import _ from "lodash";
|
||||
import errs from "../lib/error.js";
|
||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
||||
import utils from "../lib/utils.js";
|
||||
import accessListModel from "../models/access_list.js";
|
||||
import proxyHostModel from "../models/proxy_host.js";
|
||||
import internalAuditLog from "./audit-log.js";
|
||||
import internalCertificate from "./certificate.js";
|
||||
@@ -13,33 +12,6 @@ const omissions = () => {
|
||||
return ["is_deleted", "owner.is_deleted"];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches access lists for each location that has its own access_list_id.
|
||||
* Attaches the expanded access_list object (with clients and items) to each location.
|
||||
*
|
||||
* @param {Object} host
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const fetchLocationAccessLists = async (host) => {
|
||||
if (!host.locations?.length) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < host.locations.length; i++) {
|
||||
const loc = host.locations[i];
|
||||
if (loc.access_list_id && loc.access_list_id > 0) {
|
||||
const accessList = await accessListModel
|
||||
.query()
|
||||
.where("is_deleted", 0)
|
||||
.andWhere("id", loc.access_list_id)
|
||||
.withGraphFetched("[clients,items]")
|
||||
.first();
|
||||
if (accessList) {
|
||||
host.locations[i].access_list = accessList;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const internalProxyHost = {
|
||||
/**
|
||||
* @param {Access} access
|
||||
@@ -112,7 +84,6 @@ const internalProxyHost = {
|
||||
});
|
||||
})
|
||||
.then(async (row) => {
|
||||
await fetchLocationAccessLists(row);
|
||||
// Configure nginx
|
||||
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
|
||||
return row;
|
||||
@@ -242,7 +213,6 @@ const internalProxyHost = {
|
||||
// No need to add nginx config if host is disabled
|
||||
return row;
|
||||
}
|
||||
await fetchLocationAccessLists(row);
|
||||
// Configure nginx
|
||||
return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => {
|
||||
row.meta = new_meta;
|
||||
@@ -373,8 +343,6 @@ const internalProxyHost = {
|
||||
enabled: 1,
|
||||
});
|
||||
|
||||
await fetchLocationAccessLists(row);
|
||||
|
||||
// Configure nginx
|
||||
await internalNginx.configure(proxyHostModel, "proxy_host", row);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import net from "node:net";
|
||||
import Ajv from "ajv/dist/2020.js";
|
||||
import errs from "../error.js";
|
||||
|
||||
@@ -9,6 +10,8 @@ const ajv = new Ajv({
|
||||
coerceTypes: true,
|
||||
});
|
||||
|
||||
ajv.addFormat("ipv6", { type: "string", validate: (value) => net.isIPv6(value) });
|
||||
|
||||
/**
|
||||
* @param {Object} schema
|
||||
* @param {Object} payload
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import net from "node:net";
|
||||
import Ajv from "ajv/dist/2020.js";
|
||||
import _ from "lodash";
|
||||
import commonDefinitions from "../../schema/common.json" with { type: "json" };
|
||||
@@ -14,6 +15,8 @@ const ajv = new Ajv({
|
||||
schemas: [commonDefinitions],
|
||||
});
|
||||
|
||||
ajv.addFormat("ipv6", { type: "string", validate: (value) => net.isIPv6(value) });
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} schema
|
||||
|
||||
@@ -31,6 +31,10 @@ class AccessList extends Model {
|
||||
|
||||
$parseDatabaseJson(json) {
|
||||
const thisJson = super.$parseDatabaseJson(json);
|
||||
// Postgres returns COUNT() as a string
|
||||
if (typeof thisJson.proxy_host_count === "string") {
|
||||
thisJson.proxy_host_count = Number.parseInt(thisJson.proxy_host_count, 10);
|
||||
}
|
||||
return convertIntFieldsToBool(thisJson, boolFields);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$"
|
||||
"pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
|
||||
Reference in New Issue
Block a user