Browse Source

feat(superuser): Require u2f on superuser access form (#37520)

Richard Ma 2 years ago
parent
commit
a27f55a0d3

+ 58 - 18
static/app/components/modals/sudoModal.tsx

@@ -42,36 +42,59 @@ type State = {
   busy: boolean;
   error: boolean;
   errorType: string;
+  showAccessForms: boolean;
   superuserAccessCategory: string;
   superuserReason: string;
 };
 
 class SudoModal extends Component<Props, State> {
   state: State = {
+    authenticators: [],
+    busy: false,
     error: false,
     errorType: '',
-    busy: false,
+    showAccessForms: true,
     superuserAccessCategory: '',
     superuserReason: '',
-    authenticators: [],
   };
 
   componentDidMount() {
     this.getAuthenticators();
   }
 
-  handleSubmit = async () => {
-    const {api, isSuperuser} = this.props;
-    const data = {
-      isSuperuserModal: isSuperuser,
+  handleSubmitCOPS = () => {
+    this.setState({
       superuserAccessCategory: 'cops_csm',
       superuserReason: 'COPS and CSM use',
-    };
-    try {
-      await api.requestPromise('/auth/', {method: 'PUT', data});
-      this.handleSuccess();
-    } catch (err) {
-      this.handleError(err);
+    });
+  };
+
+  handleSubmit = async data => {
+    const {api, isSuperuser} = this.props;
+    const {superuserAccessCategory, superuserReason, authenticators} = this.state;
+
+    const suAccessCategory = superuserAccessCategory || data.superuserAccessCategory;
+
+    const suReason = superuserReason || data.superuserReason;
+
+    if (!authenticators.length) {
+      this.handleError('No Authenticator');
+      return;
+    }
+
+    if (this.state.showAccessForms && isSuperuser) {
+      this.setState({
+        showAccessForms: false,
+        superuserAccessCategory: suAccessCategory,
+        superuserReason: suReason,
+      });
+    } else {
+      try {
+        await api.requestPromise('/auth/', {method: 'PUT', data});
+        this.handleSuccess();
+      } catch (err) {
+        this.handleError(err);
+      }
     }
   };
 
@@ -94,7 +117,7 @@ class SudoModal extends Component<Props, State> {
 
     this.setState({busy: true}, () => {
       retryRequest().then(() => {
-        this.setState({busy: false}, closeModal);
+        this.setState({busy: false, showAccessForms: true}, closeModal);
       });
     });
   };
@@ -102,11 +125,17 @@ class SudoModal extends Component<Props, State> {
   handleError = err => {
     let errorType = '';
     if (err.status === 403) {
-      errorType = ErrorCodes.invalidPassword;
+      if (err.responseJSON.detail.code === 'no_u2f') {
+        errorType = ErrorCodes.noAuthenticator;
+      } else {
+        errorType = ErrorCodes.invalidPassword;
+      }
     } else if (err.status === 401) {
       errorType = ErrorCodes.invalidSSOSession;
     } else if (err.status === 400) {
       errorType = ErrorCodes.invalidAccessCategory;
+    } else if (err === 'No Authenticator') {
+      errorType = ErrorCodes.noAuthenticator;
     } else {
       errorType = ErrorCodes.unknownError;
     }
@@ -114,6 +143,7 @@ class SudoModal extends Component<Props, State> {
       busy: false,
       error: true,
       errorType,
+      showAccessForms: true,
     });
   };
 
@@ -158,7 +188,7 @@ class SudoModal extends Component<Props, State> {
 
   renderBodyContent() {
     const {isSuperuser} = this.props;
-    const {authenticators, error, errorType} = this.state;
+    const {authenticators, error, errorType, showAccessForms} = this.state;
     const user = ConfigStore.get('user');
     const isSelfHosted = ConfigStore.get('isSelfHosted');
     const validateSUForm = ConfigStore.get('validateSUForm');
@@ -190,18 +220,28 @@ class SudoModal extends Component<Props, State> {
             <Form
               apiMethod="PUT"
               apiEndpoint="/auth/"
-              submitLabel={t('Re-authenticate')}
+              submitLabel={showAccessForms ? t('Continue') : t('Re-authenticate')}
+              onSubmit={this.handleSubmit}
               onSubmitSuccess={this.handleSuccess}
               onSubmitError={this.handleError}
               initialData={{isSuperuserModal: isSuperuser}}
               extraButton={
                 <BackWrapper>
-                  <Button onClick={this.handleSubmit}>{t('COPS/CSM')}</Button>
+                  <Button onClick={this.handleSubmitCOPS}>{t('COPS/CSM')}</Button>
                 </BackWrapper>
               }
               resetOnError
             >
-              {!isSelfHosted && <Hook name="component:superuser-access-category" />}
+              {!isSelfHosted && showAccessForms && (
+                <Hook name="component:superuser-access-category" />
+              )}
+              {!isSelfHosted && !showAccessForms && (
+                <U2fContainer
+                  authenticators={authenticators}
+                  displayMode="sudo"
+                  onTap={this.handleU2fTap}
+                />
+              )}
             </Form>
           ) : (
             <Button

+ 1 - 0
static/app/constants/superuserAccessErrors.tsx

@@ -2,5 +2,6 @@ export enum ErrorCodes {
   invalidPassword = 'Incorrect password',
   invalidSSOSession = 'Your SSO Session has expired, please reauthenticate',
   invalidAccessCategory = 'Please fill out the access category and reason correctly',
+  noAuthenticator = 'Please add a U2F authenticator to your account',
   unknownError = 'An error ocurred, please try again',
 }