mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-14 19:02:35 +00:00
Introducing the Setup Wizard for creating the first user
- no longer setup a default - still able to do that with env vars however
This commit is contained in:
@@ -22,13 +22,13 @@ import errs from "./error.js";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export default function (token_string) {
|
||||
export default function (tokenString) {
|
||||
const Token = TokenModel();
|
||||
let token_data = null;
|
||||
let tokenData = null;
|
||||
let initialised = false;
|
||||
const object_cache = {};
|
||||
let allow_internal_access = false;
|
||||
let user_roles = [];
|
||||
const objectCache = {};
|
||||
let allowInternalAccess = false;
|
||||
let userRoles = [];
|
||||
let permissions = {};
|
||||
|
||||
/**
|
||||
@@ -36,65 +36,58 @@ export default function (token_string) {
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
this.init = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (initialised) {
|
||||
resolve();
|
||||
} else if (!token_string) {
|
||||
reject(new errs.PermissionError("Permission Denied"));
|
||||
this.init = async () => {
|
||||
if (initialised) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tokenString) {
|
||||
throw new errs.PermissionError("Permission Denied");
|
||||
}
|
||||
|
||||
tokenData = await Token.load(tokenString);
|
||||
|
||||
// At this point we need to load the user from the DB and make sure they:
|
||||
// - exist (and not soft deleted)
|
||||
// - still have the appropriate scopes for this token
|
||||
// This is only required when the User ID is supplied or if the token scope has `user`
|
||||
if (
|
||||
tokenData.attrs.id ||
|
||||
(typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, "user") !== -1)
|
||||
) {
|
||||
// Has token user id or token user scope
|
||||
const user = await userModel
|
||||
.query()
|
||||
.where("id", tokenData.attrs.id)
|
||||
.andWhere("is_deleted", 0)
|
||||
.andWhere("is_disabled", 0)
|
||||
.allowGraph("[permissions]")
|
||||
.withGraphFetched("[permissions]")
|
||||
.first();
|
||||
|
||||
if (user) {
|
||||
// make sure user has all scopes of the token
|
||||
// The `user` role is not added against the user row, so we have to just add it here to get past this check.
|
||||
user.roles.push("user");
|
||||
|
||||
let ok = true;
|
||||
_.forEach(tokenData.scope, (scope_item) => {
|
||||
if (_.indexOf(user.roles, scope_item) === -1) {
|
||||
ok = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!ok) {
|
||||
throw new errs.AuthError("Invalid token scope for User");
|
||||
}
|
||||
initialised = true;
|
||||
userRoles = user.roles;
|
||||
permissions = user.permissions;
|
||||
} else {
|
||||
resolve(
|
||||
Token.load(token_string).then((data) => {
|
||||
token_data = data;
|
||||
|
||||
// At this point we need to load the user from the DB and make sure they:
|
||||
// - exist (and not soft deleted)
|
||||
// - still have the appropriate scopes for this token
|
||||
// This is only required when the User ID is supplied or if the token scope has `user`
|
||||
|
||||
if (
|
||||
token_data.attrs.id ||
|
||||
(typeof token_data.scope !== "undefined" &&
|
||||
_.indexOf(token_data.scope, "user") !== -1)
|
||||
) {
|
||||
// Has token user id or token user scope
|
||||
return userModel
|
||||
.query()
|
||||
.where("id", token_data.attrs.id)
|
||||
.andWhere("is_deleted", 0)
|
||||
.andWhere("is_disabled", 0)
|
||||
.allowGraph("[permissions]")
|
||||
.withGraphFetched("[permissions]")
|
||||
.first()
|
||||
.then((user) => {
|
||||
if (user) {
|
||||
// make sure user has all scopes of the token
|
||||
// The `user` role is not added against the user row, so we have to just add it here to get past this check.
|
||||
user.roles.push("user");
|
||||
|
||||
let is_ok = true;
|
||||
_.forEach(token_data.scope, (scope_item) => {
|
||||
if (_.indexOf(user.roles, scope_item) === -1) {
|
||||
is_ok = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!is_ok) {
|
||||
throw new errs.AuthError("Invalid token scope for User");
|
||||
}
|
||||
initialised = true;
|
||||
user_roles = user.roles;
|
||||
permissions = user.permissions;
|
||||
} else {
|
||||
throw new errs.AuthError("User cannot be loaded for Token");
|
||||
}
|
||||
});
|
||||
}
|
||||
initialised = true;
|
||||
}),
|
||||
);
|
||||
throw new errs.AuthError("User cannot be loaded for Token");
|
||||
}
|
||||
});
|
||||
}
|
||||
initialised = true;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -102,82 +95,64 @@ export default function (token_string) {
|
||||
* This only applies to USER token scopes, as all other tokens are not really bound
|
||||
* by object scopes
|
||||
*
|
||||
* @param {String} object_type
|
||||
* @param {String} objectType
|
||||
* @returns {Promise}
|
||||
*/
|
||||
this.loadObjects = (object_type) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (Token.hasScope("user")) {
|
||||
if (
|
||||
typeof token_data.attrs.id === "undefined" ||
|
||||
!token_data.attrs.id
|
||||
) {
|
||||
reject(new errs.AuthError("User Token supplied without a User ID"));
|
||||
} else {
|
||||
const token_user_id = token_data.attrs.id ? token_data.attrs.id : 0;
|
||||
let query;
|
||||
this.loadObjects = async (objectType) => {
|
||||
let objects = null;
|
||||
|
||||
if (typeof object_cache[object_type] === "undefined") {
|
||||
switch (object_type) {
|
||||
// USERS - should only return yourself
|
||||
case "users":
|
||||
resolve(token_user_id ? [token_user_id] : []);
|
||||
break;
|
||||
if (Token.hasScope("user")) {
|
||||
if (typeof tokenData.attrs.id === "undefined" || !tokenData.attrs.id) {
|
||||
throw new errs.AuthError("User Token supplied without a User ID");
|
||||
}
|
||||
|
||||
// Proxy Hosts
|
||||
case "proxy_hosts":
|
||||
query = proxyHostModel
|
||||
.query()
|
||||
.select("id")
|
||||
.andWhere("is_deleted", 0);
|
||||
const tokenUserId = tokenData.attrs.id ? tokenData.attrs.id : 0;
|
||||
let query;
|
||||
|
||||
if (permissions.visibility === "user") {
|
||||
query.andWhere("owner_user_id", token_user_id);
|
||||
}
|
||||
if (typeof objectCache[objectType] !== "undefined") {
|
||||
objects = objectCache[objectType];
|
||||
} else {
|
||||
switch (objectType) {
|
||||
// USERS - should only return yourself
|
||||
case "users":
|
||||
objects = tokenUserId ? [tokenUserId] : [];
|
||||
break;
|
||||
|
||||
resolve(
|
||||
query.then((rows) => {
|
||||
const result = [];
|
||||
_.forEach(rows, (rule_row) => {
|
||||
result.push(rule_row.id);
|
||||
});
|
||||
|
||||
// enum should not have less than 1 item
|
||||
if (!result.length) {
|
||||
result.push(0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
break;
|
||||
|
||||
// DEFAULT: null
|
||||
default:
|
||||
resolve(null);
|
||||
break;
|
||||
// Proxy Hosts
|
||||
case "proxy_hosts": {
|
||||
query = proxyHostModel.query().select("id").andWhere("is_deleted", 0);
|
||||
if (permissions.visibility === "user") {
|
||||
query.andWhere("owner_user_id", tokenUserId);
|
||||
}
|
||||
} else {
|
||||
resolve(object_cache[object_type]);
|
||||
|
||||
const rows = await query();
|
||||
objects = [];
|
||||
_.forEach(rows, (ruleRow) => {
|
||||
result.push(ruleRow.id);
|
||||
});
|
||||
|
||||
// enum should not have less than 1 item
|
||||
if (!objects.length) {
|
||||
objects.push(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolve(null);
|
||||
objectCache[objectType] = objects;
|
||||
}
|
||||
}).then((objects) => {
|
||||
object_cache[object_type] = objects;
|
||||
return objects;
|
||||
});
|
||||
}
|
||||
|
||||
return objects;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema
|
||||
*
|
||||
* @param {String} permission_label
|
||||
* @param {String} permissionLabel
|
||||
* @returns {Object}
|
||||
*/
|
||||
this.getObjectSchema = (permission_label) => {
|
||||
const base_object_type = permission_label.split(":").shift();
|
||||
this.getObjectSchema = async (permissionLabel) => {
|
||||
const baseObjectType = permissionLabel.split(":").shift();
|
||||
|
||||
const schema = {
|
||||
$id: "objects",
|
||||
@@ -200,41 +175,39 @@ export default function (token_string) {
|
||||
},
|
||||
};
|
||||
|
||||
return this.loadObjects(base_object_type).then((object_result) => {
|
||||
if (typeof object_result === "object" && object_result !== null) {
|
||||
schema.properties[base_object_type] = {
|
||||
type: "number",
|
||||
enum: object_result,
|
||||
minimum: 1,
|
||||
};
|
||||
} else {
|
||||
schema.properties[base_object_type] = {
|
||||
type: "number",
|
||||
minimum: 1,
|
||||
};
|
||||
}
|
||||
const result = await this.loadObjects(baseObjectType);
|
||||
if (typeof result === "object" && result !== null) {
|
||||
schema.properties[baseObjectType] = {
|
||||
type: "number",
|
||||
enum: result,
|
||||
minimum: 1,
|
||||
};
|
||||
} else {
|
||||
schema.properties[baseObjectType] = {
|
||||
type: "number",
|
||||
minimum: 1,
|
||||
};
|
||||
}
|
||||
|
||||
return schema;
|
||||
});
|
||||
return schema;
|
||||
};
|
||||
|
||||
// here:
|
||||
|
||||
return {
|
||||
token: Token,
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Boolean} [allow_internal]
|
||||
* @param {Boolean} [allowInternal]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
load: (allow_internal) => {
|
||||
return new Promise((resolve /*, reject*/) => {
|
||||
if (token_string) {
|
||||
resolve(Token.load(token_string));
|
||||
} else {
|
||||
allow_internal_access = allow_internal;
|
||||
resolve(allow_internal_access || null);
|
||||
}
|
||||
});
|
||||
load: async (allowInternal) => {
|
||||
if (tokenString) {
|
||||
return await Token.load(tokenString);
|
||||
}
|
||||
allowInternalAccess = allowInternal;
|
||||
return allowInternal || null;
|
||||
},
|
||||
|
||||
reloadObjects: this.loadObjects,
|
||||
@@ -246,7 +219,7 @@ export default function (token_string) {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
can: async (permission, data) => {
|
||||
if (allow_internal_access === true) {
|
||||
if (allowInternalAccess === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -258,7 +231,7 @@ export default function (token_string) {
|
||||
[permission]: {
|
||||
data: data,
|
||||
scope: Token.get("scope"),
|
||||
roles: user_roles,
|
||||
roles: userRoles,
|
||||
permission_visibility: permissions.visibility,
|
||||
permission_proxy_hosts: permissions.proxy_hosts,
|
||||
permission_redirection_hosts: permissions.redirection_hosts,
|
||||
@@ -277,10 +250,9 @@ export default function (token_string) {
|
||||
properties: {},
|
||||
};
|
||||
|
||||
const rawData = fs.readFileSync(
|
||||
`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`,
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
const rawData = fs.readFileSync(`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
permissionSchema.properties[permission] = JSON.parse(rawData);
|
||||
|
||||
const ajv = new Ajv({
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import Access from "../access.js";
|
||||
|
||||
export default () => {
|
||||
return (_, res, next) => {
|
||||
res.locals.access = null;
|
||||
const access = new Access(res.locals.token || null);
|
||||
access
|
||||
.load()
|
||||
.then(() => {
|
||||
res.locals.access = access;
|
||||
next();
|
||||
})
|
||||
.catch(next);
|
||||
return async (_, res, next) => {
|
||||
try {
|
||||
res.locals.access = null;
|
||||
const access = new Access(res.locals.token || null);
|
||||
await access.load();
|
||||
res.locals.access = access;
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@@ -14,30 +14,27 @@ const ajv = new Ajv({
|
||||
* @param {Object} payload
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function apiValidator(schema, payload /*, description*/) {
|
||||
return new Promise(function Promise_apiValidator(resolve, reject) {
|
||||
if (schema === null) {
|
||||
reject(new errs.ValidationError("Schema is undefined"));
|
||||
return;
|
||||
}
|
||||
const apiValidator = async (schema, payload /*, description*/) => {
|
||||
if (!schema) {
|
||||
throw new errs.ValidationError("Schema is undefined");
|
||||
}
|
||||
|
||||
if (typeof payload === "undefined") {
|
||||
reject(new errs.ValidationError("Payload is undefined"));
|
||||
return;
|
||||
}
|
||||
// Can't use falsy check here as valid payload could be `0` or `false`
|
||||
if (typeof payload === "undefined") {
|
||||
throw new errs.ValidationError("Payload is undefined");
|
||||
}
|
||||
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(payload);
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(payload);
|
||||
|
||||
if (valid && !validate.errors) {
|
||||
resolve(payload);
|
||||
} else {
|
||||
const message = ajv.errorsText(validate.errors);
|
||||
const err = new errs.ValidationError(message);
|
||||
err.debug = [validate.errors, payload];
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (valid && !validate.errors) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
const message = ajv.errorsText(validate.errors);
|
||||
const err = new errs.ValidationError(message);
|
||||
err.debug = [validate.errors, payload];
|
||||
throw err;
|
||||
};
|
||||
|
||||
export default apiValidator;
|
||||
|
Reference in New Issue
Block a user