Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

SamlAuthentication.tsx 8.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2022 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { keyBy } from 'lodash';
  21. import React from 'react';
  22. import { getValues, resetSettingValue, setSettingValue } from '../../../../api/settings';
  23. import { SubmitButton } from '../../../../components/controls/buttons';
  24. import DeferredSpinner from '../../../../components/ui/DeferredSpinner';
  25. import { translate } from '../../../../helpers/l10n';
  26. import { parseError } from '../../../../helpers/request';
  27. import { ExtendedSettingDefinition, SettingType, SettingValue } from '../../../../types/settings';
  28. import SamlFormField from './SamlFormField';
  29. import SamlToggleField from './SamlToggleField';
  30. interface SamlAuthenticationProps {
  31. definitions: ExtendedSettingDefinition[];
  32. }
  33. interface SamlAuthenticationState {
  34. settingValue: Pick<SettingValue, 'key' | 'value'>[];
  35. submitting: boolean;
  36. dirtyFields: string[];
  37. securedFieldsSubmitted: string[];
  38. error: { [key: string]: string };
  39. }
  40. const SAML_ENABLED_FIELD = 'sonar.auth.saml.enabled';
  41. const OPTIONAL_FIELDS = [
  42. 'sonar.auth.saml.sp.certificate.secured',
  43. 'sonar.auth.saml.sp.privateKey.secured',
  44. 'sonar.auth.saml.signature.enabled',
  45. 'sonar.auth.saml.user.email',
  46. 'sonar.auth.saml.group.name'
  47. ];
  48. class SamlAuthentication extends React.PureComponent<
  49. SamlAuthenticationProps,
  50. SamlAuthenticationState
  51. > {
  52. constructor(props: SamlAuthenticationProps) {
  53. super(props);
  54. const settingValue = props.definitions.map(def => {
  55. return {
  56. key: def.key
  57. };
  58. });
  59. this.state = {
  60. settingValue,
  61. submitting: false,
  62. dirtyFields: [],
  63. securedFieldsSubmitted: [],
  64. error: {}
  65. };
  66. }
  67. componentDidMount() {
  68. const { definitions } = this.props;
  69. const keys = definitions.map(definition => definition.key).join(',');
  70. this.loadSettingValues(keys);
  71. }
  72. onFieldChange = (id: string, value: string | boolean) => {
  73. const { settingValue, dirtyFields } = this.state;
  74. const updatedSettingValue = settingValue?.map(set => {
  75. if (set.key === id) {
  76. set.value = String(value);
  77. }
  78. return set;
  79. });
  80. if (!dirtyFields.includes(id)) {
  81. const updatedDirtyFields = [...dirtyFields, id];
  82. this.setState({
  83. dirtyFields: updatedDirtyFields
  84. });
  85. }
  86. this.setState({
  87. settingValue: updatedSettingValue
  88. });
  89. };
  90. async loadSettingValues(keys: string) {
  91. const { settingValue, securedFieldsSubmitted } = this.state;
  92. const values = await getValues({
  93. keys
  94. });
  95. const valuesByDefinitionKey = keyBy(values, 'key');
  96. const updatedSecuredFieldsSubmitted: string[] = [...securedFieldsSubmitted];
  97. const updateSettingValue = settingValue?.map(set => {
  98. if (valuesByDefinitionKey[set.key]) {
  99. set.value =
  100. valuesByDefinitionKey[set.key].value ?? valuesByDefinitionKey[set.key].parentValue;
  101. }
  102. if (
  103. this.isSecuredField(set.key) &&
  104. valuesByDefinitionKey[set.key] &&
  105. !securedFieldsSubmitted.includes(set.key)
  106. ) {
  107. updatedSecuredFieldsSubmitted.push(set.key);
  108. }
  109. return set;
  110. });
  111. this.setState({
  112. settingValue: updateSettingValue,
  113. securedFieldsSubmitted: updatedSecuredFieldsSubmitted
  114. });
  115. }
  116. isSecuredField = (key: string) => {
  117. const { definitions } = this.props;
  118. const fieldDefinition = definitions.find(def => def.key === key);
  119. if (fieldDefinition && fieldDefinition.type === SettingType.PASSWORD) {
  120. return true;
  121. }
  122. return false;
  123. };
  124. onSaveConfig = async () => {
  125. const { settingValue, dirtyFields } = this.state;
  126. const { definitions } = this.props;
  127. if (dirtyFields.length === 0) {
  128. return;
  129. }
  130. this.setState({ submitting: true, error: {} });
  131. const promises: Promise<void>[] = [];
  132. settingValue?.forEach(set => {
  133. const definition = definitions.find(def => def.key === set.key);
  134. if (definition && set.value !== undefined && dirtyFields.includes(set.key)) {
  135. const apiCall =
  136. set.value.length > 0
  137. ? setSettingValue(definition, set.value)
  138. : resetSettingValue({ keys: definition.key });
  139. const promise = apiCall.catch(async e => {
  140. const { error } = this.state;
  141. const validationMessage = await parseError(e as Response);
  142. this.setState({
  143. submitting: false,
  144. dirtyFields: [],
  145. error: { ...error, ...{ [set.key]: validationMessage } }
  146. });
  147. });
  148. promises.push(promise);
  149. }
  150. });
  151. await Promise.all(promises);
  152. await this.loadSettingValues(dirtyFields.join(','));
  153. this.setState({ submitting: false, dirtyFields: [] });
  154. };
  155. allowEnabling = () => {
  156. const { settingValue, securedFieldsSubmitted } = this.state;
  157. const enabledFlagSettingValue = settingValue.find(set => set.key === SAML_ENABLED_FIELD);
  158. if (enabledFlagSettingValue && enabledFlagSettingValue.value === 'true') {
  159. return true;
  160. }
  161. for (const setting of settingValue) {
  162. const isMandatory = !OPTIONAL_FIELDS.includes(setting.key);
  163. const isSecured = this.isSecuredField(setting.key);
  164. const isSecuredAndNotSubmitted = isSecured && !securedFieldsSubmitted.includes(setting.key);
  165. const isNotSecuredAndNotSubmitted =
  166. !isSecured && (setting.value === '' || setting.value === undefined);
  167. if (isMandatory && (isSecuredAndNotSubmitted || isNotSecuredAndNotSubmitted)) {
  168. return false;
  169. }
  170. }
  171. return true;
  172. };
  173. onEnableFlagChange = (value: boolean) => {
  174. const { settingValue, dirtyFields } = this.state;
  175. const updatedSettingValue = settingValue?.map(set => {
  176. if (set.key === SAML_ENABLED_FIELD) {
  177. set.value = String(value);
  178. }
  179. return set;
  180. });
  181. this.setState(
  182. {
  183. settingValue: updatedSettingValue,
  184. dirtyFields: [...dirtyFields, SAML_ENABLED_FIELD]
  185. },
  186. () => {
  187. this.onSaveConfig();
  188. }
  189. );
  190. };
  191. render() {
  192. const { definitions } = this.props;
  193. const { submitting, settingValue, securedFieldsSubmitted, error, dirtyFields } = this.state;
  194. const enabledFlagDefinition = definitions.find(def => def.key === SAML_ENABLED_FIELD);
  195. return (
  196. <div>
  197. {definitions.map(def => {
  198. if (def.key === SAML_ENABLED_FIELD) {
  199. return null;
  200. }
  201. return (
  202. <SamlFormField
  203. settingValue={settingValue?.find(set => set.key === def.key)}
  204. definition={def}
  205. mandatory={!OPTIONAL_FIELDS.includes(def.key)}
  206. onFieldChange={this.onFieldChange}
  207. showSecuredTextArea={
  208. !securedFieldsSubmitted.includes(def.key) || dirtyFields.includes(def.key)
  209. }
  210. error={error}
  211. key={def.key}
  212. />
  213. );
  214. })}
  215. <div className="fixed-footer padded-left padded-right">
  216. {enabledFlagDefinition && (
  217. <div>
  218. <label className="h3 spacer-right">{enabledFlagDefinition.name}</label>
  219. <SamlToggleField
  220. definition={enabledFlagDefinition}
  221. settingValue={settingValue?.find(set => set.key === enabledFlagDefinition.key)}
  222. toggleDisabled={!this.allowEnabling()}
  223. onChange={this.onEnableFlagChange}
  224. />
  225. </div>
  226. )}
  227. <div>
  228. <SubmitButton onClick={this.onSaveConfig}>
  229. {translate('settings.authentication.saml.form.save')}
  230. <DeferredSpinner className="spacer-left" loading={submitting} />
  231. </SubmitButton>
  232. </div>
  233. </div>
  234. </div>
  235. );
  236. }
  237. }
  238. export default SamlAuthentication;