mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-11-15 18:58:04 +00:00
Optimise isAdmin lookups
This commit is contained in:
1 parent
8342713d45
commit
87b77d9461
6 files changed
+120
-127
No files matched your search
@@ -54,10 +54,7 @@ const internalDeadHost = {
|
||||
thisData.advanced_config = "";
|
||||
}
|
||||
|
||||
const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
|
||||
if (!isAdmin && thisData.advanced_config && thisData.advanced_config.trim() !== "") {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
}
|
||||
internalHost.assertAdvancedConfigAllowed(access, thisData);
|
||||
|
||||
const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
|
||||
|
||||
@@ -156,14 +153,7 @@ const internalDeadHost = {
|
||||
|
||||
thisData = internalHost.cleanSslHstsData(thisData, row);
|
||||
|
||||
const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
|
||||
if (
|
||||
!isAdmin &&
|
||||
typeof data.advanced_config !== "undefined" &&
|
||||
data.advanced_config !== row.advanced_config
|
||||
) {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
}
|
||||
internalHost.assertAdvancedConfigAllowed(access, data, row);
|
||||
|
||||
// do the row update
|
||||
await deadHostModel.query().where({ id: data.id }).patch(data);
|
||||
|
||||
@@ -1,10 +1,98 @@
|
||||
import _ from "lodash";
|
||||
import errs from "../lib/error.js";
|
||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
||||
import deadHostModel from "../models/dead_host.js";
|
||||
import proxyHostModel from "../models/proxy_host.js";
|
||||
import redirectionHostModel from "../models/redirection_host.js";
|
||||
|
||||
const internalHost = {
|
||||
/**
|
||||
* Only admins may set or change advanced_config (raw nginx config). Throws for anyone else.
|
||||
* On create (no `row`), any non-empty advanced_config is refused, including on locations.
|
||||
* On update, the host's advanced_config must be unchanged (or omitted), and locations must
|
||||
* only carry advanced configs the host already has (see assertLocationAdvancedConfigUnchanged).
|
||||
*
|
||||
* @param {Access} access
|
||||
* @param {Object} data incoming host data; data.locations may be filled in on update
|
||||
* @param {Object} [row] the existing host, when updating
|
||||
* @throws {PermissionError}
|
||||
*/
|
||||
assertAdvancedConfigAllowed: (access, data, row) => {
|
||||
if (access.isAdmin?.()) {
|
||||
return;
|
||||
}
|
||||
const deny = () => {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
};
|
||||
|
||||
if (!row) {
|
||||
const hasConfig = (v) => typeof v === "string" && v.trim() !== "";
|
||||
if (hasConfig(data.advanced_config)) {
|
||||
deny();
|
||||
}
|
||||
if (Array.isArray(data.locations) && data.locations.some((loc) => hasConfig(loc?.advanced_config))) {
|
||||
deny();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.advanced_config !== "undefined" && data.advanced_config !== row.advanced_config) {
|
||||
deny();
|
||||
}
|
||||
if (Array.isArray(data.locations)) {
|
||||
internalHost.assertLocationAdvancedConfigUnchanged(data.locations, row.locations);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* For non-admins: every location's advanced_config must be one that already exists on this
|
||||
* host. Matching is by value, not path, so renaming or reordering a location that carries
|
||||
* admin-written config is allowed, but new or edited config is not. Each existing config can
|
||||
* be used once (no duplicating it onto other paths), and same-path matches are taken first.
|
||||
* A location that omits advanced_config keeps the one at its path. Mutates `locations`.
|
||||
*
|
||||
* @param {Array} locations incoming locations
|
||||
* @param {Array} [existingLocations] the host's current locations
|
||||
* @throws {PermissionError}
|
||||
*/
|
||||
assertLocationAdvancedConfigUnchanged: (locations, existingLocations) => {
|
||||
const existing = Array.isArray(existingLocations) ? existingLocations : [];
|
||||
const advAtPath = (path) => existing.find((l) => l.path === path)?.advanced_config || "";
|
||||
const pool = existing.map((l) => l.advanced_config || "").filter(Boolean);
|
||||
const take = (value) => {
|
||||
const i = pool.indexOf(value);
|
||||
if (i === -1) {
|
||||
return false;
|
||||
}
|
||||
pool.splice(i, 1);
|
||||
return true;
|
||||
};
|
||||
const deny = () => {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
};
|
||||
|
||||
// Pass 1: locations that keep (or omit) the config at their own path
|
||||
const unresolved = [];
|
||||
for (const loc of locations) {
|
||||
const atPath = advAtPath(loc.path);
|
||||
if (typeof loc.advanced_config === "undefined") {
|
||||
loc.advanced_config = atPath && take(atPath) ? atPath : "";
|
||||
} else if ((loc.advanced_config || "") === atPath && (!atPath || take(atPath))) {
|
||||
// unchanged
|
||||
} else {
|
||||
unresolved.push(loc);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: renamed/moved locations may carry an existing config not already claimed.
|
||||
// Clearing a config is not allowed, as before.
|
||||
for (const loc of unresolved) {
|
||||
if (!loc.advanced_config || !take(loc.advanced_config)) {
|
||||
deny();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes sure that the ssl_* and hsts_* fields play nicely together.
|
||||
* ie: if there is no cert, then force_ssl is off.
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from "lodash";
|
||||
import errs from "../lib/error.js";
|
||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
||||
import utils from "../lib/utils.js";
|
||||
import proxyHostModel from "../models/proxy_host.js";
|
||||
import proxyHostModel, { scopeToOwnerOrAllowed } from "../models/proxy_host.js";
|
||||
import userPermissionModel from "../models/user_permission.js";
|
||||
import internalAuditLog from "./audit-log.js";
|
||||
import internalCertificate from "./certificate.js";
|
||||
@@ -14,55 +14,6 @@ const omissions = () => {
|
||||
};
|
||||
|
||||
const internalProxyHost = {
|
||||
/**
|
||||
* For non-admins: every location's advanced_config must be one that already exists on this
|
||||
* host. Matching is by value, not path, so renaming or reordering a location that carries
|
||||
* admin-written config is allowed, but new or edited config is not. Each existing config can
|
||||
* be used once (no duplicating it onto other paths), and same-path matches are taken first.
|
||||
* A location that omits advanced_config keeps the one at its path. Mutates `locations`.
|
||||
*
|
||||
* @param {Array} locations incoming locations
|
||||
* @param {Array} [existingLocations] the host's current locations
|
||||
* @throws {PermissionError}
|
||||
*/
|
||||
assertLocationAdvancedConfigUnchanged: (locations, existingLocations) => {
|
||||
const existing = Array.isArray(existingLocations) ? existingLocations : [];
|
||||
const advAtPath = (path) => existing.find((l) => l.path === path)?.advanced_config || "";
|
||||
const pool = existing.map((l) => l.advanced_config || "").filter(Boolean);
|
||||
const take = (value) => {
|
||||
const i = pool.indexOf(value);
|
||||
if (i === -1) {
|
||||
return false;
|
||||
}
|
||||
pool.splice(i, 1);
|
||||
return true;
|
||||
};
|
||||
const deny = () => {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
};
|
||||
|
||||
// Pass 1: locations that keep (or omit) the config at their own path
|
||||
const unresolved = [];
|
||||
for (const loc of locations) {
|
||||
const atPath = advAtPath(loc.path);
|
||||
if (typeof loc.advanced_config === "undefined") {
|
||||
loc.advanced_config = atPath && take(atPath) ? atPath : "";
|
||||
} else if ((loc.advanced_config || "") === atPath && (!atPath || take(atPath))) {
|
||||
// unchanged
|
||||
} else {
|
||||
unresolved.push(loc);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: renamed/moved locations may carry an existing config not already claimed.
|
||||
// Clearing a config is not allowed, as before.
|
||||
for (const loc of unresolved) {
|
||||
if (!loc.advanced_config || !take(loc.advanced_config)) {
|
||||
deny();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} data
|
||||
@@ -107,17 +58,7 @@ const internalProxyHost = {
|
||||
thisData.advanced_config = "";
|
||||
}
|
||||
|
||||
const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
|
||||
if (!isAdmin && thisData.advanced_config && thisData.advanced_config.trim() !== "") {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
}
|
||||
if (
|
||||
!isAdmin &&
|
||||
Array.isArray(thisData.locations) &&
|
||||
thisData.locations.some((loc) => loc?.advanced_config && loc.advanced_config.trim() !== "")
|
||||
) {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
}
|
||||
internalHost.assertAdvancedConfigAllowed(access, thisData);
|
||||
|
||||
return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
|
||||
})
|
||||
@@ -245,17 +186,7 @@ const internalProxyHost = {
|
||||
|
||||
thisData = internalHost.cleanSslHstsData(thisData, row);
|
||||
|
||||
const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
|
||||
if (
|
||||
!isAdmin &&
|
||||
typeof thisData.advanced_config !== "undefined" &&
|
||||
thisData.advanced_config !== row.advanced_config
|
||||
) {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
}
|
||||
if (!isAdmin && Array.isArray(thisData.locations)) {
|
||||
internalProxyHost.assertLocationAdvancedConfigUnchanged(thisData.locations, row.locations);
|
||||
}
|
||||
internalHost.assertAdvancedConfigAllowed(access, thisData, row);
|
||||
|
||||
return proxyHostModel
|
||||
.query()
|
||||
@@ -317,14 +248,7 @@ const internalProxyHost = {
|
||||
.first();
|
||||
|
||||
if (access_data.permission_visibility !== "all") {
|
||||
const permissions = access.getPermissions ? access.getPermissions() : {};
|
||||
const allowedIds = permissions.meta?.proxy_host_ids || permissions.meta?.proxyHostIds || [];
|
||||
query.andWhere((builder) => {
|
||||
builder.where("owner_user_id", access.token.getUserId(1));
|
||||
if (allowedIds.length > 0) {
|
||||
builder.orWhereIn("id", allowedIds);
|
||||
}
|
||||
});
|
||||
scopeToOwnerOrAllowed(query, access.token.getUserId(1), access.getPermissions?.()?.meta);
|
||||
}
|
||||
|
||||
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
|
||||
@@ -508,14 +432,7 @@ const internalProxyHost = {
|
||||
.orderBy(castJsonIfNeed("domain_names"), "ASC");
|
||||
|
||||
if (accessData.permission_visibility !== "all") {
|
||||
const permissions = access.getPermissions ? access.getPermissions() : {};
|
||||
const allowedIds = permissions.meta?.proxy_host_ids || permissions.meta?.proxyHostIds || [];
|
||||
query.andWhere((builder) => {
|
||||
builder.where("owner_user_id", access.token.getUserId(1));
|
||||
if (allowedIds.length > 0) {
|
||||
builder.orWhereIn("id", allowedIds);
|
||||
}
|
||||
});
|
||||
scopeToOwnerOrAllowed(query, access.token.getUserId(1), access.getPermissions?.()?.meta);
|
||||
}
|
||||
|
||||
// Query is used for searching
|
||||
@@ -548,13 +465,7 @@ const internalProxyHost = {
|
||||
|
||||
if (visibility !== "all") {
|
||||
const perm = await userPermissionModel.query().where("user_id", user_id).first();
|
||||
const allowedIds = perm?.meta?.proxy_host_ids || perm?.meta?.proxyHostIds || [];
|
||||
query.andWhere((builder) => {
|
||||
builder.where("owner_user_id", user_id);
|
||||
if (allowedIds.length > 0) {
|
||||
builder.orWhereIn("id", allowedIds);
|
||||
}
|
||||
});
|
||||
scopeToOwnerOrAllowed(query, user_id, perm?.meta);
|
||||
}
|
||||
|
||||
const row = await query.first();
|
||||
|
||||
@@ -57,10 +57,7 @@ const internalRedirectionHost = {
|
||||
thisData.advanced_config = "";
|
||||
}
|
||||
|
||||
const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
|
||||
if (!isAdmin && thisData.advanced_config && thisData.advanced_config.trim() !== "") {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
}
|
||||
internalHost.assertAdvancedConfigAllowed(access, thisData);
|
||||
|
||||
return redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
|
||||
})
|
||||
@@ -188,14 +185,7 @@ const internalRedirectionHost = {
|
||||
|
||||
thisData = internalHost.cleanSslHstsData(thisData, row);
|
||||
|
||||
const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
|
||||
if (
|
||||
!isAdmin &&
|
||||
typeof thisData.advanced_config !== "undefined" &&
|
||||
thisData.advanced_config !== row.advanced_config
|
||||
) {
|
||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
||||
}
|
||||
internalHost.assertAdvancedConfigAllowed(access, thisData, row);
|
||||
|
||||
return redirectionHostModel
|
||||
.query()
|
||||
|
||||
@@ -13,7 +13,7 @@ import Ajv from "ajv/dist/2020.js";
|
||||
import _ from "lodash";
|
||||
import { access as logger } from "../logger.js";
|
||||
import authModel from "../models/auth.js";
|
||||
import proxyHostModel from "../models/proxy_host.js";
|
||||
import proxyHostModel, { scopeToOwnerOrAllowed } from "../models/proxy_host.js";
|
||||
import TokenModel from "../models/token.js";
|
||||
import userModel from "../models/user.js";
|
||||
import permsSchema from "./access/permissions.json" with { type: "json" };
|
||||
@@ -139,13 +139,7 @@ export default function (tokenString) {
|
||||
const query = proxyHostModel.query().select("id").andWhere("is_deleted", 0);
|
||||
|
||||
if (permissions.visibility === "user") {
|
||||
const allowedIds = permissions.meta?.proxy_host_ids || permissions.meta?.proxyHostIds || [];
|
||||
query.andWhere((builder) => {
|
||||
builder.where("owner_user_id", tokenUserId);
|
||||
if (allowedIds.length > 0) {
|
||||
builder.orWhereIn("id", allowedIds);
|
||||
}
|
||||
});
|
||||
scopeToOwnerOrAllowed(query, tokenUserId, permissions.meta);
|
||||
}
|
||||
|
||||
const rows = await query;
|
||||
@@ -222,6 +216,7 @@ export default function (tokenString) {
|
||||
getPermissions: () => permissions,
|
||||
getRoles: () => userRoles,
|
||||
hasRole: (role) => userRoles.includes(role),
|
||||
isAdmin: () => userRoles.includes("admin") || Token.hasScope("admin"),
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -124,4 +124,23 @@ class ProxyHost extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restricts a proxy host query to hosts the user owns or has been granted by id
|
||||
* (user permission meta.proxy_host_ids).
|
||||
*
|
||||
* @param {Object} query objection query on proxy_host
|
||||
* @param {Number} userId
|
||||
* @param {Object} [permissionMeta] the user's permission meta
|
||||
* @returns {Object} the query
|
||||
*/
|
||||
export const scopeToOwnerOrAllowed = (query, userId, permissionMeta) => {
|
||||
const allowedIds = permissionMeta?.proxy_host_ids || [];
|
||||
return query.andWhere((builder) => {
|
||||
builder.where("owner_user_id", userId);
|
||||
if (allowedIds.length > 0) {
|
||||
builder.orWhereIn("id", allowedIds);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default ProxyHost;
|
||||
Reference in new issue
Block a user