sudoModal.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import {Component, Fragment} from 'react';
  2. // eslint-disable-next-line no-restricted-imports
  3. import {withRouter, WithRouterProps} from 'react-router';
  4. import styled from '@emotion/styled';
  5. import {logout} from 'sentry/actionCreators/account';
  6. import {ModalRenderProps} from 'sentry/actionCreators/modal';
  7. import {Client} from 'sentry/api';
  8. import Alert from 'sentry/components/alert';
  9. import Button from 'sentry/components/button';
  10. import Form from 'sentry/components/forms/form';
  11. import InputField from 'sentry/components/forms/inputField';
  12. import Hook from 'sentry/components/hook';
  13. import U2fContainer from 'sentry/components/u2f/u2fContainer';
  14. import {ErrorCodes} from 'sentry/constants/superuserAccessErrors';
  15. import {t} from 'sentry/locale';
  16. import ConfigStore from 'sentry/stores/configStore';
  17. import space from 'sentry/styles/space';
  18. import {Authenticator} from 'sentry/types';
  19. import withApi from 'sentry/utils/withApi';
  20. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  21. type OnTapProps = NonNullable<React.ComponentProps<typeof U2fContainer>['onTap']>;
  22. type Props = WithRouterProps &
  23. Pick<ModalRenderProps, 'Body' | 'Header'> & {
  24. api: Client;
  25. closeModal: () => void;
  26. /**
  27. * User is a superuser without an active su session
  28. */
  29. isSuperuser?: boolean;
  30. needsReload?: boolean;
  31. /**
  32. * expects a function that returns a Promise
  33. */
  34. retryRequest?: () => Promise<any>;
  35. };
  36. type State = {
  37. authenticators: Array<Authenticator>;
  38. busy: boolean;
  39. error: boolean;
  40. errorType: string;
  41. superuserAccessCategory: string;
  42. superuserReason: string;
  43. };
  44. class SudoModal extends Component<Props, State> {
  45. state: State = {
  46. error: false,
  47. errorType: '',
  48. busy: false,
  49. superuserAccessCategory: '',
  50. superuserReason: '',
  51. authenticators: [],
  52. };
  53. componentDidMount() {
  54. this.getAuthenticators();
  55. }
  56. handleSubmit = async () => {
  57. const {api, isSuperuser} = this.props;
  58. const data = {
  59. isSuperuserModal: isSuperuser,
  60. superuserAccessCategory: 'cops_csm',
  61. superuserReason: 'COPS and CSM use',
  62. };
  63. try {
  64. await api.requestPromise('/auth/', {method: 'PUT', data});
  65. this.handleSuccess();
  66. } catch (err) {
  67. this.handleError(err);
  68. }
  69. };
  70. handleSuccess = () => {
  71. const {closeModal, isSuperuser, location, needsReload, router, retryRequest} =
  72. this.props;
  73. if (!retryRequest) {
  74. closeModal();
  75. return;
  76. }
  77. if (isSuperuser) {
  78. router.replace({pathname: location.pathname, state: {forceUpdate: new Date()}});
  79. if (needsReload) {
  80. window.location.reload();
  81. }
  82. return;
  83. }
  84. this.setState({busy: true}, () => {
  85. retryRequest().then(() => {
  86. this.setState({busy: false}, closeModal);
  87. });
  88. });
  89. };
  90. handleError = err => {
  91. let errorType = '';
  92. if (err.status === 403) {
  93. errorType = ErrorCodes.invalidPassword;
  94. } else if (err.status === 401) {
  95. errorType = ErrorCodes.invalidSSOSession;
  96. } else if (err.status === 400) {
  97. errorType = ErrorCodes.invalidAccessCategory;
  98. } else {
  99. errorType = ErrorCodes.unknownError;
  100. }
  101. this.setState({
  102. busy: false,
  103. error: true,
  104. errorType,
  105. });
  106. };
  107. handleU2fTap = async (data: Parameters<OnTapProps>[0]) => {
  108. this.setState({busy: true});
  109. const {api, isSuperuser} = this.props;
  110. try {
  111. data.isSuperuserModal = isSuperuser;
  112. data.superuserAccessCategory = this.state.superuserAccessCategory;
  113. data.superuserReason = this.state.superuserReason;
  114. await api.requestPromise('/auth/', {method: 'PUT', data});
  115. this.handleSuccess();
  116. } catch (err) {
  117. this.setState({busy: false});
  118. // u2fInterface relies on this
  119. throw err;
  120. }
  121. };
  122. handleLogout = async () => {
  123. const {api} = this.props;
  124. try {
  125. await logout(api);
  126. } catch {
  127. // ignore errors
  128. }
  129. window.location.assign(`/auth/login/?next=${encodeURIComponent(location.pathname)}`);
  130. };
  131. async getAuthenticators() {
  132. const {api} = this.props;
  133. try {
  134. const authenticators = await api.requestPromise('/authenticators/');
  135. this.setState({authenticators: authenticators ?? []});
  136. } catch {
  137. // ignore errors
  138. }
  139. }
  140. renderBodyContent() {
  141. const {isSuperuser} = this.props;
  142. const {authenticators, error, errorType} = this.state;
  143. const user = ConfigStore.get('user');
  144. const isSelfHosted = ConfigStore.get('isSelfHosted');
  145. const validateSUForm = ConfigStore.get('validateSUForm');
  146. if (errorType === ErrorCodes.invalidSSOSession) {
  147. this.handleLogout();
  148. return null;
  149. }
  150. if (
  151. (!user.hasPasswordAuth && authenticators.length === 0) ||
  152. (isSuperuser && !isSelfHosted && validateSUForm)
  153. ) {
  154. return (
  155. <Fragment>
  156. <StyledTextBlock>
  157. {isSuperuser
  158. ? t(
  159. 'You are attempting to access a resource that requires superuser access, please re-authenticate as a superuser.'
  160. )
  161. : t('You will need to reauthenticate to continue')}
  162. </StyledTextBlock>
  163. {error && (
  164. <StyledAlert type="error" showIcon>
  165. {t(errorType)}
  166. </StyledAlert>
  167. )}
  168. {isSuperuser ? (
  169. <Form
  170. apiMethod="PUT"
  171. apiEndpoint="/auth/"
  172. submitLabel={t('Re-authenticate')}
  173. onSubmitSuccess={this.handleSuccess}
  174. onSubmitError={this.handleError}
  175. initialData={{isSuperuserModal: isSuperuser}}
  176. extraButton={
  177. <BackWrapper>
  178. <Button onClick={this.handleSubmit}>{t('COPS/CSM')}</Button>
  179. </BackWrapper>
  180. }
  181. resetOnError
  182. >
  183. {!isSelfHosted && <Hook name="component:superuser-access-category" />}
  184. </Form>
  185. ) : (
  186. <Button
  187. priority="primary"
  188. href={`/auth/login/?next=${encodeURIComponent(location.pathname)}`}
  189. >
  190. {t('Continue')}
  191. </Button>
  192. )}
  193. </Fragment>
  194. );
  195. }
  196. return (
  197. <Fragment>
  198. <StyledTextBlock>
  199. {isSuperuser
  200. ? t(
  201. 'You are attempting to access a resource that requires superuser access, please re-authenticate as a superuser.'
  202. )
  203. : t('Help us keep your account safe by confirming your identity.')}
  204. </StyledTextBlock>
  205. {error && (
  206. <StyledAlert type="error" showIcon>
  207. {t(errorType)}
  208. </StyledAlert>
  209. )}
  210. <Form
  211. apiMethod="PUT"
  212. apiEndpoint="/auth/"
  213. submitLabel={t('Confirm Password')}
  214. onSubmitSuccess={this.handleSuccess}
  215. onSubmitError={this.handleError}
  216. hideFooter={!user.hasPasswordAuth && authenticators.length === 0}
  217. initialData={{isSuperuserModal: isSuperuser}}
  218. resetOnError
  219. >
  220. {user.hasPasswordAuth && (
  221. <StyledInputField
  222. type="password"
  223. inline={false}
  224. label={t('Password')}
  225. name="password"
  226. autoFocus
  227. flexibleControlStateSize
  228. />
  229. )}
  230. <U2fContainer
  231. authenticators={authenticators}
  232. displayMode="sudo"
  233. onTap={this.handleU2fTap}
  234. />
  235. </Form>
  236. </Fragment>
  237. );
  238. }
  239. render() {
  240. const {Header, Body} = this.props;
  241. return (
  242. <Fragment>
  243. <Header closeButton>{t('Confirm Password to Continue')}</Header>
  244. <Body>{this.renderBodyContent()}</Body>
  245. </Fragment>
  246. );
  247. }
  248. }
  249. export default withRouter(withApi(SudoModal));
  250. export {SudoModal};
  251. const StyledTextBlock = styled(TextBlock)`
  252. margin-bottom: ${space(1)};
  253. `;
  254. const StyledInputField = styled(InputField)`
  255. padding-left: 0;
  256. `;
  257. const StyledAlert = styled(Alert)`
  258. margin-bottom: 0;
  259. `;
  260. const BackWrapper = styled('div')`
  261. width: 100%;
  262. margin-left: ${space(4)};
  263. `;