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
101 changes: 101 additions & 0 deletions backend/internal/access-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import fs from "node:fs";
import batchflow from "batchflow";
import _ from "lodash";
import errs from "../lib/error.js";
import { isMysql, isPostgres } from "../lib/config.js";
import utils from "../lib/utils.js";
import db from "../db.js";
import { access as logger } from "../logger.js";
import accessListModel from "../models/access_list.js";
import accessListAuthModel from "../models/access_list_auth.js";
Expand All @@ -15,6 +17,36 @@ const omissions = () => {
return ["is_deleted"];
};

/**
* Find proxy hosts that reference an access list in their locations JSON.
*
* @param {Integer} accessListId
* @returns {Promise<Array>}
*/
const getProxyHostsUsingAccessListInLocations = async (accessListId) => {
let result;
if (isMysql()) {
const searchObj = JSON.stringify([{ access_list_id: accessListId }]);
result = await db().raw(
"SELECT id FROM proxy_host WHERE is_deleted = 0 AND JSON_CONTAINS(locations, ?, ?)",
[searchObj, "$"],
);
} else if (isPostgres()) {
result = await db().raw(
"SELECT id FROM proxy_host WHERE is_deleted = 0 AND locations::jsonb @> ?::jsonb",
[JSON.stringify([{ access_list_id: accessListId }])],
);
} else {
result = await db().raw(
"SELECT id FROM proxy_host WHERE is_deleted = 0 AND locations LIKE ?",
[`%"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 || [];
};

const internalAccessList = {
/**
* @param {Access} access
Expand Down Expand Up @@ -187,6 +219,44 @@ const internalAccessList = {
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
}

// Also regenerate configs for proxy hosts that reference this access list in their locations
const locationHostRows = await getProxyHostsUsingAccessListInLocations(data.id);
if (locationHostRows?.length) {
const locationHostIds = locationHostRows.map((r) => r.id).filter((id) => {
// Exclude hosts already regenerated above
return !freshRow.proxy_hosts?.find((h) => h.id === id);
});
if (locationHostIds.length) {
const locationHosts = await proxyHostModel.query()
.where("is_deleted", 0)
.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);
}
}

await internalNginx.reload();
return internalAccessList.maskItems(freshRow);
},
Expand Down Expand Up @@ -291,6 +361,37 @@ const internalAccessList = {
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
}

// Also handle proxy hosts that reference this access list in their locations JSON
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 });
}
}

// 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);
}
}

await internalNginx.reload();

// delete the htpasswd file
Expand Down
163 changes: 96 additions & 67 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,33 @@ 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
Expand Down Expand Up @@ -83,28 +111,29 @@ const internalProxyHost = {
expand: ["certificate", "owner", "access_list.[clients,items]"],
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
.then(async (row) => {
await fetchLocationAccessLists(row);
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
return row;
});
})
.then((row) => {
// Audit log
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);

// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "proxy-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return row;
});
})
.then((row) => {
// Audit log
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);

// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "proxy-host",
object_id: row.id,
meta: thisData,
})
.then(() => {
return row;
});
});
});
},

/**
Expand Down Expand Up @@ -202,24 +231,25 @@ const internalProxyHost = {
});
});
})
.then(() => {
return internalProxyHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate", "access_list.[clients,items]"],
})
.then((row) => {
if (!row.enabled) {
// No need to add nginx config if host is disabled
return row;
}
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
.then(() => {
return internalProxyHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate", "access_list.[clients,items]"],
})
.then(async (row) => {
if (!row.enabled) {
// 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;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
});
});
});
},

/**
Expand Down Expand Up @@ -326,39 +356,38 @@ const internalProxyHost = {
expand: ["certificate", "owner", "access_list"],
});
})
.then((row) => {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}
.then(async (row) => {
if (!row?.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}

row.enabled = 1;

await proxyHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
});

row.enabled = 1;
await fetchLocationAccessLists(row);

return proxyHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
})
.then(() => {
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "enabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
});
})
.then(() => {
return true;
// Configure nginx
await internalNginx.configure(proxyHostModel, "proxy_host", row);

// Add to audit log
await internalAuditLog.add(access, {
action: "enabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});

return true;
});
},

/**
Expand Down
Loading