config-manager.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. const debug = require('debug')('growi:service:ConfigManager');
  2. const ConfigLoader = require('../service/config-loader');
  3. const KEYS_FOR_SAML_USE_ONLY_ENV_OPTION = [
  4. 'security:passport-saml:isEnabled',
  5. 'security:passport-saml:entryPoint',
  6. 'security:passport-saml:issuer',
  7. 'security:passport-saml:attrMapId',
  8. 'security:passport-saml:attrMapUsername',
  9. 'security:passport-saml:attrMapMail',
  10. 'security:passport-saml:attrMapFirstName',
  11. 'security:passport-saml:attrMapLastName',
  12. 'security:passport-saml:cert',
  13. ];
  14. class ConfigManager {
  15. constructor(configModel) {
  16. this.configModel = configModel;
  17. this.configLoader = new ConfigLoader(this.configModel);
  18. this.configObject = null;
  19. this.configKeys = [];
  20. this.getConfig = this.getConfig.bind(this);
  21. }
  22. /**
  23. * load configs from the database and the environment variables
  24. */
  25. async loadConfigs() {
  26. this.configObject = await this.configLoader.load();
  27. debug('ConfigManager#loadConfigs', this.configObject);
  28. // cache all config keys
  29. this.configKeys = this.getAllConfigKeys();
  30. }
  31. /**
  32. * get a config specified by namespace & key
  33. *
  34. * Basically, this searches a specified config from configs loaded from the database at first
  35. * and then from configs loaded from the environment variables.
  36. *
  37. * In some case, this search method changes.
  38. *
  39. * the followings are the meanings of each special return value.
  40. * - null: a specified config is not set.
  41. * - undefined: a specified config does not exist.
  42. */
  43. getConfig(namespace, key) {
  44. if (this.searchOnlyFromEnvVarConfigs('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions')) {
  45. return this.searchInSAMLUseOnlyEnvMode(namespace, key);
  46. }
  47. return this.defaultSearch(namespace, key);
  48. }
  49. /**
  50. * get a config specified by namespace and regular expresssion
  51. */
  52. getConfigByRegExp(namespace, regexp) {
  53. const result = {};
  54. for (const key of this.configKeys) {
  55. if (regexp.test(key)) {
  56. result[key] = this.getConfig(namespace, key);
  57. }
  58. }
  59. return result;
  60. }
  61. /**
  62. * get a config specified by namespace and prefix
  63. */
  64. getConfigByPrefix(namespace, prefix) {
  65. const regexp = new RegExp(`^${prefix}`);
  66. return this.getConfigByRegExp(namespace, regexp);
  67. }
  68. /**
  69. * generate an array of config keys from this.configObject
  70. */
  71. getAllConfigKeys() {
  72. // type: fromDB, fromEnvVars
  73. const types = Object.keys(this.configObject);
  74. let namespaces = [];
  75. let keys = [];
  76. for (const type of types) {
  77. if (this.configObject[type] != null) {
  78. // ns: crowi, markdown, notification
  79. namespaces = [...namespaces, ...Object.keys(this.configObject[type])];
  80. }
  81. }
  82. // remove duplicates
  83. namespaces = [...new Set(namespaces)];
  84. for (const type of types) {
  85. for (const ns of namespaces) {
  86. if (this.configObject[type][ns] != null) {
  87. keys = [...keys, ...Object.keys(this.configObject[type][ns])];
  88. }
  89. }
  90. }
  91. // remove duplicates
  92. keys = [...new Set(keys)];
  93. return keys;
  94. }
  95. /**
  96. * get a config specified by namespace & key from configs loaded from the database
  97. *
  98. * **Do not use this unless absolutely necessary. Use getConfig instead.**
  99. */
  100. getConfigFromDB(namespace, key) {
  101. return this.searchOnlyFromDBConfigs(namespace, key);
  102. }
  103. /**
  104. * get a config specified by namespace & key from configs loaded from the environment variables
  105. *
  106. * **Do not use this unless absolutely necessary. Use getConfig instead.**
  107. */
  108. getConfigFromEnvVars(namespace, key) {
  109. return this.searchOnlyFromEnvVarConfigs(namespace, key);
  110. }
  111. // CONF.RF refactor file-uploader
  112. // create parent class and each uploader inherits from it.
  113. getIsUploadable() {
  114. const method = process.env.FILE_UPLOAD || 'aws';
  115. if (method === 'aws' && (
  116. !this.getConfig('crowi', 'aws:accessKeyId')
  117. || !this.getConfig('crowi', 'aws:secretAccessKey')
  118. || !this.getConfig('crowi', 'aws:region')
  119. || !this.getConfig('crowi', 'aws:bucket'))) {
  120. return false;
  121. }
  122. return method !== 'none';
  123. }
  124. /**
  125. * update configs in the same namespace
  126. *
  127. * Specified values are encoded by convertInsertValue.
  128. * In it, an empty string is converted to null that indicates a config is not set.
  129. *
  130. * For example:
  131. * ```
  132. * updateConfigsInTheSameNamespace(
  133. * 'some namespace',
  134. * {
  135. * 'some key 1': 'value 1',
  136. * 'some key 2': 'value 2',
  137. * ...
  138. * }
  139. * );
  140. * ```
  141. */
  142. async updateConfigsInTheSameNamespace(namespace, configs) {
  143. const queries = [];
  144. for (const key of Object.keys(configs)) {
  145. queries.push({
  146. updateOne: {
  147. filter: { ns: namespace, key },
  148. update: { ns: namespace, key, value: this.convertInsertValue(configs[key]) },
  149. upsert: true,
  150. },
  151. });
  152. }
  153. await this.configModel.bulkWrite(queries);
  154. await this.loadConfigs();
  155. }
  156. /**
  157. * Execute only once for installing application
  158. */
  159. async initDB(globalLang) {
  160. const initialConfig = this.configModel.getConfigsObjectForInstalling();
  161. initialConfig['app:globalLang'] = globalLang;
  162. await this.updateConfigsInTheSameNamespace('crowi', initialConfig);
  163. }
  164. async isDBInitialized() {
  165. const appInstalled = await this.getConfigFromDB('crowi', 'app:installed');
  166. return appInstalled;
  167. }
  168. /*
  169. * All of the methods below are private APIs.
  170. */
  171. /**
  172. * search a specified config from configs loaded from the database at first
  173. * and then from configs loaded from the environment variables
  174. */
  175. defaultSearch(namespace, key) {
  176. // does not exist neither in db nor in env vars
  177. if (!this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key)) {
  178. debug(`${namespace}.${key} does not exist neither in db nor in env vars`);
  179. return undefined;
  180. }
  181. // only exists in db
  182. if (this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key)) {
  183. debug(`${namespace}.${key} only exists in db`);
  184. return this.configObject.fromDB[namespace][key];
  185. }
  186. // only exists env vars
  187. if (!this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key)) {
  188. debug(`${namespace}.${key} only exists in db`);
  189. return this.configObject.fromEnvVars[namespace][key];
  190. }
  191. // exists both in db and in env vars [db > env var]
  192. if (this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key)) {
  193. if (this.configObject.fromDB[namespace][key] !== null) {
  194. debug(`${namespace}.${key} exists both in db and in env vars. loaded from db`);
  195. return this.configObject.fromDB[namespace][key];
  196. }
  197. /* eslint-disable-next-line no-else-return */
  198. else {
  199. debug(`${namespace}.${key} exists both in db and in env vars. loaded from env vars`);
  200. return this.configObject.fromEnvVars[namespace][key];
  201. }
  202. }
  203. }
  204. /**
  205. * For the configs specified by KEYS_FOR_SAML_USE_ONLY_ENV_OPTION,
  206. * this searches only from configs loaded from the environment variables.
  207. * For the other configs, this searches as the same way to defaultSearch.
  208. */
  209. /* eslint-disable no-else-return */
  210. searchInSAMLUseOnlyEnvMode(namespace, key) {
  211. if (namespace === 'crowi' && KEYS_FOR_SAML_USE_ONLY_ENV_OPTION.includes(key)) {
  212. return this.searchOnlyFromEnvVarConfigs(namespace, key);
  213. }
  214. else {
  215. return this.defaultSearch(namespace, key);
  216. }
  217. }
  218. /* eslint-enable no-else-return */
  219. /**
  220. * search a specified config from configs loaded from the database
  221. */
  222. searchOnlyFromDBConfigs(namespace, key) {
  223. if (!this.configExistsInDB(namespace, key)) {
  224. return undefined;
  225. }
  226. return this.configObject.fromDB[namespace][key];
  227. }
  228. /**
  229. * search a specified config from configs loaded from the environment variables
  230. */
  231. searchOnlyFromEnvVarConfigs(namespace, key) {
  232. if (!this.configExistsInEnvVars(namespace, key)) {
  233. return undefined;
  234. }
  235. return this.configObject.fromEnvVars[namespace][key];
  236. }
  237. /**
  238. * check whether a specified config exists in configs loaded from the database
  239. */
  240. configExistsInDB(namespace, key) {
  241. if (this.configObject.fromDB[namespace] === undefined) {
  242. return false;
  243. }
  244. return this.configObject.fromDB[namespace][key] !== undefined;
  245. }
  246. /**
  247. * check whether a specified config exists in configs loaded from the environment variables
  248. */
  249. configExistsInEnvVars(namespace, key) {
  250. if (this.configObject.fromEnvVars[namespace] === undefined) {
  251. return false;
  252. }
  253. return this.configObject.fromEnvVars[namespace][key] !== undefined;
  254. }
  255. convertInsertValue(value) {
  256. return JSON.stringify(value === '' ? null : value);
  257. }
  258. }
  259. module.exports = ConfigManager;