grommet/grommet-ferret

View on GitHub
src/js/components/settings/DirectoryEdit.js

Summary

Maintainability
F
5 days
Test Coverage
// (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { changeDirectory, loadDirectoryCertificate, trustDirectory,
  verifyDirectory, searchDirectory, changeSettings }
  from '../../actions/actions';
import LayerForm from 'grommet-templates/components/LayerForm';
import FormField from 'grommet/components/FormField';
import Header from 'grommet/components/Header';
import Heading from 'grommet/components/Heading';
import Box from 'grommet/components/Box';
import Button from 'grommet/components/Button';
import Anchor from 'grommet/components/Anchor';
import Paragraph from 'grommet/components/Paragraph';
import Select from 'grommet/components/Select';
import RadioButton from 'grommet/components/RadioButton';
import AddIcon from 'grommet/components/icons/base/Add';
import EditIcon from 'grommet/components/icons/base/Edit';
import Certificate from '../Certificate';
import ErrorList from './ErrorList';

// Setting up a directory and groups takes place in multiple phases which
// must occur distinctly in this order:
// 1) Get the IP address or hostname of a directory server from the user
//    on Connect -> check whether this address is trusted
// 2) If not already trusted, the user must explicitly confirm trust
//    on Trust -> remember that the certificate is trusted
// 3) Get the Base DN to use for any search or authentication requests
//    from the user on Verify -> prompt for credentials, verify Base DN.
// 4) Configure directory group and role mappings
//    on OK -> done

class DirectoryEdit extends Component {

  constructor (props) {
    super(props);

    this._onSubmit = this._onSubmit.bind(this);
    this._onChange = this._onChange.bind(this);
    this._onCredentialsChange = this._onCredentialsChange.bind(this);
    this._onTypeChange = this._onTypeChange.bind(this);
    this._onSelectSearch = this._onSelectSearch.bind(this);
    this._onSelectChange = this._onSelectChange.bind(this);
    this._onTrust = this._onTrust.bind(this);
    this._onDontTrust = this._onDontTrust.bind(this);
    this._onAddGroup = this._onAddGroup.bind(this);
    this._onEditBase = this._onEditBase.bind(this);

    this.state = {
      busy: undefined,
      credentials: undefined,
      editBase: false,
      errors: {},
      needCredentials: false
    };
  }

  componentWillReceiveProps (nextProps) {
    var { directory, phase, response, roles } = nextProps;
    if (directory.certificate && 'Connecting' === this.state.busy) {
      this.setState({ busy: undefined });
    } else if ('search' === phase && 'Verifying' === this.state.busy) {
      this.setState({ busy: undefined });
    }
    if (response && 'ok' === response.result) {
      if (this.state.closeOnOk) {
        this.props.dispatch(changeSettings({directory: directory}));
        this.props.onClose();
      } else {
        // Ensure we have at least one of each type of role.
        // NOTE: consider moving this into the reducer.
        if (directory && directory.groups.length !== roles.length) {
          let usedRoles = {};
          directory.groups.forEach((group) => {
            usedRoles[group.role] = true;
          });
          while (directory.groups.length < roles.length) {
            // pick roles that haven't been used yet
            let index = 0;
            while (usedRoles[roles[index]]) index += 1;
            usedRoles[roles[index]] = true;
            directory.groups.push({ role: roles[index]});
          }
        }
      }
    }
  }

  _onSubmit () {
    const { directory, phase } = this.props;
    const { credentials, needCredentials } = this.state;
    let errors = {...this.state.errors};
    let noErrors = true;

    if ('identity' === phase) {
      if (! directory.address) {
        errors.address = 'required';
        noErrors = false;
      }
      if (noErrors) {
        this.setState({ busy: 'Connecting' });
        this.props.dispatch(loadDirectoryCertificate(directory));
      } else {
        this.setState({ errors: errors });
      }
    } else if ('verify' === phase || this.state.editBase) {
      if (! directory.baseDn) {
        errors.baseDn = 'required';
        noErrors = false;
      }
      if (noErrors) {
        if (needCredentials) {
          if (! credentials.userName) {
            errors.userName = 'required';
            noErrors = false;
          }
          if (! credentials.password) {
            errors.password = 'required';
            noErrors = false;
          }
        }
        if (noErrors) {
          if (credentials) {
            this.setState({ needCredentials: false, credentials: null,
              editBase: false, busy: 'Verifying' });
            this.props.dispatch(verifyDirectory(directory, credentials));
          } else {
            this.setState({
              needCredentials: true, credentials: {}, edtiBase: false
            });
          }
        } else {
          this.setState({ errors: errors });
        }
      } else {
        this.setState({ errors: errors });
      }
    } else if ('search' === phase) {
      // prune out any unfinished groups
      directory.groups = directory.groups.filter((group) => {
        return (group.role && group.dn);
      });
      this.props.dispatch(changeSettings({directory: directory}));
      this.props.onClose();
    }
  }

  _onChange (event) {
    let directory = { ...this.props.directory };
    let errors = { ...this.state.errors };
    const attribute = event.target.getAttribute('name');
    const value = event.target.value;
    const parts = attribute.split('.');
    if (parts.length === 1) {
      directory[attribute] = value;
      delete errors[attribute];
      if ('address' === attribute) {

      }
    } else {
      // e.g. groups.1.name
      const index = parseInt(parts[1], 10);
      while (directory.groups.length <= index) {
        directory.groups.push({});
      }
      directory.groups[index][parts[2]] = value;
    }
    this.setState({ errors: errors });
    this.props.dispatch(changeDirectory(directory));
  }

  _onCredentialsChange (event) {
    let credentials = { ...this.state.credentials };
    let errors = { ...this.state.errors };
    const attribute = event.target.getAttribute('name');
    const value = event.target.value;
    credentials[attribute] = value;
    delete errors[attribute];
    this.setState({ credentials: credentials, errors: errors });
  }

  _onTypeChange (type) {
    let directory = { ...this.props.directory };
    directory.type = type;
    this.props.dispatch(changeDirectory(directory));
  }

  _onSelectSearch (event) {
    let directory = {...this.props.directory};
    const attribute = event.target.getAttribute('name');
    const value = event.target.value;
    const parts = attribute.split('.');
    const index = parseInt(parts[1], 10);
    while (directory.groups.length <= index) {
      directory.groups.push({});
    }
    if (value.indexOf('=') !== -1) {
      // looks like a dn
      directory.groups[index].dn = value;
    } else {
      directory.groups[index].cn = value;
      directory.groups[index].dn = undefined;
    }

    this.props.dispatch(searchDirectory(directory, index, value));
  }

  _onSelectChange (pseudoEvent) {
    let directory = {...this.props.directory};
    const attribute = pseudoEvent.target.getAttribute('name');
    const suggestion = pseudoEvent.option;
    const parts = attribute.split('.');
    const index = parseInt(parts[1], 10);
    while (directory.groups.length <= index) {
      directory.groups.push({});
    }
    directory.groups[index].dn = suggestion.value;
    directory.groups[index].cn = suggestion.label;
    this.props.dispatch(changeDirectory(directory));
  }

  _onTrust () {
    var directory = {...this.props.directory};
    directory.trust = true;
    this.props.dispatch(trustDirectory(directory));
  }

  _onDontTrust () {
    var directory = {...this.props.directory};
    delete directory.certificate;
    this.props.dispatch(changeDirectory(directory));
  }

  _onAddGroup (role) {
    var directory = {...this.props.directory};
    directory.groups.push({ role: role });
    this.props.dispatch(changeDirectory(directory));
  }

  _onEditBase () {
    this.setState({ editBase: true });
  }

  _renderBaseFields () {
    const { directory, phase, productName, settings } = this.props;
    const { errors } = this.state;

    let directoryErrors;
    if (settings.errors && settings.errors.directory) {
      directoryErrors = <ErrorList errors={settings.errors.directory} />;
    }

    let baseDnField;
    if ('identity' !== phase) {
      baseDnField = (
        <FormField htmlFor="baseDn"
          label="Base distinguished name (DN). For example: ou=groups,o=my.com"
          error={errors.baseDn}>
          <input id="baseDn" name="baseDn" type="text"
            value={directory.baseDn || ''} onChange={this._onChange} />
        </FormField>
      );
    }

    return (
      <fieldset>
        <Paragraph>Connect to a directory server to enable others to access
          this {productName}.</Paragraph>
        {directoryErrors}
        <FormField htmlFor="type">
          <RadioButton id="type-ldap" label="LDAP" name="type"
            checked={'ldap' === directory.type}
            onChange={this._onTypeChange.bind(this, 'ldap')} />
          <RadioButton id="type-ad" label="Active Directory" name="type"
            checked={'active directory' === directory.type}
            onChange={this._onTypeChange.bind(this, 'active directory')} />
        </FormField>
        <FormField htmlFor="address"
          label="Directory server host name or IP address. A :port is optional."
          error={errors.address}>
          <input id="address" name="address" type="text"
            value={directory.address || ''} onChange={this._onChange} />
        </FormField>
        {baseDnField}
      </fieldset>
    );
  }

  _renderBaseSummary () {
    const { directory } = this.props;
    return (
      <Box direction="row" justify="between" align="center"
        separator="horizontal">
        <span><strong>{directory.baseDn}</strong> via {directory.address}</span>
        <Button icon={<EditIcon />} onClick={this._onEditBase}
          a11yTitle={`Edit ${directory.baseDn} Directory`} />
      </Box>
    );
  }

  _renderCertificate (certificate) {
    return (
      <Certificate certificate={certificate}
        onTrust={this._onTrust} onDontTrust={this._onDontTrust} />
    );
  }

  _renderCredentials () {
    const { credentials, errors } = this.state;
    return (
      <LayerForm title="Directory Credentials" submitLabel="OK"
        onClose={this.props.onClose} onSubmit={this._onSubmit}>
        <Paragraph>To verify the configuration, we need credentials to
          access the directory. These will not be persisted.</Paragraph>
        <fieldset>
          <FormField htmlFor="userName" label="User name"
            error={errors.userName}>
            <input id="userName" name="userName" type="text"
              value={credentials.userName || ''}
              onChange={this._onCredentialsChange} />
          </FormField>
          <FormField htmlFor="password" label="Password"
            error={errors.password}>
            <input id="password" name="password" type="password"
              value={credentials.password || ''}
              onChange={this._onCredentialsChange} />
          </FormField>
        </fieldset>
      </LayerForm>
    );
  }

  _renderRole (role) {
    const { directory, search, searchResponse } = this.props;
    let groups = [];
    directory.groups.forEach((group, index) => {
      if (group.role === role) {
        let value = { label: group.cn, value: group.dn };
        let options;
        if (search && search.groupIndex === index && searchResponse) {
          options = searchResponse.map((suggestion) => {
            return { label: suggestion.cn, value: suggestion.dn };
          });
        }
        groups.push(
          <FormField key={index} htmlFor={`groups.${index}.id`}
            label="Directory group identifier"
            help="Search via a plain string or an LDAP query">
            <Select id={`groups.${index}.id`}
              name={`groups.${index}.id`} type="text"
              value={value.label} onSearch={this._onSelectSearch}
              onChange={this._onSelectChange}
              options={options} />
          </FormField>
        );
      }
    });
    return (
      <fieldset key={role}>
        <Header size="small" justify="between">
          <Heading tag="h3">{role}</Heading>
          <Button icon={<AddIcon />} onClick={this._onAddGroup.bind(this, role)}
            a11yTitle={`Add ${role}`} />
        </Header>
        {groups}
      </fieldset>
    );
  }

  _renderGroups () {
    const { productName, roles } = this.props;
    const groupFields = roles.map((role) => {
      return this._renderRole(role);
    });
    return (
      <fieldset>
        <Heading tag="h3">Directory Groups</Heading>
        <Paragraph>Associate groups in the directory with
          the {productName} user roles.</Paragraph>
        {groupFields}
      </fieldset>
    );
  }

  _renderForm () {
    const { phase } = this.props;

    let summary;
    let contents;
    let submitLabel;
    if ('identity' === phase) {
      submitLabel = 'Connect';
      contents = this._renderBaseFields();
    } else if ('verify' === phase || this.state.editBase) {
      submitLabel = 'Verify';
      contents = this._renderBaseFields();
    } else if ('search' === phase) {
      submitLabel = 'OK';
      summary = this._renderBaseSummary();
      contents = this._renderGroups();
    }

    const accountControl = (
      <Anchor href="#" label="Edit local account" onClick={event => {
        event.preventDefault();
        this.props.onClose('accountEdit');
      }}/>
    );

    return (
      <LayerForm title="Directory" submitLabel={submitLabel}
        busy={this.state.busy}
        onClose={this.props.onClose} onSubmit={this._onSubmit}
        secondaryControl={accountControl}>
        {summary}
        {contents}
      </LayerForm>
    );
  }

  render () {
    const { directory, phase } = this.props;
    const { needCredentials } = this.state;
    let result;
    if ('trust' === phase) {
      result = this._renderCertificate(directory.certificate);
    } else if (needCredentials) {
      result = this._renderCredentials();
    } else {
      result = this._renderForm();
    }
    return result;
  }
}

DirectoryEdit.propTypes = {
  crendentials: PropTypes.object,
  directory: PropTypes.object.isRequired,
  onClose: PropTypes.func,
  phase: PropTypes.oneOf(['identity', 'trust', 'verify', 'search']),
  productName: PropTypes.string,
  response: PropTypes.object,
  search: PropTypes.object,
  searchResponse: PropTypes.arrayOf(PropTypes.object),
  settings: PropTypes.object
};

let select = (state) => ({
  directory: state.directory.directory,
  phase: state.directory.phase,
  productName: state.settings.productName.short,
  response: state.directory.response,
  roles: state.directory.roles,
  search: state.directory.search,
  searchResponse: state.directory.searchResponse,
  settings: state.settings.settings
});

export default connect(select)(DirectoryEdit);