superuserAccessForm.tsx 5.7 KB

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