superuserAccessForm.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import {logout} from 'sentry/actionCreators/account';
  4. import {Client} from 'sentry/api';
  5. import Alert from 'sentry/components/alert';
  6. import Form from 'sentry/components/forms/form';
  7. import Hook from 'sentry/components/hook';
  8. import ThemeAndStyleProvider from 'sentry/components/themeAndStyleProvider';
  9. import U2fContainer from 'sentry/components/u2f/u2fContainer';
  10. import {ErrorCodes} from 'sentry/constants/superuserAccessErrors';
  11. import {t} from 'sentry/locale';
  12. import ConfigStore from 'sentry/stores/configStore';
  13. import space from 'sentry/styles/space';
  14. import {Authenticator} from 'sentry/types';
  15. import withApi from 'sentry/utils/withApi';
  16. import Button from './button';
  17. type OnTapProps = NonNullable<React.ComponentProps<typeof U2fContainer>['onTap']>;
  18. type Props = {
  19. api: Client;
  20. };
  21. type State = {
  22. authenticators: Array<Authenticator>;
  23. error: boolean;
  24. errorType: string;
  25. showAccessForms: boolean;
  26. superuserAccessCategory: string;
  27. superuserReason: string;
  28. };
  29. class SuperuserAccessForm extends Component<Props, State> {
  30. state: State = {
  31. authenticators: [],
  32. error: false,
  33. errorType: '',
  34. showAccessForms: true,
  35. superuserAccessCategory: '',
  36. superuserReason: '',
  37. };
  38. componentDidMount() {
  39. this.getAuthenticators();
  40. }
  41. handleSubmitCOPS = () => {
  42. this.setState({
  43. superuserAccessCategory: 'cops_csm',
  44. superuserReason: 'COPS and CSM use',
  45. });
  46. };
  47. handleSubmit = data => {
  48. const {superuserAccessCategory, superuserReason, authenticators} = this.state;
  49. const disableU2FForSUForm = ConfigStore.get('disableU2FForSUForm');
  50. const suAccessCategory = superuserAccessCategory || data.superuserAccessCategory;
  51. const suReason = superuserReason || data.superuserReason;
  52. if (!authenticators.length && !disableU2FForSUForm) {
  53. this.handleError(ErrorCodes.noAuthenticator);
  54. return;
  55. }
  56. if (this.state.showAccessForms && !disableU2FForSUForm) {
  57. this.setState({
  58. showAccessForms: false,
  59. superuserAccessCategory: suAccessCategory,
  60. superuserReason: suReason,
  61. });
  62. } else {
  63. this.handleSuccess();
  64. }
  65. };
  66. handleU2fTap = async (data: Parameters<OnTapProps>[0]) => {
  67. const {api} = this.props;
  68. try {
  69. data.isSuperuserModal = true;
  70. data.superuserAccessCategory = this.state.superuserAccessCategory;
  71. data.superuserReason = this.state.superuserReason;
  72. await api.requestPromise('/auth/', {method: 'PUT', data});
  73. this.handleSuccess();
  74. } catch (err) {
  75. this.setState({showAccessForms: true});
  76. // u2fInterface relies on this
  77. throw err;
  78. }
  79. };
  80. handleSuccess = () => {
  81. window.location.reload();
  82. };
  83. handleError = err => {
  84. let errorType = '';
  85. if (err.status === 403) {
  86. if (err.responseJSON.detail.code === 'no_u2f') {
  87. errorType = ErrorCodes.noAuthenticator;
  88. } else {
  89. errorType = ErrorCodes.invalidPassword;
  90. }
  91. } else if (err.status === 401) {
  92. errorType = ErrorCodes.invalidSSOSession;
  93. } else if (err.status === 400) {
  94. errorType = ErrorCodes.invalidAccessCategory;
  95. } else if (err === ErrorCodes.noAuthenticator) {
  96. errorType = ErrorCodes.noAuthenticator;
  97. } else {
  98. errorType = ErrorCodes.unknownError;
  99. }
  100. this.setState({
  101. error: true,
  102. errorType,
  103. showAccessForms: true,
  104. });
  105. };
  106. handleLogout = async () => {
  107. const {api} = this.props;
  108. try {
  109. await logout(api);
  110. } catch {
  111. // ignore errors
  112. }
  113. window.location.assign('/auth/login/');
  114. };
  115. async getAuthenticators() {
  116. const {api} = this.props;
  117. try {
  118. const authenticators = await api.requestPromise('/authenticators/');
  119. this.setState({authenticators: authenticators ?? []});
  120. } catch {
  121. // ignore errors
  122. }
  123. }
  124. render() {
  125. const {authenticators, error, errorType, showAccessForms} = this.state;
  126. if (errorType === ErrorCodes.invalidSSOSession) {
  127. this.handleLogout();
  128. return null;
  129. }
  130. return (
  131. <ThemeAndStyleProvider>
  132. <Form
  133. submitLabel={t('Continue')}
  134. onSubmit={this.handleSubmit}
  135. initialData={{isSuperuserModal: true}}
  136. extraButton={
  137. <BackWrapper>
  138. <Button onClick={this.handleSubmitCOPS}>{t('COPS/CSM')}</Button>
  139. </BackWrapper>
  140. }
  141. resetOnError
  142. >
  143. {error && (
  144. <StyledAlert type="error" showIcon>
  145. {t(errorType)}
  146. </StyledAlert>
  147. )}
  148. {showAccessForms && <Hook name="component:superuser-access-category" />}
  149. {!showAccessForms && (
  150. <U2fContainer
  151. authenticators={authenticators}
  152. displayMode="sudo"
  153. onTap={this.handleU2fTap}
  154. />
  155. )}
  156. </Form>
  157. </ThemeAndStyleProvider>
  158. );
  159. }
  160. }
  161. const StyledAlert = styled(Alert)`
  162. margin-bottom: 0;
  163. `;
  164. const BackWrapper = styled('div')`
  165. width: 100%;
  166. margin-left: ${space(4)};
  167. `;
  168. export default withApi(SuperuserAccessForm);