config-manager.js 8.1 KB

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