Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 100 additions & 17 deletions backend/internal/access-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,47 @@ const omissions = () => {
return ["is_deleted"];
};

// Build a map of ACL id -> usage count from locations only (host-level usage is counted by SQL)
const buildAccessListUsageMap = async () => {
const usage = {};
const hosts = await proxyHostModel.query().select("locations").where("is_deleted", 0);
hosts.forEach((host) => {
if (Array.isArray(host.locations)) {
host.locations.forEach((loc) => {
if (loc && loc.use_parent_access_list === false && loc.access_list_id) {
usage[loc.access_list_id] = (usage[loc.access_list_id] || 0) + 1;
}
});
}
});
return usage;
};

// Find proxy hosts that reference an ACL either directly or via locations
const findProxyHostsUsingAcl = async (accessListId) => {
const hosts = await proxyHostModel.query().where("is_deleted", 0);
return hosts.filter((host) => {
if (host.access_list_id === accessListId) return true;
if (Array.isArray(host.locations)) {
return host.locations.some(
(loc) => loc && loc.use_parent_access_list === false && loc.access_list_id === accessListId,
);
}
return false;
});
};

// Fetch proxy hosts with full expansions needed for config generation
const fetchHostsForConfigGeneration = async (hostIds) => {
if (!hostIds.length) return [];
const hosts = await proxyHostModel
.query()
.whereIn("id", hostIds)
.where("is_deleted", 0)
.withGraphFetched("[certificate, access_list.[clients,items]]");
return hosts;
};

const internalAccessList = {
/**
* @param {Access} access
Expand Down Expand Up @@ -194,9 +235,29 @@ const internalAccessList = {
true // skip masking
);

await internalAccessList.build(freshRow)
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
// Find all hosts that use this ACL (directly or via locations)
const allAffectedHosts = await findProxyHostsUsingAcl(data.id);
const hostIds = allAffectedHosts.map((h) => h.id);

// Re-fetch with proper expansions for config generation
const hostsForConfig = await fetchHostsForConfigGeneration(hostIds);

// Enrich locations with the updated ACL data
for (const host of hostsForConfig) {
if (Array.isArray(host.locations)) {
host.locations.forEach((loc) => {
if (loc && loc.use_parent_access_list === false && loc.access_list_id === data.id) {
loc.access_list = freshRow;
}
});
}
}

freshRow.proxy_host_count = hostIds.length;

await internalAccessList.build(freshRow);
if (hostsForConfig.length) {
await internalNginx.bulkGenerateConfigs("proxy_host", hostsForConfig);
}
await internalNginx.reload();
return internalAccessList.maskItems(freshRow);
Expand All @@ -215,6 +276,7 @@ const internalAccessList = {
const thisData = data || {};
const accessData = await access.can("access_lists:get", thisData.id)

const usageMap = await buildAccessListUsageMap();
const query = accessListModel
.query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
Expand All @@ -240,6 +302,8 @@ const internalAccessList = {
}

let row = await query.then(utils.omitRow(omissions()));
row.proxy_host_count = Number.parseInt(row.proxy_host_count || 0, 10);
row.location_count = usageMap[row.id] || 0;

if (!row || !row.id) {
throw new errs.ItemNotFoundError(thisData.id);
Expand Down Expand Up @@ -286,20 +350,36 @@ const internalAccessList = {
});

// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
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;
});
const affectedHosts = await findProxyHostsUsingAcl(row.id);
const hostIds = affectedHosts.map((h) => h.id);

if (affectedHosts.length) {
// clear direct ACL assignment
await proxyHostModel.query().where("access_list_id", "=", row.id).patch({ access_list_id: 0 });

// clear location-level ACLs
await Promise.all(
affectedHosts.map(async (host) => {
if (!Array.isArray(host.locations)) return;
const updatedLocations = host.locations.map((loc) => {
if (loc && loc.use_parent_access_list === false && loc.access_list_id === row.id) {
return {
...loc,
use_parent_access_list: true,
access_list_id: 0,
access_list: null,
};
}
return loc;
});
await proxyHostModel.query().where("id", host.id).patch({ locations: updatedLocations });
return true;
}),
);

await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
// 3. re-fetch hosts with proper expansions for config generation
const hostsForConfig = await fetchHostsForConfigGeneration(hostIds);
await internalNginx.bulkGenerateConfigs("proxy_host", hostsForConfig);
}

await internalNginx.reload();
Expand Down Expand Up @@ -331,6 +411,7 @@ const internalAccessList = {
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("access_lists:list");
const usageMap = await buildAccessListUsageMap();

const query = accessListModel
.query()
Expand Down Expand Up @@ -365,8 +446,10 @@ const internalAccessList = {
const rows = await query.then(utils.omitRows(omissions()));
if (rows) {
rows.map((row, idx) => {
rows[idx].proxy_host_count = Number.parseInt(row.proxy_host_count || 0, 10);
rows[idx].location_count = usageMap[row.id] || 0;
if (typeof row.items !== "undefined" && row.items) {
rows[idx] = internalAccessList.maskItems(row);
rows[idx] = internalAccessList.maskItems(rows[idx]);
}
return true;
});
Expand Down
16 changes: 13 additions & 3 deletions backend/internal/nginx.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,21 @@ const internalNginx = {

const locationRendering = async () => {
for (let i = 0; i < host.locations.length; i++) {
const location = host.locations[i];

// Determine which access list to use
// If use_parent_access_list is false and access_list_id is set, use location-specific access list
// Otherwise, use parent proxy host access list (default behavior)
const useParentAccessList = location.use_parent_access_list !== false; // default to true
const effectiveAccessListId = useParentAccessList ? host.access_list_id : (location.access_list_id || 0);
const effectiveAccessList = useParentAccessList ? host.access_list : location.access_list;

// Spread location first, then override with effective ACL and host-level settings
const locationCopy = Object.assign(
{},
{ access_list_id: host.access_list_id },
location,
{ access_list_id: effectiveAccessListId },
{ access_list: effectiveAccessList },
{ certificate_id: host.certificate_id },
{ ssl_forced: host.ssl_forced },
{ caching_enabled: host.caching_enabled },
Expand All @@ -160,9 +172,7 @@ const internalNginx = {
{ http2_support: host.http2_support },
{ hsts_enabled: host.hsts_enabled },
{ hsts_subdomains: host.hsts_subdomains },
{ access_list: host.access_list },
{ certificate: host.certificate },
host.locations[i],
);

if (locationCopy.forward_host.indexOf("/") > -1) {
Expand Down
113 changes: 108 additions & 5 deletions backend/internal/proxy-host.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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";
Expand All @@ -12,6 +13,23 @@ const omissions = () => {
return ["is_deleted", "owner.is_deleted"];
};

// Normalize location ACL flags for persistence
const normalizeLocations = (locations) => {
if (!Array.isArray(locations)) return locations;
return locations.map((loc) => {
if (!loc) return loc;
const normalized = { ...loc };
if (normalized.use_parent_access_list !== false) {
normalized.use_parent_access_list = true;
normalized.access_list_id = normalized.access_list_id || 0;
delete normalized.access_list; // avoid stale embedded ACLs in DB JSON
} else {
normalized.access_list_id = normalized.access_list_id || 0;
}
return normalized;
});
};

const internalProxyHost = {
/**
* @param {Access} access
Expand All @@ -20,6 +38,7 @@ const internalProxyHost = {
*/
create: (access, data) => {
let thisData = data;
thisData.locations = normalizeLocations(thisData.locations);
const createCertificate = thisData.certificate_id === "new";

if (createCertificate) {
Expand Down Expand Up @@ -115,6 +134,7 @@ const internalProxyHost = {
*/
update: (access, data) => {
let thisData = data;
thisData.locations = normalizeLocations(thisData.locations);
const create_certificate = thisData.certificate_id === "new";

if (create_certificate) {
Expand Down Expand Up @@ -258,11 +278,15 @@ const internalProxyHost = {
throw new errs.ItemNotFoundError(thisData.id);
}
const thisRow = internalHost.cleanRowCertificateMeta(row);
// Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(row, thisData.omit);
}
return thisRow;

// Load access lists for locations if they have custom access lists
return internalProxyHost.enrichLocationsWithAccessLists(thisRow).then(() => {
// Custom omissions
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
return _.omit(thisRow, thisData.omit);
}
return thisRow;
});
});
},

Expand Down Expand Up @@ -469,6 +493,85 @@ const internalProxyHost = {
return Number.parseInt(row.count, 10);
});
},

/**
* Enriches locations with their access lists if they have custom ones
*
* @param {Object} row - Proxy host row with locations
* @returns {Promise}
*/
enrichLocationsWithAccessLists: async (row) => {
if (!row.locations || !Array.isArray(row.locations) || row.locations.length === 0) {
return row;
}

// Find all unique access list IDs that need to be loaded
const accessListIds = [];
row.locations.forEach((location) => {
if (location.use_parent_access_list === false && location.access_list_id && location.access_list_id > 0) {
if (!accessListIds.includes(location.access_list_id)) {
accessListIds.push(location.access_list_id);
}
}
});

// If no custom access lists, return as-is
if (accessListIds.length === 0) {
return row;
}

// Load all needed access lists; include proxy_host_count to satisfy schema
const accessLists = await accessListModel
.query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
.leftJoin("proxy_host", function () {
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn("proxy_host.is_deleted", "=", 0);
})
.whereIn("access_list.id", accessListIds)
.where("access_list.is_deleted", 0)
.groupBy("access_list.id")
.withGraphFetched("[clients, items]");

// Create a map for easy lookup
const accessListMap = {};
accessLists.forEach((al) => {
const withCount = {
...al,
proxy_host_count: typeof al.proxy_host_count === "number" ? al.proxy_host_count : 0,
location_count: 0, // Will be calculated below
};
accessListMap[al.id] = withCount;
});

// Count location usage
const locationUsage = {};
row.locations.forEach((location) => {
if (location.use_parent_access_list === false && location.access_list_id) {
locationUsage[location.access_list_id] = (locationUsage[location.access_list_id] || 0) + 1;
}
});

// Attach access lists to their respective locations and update counts
row.locations.forEach((location) => {
if (location.use_parent_access_list === false && location.access_list_id) {
const found = accessListMap[location.access_list_id];
if (found) {
const usageCount = locationUsage[location.access_list_id] || 0;
location.access_list = {
...found,
location_count: usageCount,
};
} else {
// Missing ACL (deleted) → fall back to parent/public
location.use_parent_access_list = true;
location.access_list_id = 0;
delete location.access_list;
}
}
});

return row;
},
};

export default internalProxyHost;
7 changes: 6 additions & 1 deletion backend/schema/components/access-list-object.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "object",
"description": "Access List object",
"required": ["id", "created_on", "modified_on", "owner_user_id", "name", "meta", "satisfy_any", "pass_auth", "proxy_host_count"],
"required": ["id", "created_on", "modified_on", "owner_user_id", "name", "meta", "satisfy_any", "pass_auth", "proxy_host_count", "location_count"],
"properties": {
"id": {
"$ref": "../common.json#/properties/id"
Expand Down Expand Up @@ -36,6 +36,11 @@
"type": "integer",
"minimum": 0,
"example": 3
},
"location_count": {
"type": "integer",
"minimum": 0,
"example": 1
}
}
}
Loading