config-manager.js 8.0 KB

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