access.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /**
  2. * Some Notes: This is a friggin complicated piece of code.
  3. *
  4. * "scope" in this file means "where did this token come from and what is using it", so 99% of the time
  5. * the "scope" is going to be "user" because it would be a user token. This is not to be confused with
  6. * the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else.
  7. */
  8. import fs from "node:fs";
  9. import { dirname } from "node:path";
  10. import { fileURLToPath } from "node:url";
  11. import Ajv from "ajv/dist/2020.js";
  12. import _ from "lodash";
  13. import { access as logger } from "../logger.js";
  14. import proxyHostModel from "../models/proxy_host.js";
  15. import TokenModel from "../models/token.js";
  16. import userModel from "../models/user.js";
  17. import permsSchema from "./access/permissions.json" with { type: "json" };
  18. import roleSchema from "./access/roles.json" with { type: "json" };
  19. import errs from "./error.js";
  20. const __filename = fileURLToPath(import.meta.url);
  21. const __dirname = dirname(__filename);
  22. export default function (token_string) {
  23. const Token = TokenModel();
  24. let token_data = null;
  25. let initialised = false;
  26. const object_cache = {};
  27. let allow_internal_access = false;
  28. let user_roles = [];
  29. let permissions = {};
  30. /**
  31. * Loads the Token object from the token string
  32. *
  33. * @returns {Promise}
  34. */
  35. this.init = () => {
  36. return new Promise((resolve, reject) => {
  37. if (initialised) {
  38. resolve();
  39. } else if (!token_string) {
  40. reject(new errs.PermissionError("Permission Denied"));
  41. } else {
  42. resolve(
  43. Token.load(token_string).then((data) => {
  44. token_data = data;
  45. // At this point we need to load the user from the DB and make sure they:
  46. // - exist (and not soft deleted)
  47. // - still have the appropriate scopes for this token
  48. // This is only required when the User ID is supplied or if the token scope has `user`
  49. if (
  50. token_data.attrs.id ||
  51. (typeof token_data.scope !== "undefined" &&
  52. _.indexOf(token_data.scope, "user") !== -1)
  53. ) {
  54. // Has token user id or token user scope
  55. return userModel
  56. .query()
  57. .where("id", token_data.attrs.id)
  58. .andWhere("is_deleted", 0)
  59. .andWhere("is_disabled", 0)
  60. .allowGraph("[permissions]")
  61. .withGraphFetched("[permissions]")
  62. .first()
  63. .then((user) => {
  64. if (user) {
  65. // make sure user has all scopes of the token
  66. // The `user` role is not added against the user row, so we have to just add it here to get past this check.
  67. user.roles.push("user");
  68. let is_ok = true;
  69. _.forEach(token_data.scope, (scope_item) => {
  70. if (_.indexOf(user.roles, scope_item) === -1) {
  71. is_ok = false;
  72. }
  73. });
  74. if (!is_ok) {
  75. throw new errs.AuthError("Invalid token scope for User");
  76. }
  77. initialised = true;
  78. user_roles = user.roles;
  79. permissions = user.permissions;
  80. } else {
  81. throw new errs.AuthError("User cannot be loaded for Token");
  82. }
  83. });
  84. }
  85. initialised = true;
  86. }),
  87. );
  88. }
  89. });
  90. };
  91. /**
  92. * Fetches the object ids from the database, only once per object type, for this token.
  93. * This only applies to USER token scopes, as all other tokens are not really bound
  94. * by object scopes
  95. *
  96. * @param {String} object_type
  97. * @returns {Promise}
  98. */
  99. this.loadObjects = (object_type) => {
  100. return new Promise((resolve, reject) => {
  101. if (Token.hasScope("user")) {
  102. if (
  103. typeof token_data.attrs.id === "undefined" ||
  104. !token_data.attrs.id
  105. ) {
  106. reject(new errs.AuthError("User Token supplied without a User ID"));
  107. } else {
  108. const token_user_id = token_data.attrs.id ? token_data.attrs.id : 0;
  109. let query;
  110. if (typeof object_cache[object_type] === "undefined") {
  111. switch (object_type) {
  112. // USERS - should only return yourself
  113. case "users":
  114. resolve(token_user_id ? [token_user_id] : []);
  115. break;
  116. // Proxy Hosts
  117. case "proxy_hosts":
  118. query = proxyHostModel
  119. .query()
  120. .select("id")
  121. .andWhere("is_deleted", 0);
  122. if (permissions.visibility === "user") {
  123. query.andWhere("owner_user_id", token_user_id);
  124. }
  125. resolve(
  126. query.then((rows) => {
  127. const result = [];
  128. _.forEach(rows, (rule_row) => {
  129. result.push(rule_row.id);
  130. });
  131. // enum should not have less than 1 item
  132. if (!result.length) {
  133. result.push(0);
  134. }
  135. return result;
  136. }),
  137. );
  138. break;
  139. // DEFAULT: null
  140. default:
  141. resolve(null);
  142. break;
  143. }
  144. } else {
  145. resolve(object_cache[object_type]);
  146. }
  147. }
  148. } else {
  149. resolve(null);
  150. }
  151. }).then((objects) => {
  152. object_cache[object_type] = objects;
  153. return objects;
  154. });
  155. };
  156. /**
  157. * Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema
  158. *
  159. * @param {String} permission_label
  160. * @returns {Object}
  161. */
  162. this.getObjectSchema = (permission_label) => {
  163. const base_object_type = permission_label.split(":").shift();
  164. const schema = {
  165. $id: "objects",
  166. description: "Actor Properties",
  167. type: "object",
  168. additionalProperties: false,
  169. properties: {
  170. user_id: {
  171. anyOf: [
  172. {
  173. type: "number",
  174. enum: [Token.get("attrs").id],
  175. },
  176. ],
  177. },
  178. scope: {
  179. type: "string",
  180. pattern: `^${Token.get("scope")}$`,
  181. },
  182. },
  183. };
  184. return this.loadObjects(base_object_type).then((object_result) => {
  185. if (typeof object_result === "object" && object_result !== null) {
  186. schema.properties[base_object_type] = {
  187. type: "number",
  188. enum: object_result,
  189. minimum: 1,
  190. };
  191. } else {
  192. schema.properties[base_object_type] = {
  193. type: "number",
  194. minimum: 1,
  195. };
  196. }
  197. return schema;
  198. });
  199. };
  200. return {
  201. token: Token,
  202. /**
  203. *
  204. * @param {Boolean} [allow_internal]
  205. * @returns {Promise}
  206. */
  207. load: (allow_internal) => {
  208. return new Promise((resolve /*, reject*/) => {
  209. if (token_string) {
  210. resolve(Token.load(token_string));
  211. } else {
  212. allow_internal_access = allow_internal;
  213. resolve(allow_internal_access || null);
  214. }
  215. });
  216. },
  217. reloadObjects: this.loadObjects,
  218. /**
  219. *
  220. * @param {String} permission
  221. * @param {*} [data]
  222. * @returns {Promise}
  223. */
  224. can: async (permission, data) => {
  225. if (allow_internal_access === true) {
  226. return true;
  227. }
  228. try {
  229. await this.init();
  230. const objectSchema = await this.getObjectSchema(permission);
  231. const dataSchema = {
  232. [permission]: {
  233. data: data,
  234. scope: Token.get("scope"),
  235. roles: user_roles,
  236. permission_visibility: permissions.visibility,
  237. permission_proxy_hosts: permissions.proxy_hosts,
  238. permission_redirection_hosts: permissions.redirection_hosts,
  239. permission_dead_hosts: permissions.dead_hosts,
  240. permission_streams: permissions.streams,
  241. permission_access_lists: permissions.access_lists,
  242. permission_certificates: permissions.certificates,
  243. },
  244. };
  245. const permissionSchema = {
  246. $async: true,
  247. $id: "permissions",
  248. type: "object",
  249. additionalProperties: false,
  250. properties: {},
  251. };
  252. const rawData = fs.readFileSync(
  253. `${__dirname}/access/${permission.replace(/:/gim, "-")}.json`,
  254. { encoding: "utf8" },
  255. );
  256. permissionSchema.properties[permission] = JSON.parse(rawData);
  257. const ajv = new Ajv({
  258. verbose: true,
  259. allErrors: true,
  260. breakOnError: true,
  261. coerceTypes: true,
  262. schemas: [roleSchema, permsSchema, objectSchema, permissionSchema],
  263. });
  264. const valid = ajv.validate("permissions", dataSchema);
  265. return valid && dataSchema[permission];
  266. } catch (err) {
  267. err.permission = permission;
  268. err.permission_data = data;
  269. logger.error(permission, data, err.message);
  270. throw errs.PermissionError("Permission Denied", err);
  271. }
  272. },
  273. };
  274. }