superuserAccessForm.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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 = async data => {
  48. const {api} = this.props;
  49. const {superuserAccessCategory, superuserReason, authenticators} = this.state;
  50. const disableU2FForSUForm = ConfigStore.get('disableU2FForSUForm');
  51. const suAccessCategory = superuserAccessCategory || data.superuserAccessCategory;
  52. const suReason = superuserReason || data.superuserReason;
  53. if (!authenticators.length && !disableU2FForSUForm) {
  54. this.handleError(ErrorCodes.noAuthenticator);
  55. return;
  56. }
  57. if (this.state.showAccessForms && !disableU2FForSUForm) {
  58. this.setState({
  59. showAccessForms: false,
  60. superuserAccessCategory: suAccessCategory,
  61. superuserReason: suReason,
  62. });
  63. } else {
  64. await api.requestPromise('/auth/', {method: 'PUT', data});
  65. this.handleSuccess();
  66. }
  67. };
  68. handleU2fTap = async (data: Parameters<OnTapProps>[0]) => {
  69. const {api} = this.props;
  70. try {
  71. data.isSuperuserModal = true;
  72. data.superuserAccessCategory = this.state.superuserAccessCategory;
  73. data.superuserReason = this.state.superuserReason;
  74. await api.requestPromise('/auth/', {method: 'PUT', data});
  75. this.handleSuccess();
  76. } catch (err) {
  77. this.setState({showAccessForms: true});
  78. // u2fInterface relies on this
  79. throw err;
  80. }
  81. };
  82. handleSuccess = () => {
  83. window.location.reload();
  84. };
  85. handleError = err => {
  86. let errorType = '';
  87. if (err.status === 403) {
  88. if (err.responseJSON.detail.code === 'no_u2f') {
  89. errorType = ErrorCodes.noAuthenticator;
  90. } else {
  91. errorType = ErrorCodes.invalidPassword;
  92. }
  93. } else if (err.status === 401) {
  94. errorType = ErrorCodes.invalidSSOSession;
  95. } else if (err.status === 400) {
  96. errorType = ErrorCodes.invalidAccessCategory;
  97. } else if (err === ErrorCodes.noAuthenticator) {
  98. errorType = ErrorCodes.noAuthenticator;
  99. } else {
  100. errorType = ErrorCodes.unknownError;
  101. }
  102. this.setState({
  103. error: true,
  104. errorType,
  105. showAccessForms: true,
  106. });
  107. };
  108. handleLogout = async () => {
  109. const {api} = this.props;
  110. try {
  111. await logout(api);
  112. } catch {
  113. // ignore errors
  114. }
  115. window.location.assign('/auth/login/');
  116. };
  117. async getAuthenticators() {
  118. const {api} = this.props;
  119. try {
  120. const authenticators = await api.requestPromise('/authenticators/');
  121. this.setState({authenticators: authenticators ?? []});
  122. } catch {
  123. // ignore errors
  124. }
  125. }
  126. render() {
  127. const {authenticators, error, errorType, showAccessForms} = this.state;
  128. if (errorType === ErrorCodes.invalidSSOSession) {
  129. this.handleLogout();
  130. return null;
  131. }
  132. return (
  133. <ThemeAndStyleProvider>
  134. <Form
  135. submitLabel={t('Continue')}
  136. onSubmit={this.handleSubmit}
  137. initialData={{isSuperuserModal: true}}
  138. extraButton={
  139. <BackWrapper>
  140. <Button onClick={this.handleSubmitCOPS}>{t('COPS/CSM')}</Button>
  141. </BackWrapper>
  142. }
  143. resetOnError
  144. >
  145. {error && (
  146. <StyledAlert type="error" showIcon>
  147. {errorType}
  148. </StyledAlert>
  149. )}
  150. {showAccessForms && <Hook name="component:superuser-access-category" />}
  151. {!showAccessForms && (
  152. <U2fContainer
  153. authenticators={authenticators}
  154. displayMode="sudo"
  155. onTap={this.handleU2fTap}
  156. />
  157. )}
  158. </Form>
  159. </ThemeAndStyleProvider>
  160. );
  161. }
  162. }
  163. const StyledAlert = styled(Alert)`
  164. margin-bottom: 0;
  165. `;
  166. const BackWrapper = styled('div')`
  167. width: 100%;
  168. margin-left: ${space(4)};
  169. `;
  170. export default withApi(SuperuserAccessForm);