config-manager.js 8.1 KB

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