From 0e56dce405b84dee4fc0e4171daca1d06e9f5e7f Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Wed, 1 Feb 2017 16:06:33 +1100 Subject: [PATCH 01/38] Begin role management feature --- app/components/Tokens/Manage.jsx | 200 ++++++++++++++++++++----------- app/components/Tokens/tokens.css | 20 ++++ 2 files changed, 152 insertions(+), 68 deletions(-) diff --git a/app/components/Tokens/Manage.jsx b/app/components/Tokens/Manage.jsx index 8b1130a..8dc748d 100644 --- a/app/components/Tokens/Manage.jsx +++ b/app/components/Tokens/Manage.jsx @@ -14,6 +14,7 @@ import Subheader from 'material-ui/Subheader'; import Divider from 'material-ui/Divider'; import LinearProgress from 'material-ui/LinearProgress'; import Snackbar from 'material-ui/Snackbar'; +import {Tabs, Tab} from 'material-ui/Tabs'; import Checkbox from 'material-ui/Checkbox'; import Toggle from 'material-ui/Toggle'; import Paper from 'material-ui/Paper'; @@ -471,80 +472,143 @@ export default class TokenManage extends React.Component { {this.renderAccessorInfoDialog()} {this.renderNewTokenDialog()}

Tokens

-

Manage Tokens

-

Here you can do stuff with tokens

- - - - {this.setState({ - newTokenDialog: true, - newTokenCodeDialog: false, - newTokenCode: '', - newTokenSelectedPolicies: ['default'], - newTokenIsOrphan: false, - newTokenIsRenewable: true, - newTokenMaxUses: 0, - newTokenOverrideTTL: 0 - })}} - /> - - - - }> - this.setState({accessorInfoDialog: true})} - /> - - { - this.setState({ revokeConfirmDialog: true, revokeAccessorId: this.state.selectedAccessor }) - } } - /> - - - - - - - Accessor ID - Display Name - Additional Policies - Created - Orphan - - - - {this.renderAccessorTableItems()} - - - - - - - - -
+ + + + + Here you can create new tokens and list active tokens.
+ Existing tokens are represented by their respective Accessor ID. +
+ + + + { + this.setState({ + newTokenDialog: true, + newTokenCodeDialog: false, + newTokenCode: '', + newTokenSelectedPolicies: ['default'], + newTokenIsOrphan: false, + newTokenIsRenewable: true, + newTokenMaxUses: 0, + newTokenOverrideTTL: 0 + }) + } } + /> + + + + }> + this.setState({ accessorInfoDialog: true })} + /> + + { + this.setState({ revokeConfirmDialog: true, revokeAccessorId: this.state.selectedAccessor }) + } } + /> + + + + + + + Accessor ID + Display Name + Additional Policies + Created + Orphan + + + + {this.renderAccessorTableItems()} + + + + + + + + +
+
+
+ + + Here you can create, list and edit token roles.
+ Roles can enforce specific behaviors when creating new tokens. +
+ + + + { + this.setState({ + newTokenDialog: true, + newTokenCodeDialog: false, + newTokenCode: '', + newTokenSelectedPolicies: ['default'], + newTokenIsOrphan: false, + newTokenIsRenewable: true, + newTokenMaxUses: 0, + newTokenOverrideTTL: 0 + }) + } } + /> + + + + }> + this.setState({ accessorInfoDialog: true })} + /> + + { + this.setState({ revokeConfirmDialog: true, revokeAccessorId: this.state.selectedAccessor }) + } } + /> + + + } + > + + +
+
this.setState({snackBarMsg: ''})} + onActionTouchTap={() => this.setState({ snackBarMsg: '' })} autoHideDuration={4000} - onRequestClose={() => this.setState({snackBarMsg: ''})} - /> + onRequestClose={() => this.setState({ snackBarMsg: '' })} + /> ); } diff --git a/app/components/Tokens/tokens.css b/app/components/Tokens/tokens.css index 88955a7..083832c 100644 --- a/app/components/Tokens/tokens.css +++ b/app/components/Tokens/tokens.css @@ -30,4 +30,24 @@ span.policiesList > div > div > div { .newTokenCodeEmitted { text-align: center; +} + +.accessorListSection { + padding: 10px; +} + +.rolesListSection { + padding: 10px; +} + +.TabInfoSection { + padding: 10px; + text-align: center; + font-style: italic; +} + +.classActionDelete { + position: absolute !important; + right: 4px; + top: 0px; } \ No newline at end of file From e0224f408b44e75a9547078e5461133bcbb6faa4 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Wed, 1 Feb 2017 19:43:24 +1100 Subject: [PATCH 02/38] First commit dynamic navigation --- app/App.jsx | 18 +-- .../{Secrets.jsx => Generic/Generic.jsx} | 6 +- .../{secrets.css => Generic/generic.css} | 0 app/components/shared/Menu/Menu.jsx | 148 +++++++++++++----- app/components/shared/Menu/menu.css | 18 +-- 5 files changed, 125 insertions(+), 65 deletions(-) rename app/components/Secrets/{Secrets.jsx => Generic/Generic.jsx} (99%) rename app/components/Secrets/{secrets.css => Generic/generic.css} (100%) diff --git a/app/App.jsx b/app/App.jsx index 454d77a..5f3ec5e 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -6,7 +6,7 @@ import injectTapEventPlugin from 'react-tap-event-plugin'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import getMuiTheme from 'material-ui/styles/getMuiTheme'; import App from './components/App/App.jsx'; -import Secrets from './components/Secrets/Secrets.jsx'; +import SecretsGeneric from './components/Secrets/Generic/Generic.jsx'; import Health from './components/Health/Health.jsx'; import Policies from './components/Policies/Home.jsx'; import Settings from './components/Settings/Settings.jsx'; @@ -40,7 +40,7 @@ const checkAccessToken = (nextState, replace, callback) => { } const muiTheme = getMuiTheme({ - fontFamily: 'Source Sans Pro, sans-serif' + fontFamily: 'Source Sans Pro, sans-serif', }); ReactDOM.render(( @@ -48,14 +48,12 @@ ReactDOM.render(( - - - - - - - - + + + + + + diff --git a/app/components/Secrets/Secrets.jsx b/app/components/Secrets/Generic/Generic.jsx similarity index 99% rename from app/components/Secrets/Secrets.jsx rename to app/components/Secrets/Generic/Generic.jsx index 1a4f685..5273654 100644 --- a/app/components/Secrets/Secrets.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -5,7 +5,7 @@ import { List, ListItem } from 'material-ui/List'; import Edit from 'material-ui/svg-icons/editor/mode-edit'; import Copy from 'material-ui/svg-icons/action/assignment'; import Checkbox from 'material-ui/Checkbox'; -import styles from './secrets.css'; +import styles from './generic.css'; import _ from 'lodash'; import copy from 'copy-to-clipboard'; import Dialog from 'material-ui/Dialog'; @@ -13,8 +13,8 @@ import FlatButton from 'material-ui/FlatButton'; import TextField from 'material-ui/TextField'; import { green500, green400, red500, red300, yellow500, white } from 'material-ui/styles/colors.js' import axios from 'axios'; -import { callVaultApi } from '../shared/VaultUtils.jsx' -import JsonEditor from '../shared/JsonEditor.jsx'; +import { callVaultApi } from '../../shared/VaultUtils.jsx' +import JsonEditor from '../../shared/JsonEditor.jsx'; import { browserHistory } from 'react-router' import Snackbar from 'material-ui/Snackbar'; diff --git a/app/components/Secrets/secrets.css b/app/components/Secrets/Generic/generic.css similarity index 100% rename from app/components/Secrets/secrets.css rename to app/components/Secrets/Generic/generic.css diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index a4540ac..ef688e1 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -1,59 +1,123 @@ -import React, { PropTypes } from 'react'; +import React, {PropTypes} from 'react'; import styles from './menu.css'; +import Drawer from 'material-ui/Drawer'; import { browserHistory } from 'react-router'; +import { List, ListItem, makeSelectable } from 'material-ui/List'; +import {tokenHasCapabilities, callVaultApi} from '../VaultUtils.jsx' + +const SelectableList = makeSelectable(List); + + +const supported_secret_backend_types = [ + 'generic' +] + +const supported_auth_backend_types = [ + 'token', + 'github' +] class Menu extends React.Component { + static propTypes = { + pathname: PropTypes.string.isRequired, + }; + constructor(props) { super(props); - this.applyActiveLink = this.applyActiveLink.bind(this); - this.state = { - togglePoliciesSubMenu: false - } } - applyActiveLink(name) { - if (name === this.props.pathname) { - return styles.activeLink - }; + state = { + authBackends: [ + { + path: 'token/', + type: 'token', + description: 'token based credentials' + } + ], + secretBackends: [ + { + path: 'secret/', + type: 'generic', + description: 'generic secret storage' + } + ] + }; + + componentDidMount() { + tokenHasCapabilities(['read'], 'sys/mounts') + .then(() => { + return callVaultApi('get', 'sys/mounts').then((resp) => { + let entries = _.get(resp, 'data.data', _.get(resp, 'data', {})); + let discoveredSecretBackends = _.map(entries, (v, k) => { + if ( _.indexOf(supported_secret_backend_types, v.type) != -1 ) { + let entry = { + path: k, + type: v.type, + description: v.description + } + return entry; + } + }).filter(Boolean); + this.setState({secretBackends: discoveredSecretBackends}); + }); + }) + .catch((err) => { + // Not allowed to list secret backends, using default + console.log("unable to list: " + err); + }) } + render() { + + let renderSecretBackendList = () => { + return _.map(this.state.secretBackends, (backend, idx) => { + return ( + + ) + }) + } + + return ( -
-
-

browserHistory.push('/secrets')}>Secrets

-
-
-

this.setState({ togglePoliciesSubMenu: !this.state.togglePoliciesSubMenu})}>Policies

-
- {this.state.togglePoliciesSubMenu && -
-
-

browserHistory.push('/policies/manage')}>Manage

-
-
-

browserHistory.push('/policies/github')}>Github

-
-
-

browserHistory.push('/policies/ec2')}>EC2

-
-
- } -
-

browserHistory.push('/tokens')}>Tokens

-
-
-

browserHistory.push('/settings')}>Settings

-
-
-

browserHistory.push('/responsewrapper')}>Response Wrapper

-
-
+ + browserHistory.push(v)}> + + , + + ]} + /> + , + + ]} + /> + + + + + ); } } -/*
-

browserHistory.push('/health')}>Health

-
*/ export default Menu; diff --git a/app/components/shared/Menu/menu.css b/app/components/shared/Menu/menu.css index b4a3065..f1d1df0 100644 --- a/app/components/shared/Menu/menu.css +++ b/app/components/shared/Menu/menu.css @@ -1,13 +1,11 @@ -#root { - padding-left: 63px; - padding-top: 20px; - font-size: 20px; - font-weight: 200; - border-right: 1px solid black; - height: calc(100vh - 80px); - width: 200px; - position: fixed; - top: 60px; +.root { + padding-left: 16px; + margin-top: 64px; +} + +.root span { + font-size: 20px !important; + font-weight: 200 !important; } .link { From c75010c62b9e1ab9584a37e7f21dd077be8ad399 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 2 Feb 2017 17:25:57 +1100 Subject: [PATCH 03/38] Refactor secret management --- app/App.jsx | 2 +- app/components/App/App.jsx | 32 +- app/components/App/app.css | 2 +- app/components/Secrets/Generic/Generic.jsx | 707 ++++++++++----------- app/components/Secrets/Generic/generic.css | 40 +- app/components/shared/Menu/menu.css | 1 + app/components/shared/VaultUtils.jsx | 2 +- app/components/shared/styles.css | 9 + 8 files changed, 353 insertions(+), 442 deletions(-) create mode 100644 app/components/shared/styles.css diff --git a/app/App.jsx b/app/App.jsx index 5f3ec5e..eaa49dc 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -48,7 +48,7 @@ ReactDOM.render(( - + diff --git a/app/components/App/App.jsx b/app/components/App/App.jsx index 0935b57..ac18be2 100644 --- a/app/components/App/App.jsx +++ b/app/components/App/App.jsx @@ -4,6 +4,7 @@ import Header from '../shared/Header/Header.jsx'; import Snackbar from 'material-ui/Snackbar'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; +import Paper from 'material-ui/Paper'; import { browserHistory } from 'react-router'; import { green500, red500, yellow500 } from 'material-ui/styles/colors.js' import styles from './app.css'; @@ -17,8 +18,8 @@ export default class App extends React.Component { this.renderLogoutDialog = this.renderLogoutDialog.bind(this); this.state = { snackbarMessage: '', - snackbarOpen: false, snackbarType: 'OK', + snackbarStyle: {}, namespace: '/', logoutOpen: false, logoutPromptSeen: false @@ -33,10 +34,15 @@ export default class App extends React.Component { window.localStorage.setItem('enableCapabilitiesCache', 'true'); } document.addEventListener("snackbar", (e) => { + let messageStyle = { backgroundColor: green500 }; + if ( e.detail.message instanceof Error ) { + messageStyle = { backgroundColor: red500 }; + } + this.setState({ - snackbarMessage: e.detail.message, + snackbarMessage: e.detail.message.toString(), snackbarType: e.detail.type || 'OK', - snackbarOpen: true + snackbarStyle: messageStyle }); }); @@ -92,27 +98,23 @@ export default class App extends React.Component { Use the menu on the left to navigate around.

); - let messageStyle = { backgroundColor: green500 }; - if (this.state.snackbarType == 'warn') { - messageStyle = { backgroundColor: yellow500 }; - } - if (this.state.snackbarType == 'error') { - messageStyle = { backgroundColor: red500 }; - } return
this.setState({ snackbarOpen: false })} + autoHideDuration={3000} + onRequestClose={() => this.setState({ snackbarMessage: '' })} + onActionTouchTap={() => this.setState({ snackbarMessage: '' })} /> {this.state.logoutOpen && this.renderLogoutDialog()}
- {this.props.children || welcome} + + {this.props.children || welcome} +
; diff --git a/app/components/App/app.css b/app/components/App/app.css index c873251..c6fd36d 100644 --- a/app/components/App/app.css +++ b/app/components/App/app.css @@ -3,7 +3,7 @@ width: calc(100vw - 305px - 50px); display: inline-block; margin-left: 250px; - margin-top: 60px; + margin-top: 80px; } #welcomeHeadline { diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index 5273654..385f749 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -1,99 +1,159 @@ import React, { PropTypes } from 'react'; +import { Tabs, Tab } from 'material-ui/Tabs'; +import { Toolbar, ToolbarGroup, ToolbarSeparator } from 'material-ui/Toolbar'; +import Subheader from 'material-ui/Subheader'; +import Paper from 'material-ui/Paper'; +import Avatar from 'material-ui/Avatar'; +import FileFolder from 'material-ui/svg-icons/file/folder'; +import ActionAssignment from 'material-ui/svg-icons/action/assignment'; +import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; +import ArrowForwardIcon from 'material-ui/svg-icons/navigation/arrow-forward'; import IconButton from 'material-ui/IconButton'; import FontIcon from 'material-ui/FontIcon'; import { List, ListItem } from 'material-ui/List'; -import Edit from 'material-ui/svg-icons/editor/mode-edit'; -import Copy from 'material-ui/svg-icons/action/assignment'; +import { Step, Stepper, StepLabel } from 'material-ui/Stepper'; import Checkbox from 'material-ui/Checkbox'; import styles from './generic.css'; +import sharedStyles from '../../shared/styles.css'; import _ from 'lodash'; -import copy from 'copy-to-clipboard'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import TextField from 'material-ui/TextField'; import { green500, green400, red500, red300, yellow500, white } from 'material-ui/styles/colors.js' -import axios from 'axios'; -import { callVaultApi } from '../../shared/VaultUtils.jsx' +import { callVaultApi, tokenHasCapabilities } from '../../shared/VaultUtils.jsx' import JsonEditor from '../../shared/JsonEditor.jsx'; -import { browserHistory } from 'react-router' -import Snackbar from 'material-ui/Snackbar'; +import { browserHistory, Link } from 'react-router' -const copyEvent = new CustomEvent("snackbar", { - detail: { - message: 'Copied!' - } -}); -class Secrets extends React.Component { +function snackBarMessage(message) { + let ev = new CustomEvent("snackbar", { detail: { message: message } }); + document.dispatchEvent(ev); +} + +class GenericSecretBackend extends React.Component { + static propTypes = { + params: PropTypes.object.isRequired, + }; + constructor(props) { super(props); + this.state = { - openEditModal: false, - openNewKeyModal: false, - errorMessage: '', + newSecretBtnDisabled: true, + secretList: [], + secretContent: {}, + newSecretName: '', + currentLogicalPath: '', + disableSubmit: true, + openNewObjectModal: false, + openEditObjectModal: false, openDeleteModal: false, - disableSubmit: false, - disableTextField: false, - focusKey: '', - focusSecret: '', - listBackends: false, - secretBackends: [], - secrets: [], - namespace: this.props.params.splat === undefined ? '/' : `/${this.props.params.splat}`, + deletingKey: '', useRootKey: window.localStorage.getItem("useRootKey") === 'true' || false, rootKey: window.localStorage.getItem("secretsRootKey") || '', - disableAddButton: true, - buttonColor: 'lightgrey', - snackBarMsg: '' - }; + } _.bindAll( this, - 'listSecretBackends', - 'getSecrets', - 'renderList', - 'renderNamespace', - 'clickSecret', - 'secretChangedTextEditor', 'secretChangedJsonEditor', - 'updateSecret', - 'renderEditDialog', - 'renderNewKeyDialog', - 'renderDeleteConfirmationDialog', - 'copyText', - 'deleteKey', - 'clickRoot' + 'secretChangedTextEditor', + 'loadSecretsList', + 'displaySecret', + 'CreateUpdateObject', + 'DeleteObject', + 'renderNewObjectDialog', + 'renderEditObjectDialog', + 'renderDeleteConfirmationDialog' ); } + isPathDirectory(path) { + if (!path) path = '/'; + return (path[path.length - 1] === '/'); + } + + getBaseDir(path) { + if (!path) return '/'; + return path.substring(0, _.lastIndexOf(path, '/') + 1); + } + + loadSecretsList() { + // Control the new secret button + tokenHasCapabilities(['create'], this.state.currentLogicalPath) + .then(() => { + this.setState({ newSecretBtnDisabled: false }) + }) + .catch(() => { + this.setState({ newSecretBtnDisabled: true }) + }) + + tokenHasCapabilities(['list'], this.state.currentLogicalPath) + .then(() => { + // Load secret list at current path + callVaultApi('get', this.state.currentLogicalPath, { list: true }, null, null) + .then((resp) => { + this.setState({ secretList: resp.data.data.keys }); + }) + .catch(snackBarMessage) + }) + .catch(() => { + this.setState({ secretList: [] }) + snackBarMessage(`No permissions to list content at ${this.state.currentLogicalPath}`); + }) + } + + + displaySecret() { + tokenHasCapabilities(['read'], this.state.currentLogicalPath) + .then(() => { + // Load content of the secret + callVaultApi('get', this.state.currentLogicalPath, null, null, null) + .then((resp) => { + this.setState({ secretContent: resp.data.data, openEditObjectModal: true }); + }) + .catch(snackBarMessage) + }) + .catch(() => { + this.setState({ secretContent: {} }) + snackBarMessage(`No permissions to read content of ${this.state.currentLogicalPath}`); + }) + } + componentWillMount() { - this.listSecretBackends(); - if (this.state.namespace === '/') { - this.clickRoot(); + this.setState({ currentLogicalPath: `${this.props.params.namespace}/${this.props.params.splat}` }) + } + + componentDidMount() { + if (this.isPathDirectory(this.props.params.splat)) { + this.loadSecretsList(); } else { - if (this.props.params.splat[this.props.params.splat.length - 1] === '/') { - this.getSecrets(this.state.namespace); - } else { - let paths = this.props.params.splat.split('/'); - let key = paths[paths.length - 1]; - //console.log(`key: ${key}`); - this.state.namespace = `/${this.props.params.splat.replace(key, '')}`; - //console.log(`namespace: ${this.state.namespace}`); - this.getSecrets(`${this.state.namespace}`); - this.clickSecret(key, false); - } + this.displaySecret(); + } + } + componentWillReceiveProps(nextProps) { + this.setState({ currentLogicalPath: `${nextProps.params.namespace}/${nextProps.params.splat}` }) + if (!_.isEqual(this.props.params.namespace, nextProps.params.namespace)) { + // Reset + this.setState({ + secretList: [] + }) } } - copyText(value) { - copy(value); - document.dispatchEvent(copyEvent); + componentDidUpdate(prevProps, prevState) { + if (!_.isEqual(this.props.params, prevProps.params)) { + if (this.isPathDirectory(this.props.params.splat)) { + this.loadSecretsList(); + } else { + this.displaySecret(); + } + } } secretChangedJsonEditor(v, syntaxCheckOk) { if (syntaxCheckOk && v) { - this.setState({ disableSubmit: false, focusSecret: v }); + this.setState({ disableSubmit: false, secretContent: v }); } else { this.setState({ disableSubmit: true }); } @@ -103,415 +163,290 @@ class Secrets extends React.Component { this.setState({ disableSubmit: false }); let tmp = {}; _.set(tmp, `${this.state.rootKey}`, v); - this.state.focusSecret = tmp; + this.setState({ secretContent: tmp }); } - renderEditDialog() { - const actions = [ - { - this.setState({ openEditModal: false }); - browserHistory.push(`/secrets${this.state.namespace}`); + CreateUpdateObject() { + let secret = this.state.secretContent; + let fullpath = this.state.currentLogicalPath + this.state.newSecretName; + callVaultApi('post', fullpath, null, secret, null) + .then((resp) => { + if (this.state.newSecretName) { + let secrets = this.state.secretList; + secrets.push(this.state.newSecretName); + this.setState({ + secretList: secrets, + }); + snackBarMessage(`Secret ${fullpath} added`); + } else { + snackBarMessage(`Secret ${fullpath} updated`); + } + }) + .catch(snackBarMessage) + } + + DeleteObject(key) { + let fullpath = this.state.currentLogicalPath + key; + callVaultApi('delete', fullpath, null, null, null) + .then((resp) => { + let secrets = this.state.secretList; + let secretToDelete = _.find(secrets, (secretToDelete) => { return secretToDelete == key; }); + secrets = _.pull(secrets, secretToDelete); + this.setState({ + secretList: secrets, + }); + snackBarMessage(`Secret ${fullpath} deleted`); + }) + .catch(snackBarMessage) + } + + + renderNewObjectDialog() { + const MISSING_KEY_ERROR = "Key cannot be empty."; + const DUPLICATE_KEY_ERROR = `Key '${this.state.currentLogicalPath}${this.state.newSecretName}' already exists.`; + + let validateAndSubmit = (e, v) => { + if (this.state.newSecretName === '') { + snackBarMessage(new Error(MISSING_KEY_ERROR)); + return; } - } />, - submitUpdate()} /> - ]; - let submitUpdate = () => { - this.updateSecret(false); - this.setState({ openEditModal: false }); + if (_.filter(this.state.secretList, x => x === this.state.newSecretName).length > 0) { + snackBarMessage(new Error(DUPLICATE_KEY_ERROR)); + return; + } + this.CreateUpdateObject(); + this.setState({ openNewObjectModal: false }); } - var objectIsBasicRootKey = _.size(this.state.focusSecret) == 1 && this.state.focusSecret.hasOwnProperty(this.state.rootKey); + const actions = [ + this.setState({ openNewObjectModal: false, secretContent: '' })} />, + + ]; + + var rootKeyInfo; var content; - if (objectIsBasicRootKey && this.state.useRootKey) { - var title = `Editing ${this.state.namespace}${this.state.focusKey} with specified root key`; + if (this.state.useRootKey) { + rootKeyInfo = "Current Root Key: " + this.state.rootKey; content = ( ); } else { - var title = `Editing ${this.state.namespace}${this.state.focusKey}`; content = ( ); } + return ( this.setState({ openEditModal: false })} + open={this.state.openNewObjectModal} autoScrollBodyContent={true} > + this.setState({ newSecretName: v })} /> {content} - < div className={styles.error} > {this.state.errorMessage} +
{rootKeyInfo}
); } - renderDeleteConfirmationDialog() { + renderEditObjectDialog() { const actions = [ - this.setState({ openDeleteModal: false, deletingKey: '' })} />, - this.deleteKey(this.state.deletingKey)} /> - ]; - - return ( - this.setState({ openDeleteModal: false, errorMessage: '' })} - > - -

You are about to permanently delete {this.state.namespace}{this.state.deletingKey}. Are you sure?

- To disable this prompt, visit the settings page. -
- ) - } - - renderNewKeyDialog() { - const MISSING_KEY_ERROR = "Key cannot be empty."; - const DUPLICATE_KEY_ERROR = `Key '${this.state.namespace}${this.state.focusKey}' already exists.`; - - let validateAndSubmit = (e, v) => { - if (this.state.focusKey === '') { - this.setState({ - errorMessage: MISSING_KEY_ERROR - }); - return; + { + this.setState({ openEditObjectModal: false, secretContent: '' }); + browserHistory.goBack(); } + } />, + submitUpdate()} /> + ]; - if (_.filter(this.state.secrets, x => x.key === this.state.focusKey).length > 0) { - this.setState({ - errorMessage: DUPLICATE_KEY_ERROR - }); - return; - } - this.updateSecret(true); - this.setState({ openNewKeyModal: false, errorMessage: '' }); + let submitUpdate = () => { + this.CreateUpdateObject(); + this.setState({ openEditObjectModal: false, secretContent: '' }); + browserHistory.goBack(); } - const actions = [ - this.setState({ openNewKeyModal: false, errorMessage: '' })} />, - - ]; - - var rootKeyInfo; + var objectIsBasicRootKey = _.size(this.state.secretContent) == 1 && this.state.secretContent.hasOwnProperty(this.state.rootKey); var content; + var title; - if (this.state.useRootKey) { - rootKeyInfo = "Current Root Key: " + this.state.rootKey; - var content = ( + if (objectIsBasicRootKey && this.state.useRootKey) { + title = `Editing ${this.state.currentLogicalPath} with specified root key`; + content = ( ); } else { + title = `Editing ${this.state.currentLogicalPath}`; content = ( ); } - return ( this.setState({ openNewKeyModal: false, errorMessage: '' })} + open={this.state.openEditObjectModal} autoScrollBodyContent={true} > - this.setState({ focusKey: v })} /> {content} -
{this.state.errorMessage}
-
{rootKeyInfo}
); } - listSecretBackends() { - callVaultApi('get', 'sys/mounts', null, null, null) - .then((resp) => { - // Backwards compatability for Vault 0.6 - let data = _.get(resp, 'data.data', _.get(resp, 'data', {})); - let secretBackends = []; - - _.forEach(Object.keys(data), (key) => { - if (_.get(data, `${key}.type`) === "generic") { - secretBackends.push({ key: key }); - } - }); - - this.setState({ - secretBackends: secretBackends - }); - }) - .catch((err) => { - console.error(err); - this.setState({ errorMessage: `Error: ${err}` }); - }); - } - - getSecrets(namespace) { - callVaultApi('get', `${encodeURI(namespace)}`, { list: true }, null, null) - .then((resp) => { - var secrets = _.map(resp.data.data.keys, (key) => { - return { - key: key - } - }); - this.setState({ - namespace: namespace, - secrets: secrets, - disableAddButton: false, - buttonColor: green500 - }); - }) - .catch((err) => { - console.error(err); - this.setState({ - errorMessage: err, - disableAddButton: true, - buttonColor: 'lightgrey' - }); - }); - } + renderDeleteConfirmationDialog() { + const actions = [ + this.setState({ openDeleteModal: false, deletingKey: '' })} />, + submitDelete()} /> + ]; - clickSecret(key, isFullPath) { - let isDir = key[key.length - 1] === '/'; - if (isDir) { - let secret = isFullPath ? key : `${this.state.namespace}${key}`; - this.getSecrets(secret); - browserHistory.push(`/secrets${secret}`); - } else { - let fullKey = `${this.state.namespace}${key}`; - callVaultApi('get', `${encodeURI(fullKey)}`, null, null, null) - .then((resp) => { - this.setState({ - errorMessage: '', - disableSubmit: false, - disableTextField: false, - openEditModal: true, - focusKey: key, - focusSecret: resp.data.data, - listBackends: false - }); - browserHistory.push(`/secrets${fullKey}`); - }) - .catch((err) => { - console.error(err); - this.setState({ errorMessage: `Error: ${err}` }); - }); + let submitDelete = () => { + this.DeleteObject(this.state.deletingKey); + this.setState({ openDeleteModal: false }); } - } - deleteKey(key) { - let fullKey = `${this.state.namespace}${key}`; - callVaultApi('delete', `${encodeURI(fullKey)}`, null, null, null) - .then((resp) => { - if (resp.status !== 204 && resp.status !== 200) { - this.setState({ errorMessage: `Delete returned status of ${resp.status}:${resp.statusText}` }); - } else { - let secrets = this.state.secrets; - let secretToDelete = _.find(secrets, (secretToDelete) => { return secretToDelete.key == key; }); - secrets = _.pull(secrets, secretToDelete); - this.setState({ - secrets: secrets, - snackBarMsg: `Secret ${key} deleted` - }); - } - }) - .catch((err) => { - console.error(err); - this.setState({ errorMessage: `Error: ${err}` }); - }); + return ( + - this.setState({ - deletingKey: '', - openDeleteModal: false - }); +

You are about to permanently delete {this.state.currentLogicalPath}{this.state.deletingKey}. Are you sure?

+ To disable this prompt, visit the settings page. +
+ ) } - updateSecret(isNewKey) { - let fullKey = `${this.state.namespace}${this.state.focusKey}`; - //Check if the secret is a json object, if so stringify it. This is needed to properly escape characters. - //let secret = typeof this.state.focusSecret == 'object' ? JSON.stringify(this.state.focusSecret) : this.state.focusSecret; - let secret = this.state.focusSecret; - callVaultApi('post', `${encodeURI(fullKey)}`, null, secret, null) - .then((resp) => { - if (isNewKey) { - let secrets = this.state.secrets; - let key = this.state.focusKey.includes('/') ? `${this.state.focusKey.split('/')[0]}/` : this.state.focusKey; - secrets.push({ key: key, value: this.state.focusSecret }); - this.setState({ - secrets: secrets, - snackBarMsg: `Secret ${this.state.focusKey} added` - }); - } else { - this.setState({ snackBarMsg: `Secret ${this.state.focusKey} updated` }); - } - }) - .catch((err) => { - console.error(err); - this.setState({ errorMessage: `Error: ${err}` }); - }); - } - showDelete(key) { - if (key[key.length - 1] === '/') { - return (); - } else { - return ( - { - if (window.localStorage.getItem("showDeleteModal") === 'false') { - this.deleteKey(key); - } else { - this.setState({ deletingKey: key, openDeleteModal: true }) - } - } } - > - - ); - } - } - renderList() { - if (this.state.listBackends) { - return _.map(this.state.secretBackends, (secretBackend) => { - return ( - { + return _.map(this.state.secretList, (key) => { + let avatar = (} />); + let action = ( + { - this.setState( - { - namespace: '/' + secretBackend.key, - listBackends: false, - secrets: this.getSecrets('/' + secretBackend.key) - }); - browserHistory.push(`/secrets/${secretBackend.key}`); + if (window.localStorage.getItem("showDeleteModal") === 'false') { + this.DeleteObject(key); + } else { + this.setState({ openDeleteModal: true, deletingKey: key }) + } } } - primaryText={
{secretBackend.key}
} - //secondaryText={
{secret.value}
} > -
+ >
); - }); - } else { - return _.map(this.state.secrets, (secret) => { - return ( + if (this.isPathDirectory(key)) { + avatar = (} />); + action = (); + } + let item = ( { this.clickSecret(secret.key) } } - primaryText={
{secret.key}
} - //secondaryText={
{secret.value}
} - rightIconButton={this.showDelete(secret.key)}> -
- ); + key={key} + primaryText={key} + insetChildren={true} + leftAvatar={avatar} + rightIconButton={action} + onTouchTap={() => { + this.setState({ newSecretName: '' }); + tokenHasCapabilities(['read'], this.state.currentLogicalPath + key).then(() => { + browserHistory.push(`/secrets/generic/${this.state.currentLogicalPath}${key}`); + }).catch(() => { + snackBarMessage("Access denied"); + }) + + } } + /> + ) + if (this.isPathDirectory(key) && returndirs) { return item } + if (!this.isPathDirectory(key) && returnobjs) { return item } }); } - } - - clickRoot() { - this.setState({ - listBackends: true, - namespace: '/', - disableAddButton: true, - buttonColor: 'lightgrey' - }); - if (this.props.params.splat !== undefined) - browserHistory.push(`/secrets/`); - } - renderNamespace() { - let namespaceParts = this.state.namespace.split('/'); - return ( - _.map(namespaceParts, (dir, index) => { - if (index === 0) { - return ( -
- ROOT - {index !== namespaceParts.length - 1 && /} -
- ); - } - var link = [].concat(namespaceParts).slice(0, index + 1).join('/') + '/'; - return ( -
- this.clickSecret(link, true)}>{dir.toUpperCase()} - {index !== namespaceParts.length - 1 && /} -
- ); - }) - ); - } + let renderBreadcrumb = () => { + let components = _.initial(this.getBaseDir(this.state.currentLogicalPath).split('/')); + return _.map(components, (dir, index) => { + var relativelink = [].concat(components).slice(0, index + 1).join('/') + '/'; + return ({dir}) + }); + } - render() { return (
- {this.state.openEditModal && this.renderEditDialog()} - {this.state.openNewKeyModal && this.renderNewKeyDialog()} - {this.state.openDeleteModal && this.renderDeleteConfirmationDialog()} -

Secrets

-

Here you can view, update, and delete keys stored in your Vault. Just remember, deleting keys cannot be undone!

- this.setState({ - disableSubmit: true, - openNewKeyModal: true, - focusKey: '', - focusSecret: '', - errorMessage: '' - })} /> -
{this.renderNamespace()}
- - {this.renderList()} - - this.setState({ snackBarMsg: '' })} - autoHideDuration={4000} - onRequestClose={() => this.setState({ snackBarMsg: '' })} - /> + {this.renderEditObjectDialog()} + {this.renderNewObjectDialog()} + {this.renderDeleteConfirmationDialog()} + + + + Here you can create browse, edit, create and delete secrets. + + + + + { + this.setState({ + openNewObjectModal: true, + newSecretName: '', + secretContent: '' + }) + } } + /> + + + + + } + > + {renderBreadcrumb()} + + + Directories + {renderSecretListItems(true, false)} + Objects + {renderSecretListItems(false, true)} + + + +
- ); + ) } } -export default Secrets; +export default GenericSecretBackend; \ No newline at end of file diff --git a/app/components/Secrets/Generic/generic.css b/app/components/Secrets/Generic/generic.css index 591b5e3..4dd796f 100644 --- a/app/components/Secrets/Generic/generic.css +++ b/app/components/Secrets/Generic/generic.css @@ -1,40 +1,4 @@ -#welcomeHeadline { - font-size: 60px; - font-weight: 200; +.listStyle span { + font-family: monospace !important; } -.actionButtons { - position: absolute; - right: 0; -} - -.key{ - max-width: 600px; - word-wrap: break-word; -} - -.namespace { - padding: 6px 4px; - font-size: 140%; - background-color: white; - color: gray; - border-radius: 4px; - margin-top: 40px; -} - -.error { - color: rgb(244, 67, 54); -} - -.link { - cursor: pointer; -} - -.activeLink { - color: #00ABE0; - font-weight: bold; -} - -.link:hover { - font-weight: bold; -} diff --git a/app/components/shared/Menu/menu.css b/app/components/shared/Menu/menu.css index f1d1df0..51d6d50 100644 --- a/app/components/shared/Menu/menu.css +++ b/app/components/shared/Menu/menu.css @@ -1,6 +1,7 @@ .root { padding-left: 16px; margin-top: 64px; + height: calc(100% - 60px) !important; } .root span { diff --git a/app/components/shared/VaultUtils.jsx b/app/components/shared/VaultUtils.jsx index 47cdec9..c71d608 100644 --- a/app/components/shared/VaultUtils.jsx +++ b/app/components/shared/VaultUtils.jsx @@ -36,7 +36,7 @@ function callVaultApi(method, path, query = {}, data, headers = {}) { }); return instance.request({ - url: path, + url: encodeURI(path), method: method, data: data, params: query, diff --git a/app/components/shared/styles.css b/app/components/shared/styles.css new file mode 100644 index 0000000..91a4dfc --- /dev/null +++ b/app/components/shared/styles.css @@ -0,0 +1,9 @@ +.TabInfoSection { + padding: 10px; + text-align: center; + font-style: italic; +} + +.TabContentSection { + padding: 10px; +} \ No newline at end of file From f1920fb24609f3b524ad570a6d6cfd9211683087 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 2 Feb 2017 17:35:59 +1100 Subject: [PATCH 04/38] Support dynamic auth backends --- app/App.jsx | 4 +-- .../Token/Token.jsx} | 8 ++--- .../Token/token.css} | 0 app/components/shared/Menu/Menu.jsx | 36 ++++++++++++++++--- 4 files changed, 38 insertions(+), 10 deletions(-) rename app/components/{Tokens/Manage.jsx => Authentication/Token/Token.jsx} (99%) rename app/components/{Tokens/tokens.css => Authentication/Token/token.css} (100%) diff --git a/app/App.jsx b/app/App.jsx index eaa49dc..bc743cb 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -11,7 +11,7 @@ import Health from './components/Health/Health.jsx'; import Policies from './components/Policies/Home.jsx'; import Settings from './components/Settings/Settings.jsx'; import ResponseWrapper from './components/ResponseWrapper/ResponseWrapper.jsx'; -import TokenManage from './components/Tokens/Manage.jsx' +import TokenAuthBackend from './components/Authentication/Token/Token.jsx' injectTapEventPlugin(); @@ -53,7 +53,7 @@ ReactDOM.render(( - +
diff --git a/app/components/Tokens/Manage.jsx b/app/components/Authentication/Token/Token.jsx similarity index 99% rename from app/components/Tokens/Manage.jsx rename to app/components/Authentication/Token/Token.jsx index 8b1130a..a928378 100644 --- a/app/components/Tokens/Manage.jsx +++ b/app/components/Authentication/Token/Token.jsx @@ -1,6 +1,6 @@ import React from 'react' import _ from 'lodash'; -import styles from './tokens.css'; +import styles from './token.css'; import { red500, orange500, red300, white } from 'material-ui/styles/colors.js' import RaisedButton from 'material-ui/RaisedButton'; import {Table, TableBody, TableFooter, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table'; @@ -21,12 +21,12 @@ import {List, ListItem} from 'material-ui/List'; import TextField from 'material-ui/TextField'; import FlatButton from 'material-ui/FlatButton'; import FontIcon from 'material-ui/FontIcon'; -import {tokenHasCapabilities, callVaultApi} from '../shared/VaultUtils.jsx' -import JsonEditor from '../shared/JsonEditor.jsx'; +import {tokenHasCapabilities, callVaultApi} from '../../shared/VaultUtils.jsx' +import JsonEditor from '../../shared/JsonEditor.jsx'; import UltimatePagination from 'react-ultimate-pagination-material-ui' -export default class TokenManage extends React.Component { +export default class TokenAuthBackend extends React.Component { constructor(props) { super(props); diff --git a/app/components/Tokens/tokens.css b/app/components/Authentication/Token/token.css similarity index 100% rename from app/components/Tokens/tokens.css rename to app/components/Authentication/Token/token.css diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index ef688e1..27f5229 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -65,6 +65,29 @@ class Menu extends React.Component { // Not allowed to list secret backends, using default console.log("unable to list: " + err); }) + + + tokenHasCapabilities(['read'], 'sys/auth/') + .then(() => { + return callVaultApi('get', 'sys/auth').then((resp) => { + let entries = _.get(resp, 'data.data', _.get(resp, 'data', {})); + let discoveredAuthBackends = _.map(entries, (v, k) => { + if ( _.indexOf(supported_auth_backend_types, v.type) != -1 ) { + let entry = { + path: k, + type: v.type, + description: v.description + } + return entry; + } + }).filter(Boolean); + this.setState({authBackends: discoveredAuthBackends}); + }); + }) + .catch((err) => { + // Not allowed to list secret backends, using default + console.log("unable to list: " + err); + }) } @@ -78,6 +101,14 @@ class Menu extends React.Component { }) } + let renderAuthBackendList = () => { + return _.map(this.state.authBackends, (backend, idx) => { + return ( + + ) + }) + } + return ( @@ -92,10 +123,7 @@ class Menu extends React.Component { primaryText="Auth Backends" primaryTogglesNestedList={true} initiallyOpen={true} - nestedItems={[ - , - - ]} + nestedItems={renderAuthBackendList()} /> Date: Thu, 2 Feb 2017 19:27:10 +1100 Subject: [PATCH 05/38] Layout changes --- app/components/Secrets/Generic/Generic.jsx | 5 +++-- app/components/shared/Menu/Menu.jsx | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index 385f749..7b633a6 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -10,6 +10,7 @@ import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; import ArrowForwardIcon from 'material-ui/svg-icons/navigation/arrow-forward'; import IconButton from 'material-ui/IconButton'; import FontIcon from 'material-ui/FontIcon'; +import Divider from 'material-ui/Divider'; import { List, ListItem } from 'material-ui/List'; import { Step, Stepper, StepLabel } from 'material-ui/Stepper'; import Checkbox from 'material-ui/Checkbox'; @@ -436,9 +437,9 @@ class GenericSecretBackend extends React.Component { {renderBreadcrumb()} - Directories + {renderSecretListItems(true, false)} - Objects + {renderSecretListItems(false, true)}
diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index 27f5229..be47455 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -27,6 +27,8 @@ class Menu extends React.Component { } state = { + selectedPath: this.props.pathname, + authBackends: [ { path: 'token/', @@ -109,10 +111,15 @@ class Menu extends React.Component { }) } + let handleMenuChange = (e, v) => { + this.setState({selectedPath: v}); + browserHistory.push(v) + } + return ( - browserHistory.push(v)}> + Date: Thu, 2 Feb 2017 21:47:11 +1100 Subject: [PATCH 06/38] Various component restructuring and refactoring --- admin.hcl | 25 ++- app/App.jsx | 10 +- app/components/App/App.jsx | 9 +- .../AwsEc2/AwsEc2.jsx} | 3 +- .../Github}/Github.jsx | 6 +- .../Authentication/Github/github.css | 9 ++ app/components/Policies/Home.jsx | 109 ------------- app/components/Policies/Manage.jsx | 146 ++++++++---------- app/components/Secrets/Generic/Generic.jsx | 39 +++-- app/components/Secrets/Generic/generic.css | 5 +- app/components/shared/Menu/Menu.jsx | 9 +- app/components/shared/styles.css | 6 +- docker-compose.yaml | 12 +- run-docker-compose-dev | 7 + webpack.config.js | 2 +- 15 files changed, 167 insertions(+), 230 deletions(-) rename app/components/{Policies/Ec2.jsx => Authentication/AwsEc2/AwsEc2.jsx} (89%) rename app/components/{Policies => Authentication/Github}/Github.jsx (98%) create mode 100644 app/components/Authentication/Github/github.css delete mode 100644 app/components/Policies/Home.jsx diff --git a/admin.hcl b/admin.hcl index 99d5303..e9b4352 100644 --- a/admin.hcl +++ b/admin.hcl @@ -1,3 +1,24 @@ -path "*" { - capabilities = ["create", "read", "update", "delete", "list", "sudo"] +{ + "path": { + "*": { + "capabilities": [ + "create", + "read", + "update", + "delete", + "list", + "sudo" + ] + }, + "ultrasecret/admincantlistthis/": { + "capabilities": [ + "deny" + ] + }, + "ultrasecret/admincantreadthis": { + "capabilities": [ + "deny" + ] + } + } } diff --git a/app/App.jsx b/app/App.jsx index bc743cb..8f5fdf6 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -8,10 +8,12 @@ import getMuiTheme from 'material-ui/styles/getMuiTheme'; import App from './components/App/App.jsx'; import SecretsGeneric from './components/Secrets/Generic/Generic.jsx'; import Health from './components/Health/Health.jsx'; -import Policies from './components/Policies/Home.jsx'; +import PolicyManager from './components/Policies/Manage.jsx'; import Settings from './components/Settings/Settings.jsx'; import ResponseWrapper from './components/ResponseWrapper/ResponseWrapper.jsx'; import TokenAuthBackend from './components/Authentication/Token/Token.jsx' +import AwsEc2AuthBackend from './components/Authentication/AwsEc2/AwsEc2.jsx' +import GithubAuthBackend from './components/Authentication/Github/Github.jsx' injectTapEventPlugin(); @@ -49,11 +51,13 @@ ReactDOM.render(( + + + - - + diff --git a/app/components/App/App.jsx b/app/components/App/App.jsx index ac18be2..4b58695 100644 --- a/app/components/App/App.jsx +++ b/app/components/App/App.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import _ from 'lodash'; import Menu from '../shared/Menu/Menu.jsx'; import Header from '../shared/Header/Header.jsx'; import Snackbar from 'material-ui/Snackbar'; @@ -35,12 +36,18 @@ export default class App extends React.Component { } document.addEventListener("snackbar", (e) => { let messageStyle = { backgroundColor: green500 }; + let message = e.detail.message.toString(); if ( e.detail.message instanceof Error ) { + // Handle logical erros from vault + //debugger; + if (_.has(e.detail.message, 'response.data.errors')) + if (e.detail.message.response.data.errors.length > 0) + message = e.detail.message.response.data.errors.join(','); messageStyle = { backgroundColor: red500 }; } this.setState({ - snackbarMessage: e.detail.message.toString(), + snackbarMessage: message, snackbarType: e.detail.type || 'OK', snackbarStyle: messageStyle }); diff --git a/app/components/Policies/Ec2.jsx b/app/components/Authentication/AwsEc2/AwsEc2.jsx similarity index 89% rename from app/components/Policies/Ec2.jsx rename to app/components/Authentication/AwsEc2/AwsEc2.jsx index 9885859..0862338 100644 --- a/app/components/Policies/Ec2.jsx +++ b/app/components/Authentication/AwsEc2/AwsEc2.jsx @@ -1,7 +1,6 @@ import axios from 'axios'; import React, { PropTypes } from 'react' import _ from 'lodash'; -import styles from './policies.css'; import FlatButton from 'material-ui/FlatButton'; import { green500, green400, red500, red300, yellow500, white } from 'material-ui/styles/colors.js' import { List, ListItem } from 'material-ui/List'; @@ -10,7 +9,7 @@ import TextField from 'material-ui/TextField'; import IconButton from 'material-ui/IconButton'; import FontIcon from 'material-ui/FontIcon'; -export default class EC2 extends React.Component { +export default class AwsEc2AuthBackend extends React.Component { constructor(props) { super(props); this.state = { diff --git a/app/components/Policies/Github.jsx b/app/components/Authentication/Github/Github.jsx similarity index 98% rename from app/components/Policies/Github.jsx rename to app/components/Authentication/Github/Github.jsx index 23493c5..12e7f91 100644 --- a/app/components/Policies/Github.jsx +++ b/app/components/Authentication/Github/Github.jsx @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react' import _ from 'lodash'; -import styles from './policies.css'; +import styles from './github.css'; import FlatButton from 'material-ui/FlatButton'; import { green500, green400, red500, red300, yellow500, white } from 'material-ui/styles/colors.js' import { List, ListItem } from 'material-ui/List'; @@ -9,10 +9,10 @@ import TextField from 'material-ui/TextField'; import IconButton from 'material-ui/IconButton'; import FontIcon from 'material-ui/FontIcon'; import Checkbox from 'material-ui/Checkbox'; -import { callVaultApi } from '../shared/VaultUtils.jsx' +import { callVaultApi } from '../../shared/VaultUtils.jsx' import Snackbar from 'material-ui/Snackbar'; -export default class Github extends React.Component { +export default class GithubAuthBackend extends React.Component { constructor(props) { super(props); this.state = { diff --git a/app/components/Authentication/Github/github.css b/app/components/Authentication/Github/github.css new file mode 100644 index 0000000..c245d44 --- /dev/null +++ b/app/components/Authentication/Github/github.css @@ -0,0 +1,9 @@ +.error { + color: #f44336; + margin: 20px 0; +} + +.orgName { + color: dodgerblue; + font-weight: 1000; +} \ No newline at end of file diff --git a/app/components/Policies/Home.jsx b/app/components/Policies/Home.jsx deleted file mode 100644 index dd48bbd..0000000 --- a/app/components/Policies/Home.jsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { PropTypes } from 'react' -import _ from 'lodash'; -import styles from './policies.css'; -import Manage from './Manage.jsx'; -import Github from './Github.jsx'; -import EC2 from './Ec2.jsx'; - -export default class Home extends React.Component { - constructor(props) { - super(props); - this.renderPolicyPage = this.renderPolicyPage.bind(this); - this.state = { - currentTab: 'Manage', - requestOrganization: false, - organization: window.localStorage.getItem('githubOrganization') || '' - } - - _.bindAll( - this, - 'requestOrganization', - 'renderOrganizationDialog' - ); - } - - requestOrganization() { - this.setState({ - requestOrganization: this.state.organization ? false : true - }) - } - - renderOrganizationDialog() { - const actions = [ -
- closeDialog(e)} /> - submitOrg(e)} /> -
- ]; - - let submitOrg = (e) => { - this.setState({ - organization: this.state.tmpOrganization, - requestOrganization: false - }); - }; - - let closeDialog = (e) => { - this.setState({ - requestOrganization: false - }); - }; - - return ( - - this.setState({ tmpOrganization: v})} - /> -
{this.state.errorMessage}
-
- ) - } - - renderPolicyPage() { - switch (this.props.params.policy) { - case 'manage': - return - case 'github': - return - case 'ec2': - return - } - } - - render() { - return ( -
-

Policies

- - {this.renderPolicyPage()} -
- ); - - } -} - -// this.setState({ currentTab: v })} -// > -// -// -// -// -// -// {gh.state.requestOrganization && this.renderOrganizationDialog()} -// -// -// -// -// -// ======= diff --git a/app/components/Policies/Manage.jsx b/app/components/Policies/Manage.jsx index a7f43db..99773d4 100644 --- a/app/components/Policies/Manage.jsx +++ b/app/components/Policies/Manage.jsx @@ -1,6 +1,10 @@ import React, { PropTypes } from 'react' import _ from 'lodash'; +import { Tabs, Tab } from 'material-ui/Tabs'; +import { Toolbar, ToolbarGroup, ToolbarSeparator } from 'material-ui/Toolbar'; +import Paper from 'material-ui/Paper'; import styles from './policies.css'; +import sharedStyles from '../shared/styles.css'; import FlatButton from 'material-ui/FlatButton'; import { green500, green400, red500, red300, yellow500, white } from 'material-ui/styles/colors.js' import { List, ListItem } from 'material-ui/List'; @@ -12,9 +16,17 @@ import JsonEditor from '../shared/JsonEditor.jsx'; import hcltojson from 'hcl-to-json' import jsonschema from './vault-policy-schema.json' import { callVaultApi } from '../shared/VaultUtils.jsx' -import Snackbar from 'material-ui/Snackbar'; +import Avatar from 'material-ui/Avatar'; +import HardwareSecurity from 'material-ui/svg-icons/hardware/security'; +import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; +import ActionDelete from 'material-ui/svg-icons/action/delete'; -export default class Manage extends React.Component { +function snackBarMessage(message) { + let ev = new CustomEvent("snackbar", { detail: { message: message } }); + document.dispatchEvent(ev); +} + +export default class PolicyManager extends React.Component { constructor(props) { super(props); this.state = { @@ -28,10 +40,8 @@ export default class Manage extends React.Component { policies: [], currentPolicy: '', disableSubmit: false, - errorMessage: '', forbidden: false, - buttonColor: 'lightgrey', - snackBarMsg: '' + buttonColor: 'lightgrey' }; _.bindAll( @@ -93,16 +103,12 @@ export default class Manage extends React.Component { let validateAndSubmit = () => { if (this.state.focusPolicy === '') { - this.setState({ - newPolicyErrorMessage: MISSING_POLICY_ERROR - }); + snackBarMessage(new Error(MISSING_POLICY_ERROR)); return; } if (_.filter(this.state.policies, x => x.name === this.state.focusPolicy).length > 0) { - this.setState({ - newPolicyErrorMessage: DUPLICATE_POLICY_ERROR - }); + snackBarMessage(new Error(DUPLICATE_POLICY_ERROR)); return; } this.updatePolicy(this.state.focusPolicy, true); @@ -183,20 +189,16 @@ export default class Manage extends React.Component { let policies = this.state.policies; policies.push({ name: policyName }); this.setState({ - policies: policies, - snackBarMsg: `Policy '${policyName}' added` + policies: policies }); + snackBarMessage(`Policy '${policyName}' added`); } else { - this.setState({ snackBarMsg: `Policy '${policyName}' updated` }); + snackBarMessage(`Policy '${policyName}' updated`); } }) .catch((err) => { console.error(err.stack); - if (err.response.data.errors) { - this.setState({ - errorMessage: err.response.data.errors.join('
') - }); - } + snackBarMessage(err); }) this.setState({ openNewPolicyModal: false }); this.setState({ openEditModal: false }); @@ -213,17 +215,12 @@ export default class Manage extends React.Component { this.setState({ policies: policies, - errorMessage: '', buttonColor: green500 }); }) .catch((err) => { console.error(err.response.data); - this.setState({ - errorMessage: err.response.data, - forbidden: true, - buttonColor: 'lightgrey' - }); + snackBarMessage(err); }); } @@ -247,8 +244,7 @@ export default class Manage extends React.Component { openEditModal: true, focusPolicy: policyName, currentPolicy: rules_obj, - disableSubmit: true, - errorMessage: '', + disableSubmit: true }); } }) @@ -260,27 +256,17 @@ export default class Manage extends React.Component { deletePolicy(policyName) { callVaultApi('delete', `sys/policy/${encodeURI(policyName)}`, null, null, null) .then((resp) => { - if (resp.status !== 204 && resp.status !== 200) { - console.error(resp.status); - this.setState({ - errorMessage: 'An error occurred.' - }); - } else { - let policies = this.state.policies; - let policyToDelete = _.find(policies, (policyToDelete) => { return policyToDelete.name === policyName }); - policies = _.pull(policies, policyToDelete); - this.setState({ - policies: policies, - errorMessage: '' - }); - this.setState({ snackBarMsg: `Policy '${policyName}' deleted` }); - } + let policies = this.state.policies; + let policyToDelete = _.find(policies, (policyToDelete) => { return policyToDelete.name === policyName }); + policies = _.pull(policies, policyToDelete); + this.setState({ + policies: policies, + }); + snackBarMessage(`Policy '${policyName}' deleted`); }) .catch((err) => { console.error(err.stack); - this.setState({ - errorMessage: err.response.data - }); + snackBarMessage(err); }); this.setState({ @@ -301,7 +287,8 @@ export default class Manage extends React.Component { } } } > - + { window.localStorage.getItem("showDeleteModal") === 'false' ? : } + ); } @@ -309,8 +296,8 @@ export default class Manage extends React.Component { return _.map(this.state.policies, (policy) => { return ( } />} onTouchTap={() => { this.clickPolicy(policy.name) } } primaryText={
{policy.name}
} rightIconButton={this.showDelete(policy.name)}> @@ -325,40 +312,37 @@ export default class Manage extends React.Component { {this.state.openEditModal && this.renderEditDialog()} {this.state.openNewPolicyModal && this.renderNewPolicyDialog()} {this.state.openDeleteModal && this.renderDeleteConfirmationDialog()} -

Manage Policies

-

Here you can view, update, and delete policies stored in your Vault. Just remember, deleting policies cannot be undone!

- { this.setState({ - openNewPolicyModal: true, - errorMessage: '', - newPolicyErrorMessage: '', - newPolicyNameErrorMessage: '', - disableSubmit: true, - focusPolicy: '', - currentPolicy: { path: { 'sample/path': { capabilities: ['read'] } } } - })} />} - {this.state.errorMessage && -
- - {this.state.errorMessage} -
- } - - {this.renderPolicies()} - - this.setState({ snackBarMsg: '' })} - autoHideDuration={4000} - onRequestClose={() => this.setState({ snackBarMsg: '' })} - /> + + + + Here you can view, update, and delete policies stored in your Vault. Just remember, deleting policies cannot be undone! + + + + + this.setState({ + openNewPolicyModal: true, + newPolicyErrorMessage: '', + newPolicyNameErrorMessage: '', + disableSubmit: true, + focusPolicy: '', + currentPolicy: { path: { 'sample/path': { capabilities: ['read'] } } } + })} + /> + + + + {this.renderPolicies()} + + + + ); } diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index 7b633a6..6373cb0 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -6,6 +6,7 @@ import Paper from 'material-ui/Paper'; import Avatar from 'material-ui/Avatar'; import FileFolder from 'material-ui/svg-icons/file/folder'; import ActionAssignment from 'material-ui/svg-icons/action/assignment'; +import ActionDelete from 'material-ui/svg-icons/action/delete'; import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; import ArrowForwardIcon from 'material-ui/svg-icons/navigation/arrow-forward'; import IconButton from 'material-ui/IconButton'; @@ -99,7 +100,7 @@ class GenericSecretBackend extends React.Component { }) .catch(() => { this.setState({ secretList: [] }) - snackBarMessage(`No permissions to list content at ${this.state.currentLogicalPath}`); + snackBarMessage(new Error(`No permissions to list content at ${this.state.currentLogicalPath}`)); }) } @@ -116,7 +117,7 @@ class GenericSecretBackend extends React.Component { }) .catch(() => { this.setState({ secretContent: {} }) - snackBarMessage(`No permissions to read content of ${this.state.currentLogicalPath}`); + snackBarMessage(new Error(`No permissions to read content of ${this.state.currentLogicalPath}`)); }) } @@ -356,19 +357,28 @@ class GenericSecretBackend extends React.Component { { - if (window.localStorage.getItem("showDeleteModal") === 'false') { - this.DeleteObject(key); - } else { - this.setState({ openDeleteModal: true, deletingKey: key }) - } + tokenHasCapabilities(['delete'], this.state.currentLogicalPath + key).then(() => { + if (window.localStorage.getItem("showDeleteModal") === 'false') { + this.DeleteObject(key); + } else { + this.setState({ openDeleteModal: true, deletingKey: key }) + } + }).catch(() => { + snackBarMessage(new Error("Access denied")); + }) } } > - > + {window.localStorage.getItem("showDeleteModal") === 'false' ? : } + ); + let capability = 'read'; + if (this.isPathDirectory(key)) { avatar = (} />); action = (); + capability = 'list'; } + let item = ( { this.setState({ newSecretName: '' }); - tokenHasCapabilities(['read'], this.state.currentLogicalPath + key).then(() => { + tokenHasCapabilities([capability], this.state.currentLogicalPath + key).then(() => { browserHistory.push(`/secrets/generic/${this.state.currentLogicalPath}${key}`); }).catch(() => { - snackBarMessage("Access denied"); + snackBarMessage(new Error("Access denied")); }) } } @@ -417,6 +427,9 @@ class GenericSecretBackend extends React.Component { primary={true} label="NEW SECRET" disabled={this.state.newSecretBtnDisabled} + backgroundColor={green500} + hoverColor={green400} + labelStyle={{ color: white }} onTouchTap={() => { this.setState({ openNewObjectModal: true, @@ -427,7 +440,7 @@ class GenericSecretBackend extends React.Component { /> - + - + {renderSecretListItems(true, false)} - + {renderSecretListItems(false, true)} diff --git a/app/components/Secrets/Generic/generic.css b/app/components/Secrets/Generic/generic.css index 4dd796f..ad79d34 100644 --- a/app/components/Secrets/Generic/generic.css +++ b/app/components/Secrets/Generic/generic.css @@ -1,4 +1 @@ -.listStyle span { - font-family: monospace !important; -} - +/*Place Holder*/ \ No newline at end of file diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index be47455..39c6e2b 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -14,7 +14,8 @@ const supported_secret_backend_types = [ const supported_auth_backend_types = [ 'token', - 'github' + 'github', + 'aws-ec2' ] class Menu extends React.Component { @@ -98,7 +99,7 @@ class Menu extends React.Component { let renderSecretBackendList = () => { return _.map(this.state.secretBackends, (backend, idx) => { return ( - + ) }) } @@ -106,7 +107,7 @@ class Menu extends React.Component { let renderAuthBackendList = () => { return _.map(this.state.authBackends, (backend, idx) => { return ( - + ) }) } @@ -137,7 +138,7 @@ class Menu extends React.Component { primaryTogglesNestedList={true} initiallyOpen={true} nestedItems={[ - , + , ]} /> diff --git a/app/components/shared/styles.css b/app/components/shared/styles.css index 91a4dfc..5ca091f 100644 --- a/app/components/shared/styles.css +++ b/app/components/shared/styles.css @@ -6,4 +6,8 @@ .TabContentSection { padding: 10px; -} \ No newline at end of file +} + +.listStyle span { + font-family: monospace !important; +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 3ebaad9..1fac282 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -25,9 +25,9 @@ services: VAULT_AUTH_DEFAULT: USERNAMEPASSWORD VAULT_SUPPLIED_TOKEN_HEADER: 'X-Remote-User' - webpack: - build: . - volumes: - - .:/app - - /app/node_modules - command: webpack -w +# webpack: +# build: . +# volumes: +# - .:/app +# - /app/node_modules +# command: webpack -w diff --git a/run-docker-compose-dev b/run-docker-compose-dev index 1b74f02..3ea6a14 100755 --- a/run-docker-compose-dev +++ b/run-docker-compose-dev @@ -20,6 +20,13 @@ exec_in_vault vault auth-enable userpass exec_in_vault vault policy-write admin /app/admin.hcl exec_in_vault vault write auth/userpass/users/test password=test policies=admin exec_in_vault vault write secret/test somekey=somedata +exec_in_vault vault mount -path=ultrasecret generic +exec_in_vault vault write ultrasecret/moretest somekey=somedata +exec_in_vault vault write ultrasecret/dir1/secret somekey=somedata +exec_in_vault vault write ultrasecret/dir2/secret somekey=somedata +exec_in_vault vault write ultrasecret/dir2/secret2 somekey=somedata +exec_in_vault vault write ultrasecret/admincantlistthis/butcanreadthis somekey=somedata +exec_in_vault vault write ultrasecret/admincantreadthis somekey=somedata echo "------------- Vault Root Token -------------" docker-compose logs vault | grep 'Root Token:' | tail -n 1 diff --git a/webpack.config.js b/webpack.config.js index 51ea9ba..12f5d92 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,7 +14,7 @@ module.exports = { publicPath: '/assets/', filename: 'bundle.js' }, - devtool: 'source-map', +// devtool: 'source-map', module: { loaders: [{ test: /\.jsx?$/, From bd97d77eb583132929de1a69390dd1d2062d313d Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 2 Feb 2017 21:50:11 +1100 Subject: [PATCH 07/38] mount github in docker-compose env --- run-docker-compose-dev | 1 + 1 file changed, 1 insertion(+) diff --git a/run-docker-compose-dev b/run-docker-compose-dev index 3ea6a14..bd7c884 100755 --- a/run-docker-compose-dev +++ b/run-docker-compose-dev @@ -17,6 +17,7 @@ exec_in_vault() { exec_in_vault vault auth "$(docker-compose logs vault | grep 'Root Token:' | tail -n 1 | awk '{ print $NF }')" exec_in_vault vault status exec_in_vault vault auth-enable userpass +exec_in_vault vault auth-enable github exec_in_vault vault policy-write admin /app/admin.hcl exec_in_vault vault write auth/userpass/users/test password=test policies=admin exec_in_vault vault write secret/test somekey=somedata From be2e497b4bd4b6e066636b942086f9eff042f328 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 2 Feb 2017 21:57:55 +1100 Subject: [PATCH 08/38] fix bug in listing after new object creation --- app/components/Secrets/Generic/Generic.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index 6373cb0..423c6cf 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -174,11 +174,7 @@ class GenericSecretBackend extends React.Component { callVaultApi('post', fullpath, null, secret, null) .then((resp) => { if (this.state.newSecretName) { - let secrets = this.state.secretList; - secrets.push(this.state.newSecretName); - this.setState({ - secretList: secrets, - }); + this.loadSecretsList(); snackBarMessage(`Secret ${fullpath} added`); } else { snackBarMessage(`Secret ${fullpath} updated`); From 83fa4b05b8242541284419425d90969ec0992ff1 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 2 Feb 2017 22:05:47 +1100 Subject: [PATCH 09/38] mount aws-ec2 in docker-compose env --- run-docker-compose-dev | 1 + 1 file changed, 1 insertion(+) diff --git a/run-docker-compose-dev b/run-docker-compose-dev index bd7c884..9baae26 100755 --- a/run-docker-compose-dev +++ b/run-docker-compose-dev @@ -18,6 +18,7 @@ exec_in_vault vault auth "$(docker-compose logs vault | grep 'Root Token:' | tai exec_in_vault vault status exec_in_vault vault auth-enable userpass exec_in_vault vault auth-enable github +exec_in_vault vault auth-enable -path=awsaccount1 aws-ec2 exec_in_vault vault policy-write admin /app/admin.hcl exec_in_vault vault write auth/userpass/users/test password=test policies=admin exec_in_vault vault write secret/test somekey=somedata From af461168d88b8a0ec5005501181125bc53a301ab Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 2 Feb 2017 23:04:22 +1100 Subject: [PATCH 10/38] remove bad hack --- app/components/Secrets/Generic/Generic.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index 423c6cf..94c8361 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -266,7 +266,7 @@ class GenericSecretBackend extends React.Component { const actions = [ { this.setState({ openEditObjectModal: false, secretContent: '' }); - browserHistory.goBack(); + browserHistory.push(this.getBaseDir(this.props.location.pathname)); } } />, submitUpdate()} /> @@ -275,7 +275,7 @@ class GenericSecretBackend extends React.Component { let submitUpdate = () => { this.CreateUpdateObject(); this.setState({ openEditObjectModal: false, secretContent: '' }); - browserHistory.goBack(); + browserHistory.push(this.getBaseDir(this.props.location.pathname)); } var objectIsBasicRootKey = _.size(this.state.secretContent) == 1 && this.state.secretContent.hasOwnProperty(this.state.rootKey); @@ -414,7 +414,7 @@ class GenericSecretBackend extends React.Component { - Here you can create browse, edit, create and delete secrets. + Here you can browse, edit, create and delete secrets. From d87abae1c2a8f740fdcd7634bb17ee7a0665f29c Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Fri, 3 Feb 2017 01:46:59 +1100 Subject: [PATCH 11/38] Role listing and editing --- app/components/Tokens/Manage.jsx | 511 +++++++++++++++++++++---------- app/components/Tokens/tokens.css | 6 + 2 files changed, 361 insertions(+), 156 deletions(-) diff --git a/app/components/Tokens/Manage.jsx b/app/components/Tokens/Manage.jsx index 8dc748d..631c5f3 100644 --- a/app/components/Tokens/Manage.jsx +++ b/app/components/Tokens/Manage.jsx @@ -1,10 +1,10 @@ import React from 'react' import _ from 'lodash'; import styles from './tokens.css'; -import { red500, orange500, red300, white } from 'material-ui/styles/colors.js' +import { red500, orange500, green100, green400, red300, white } from 'material-ui/styles/colors.js' import RaisedButton from 'material-ui/RaisedButton'; -import {Table, TableBody, TableFooter, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table'; -import {Toolbar, ToolbarGroup, ToolbarSeparator} from 'material-ui/Toolbar'; +import { Table, TableBody, TableFooter, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; +import { Toolbar, ToolbarGroup, ToolbarSeparator } from 'material-ui/Toolbar'; import copy from 'copy-to-clipboard'; import IconButton from 'material-ui/IconButton'; import IconMenu from 'material-ui/IconMenu'; @@ -13,24 +13,30 @@ import Dialog from 'material-ui/Dialog'; import Subheader from 'material-ui/Subheader'; import Divider from 'material-ui/Divider'; import LinearProgress from 'material-ui/LinearProgress'; -import Snackbar from 'material-ui/Snackbar'; -import {Tabs, Tab} from 'material-ui/Tabs'; +import { Tabs, Tab } from 'material-ui/Tabs'; import Checkbox from 'material-ui/Checkbox'; import Toggle from 'material-ui/Toggle'; import Paper from 'material-ui/Paper'; -import {List, ListItem} from 'material-ui/List'; +import { List, ListItem } from 'material-ui/List'; import TextField from 'material-ui/TextField'; import FlatButton from 'material-ui/FlatButton'; import FontIcon from 'material-ui/FontIcon'; -import {tokenHasCapabilities, callVaultApi} from '../shared/VaultUtils.jsx' +import { tokenHasCapabilities, callVaultApi } from '../shared/VaultUtils.jsx' import JsonEditor from '../shared/JsonEditor.jsx'; import UltimatePagination from 'react-ultimate-pagination-material-ui' +import Avatar from 'material-ui/Avatar'; +import ActionClass from 'material-ui/svg-icons/action/class'; +import ActionDelete from 'material-ui/svg-icons/action/delete'; +function snackBarMessage(message) { + let ev = new CustomEvent("snackbar", { detail: { message: message } }); + document.dispatchEvent(ev); +} export default class TokenManage extends React.Component { constructor(props) { super(props); - + this.state = { loading: false, accessorListError: "", @@ -58,16 +64,29 @@ export default class TokenManage extends React.Component { totalPages: 1, maxItemsPerPage: 10, revokeBtnDisabled: true, - snackBarMsg:'' + roleList: [], + roleAttributes: { + name: '', + allowed_policies: [], + disallowed_policies: [], + orphan: false, + period: 0, + renewable: true, + path_suffix: '', + explicit_max_ttl: '' + }, + selectedRole: '', + newRoleName: '', + roleDialogOpen: false }; - + this.styles = { chip: { margin: '6px 6px 0 0', border: '1px solid black', } }; - + _.bindAll( this, 'onTotalPagesChange', @@ -83,45 +102,45 @@ export default class TokenManage extends React.Component { 'renderNewTokenDialog' ) } - + onTotalPagesChange(event) { - this.setState({totalPages: +event.target.value}); - } - - onPageChangeFromPagination(newPage) { - this.setState({ - currentPage: newPage, - accessorList: [], - selectedAccessor: '', - revokeBtnDisabled: true, - }); - - } + this.setState({ totalPages: +event.target.value }); + } + + onPageChangeFromPagination(newPage) { + this.setState({ + currentPage: newPage, + accessorList: [], + selectedAccessor: '', + revokeBtnDisabled: true, + }); + + } renderAccessorTableItems() { return _.map(this.state.accessorList, (acc_id) => { if (acc_id in this.state.accessorDetails) { - - let policies = _.map(this.state.accessorDetails[acc_id].policies,(policy) => { + + let policies = _.map(this.state.accessorDetails[acc_id].policies, (policy) => { if (policy != "default") { return () } }); - + let getDateStr = (epoch) => { let locale = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage) let d = new Date(0); d.setUTCSeconds(epoch); return d.toLocaleString(locale); } - + return ( {acc_id} {this.state.accessorDetails[acc_id].display_name} {policies} - {getDateStr(this.state.accessorDetails[acc_id].creation_time)} - {this.state.accessorDetails[acc_id].orphan && + {getDateStr(this.state.accessorDetails[acc_id].creation_time)} + {this.state.accessorDetails[acc_id].orphan && } @@ -135,28 +154,82 @@ export default class TokenManage extends React.Component { } }); } - - componentDidUpdate() { - if(this.state.fullAccessorList.length > 0 && this.state.accessorList.length == 0) { + + renderRoleListItems() { + return _.map(this.state.roleList, (role) => { + + let action = ( + + + + ) + + return ( + } />} + rightIconButton={action} + onTouchTap={(e, v) => { + this.setState({ selectedRole: role }); + } } + > +
+ +
+
+ ) + }); + } + + displayRole() { + tokenHasCapabilities(['read'], 'auth/token/roles/' + this.state.selectedRole) + .then(() => { + // Load content of the role + callVaultApi('get', 'auth/token/roles/' + this.state.selectedRole, null, null, null) + .then((resp) => { + this.setState({ + roleAttributes: _.clone(resp.data.data), + roleDialogOpen: true + }); + }) + .catch(snackBarMessage) + }) + .catch(() => { + snackBarMessage(new Error(`No permissions to read content of role ${his.state.selectedRole}`)); + this.setState({ selectedRole: '' }); + }) + } + + componentDidUpdate(prevProps, prevState) { + if (this.state.fullAccessorList.length > 0 && this.state.accessorList.length == 0) { this.updateAccessorList(this.state.currentPage); } + + if (this.state.selectedRole && this.state.selectedRole !== prevState.selectedRole) { + this.displayRole() + } } - + updateAccessorList(page) { - let offset = (page -1) * this.state.maxItemsPerPage; + let offset = (page - 1) * this.state.maxItemsPerPage; let displayedAccessorList = _.slice( this.state.fullAccessorList, offset, offset + this.state.maxItemsPerPage ) - + _.map(displayedAccessorList, ((id) => { - if(!(id in this.state.accessorDetails)) { + if (!(id in this.state.accessorDetails)) { tokenHasCapabilities(['update'], 'auth/token/lookup-accessor').then(() => { - return callVaultApi('post', 'auth/token/lookup-accessor', {}, {accessor: id}).then((resp) => { + return callVaultApi('post', 'auth/token/lookup-accessor', {}, { accessor: id }).then((resp) => { let current_list = this.state.accessorDetails; current_list[id] = resp.data.data; - this.setState({accessorDetails: current_list}); + this.setState({ accessorDetails: current_list }); }); }).catch(); } @@ -166,14 +239,14 @@ export default class TokenManage extends React.Component { accessorList: displayedAccessorList, }); } - + onRowSelection(selectedRows) { - if(selectedRows.length){ - this.setState({selectedAccessor: this.state.accessorList[selectedRows[0]]}); + if (selectedRows.length) { + this.setState({ selectedAccessor: this.state.accessorList[selectedRows[0]] }); tokenHasCapabilities(['update'], 'auth/token/revoke-accessor').then(() => { - this.setState({revokeBtnDisabled: false}); + this.setState({ revokeBtnDisabled: false }); }).catch(() => { - this.setState({revokeBtnDisabled: true}); + this.setState({ revokeBtnDisabled: true }); }); } else { this.setState({ @@ -184,60 +257,72 @@ export default class TokenManage extends React.Component { } componentDidMount() { - + // Check if user is allowed to create new tokens tokenHasCapabilities(['update'], 'auth/token/create') .then(() => { - this.setState({newTokenBtnDisabled: false}); + this.setState({ newTokenBtnDisabled: false }); // Check if user has sudo capability on the path return tokenHasCapabilities(['sudo'], 'auth/token/create') .then(() => { // sudo users can use the `no_parent` attribute to create orphan tokens - this.setState({'canCreateOrphan': 'no_parent'}); + this.setState({ 'canCreateOrphan': 'no_parent' }); // sudo users can assign any policy to a token. load the full list, if possible return tokenHasCapabilities(['read'], 'sys/policy').then(() => { return callVaultApi('get', 'sys/policy').then((resp) => { - this.setState({newTokenAvailablePolicies: resp.data.data.keys}); + this.setState({ newTokenAvailablePolicies: resp.data.data.keys }); }); }); }) .catch(() => { // User doesnt have sudo or policy list failed, either way use user assigned policies let p1 = callVaultApi('get', 'auth/token/lookup-self').then((resp) => { - this.setState({newTokenAvailablePolicies: resp.data.data.policies}); + this.setState({ newTokenAvailablePolicies: resp.data.data.policies }); }).catch(); // <- This shouldnt have failed // Altough sudo was not granted, we could still create orphans using a different endpoint let p2 = tokenHasCapabilities(['update'], 'auth/token/create-orphan').then(() => { // Turns out we can - this.setState({'canCreateOrphan': 'create_orphan'}); + this.setState({ 'canCreateOrphan': 'create_orphan' }); }).catch(); // <- Nothing we can really do at this point - return Promise.all([p1,p2]); + return Promise.all([p1, p2]); }); }) .catch(() => { // Not allowed to create. Disable button - this.setState({newTokenBtnDisabled: true}); + this.setState({ newTokenBtnDisabled: true }); }) - - tokenHasCapabilities(['sudo','list'], 'auth/token/accessors') + + tokenHasCapabilities(['sudo', 'list'], 'auth/token/accessors') .then(() => { - return callVaultApi('get', 'auth/token/accessors', {'list': true} ).then((resp) => { + return callVaultApi('get', 'auth/token/accessors', { 'list': true }).then((resp) => { this.setState({ fullAccessorList: resp.data.data.keys, - totalPages: Math.ceil(resp.data.data.keys.length/this.state.maxItemsPerPage) + totalPages: Math.ceil(resp.data.data.keys.length / this.state.maxItemsPerPage) + }); + }); + }) + .catch(() => { + snackBarMessage(new Error('You don\' have enough permissions to list accessors')); + }); + + tokenHasCapabilities(['list'], 'auth/token/roles') + .then(() => { + return callVaultApi('get', 'auth/token/roles', { 'list': true }).then((resp) => { + this.setState({ + roleList: resp.data.data.keys }); }); }) .catch(() => { - this.setState({snackBarMsg: 'You don\' have enough permissions to list accessors'}); + snackBarMessage(new Error('You don\' have enough permissions to list roles')); }); } - + revokeAccessor(id) { tokenHasCapabilities(['update'], 'auth/token/revoke-accessor').then(() => { - callVaultApi('post', 'auth/token/revoke-accessor', {}, {accessor: id}).then(() => { + callVaultApi('post', 'auth/token/revoke-accessor', {}, { accessor: id }).then(() => { let list = this.state.accessorList list.splice(list.indexOf(id), 1); this.setState({ @@ -247,7 +332,7 @@ export default class TokenManage extends React.Component { }); }); } - + renderRevokeConfirmDialog() { const actions = [ this.setState({ revokeConfirmDialog: false, selectedAccessor: '' })} />, @@ -260,17 +345,17 @@ export default class TokenManage extends React.Component { modal={false} actions={actions} open={this.state.revokeConfirmDialog} - onRequestClose={() => this.setState({ revokeConfirmDialog: false})} + onRequestClose={() => this.setState({ revokeConfirmDialog: false })} >

You are about to permanently delete {this.state.revokeAccessorId}. Are you sure?

To disable this prompt, visit the settings page. ) } - + renderAccessorInfoDialog() { const actions = [ - this.setState({accessorInfoDialog: false})} /> + this.setState({ accessorInfoDialog: false })} /> ]; return ( @@ -279,31 +364,185 @@ export default class TokenManage extends React.Component { modal={false} actions={actions} open={this.state.accessorInfoDialog} - onRequestClose={() => this.setState({ accessorInfoDialog: false})} - > + onRequestClose={() => this.setState({ accessorInfoDialog: false })} + > + /> ) } - + + + renderRoleDialog() { + + let handlePoliciesCheckUncheck = (policy, isInputChecked) => { + let role = this.state.roleAttributes + if (isInputChecked) { + role.allowed_policies = _.union(role.allowed_policies, policy); + } else { + role.allowed_policies = _.without(role.allowed_policies, policy); + } + this.setState({ roleAttributes: role }); + }; + + let handleSubmitAction = () => { + this.setState({ loading: true }); + let vault_endpoint; + if (this.state.newRoleName) + vault_endpoint = 'auth/token/roles/' + this.state.newRoleName; + else + vault_endpoint = 'auth/token/roles/' + this.state.selectedRole; + + let role = this.state.roleAttributes; + delete role.name; + role.allowed_policies = role.allowed_policies.join(','); + role.disallowed_policies = role.disallowed_policies.join(','); + + + callVaultApi('post', vault_endpoint, {}, role) + .then((resp) => { + this.setState({ + loading: false, + selectedRole: '', + roleDialogOpen: false, + }); + snackBarMessage("DONE") + }) + .catch((error) => { + // Despite our efforts, the request failed. show why + this.setState({ + loading: false + }); + snackBarMessage(error.toString()); + }); + } + + const RoleDialogAction = [ + this.setState({ roleDialogOpen: false, selectedRole: '' })} />, + , + + ]; + + + let policiesItems = this.state.newTokenAvailablePolicies.map((policy, idx) => { + if (policy != "default" && policy != "root") { + return ( + { handlePoliciesCheckUncheck(policy, iic) } } />} + primaryText={policy} + /> + ) + } + }); + + return ( +
+ this.setState({ roleDialogOpen: false })} + > + + { + let role = this.state.roleAttributes; + role.explicit_max_ttl = Math.max(0, Number(e.target.value)); + this.setState({ roleAttributes: role }); + } } + /> + { + let role = this.state.roleAttributes; + role.path_suffix = e.target.value; + this.setState({ roleAttributes: role }); + } } + /> + + Settings + { + let role = this.state.roleAttributes; + role.orphan = v; + this.setState({ roleAttributes: role }); + } } + /> + } + primaryText="Orphan Token" + /> + { + let role = this.state.roleAttributes; + role.disallowed_policies = v ? [] : ['default']; + this.setState({ roleAttributes: role }); + } } + /> + } + primaryText="Allow Default Policy" + /> + { + let role = this.state.roleAttributes; + role.renewable = v; + this.setState({ roleAttributes: role }); + } } + /> + } + primaryText="Renewable" + /> + + + Allowed Policies + {policiesItems} + + + +
+ ) + } + renderNewTokenDialog() { let handlePoliciesCheckUncheck = (policy, isInputChecked) => { if (isInputChecked) { - this.setState({newTokenSelectedPolicies: _.union(this.state.newTokenSelectedPolicies, [policy])}) + this.setState({ newTokenSelectedPolicies: _.union(this.state.newTokenSelectedPolicies, [policy]) }) } else { - this.setState({newTokenSelectedPolicies: _.without(this.state.newTokenSelectedPolicies, policy)}) + this.setState({ newTokenSelectedPolicies: _.without(this.state.newTokenSelectedPolicies, policy) }) } }; - + let handleCreateAction = () => { - this.setState({loading: true}); - + this.setState({ loading: true }); + let vault_endpoint = 'auth/token/create'; let params = { @@ -312,7 +551,7 @@ export default class TokenManage extends React.Component { no_default_policy: (_.indexOf(this.state.newTokenSelectedPolicies, 'default') === -1), renewable: this.state.newTokenIsRenewable } - + if (this.state.newTokenMaxUses) { params['num_uses'] = this.state.newTokenMaxUses; } @@ -340,54 +579,54 @@ export default class TokenManage extends React.Component { .catch((error) => { // Despite our efforts, the request failed. show why this.setState({ - loading: false, - snackBarMsg: `Server returned status ${error.response.status}: ${_.join(error.response.data.errors)}` - }) + loading: false + }); + snackBarMessage(error.toString()); }); } - + const NewTokenDialogAction = [ this.setState({ newTokenDialog: false })} />, - , + , ]; - + const NewTokenCodeDialogActions = [ this.setState({ newTokenCode: '', newTokenDialog: false })} /> ]; - + let policiesItems = this.state.newTokenAvailablePolicies.map((policy, idx) => { - if ( policy != "default" && policy != "root") { + if (policy != "default" && policy != "root") { return ( {handlePoliciesCheckUncheck(policy, iic)}}/>} + leftCheckbox={ { handlePoliciesCheckUncheck(policy, iic) } } />} primaryText={policy} - /> + /> ) } }); return (
- this.setState({ newTokenDialog: false})} - > - + this.setState({ newTokenDialog: false })} + > + {this.setState({newTokenDisplayName: e.target.value})}} + onChange={(e) => { this.setState({ newTokenDisplayName: e.target.value }) } } autoFocus - /> + /> {this.setState({newTokenMaxUses: Math.max(0,Number(e.target.value))})}} - /> + onChange={(e) => { this.setState({ newTokenMaxUses: Math.max(0, Number(e.target.value)) }) } } + /> {this.setState({newTokenOverrideTTL: Math.max(0,Number(e.target.value))})}} - /> + onChange={(e) => { this.setState({ newTokenOverrideTTL: Math.max(0, Number(e.target.value)) }) } } + /> Settings this.setState({newTokenIsOrphan: v}) } - /> + onToggle={(e, v) => this.setState({ newTokenIsOrphan: v })} + /> } primaryText="Orphan Token" - /> + /> handlePoliciesCheckUncheck('default', v) } - /> + onToggle={(e, v) => handlePoliciesCheckUncheck('default', v)} + /> } primaryText="Default Policy" - /> + /> this.setState({newTokenIsRenewable: v}) } - /> + onToggle={(e, v) => this.setState({ newTokenIsRenewable: v })} + /> } primaryText="Renewable" - /> + /> Assign Additional Policies {policiesItems} - - + + - +
- } label="Copy to Clipboard" onTouchTap={() => { copy(this.state.newTokenCode) }} /> + /> + } label="Copy to Clipboard" onTouchTap={() => { copy(this.state.newTokenCode) } } />
@@ -471,7 +710,7 @@ export default class TokenManage extends React.Component { {this.renderRevokeConfirmDialog()} {this.renderAccessorInfoDialog()} {this.renderNewTokenDialog()} -

Tokens

+ {this.renderRoleDialog()} @@ -558,57 +797,17 @@ export default class TokenManage extends React.Component { primary={true} label="NEW ROLE" disabled={this.state.newTokenBtnDisabled} - onTouchTap={() => { - this.setState({ - newTokenDialog: true, - newTokenCodeDialog: false, - newTokenCode: '', - newTokenSelectedPolicies: ['default'], - newTokenIsOrphan: false, - newTokenIsRenewable: true, - newTokenMaxUses: 0, - newTokenOverrideTTL: 0 - }) - } } + />
- }> - this.setState({ accessorInfoDialog: true })} - /> - - { - this.setState({ revokeConfirmDialog: true, revokeAccessorId: this.state.selectedAccessor }) - } } - /> - - - } - > + {this.renderRoleListItems()}
- this.setState({ snackBarMsg: '' })} - autoHideDuration={4000} - onRequestClose={() => this.setState({ snackBarMsg: '' })} - /> ); } diff --git a/app/components/Tokens/tokens.css b/app/components/Tokens/tokens.css index 083832c..32efa4e 100644 --- a/app/components/Tokens/tokens.css +++ b/app/components/Tokens/tokens.css @@ -50,4 +50,10 @@ span.policiesList > div > div > div { position: absolute !important; right: 4px; top: 0px; +} + +.TokenFromRoleBtn { + position: absolute; + right: 64px; + bottom: 10px; } \ No newline at end of file From aef094ecdc65b7294bbe93b5d807dfd46c5ea122 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Fri, 3 Feb 2017 02:07:51 +1100 Subject: [PATCH 12/38] Fix bugs --- app/components/Tokens/Manage.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/components/Tokens/Manage.jsx b/app/components/Tokens/Manage.jsx index 631c5f3..544957f 100644 --- a/app/components/Tokens/Manage.jsx +++ b/app/components/Tokens/Manage.jsx @@ -313,7 +313,13 @@ export default class TokenManage extends React.Component { this.setState({ roleList: resp.data.data.keys }); - }); + }) + .catch((err) => { + // This endpoint returns 404 when no roles are configured + if (err.response.status != 404) { + snackBarMessage(err); + } + }) }) .catch(() => { snackBarMessage(new Error('You don\' have enough permissions to list roles')); @@ -382,7 +388,7 @@ export default class TokenManage extends React.Component { let handlePoliciesCheckUncheck = (policy, isInputChecked) => { let role = this.state.roleAttributes if (isInputChecked) { - role.allowed_policies = _.union(role.allowed_policies, policy); + role.allowed_policies = _.union(role.allowed_policies, [policy]); } else { role.allowed_policies = _.without(role.allowed_policies, policy); } From bceb84bf0e2607bf5081d121b189cc7d08ea38fe Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Fri, 3 Feb 2017 02:27:19 +1100 Subject: [PATCH 13/38] Fixes in snackbar messages --- app/components/Tokens/Manage.jsx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/components/Tokens/Manage.jsx b/app/components/Tokens/Manage.jsx index 544957f..4767588 100644 --- a/app/components/Tokens/Manage.jsx +++ b/app/components/Tokens/Manage.jsx @@ -200,7 +200,7 @@ export default class TokenManage extends React.Component { .catch(snackBarMessage) }) .catch(() => { - snackBarMessage(new Error(`No permissions to read content of role ${his.state.selectedRole}`)); + snackBarMessage(`No permissions to read content of role ${his.state.selectedRole}`); this.setState({ selectedRole: '' }); }) } @@ -296,7 +296,7 @@ export default class TokenManage extends React.Component { tokenHasCapabilities(['sudo', 'list'], 'auth/token/accessors') .then(() => { - return callVaultApi('get', 'auth/token/accessors', { 'list': true }).then((resp) => { + return callVaultApi('get', 'auth/token/accessors', { list: true }).then((resp) => { this.setState({ fullAccessorList: resp.data.data.keys, totalPages: Math.ceil(resp.data.data.keys.length / this.state.maxItemsPerPage) @@ -304,12 +304,12 @@ export default class TokenManage extends React.Component { }); }) .catch(() => { - snackBarMessage(new Error('You don\' have enough permissions to list accessors')); + snackBarMessage('You don\' have enough permissions to list accessors'); }); tokenHasCapabilities(['list'], 'auth/token/roles') .then(() => { - return callVaultApi('get', 'auth/token/roles', { 'list': true }).then((resp) => { + return callVaultApi('get', 'auth/token/roles', { list: true }).then((resp) => { this.setState({ roleList: resp.data.data.keys }); @@ -317,12 +317,12 @@ export default class TokenManage extends React.Component { .catch((err) => { // This endpoint returns 404 when no roles are configured if (err.response.status != 404) { - snackBarMessage(err); + snackBarMessage(err.toString()); } }) }) .catch(() => { - snackBarMessage(new Error('You don\' have enough permissions to list roles')); + snackBarMessage('You don\' have enough permissions to list roles'); }); } @@ -398,10 +398,14 @@ export default class TokenManage extends React.Component { let handleSubmitAction = () => { this.setState({ loading: true }); let vault_endpoint; - if (this.state.newRoleName) + let message; + if (this.state.newRoleName) { vault_endpoint = 'auth/token/roles/' + this.state.newRoleName; - else + message = `Role ${this.state.newRoleName} created`; + } else { vault_endpoint = 'auth/token/roles/' + this.state.selectedRole; + message = `Role ${this.state.selectedRole} updated`; + } let role = this.state.roleAttributes; delete role.name; @@ -416,7 +420,7 @@ export default class TokenManage extends React.Component { selectedRole: '', roleDialogOpen: false, }); - snackBarMessage("DONE") + snackBarMessage(message); }) .catch((error) => { // Despite our efforts, the request failed. show why From c65bd4ef0caabf886a70a7e681dcbf25114eb99b Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Fri, 3 Feb 2017 23:36:36 +1100 Subject: [PATCH 14/38] Complete implementation of various role functions --- app/components/Tokens/Manage.jsx | 214 ++++++++++++++++++++++++------- 1 file changed, 166 insertions(+), 48 deletions(-) diff --git a/app/components/Tokens/Manage.jsx b/app/components/Tokens/Manage.jsx index 4767588..ac301bb 100644 --- a/app/components/Tokens/Manage.jsx +++ b/app/components/Tokens/Manage.jsx @@ -27,6 +27,7 @@ import UltimatePagination from 'react-ultimate-pagination-material-ui' import Avatar from 'material-ui/Avatar'; import ActionClass from 'material-ui/svg-icons/action/class'; import ActionDelete from 'material-ui/svg-icons/action/delete'; +import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; function snackBarMessage(message) { let ev = new CustomEvent("snackbar", { detail: { message: message } }); @@ -37,6 +38,17 @@ export default class TokenManage extends React.Component { constructor(props) { super(props); + this.defaultRoleAttributes = { + name: '', + allowed_policies: [], + disallowed_policies: [], + orphan: false, + period: 0, + renewable: true, + path_suffix: '', + explicit_max_ttl: 0 + }; + this.state = { loading: false, accessorListError: "", @@ -65,19 +77,11 @@ export default class TokenManage extends React.Component { maxItemsPerPage: 10, revokeBtnDisabled: true, roleList: [], - roleAttributes: { - name: '', - allowed_policies: [], - disallowed_policies: [], - orphan: false, - period: 0, - renewable: true, - path_suffix: '', - explicit_max_ttl: '' - }, + roleAttributes: this.defaultRoleAttributes, selectedRole: '', newRoleName: '', - roleDialogOpen: false + roleDialogOpen: false, + roleDeleteDialogOpen: false, }; this.styles = { @@ -99,7 +103,11 @@ export default class TokenManage extends React.Component { 'renderRevokeConfirmDialog', 'revokeAccessor', 'renderAccessorInfoDialog', - 'renderNewTokenDialog' + 'renderNewTokenDialog', + 'reloadRoles', + 'reloadAccessors', + 'renderRoleDeleteConfirmDialog', + 'DeleteRole' ) } @@ -159,8 +167,21 @@ export default class TokenManage extends React.Component { return _.map(this.state.roleList, (role) => { let action = ( - - + { + tokenHasCapabilities(['delete'], 'auth/token/roles/' + role).then(() => { + if (window.localStorage.getItem("showDeleteModal") === 'false') { + this.DeleteRole(role); + } else { + this.setState({ roleDeleteDialogOpen: true, selectedRole: role }) + } + }).catch(() => { + snackBarMessage(new Error("Access denied").toString()); + }) + } } + > + {window.localStorage.getItem("showDeleteModal") === 'false' ? : } ) @@ -171,7 +192,7 @@ export default class TokenManage extends React.Component { leftAvatar={} />} rightIconButton={action} onTouchTap={(e, v) => { - this.setState({ selectedRole: role }); + this.setState({ selectedRole: role, newRoleName: '' }); } } >
@@ -179,6 +200,24 @@ export default class TokenManage extends React.Component { hoverColor={green100} label="Create token from role" primary={true} + onTouchTap={(e) => { + e.stopPropagation(); + tokenHasCapabilities(['update'], 'auth/token/roles/' + role).then(() => { + callVaultApi('post', 'auth/token/create/' + role, null, null) + .then((resp) => { + this.reloadAccessors(); + this.setState({ + newTokenCode: resp.data.auth.client_token + }); + }) + .catch((error) => { + // Despite our efforts, the request failed. show why + snackBarMessage(error.toString()); + }); + }).catch(() => { + snackBarMessage(new Error("Access denied").toString()); + }) + } } />
@@ -210,7 +249,7 @@ export default class TokenManage extends React.Component { this.updateAccessorList(this.state.currentPage); } - if (this.state.selectedRole && this.state.selectedRole !== prevState.selectedRole) { + if (this.state.selectedRole && !this.state.roleDeleteDialogOpen && this.state.selectedRole !== prevState.selectedRole) { this.displayRole() } } @@ -256,6 +295,42 @@ export default class TokenManage extends React.Component { } } + reloadRoles() { + tokenHasCapabilities(['list'], 'auth/token/roles') + .then(() => { + return callVaultApi('get', 'auth/token/roles', { list: true }).then((resp) => { + this.setState({ + roleList: resp.data.data.keys + }); + }) + .catch((err) => { + // This endpoint returns 404 when no roles are configured + if (err.response.status != 404) { + snackBarMessage(err.toString()); + } + }) + }) + .catch(() => { + snackBarMessage('You don\' have enough permissions to list roles'); + }); + } + + reloadAccessors() { + tokenHasCapabilities(['sudo', 'list'], 'auth/token/accessors') + .then(() => { + return callVaultApi('get', 'auth/token/accessors', { list: true }).then((resp) => { + this.setState({ + fullAccessorList: resp.data.data.keys, + accessorList: [], + totalPages: Math.ceil(resp.data.data.keys.length / this.state.maxItemsPerPage) + }); + }); + }) + .catch(() => { + snackBarMessage('You don\' have enough permissions to list accessors'); + }); + } + componentDidMount() { // Check if user is allowed to create new tokens @@ -293,39 +368,11 @@ export default class TokenManage extends React.Component { // Not allowed to create. Disable button this.setState({ newTokenBtnDisabled: true }); }) - - tokenHasCapabilities(['sudo', 'list'], 'auth/token/accessors') - .then(() => { - return callVaultApi('get', 'auth/token/accessors', { list: true }).then((resp) => { - this.setState({ - fullAccessorList: resp.data.data.keys, - totalPages: Math.ceil(resp.data.data.keys.length / this.state.maxItemsPerPage) - }); - }); - }) - .catch(() => { - snackBarMessage('You don\' have enough permissions to list accessors'); - }); - - tokenHasCapabilities(['list'], 'auth/token/roles') - .then(() => { - return callVaultApi('get', 'auth/token/roles', { list: true }).then((resp) => { - this.setState({ - roleList: resp.data.data.keys - }); - }) - .catch((err) => { - // This endpoint returns 404 when no roles are configured - if (err.response.status != 404) { - snackBarMessage(err.toString()); - } - }) - }) - .catch(() => { - snackBarMessage('You don\' have enough permissions to list roles'); - }); + this.reloadRoles(); + this.reloadAccessors(); } + revokeAccessor(id) { tokenHasCapabilities(['update'], 'auth/token/revoke-accessor').then(() => { callVaultApi('post', 'auth/token/revoke-accessor', {}, { accessor: id }).then(() => { @@ -382,6 +429,41 @@ export default class TokenManage extends React.Component { ) } + DeleteRole(rolename) { + callVaultApi('delete', 'auth/token/roles/' + rolename, null, null, null) + .then((resp) => { + this.reloadRoles() + snackBarMessage(`Role ${rolename} deleted`); + }) + .catch((err) => { + snackBarMessage(err.toString()); + }) + } + + renderRoleDeleteConfirmDialog() { + const actions = [ + this.setState({ roleDeleteDialogOpen: false, selectedRole: '' })} />, + submitDelete()} /> + ]; + + let submitDelete = () => { + this.DeleteRole(this.state.selectedRole); + this.setState({ roleDeleteDialogOpen: false, selectedRole: '' }); + } + + return ( + this.setState({ roleDeleteDialogOpen: false })} + > +

You are about to permanently delete {this.state.selectedRole}. Are you sure?

+ To disable this prompt, visit the settings page. +
+ ) + } renderRoleDialog() { @@ -396,6 +478,17 @@ export default class TokenManage extends React.Component { }; let handleSubmitAction = () => { + + if (_.indexOf(this.state.roleList, this.state.newRoleName) !== -1) { + snackBarMessage("A role with the same name already exists"); + return; + } + + if (!this.state.selectedRole && !this.state.newRoleName) { + snackBarMessage("Role name cannot be empty"); + return; + } + this.setState({ loading: true }); let vault_endpoint; let message; @@ -419,7 +512,9 @@ export default class TokenManage extends React.Component { loading: false, selectedRole: '', roleDialogOpen: false, + newRoleName: '' }); + this.reloadRoles(); snackBarMessage(message); }) .catch((error) => { @@ -453,7 +548,7 @@ export default class TokenManage extends React.Component { return (
this.setState({ roleDialogOpen: false })} > + {this.state.selectedRole == '' ? + { + this.setState({ newRoleName: e.target.value }); + } } + /> + : ''} { + this.reloadAccessors(); this.setState({ loading: false, newTokenCode: resp.data.auth.client_token @@ -721,6 +830,7 @@ export default class TokenManage extends React.Component { {this.renderAccessorInfoDialog()} {this.renderNewTokenDialog()} {this.renderRoleDialog()} + {this.renderRoleDeleteConfirmDialog()} @@ -807,7 +917,15 @@ export default class TokenManage extends React.Component { primary={true} label="NEW ROLE" disabled={this.state.newTokenBtnDisabled} + onTouchTap={() => { + this.setState({ + selectedRole: '', + newRoleName: '', + roleAttributes: this.defaultRoleAttributes, + roleDialogOpen: true + }) + } } /> From cdfcf8ee6d3a4eed1a1d0a1454fde0d27bc51c28 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sat, 4 Feb 2017 00:03:19 +1100 Subject: [PATCH 15/38] Fix unexpected behaviour when using referenced objects --- app/components/Tokens/Manage.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/Tokens/Manage.jsx b/app/components/Tokens/Manage.jsx index ac301bb..7756a45 100644 --- a/app/components/Tokens/Manage.jsx +++ b/app/components/Tokens/Manage.jsx @@ -500,7 +500,7 @@ export default class TokenManage extends React.Component { message = `Role ${this.state.selectedRole} updated`; } - let role = this.state.roleAttributes; + let role = _.clone(this.state.roleAttributes); delete role.name; role.allowed_policies = role.allowed_policies.join(','); role.disallowed_policies = role.disallowed_policies.join(','); @@ -922,7 +922,7 @@ export default class TokenManage extends React.Component { this.setState({ selectedRole: '', newRoleName: '', - roleAttributes: this.defaultRoleAttributes, + roleAttributes: _.clone(this.defaultRoleAttributes), roleDialogOpen: true }) } } From a01c637cd14a043abea08b0512876d9b8147c8e6 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Mon, 6 Feb 2017 11:37:24 +1100 Subject: [PATCH 16/38] First commit policy picker component --- .../shared/PolicyPicker/PolicyPicker.jsx | 218 ++++++++++++++++++ .../shared/PolicyPicker/policypicker.css | 62 +++++ 2 files changed, 280 insertions(+) create mode 100644 app/components/shared/PolicyPicker/PolicyPicker.jsx create mode 100644 app/components/shared/PolicyPicker/policypicker.css diff --git a/app/components/shared/PolicyPicker/PolicyPicker.jsx b/app/components/shared/PolicyPicker/PolicyPicker.jsx new file mode 100644 index 0000000..5e64517 --- /dev/null +++ b/app/components/shared/PolicyPicker/PolicyPicker.jsx @@ -0,0 +1,218 @@ +import React, { PropTypes } from 'react'; +import { callVaultApi, tokenHasCapabilities } from '../VaultUtils.jsx' +import _ from 'lodash'; +import { browserHistory } from 'react-router'; +import { List, ListItem } from 'material-ui/List'; +import Subheader from 'material-ui/Subheader'; +import styles from './policypicker.css'; +import { lightBlue50, indigo400 } from 'material-ui/styles/colors.js' +import KeyboardArrowRight from 'material-ui/svg-icons/hardware/keyboard-arrow-right'; +import KeyboardArrowLeft from 'material-ui/svg-icons/hardware/keyboard-arrow-left'; +import AutoComplete from 'material-ui/AutoComplete'; +import Search from 'material-ui/svg-icons/action/search'; +import Paper from 'material-ui/Paper'; +import Clear from 'material-ui/svg-icons/content/clear'; +import { Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle } from 'material-ui/Toolbar'; +import TextField from 'material-ui/TextField'; +import { Menu, MenuItem } from 'material-ui/Menu'; + +class PolicyPicker extends React.Component { + static propTypes = { + onError: PropTypes.func, + onSelectedChange: PropTypes.func, + excludePolicies: PropTypes.array, + title: PropTypes.string, + height: PropTypes.string, + }; + + static defaultProps = { + onError: (err) => { console.error(err) }, + onSelectedChange: (selectedPolicies) => {}, + excludePolicies: ['default'], + title: "Policy Picker", + height: "300px", + }; + + constructor(props) { + super(props); + + this.state = { + availablePolicies: [], + displayedAvailPolicies: [], + selectedPolicies: [], + manualPolicies: [], + policyListAvailable: true, + searchText: '' + }; + + _.bindAll( + this, + 'reloadPolicyList', + 'selectedPolicyAdd', + 'selectedPolicyRemove' + ) + }; + + reloadPolicyList() { + tokenHasCapabilities(['read'], 'sys/policy') + .then(() => { + callVaultApi('get', 'sys/policy', null, null, null) + .then((resp) => { + + let policyList = _.filter(resp.data.data.keys, (item) => { + return (!_.includes(this.props.excludePolicies, item)) && ( item !== 'root'); + }) + + this.setState({ + availablePolicies: policyList, + policyListAvailable: true + }); + }) + .catch(this.props.onError) + }) + .catch(() => { + this.setState({ policyListAvailable: false }) + }) + } + + componentDidMount() { + this.reloadPolicyList(); + } + + componentWillUpdate(nextProps, nextState) { + if (!_.isEqual(this.state.selectedPolicies.sort(), nextState.selectedPolicies.sort())) { + this.props.onSelectedChange(nextState.selectedPolicies); + } + } + + + componentDidUpdate(prevProps, prevState) { + let newAvailPol = _(this.state.availablePolicies).difference(this.state.selectedPolicies).value(); + + if ( + (!_.isEqual(this.state.selectedPolicies.sort(), prevState.selectedPolicies.sort())) || + (!_.isEqual(this.state.availablePolicies.sort(), prevState.availablePolicies.sort())) || + (this.state.searchText !== prevState.searchText) + ) { + let newList = _.filter(newAvailPol, (item) => { + return _.includes(item, this.state.searchText); + }); + this.setState({ + displayedAvailPolicies: newList + }); + } + + + + } + + + selectedPolicyAdd(v) { + this.setState({ + selectedPolicies: _(this.state.selectedPolicies).concat(v).value(), + displayedAvailPolicies: _(this.state.displayedAvailPolicies).without(v).value(), + }) + } + + selectedPolicyRemove(v) { + this.setState({ + selectedPolicies: _(this.state.selectedPolicies).without(v).value(), + displayedAvailPolicies: _(this.state.displayedAvailPolicies).concat(v).value(), + }) + } + + render() { + + let renderAvailablePoliciesListItems = () => { + return _.map(this.state.displayedAvailPolicies, (key) => { + return ( + { this.selectedPolicyAdd(key) } } + key={key} + rightIcon={} + primaryText={key} + /> + ) + }); + }; + + let renderSelectedPoliciesListItems = () => { + return _.map(this.state.selectedPolicies, (key) => { + let style = {}; + + if (!_(this.state.availablePolicies).includes(key)) { + style = { color: "#FF7043" } + } + return ( + { this.selectedPolicyRemove(key) } } + style={style} + key={key} + rightIcon={} + primaryText={key} + /> + ) + }); + }; + + return ( +
+
+ + + + + + + +
+ + {renderAvailablePoliciesListItems()} + +
+
+
+ + + + + + + { + this.setState({ + searchText: searchText + }); + } } + onNewRequest={(chosenRequest, index) => { + if ( + (!_.includes(this.props.excludePolicies, chosenRequest)) && + (chosenRequest !== 'root') + ) { + this.selectedPolicyAdd(chosenRequest); + this.setState({ + searchText: '' + }) + } + } } + /> + + +
+ + {renderSelectedPoliciesListItems()} + +
+
+
+ ) + }; + +} + +export default PolicyPicker; \ No newline at end of file diff --git a/app/components/shared/PolicyPicker/policypicker.css b/app/components/shared/PolicyPicker/policypicker.css new file mode 100644 index 0000000..1dc5a44 --- /dev/null +++ b/app/components/shared/PolicyPicker/policypicker.css @@ -0,0 +1,62 @@ +.ppOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.5; + background-color: red; +} + +.ppTitle { + /*background-color: #ECEFF1; + color: #0C0C0C; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + margin: 5px;*/ + text-align: center; +} + +.ppColumn { + display: inline-block; + vertical-align: top; + width: 49%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + background-color: #E1F5FE; + color: #fff; + /*padding: 10px;*/ + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + overflow: auto; +} + +.ppToolbar span { + font-size: 12px !important; + color: black !important; + font-weight: bold !important; +} + + +.ppListContainer { + overflow-y: auto; + margin: 5px; +} + +.ppListheader { + +} + +.ppList > div > div{ + font-family: monospace; + line-height: 1px; +} + +.ppList svg{ + font-family: monospace; + line-height: 1px; + margin: 0px !important; + top: 5px !important; +} + From 0f3ad28959cfbce9f5776dcadf01bfac53bd7159 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 9 Feb 2017 14:27:55 +1100 Subject: [PATCH 17/38] Re enable webpack --- docker-compose.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 1fac282..3ebaad9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -25,9 +25,9 @@ services: VAULT_AUTH_DEFAULT: USERNAMEPASSWORD VAULT_SUPPLIED_TOKEN_HEADER: 'X-Remote-User' -# webpack: -# build: . -# volumes: -# - .:/app -# - /app/node_modules -# command: webpack -w + webpack: + build: . + volumes: + - .:/app + - /app/node_modules + command: webpack -w From f2075bb53fc4836d40fdf0b378d13b9438567306 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 9 Feb 2017 15:25:31 +1100 Subject: [PATCH 18/38] preview policy picker --- app/components/Authentication/Token/Token.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/Authentication/Token/Token.jsx b/app/components/Authentication/Token/Token.jsx index ddf25cd..6de60f6 100644 --- a/app/components/Authentication/Token/Token.jsx +++ b/app/components/Authentication/Token/Token.jsx @@ -28,6 +28,7 @@ import Avatar from 'material-ui/Avatar'; import ActionClass from 'material-ui/svg-icons/action/class'; import ActionDelete from 'material-ui/svg-icons/action/delete'; import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; +import PolicyPicker from '../../shared/PolicyPicker/PolicyPicker.jsx' function snackBarMessage(message) { let ev = new CustomEvent("snackbar", { detail: { message: message } }); @@ -640,7 +641,7 @@ export default class TokenAuthBackend extends React.Component { Allowed Policies - {policiesItems} +
@@ -796,7 +797,7 @@ export default class TokenAuthBackend extends React.Component { Assign Additional Policies - {policiesItems} + From c9554de2d11f5791dcb56906c61284e52455d213 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Mon, 13 Feb 2017 19:47:40 +1100 Subject: [PATCH 19/38] Check current token validity using lookup-self --- app/App.jsx | 55 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/app/App.jsx b/app/App.jsx index 8f5fdf6..ae53879 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -5,6 +5,7 @@ import { Router, Route, Link, browserHistory } from 'react-router' import injectTapEventPlugin from 'react-tap-event-plugin'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import getMuiTheme from 'material-ui/styles/getMuiTheme'; +import { callVaultApi } from './components/shared/VaultUtils.jsx'; import App from './components/App/App.jsx'; import SecretsGeneric from './components/Secrets/Generic/Generic.jsx'; import Health from './components/Health/Health.jsx'; @@ -19,18 +20,18 @@ injectTapEventPlugin(); (function () { - if ( typeof window.CustomEvent === "function" ) return false; + if (typeof window.CustomEvent === "function") return false; - function CustomEvent ( event, params ) { - params = params || { bubbles: false, cancelable: false, detail: undefined }; - var evt = document.createEvent( 'CustomEvent' ); - evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); - return evt; - } + function CustomEvent(event, params) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + } - CustomEvent.prototype = window.Event.prototype; + CustomEvent.prototype = window.Event.prototype; - window.CustomEvent = CustomEvent; + window.CustomEvent = CustomEvent; })(); const checkAccessToken = (nextState, replace, callback) => { @@ -38,26 +39,40 @@ const checkAccessToken = (nextState, replace, callback) => { if (!vaultAuthToken) { replace(`/login`) } - callback(); + + // Check if current token is actually valid + callVaultApi('get', 'auth/token/lookup-self') + .then(() => { + callback(); + }) + .catch((err) => { + if (err.response.status == 403) { + window.localStorage.removeItem('vaultAccessToken'); + replace(`/login`); + callback(); + } else { + callback(err); + } + }); } const muiTheme = getMuiTheme({ - fontFamily: 'Source Sans Pro, sans-serif', + fontFamily: 'Source Sans Pro, sans-serif', }); ReactDOM.render(( - + - - - - - - - - + + + + + + + + From 9e8ee585b3f367a1959b4dbb7ff940edd1424146 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Tue, 14 Feb 2017 15:16:28 +1100 Subject: [PATCH 20/38] Revert "Check current token validity using lookup-self" This reverts commit c9554de2d11f5791dcb56906c61284e52455d213. --- app/App.jsx | 55 +++++++++++++++++++---------------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/app/App.jsx b/app/App.jsx index ae53879..8f5fdf6 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -5,7 +5,6 @@ import { Router, Route, Link, browserHistory } from 'react-router' import injectTapEventPlugin from 'react-tap-event-plugin'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import getMuiTheme from 'material-ui/styles/getMuiTheme'; -import { callVaultApi } from './components/shared/VaultUtils.jsx'; import App from './components/App/App.jsx'; import SecretsGeneric from './components/Secrets/Generic/Generic.jsx'; import Health from './components/Health/Health.jsx'; @@ -20,18 +19,18 @@ injectTapEventPlugin(); (function () { - if (typeof window.CustomEvent === "function") return false; + if ( typeof window.CustomEvent === "function" ) return false; - function CustomEvent(event, params) { - params = params || { bubbles: false, cancelable: false, detail: undefined }; - var evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; - } + function CustomEvent ( event, params ) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + return evt; + } - CustomEvent.prototype = window.Event.prototype; + CustomEvent.prototype = window.Event.prototype; - window.CustomEvent = CustomEvent; + window.CustomEvent = CustomEvent; })(); const checkAccessToken = (nextState, replace, callback) => { @@ -39,40 +38,26 @@ const checkAccessToken = (nextState, replace, callback) => { if (!vaultAuthToken) { replace(`/login`) } - - // Check if current token is actually valid - callVaultApi('get', 'auth/token/lookup-self') - .then(() => { - callback(); - }) - .catch((err) => { - if (err.response.status == 403) { - window.localStorage.removeItem('vaultAccessToken'); - replace(`/login`); - callback(); - } else { - callback(err); - } - }); + callback(); } const muiTheme = getMuiTheme({ - fontFamily: 'Source Sans Pro, sans-serif', + fontFamily: 'Source Sans Pro, sans-serif', }); ReactDOM.render(( - + - - - - - - - - + + + + + + + + From ef206dbdff4bf8be698e1453923842ff0d6829a9 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Tue, 14 Feb 2017 15:16:40 +1100 Subject: [PATCH 21/38] Revert "preview policy picker" This reverts commit f2075bb53fc4836d40fdf0b378d13b9438567306. --- app/components/Authentication/Token/Token.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/components/Authentication/Token/Token.jsx b/app/components/Authentication/Token/Token.jsx index 6de60f6..ddf25cd 100644 --- a/app/components/Authentication/Token/Token.jsx +++ b/app/components/Authentication/Token/Token.jsx @@ -28,7 +28,6 @@ import Avatar from 'material-ui/Avatar'; import ActionClass from 'material-ui/svg-icons/action/class'; import ActionDelete from 'material-ui/svg-icons/action/delete'; import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; -import PolicyPicker from '../../shared/PolicyPicker/PolicyPicker.jsx' function snackBarMessage(message) { let ev = new CustomEvent("snackbar", { detail: { message: message } }); @@ -641,7 +640,7 @@ export default class TokenAuthBackend extends React.Component { Allowed Policies - + {policiesItems} @@ -797,7 +796,7 @@ export default class TokenAuthBackend extends React.Component { Assign Additional Policies - + {policiesItems} From 7a07eed417be177ce8cbf444ee9089c0f193967f Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 16 Feb 2017 19:48:20 +1100 Subject: [PATCH 22/38] First commit enhanced app header --- app/App.jsx | 1 + app/components/App/App.jsx | 111 +++++++++++++++------- app/components/Login/Login.jsx | 2 - app/components/shared/Header/Header.jsx | 90 +++++++++++++++--- app/components/shared/Header/countdown.js | 69 ++++++++++++++ app/components/shared/Header/header.css | 18 +++- app/components/shared/Menu/Menu.jsx | 7 ++ 7 files changed, 248 insertions(+), 50 deletions(-) create mode 100644 app/components/shared/Header/countdown.js diff --git a/app/App.jsx b/app/App.jsx index 8f5fdf6..71c3bf6 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -38,6 +38,7 @@ const checkAccessToken = (nextState, replace, callback) => { if (!vaultAuthToken) { replace(`/login`) } + callback(); } diff --git a/app/components/App/App.jsx b/app/components/App/App.jsx index 4b58695..4a3f321 100644 --- a/app/components/App/App.jsx +++ b/app/components/App/App.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import _ from 'lodash'; import Menu from '../shared/Menu/Menu.jsx'; import Header from '../shared/Header/Header.jsx'; @@ -9,22 +9,72 @@ import Paper from 'material-ui/Paper'; import { browserHistory } from 'react-router'; import { green500, red500, yellow500 } from 'material-ui/styles/colors.js' import styles from './app.css'; +import { callVaultApi } from '../shared/VaultUtils.jsx'; let twoMinuteWarningTimeout; let logoutTimeout; +function snackBarMessage(message) { + let ev = new CustomEvent("snackbar", { detail: { message: message } }); + document.dispatchEvent(ev); +} + export default class App extends React.Component { + constructor(props) { super(props); - this.renderLogoutDialog = this.renderLogoutDialog.bind(this); + this.state = { snackbarMessage: '', snackbarType: 'OK', snackbarStyle: {}, - namespace: '/', logoutOpen: false, - logoutPromptSeen: false + logoutPromptSeen: false, + identity: {} } + + _.bindAll( + this, + 'reloadSessionIdentity', + 'componentDidMount', + 'componentWillUnmount', + 'renderSessionExpDialog' + ); + } + + reloadSessionIdentity() { + let TWO_MINUTES = 1000 * 60 * 2; + + let twoMinuteWarningTimeout = () => { + if (!this.state.logoutPromptSeen) { + this.setState({ + logoutOpen: true + }); + } + } + + let logoutTimeout = () => { + browserHistory.push('/login'); + } + + callVaultApi('get', 'auth/token/lookup-self') + .then((resp) => { + if (_.has(resp, 'data.data')) { + this.setState({ identity: resp.data.data }) + let ttl = resp.data.data.ttl * 1000; + // The upper limit of setTimeout is 0x7FFFFFFF (or 2147483647 in decimal) + if (ttl > 0 && ttl < 2147483648) { + setTimeout(logoutTimeout, ttl); + setTimeout(twoMinuteWarningTimeout, ttl - TWO_MINUTES); + } + } + }) + .catch((err) => { + if (_.has(err, 'response.status') && err.response.status == 403) { + window.localStorage.removeItem('vaultAccessToken'); + browserHistory.push('/login'); + } else throw err; + }); } componentDidMount() { @@ -37,10 +87,10 @@ export default class App extends React.Component { document.addEventListener("snackbar", (e) => { let messageStyle = { backgroundColor: green500 }; let message = e.detail.message.toString(); - if ( e.detail.message instanceof Error ) { + if (e.detail.message instanceof Error) { // Handle logical erros from vault //debugger; - if (_.has(e.detail.message, 'response.data.errors')) + if (_.has(e.detail.message, 'response.data.errors')) if (e.detail.message.response.data.errors.length > 0) message = e.detail.message.response.data.errors.join(','); messageStyle = { backgroundColor: red500 }; @@ -53,25 +103,7 @@ export default class App extends React.Component { }); }); - let tokenExpireDate = window.localStorage.getItem('vaultAccessTokenExpiration'); - let TWO_MINUTES = 1000 * 60 * 2; - - let twoMinuteWarningTimeout = () => { - if (!this.state.logoutPromptSeen) { - this.setState({ - logoutOpen: true - }); - } - } - - let logoutTimeout = () => { - browserHistory.push('/login'); - } - // The upper limit of setTimeout is 0x7FFFFFFF (or 2147483647 in decimal) - if (tokenExpireDate >= 0 && tokenExpireDate < 2147483648) { - setTimeout(logoutTimeout, tokenExpireDate); - setTimeout(twoMinuteWarningTimeout, tokenExpireDate - TWO_MINUTES); - } + this.reloadSessionIdentity() } componentWillUnmount() { @@ -79,20 +111,33 @@ export default class App extends React.Component { clearTimeout(twoMinuteWarningTimeout); } - renderLogoutDialog() { + renderSessionExpDialog() { const actions = [ - this.setState({ logoutOpen: false, logoutPromptSeen: true })} /> + { + callVaultApi('post', 'auth/token/renew-self') + .then(() => { + this.reloadSessionIdentity(); + snackBarMessage("Session renewed"); + }) + .catch(snackBarMessage) + this.setState({ logoutOpen: false }) + }} + />, + this.setState({ logoutOpen: false, logoutPromptSeen: true })} /> ]; return ( this.setState({ logoutOpen: false, logoutPromptSeen: true })} - > -
Your token will expire in 2 minutes. You will want to finish up what you are working on!
+ > +
Your session token will expire soon. Use the renew button to request a lease extension
); } @@ -114,9 +159,9 @@ export default class App extends React.Component { autoHideDuration={3000} onRequestClose={() => this.setState({ snackbarMessage: '' })} onActionTouchTap={() => this.setState({ snackbarMessage: '' })} - /> - {this.state.logoutOpen && this.renderLogoutDialog()} -
+ /> + {this.state.logoutOpen && this.renderSessionExpDialog()} +
diff --git a/app/components/Login/Login.jsx b/app/components/Login/Login.jsx index 6d700e4..6a117c6 100644 --- a/app/components/Login/Login.jsx +++ b/app/components/Login/Login.jsx @@ -188,8 +188,6 @@ export default class Login extends React.Component { if (accessToken) { window.localStorage.setItem('capability_cache', JSON.stringify({})); window.localStorage.setItem("vaultAccessToken", accessToken); - let leaseDuration = _.get(resp, 'lease_duration') === 0 ? -1 : _.get(resp, 'lease_duration') * 1000 - window.localStorage.setItem('vaultAccessTokenExpiration', leaseDuration) window.localStorage.setItem('vaultUrl', this.getVaultUrl()); window.localStorage.setItem('loginMethodType', this.getVaultAuthMethod()); window.location.href = '/'; diff --git a/app/components/shared/Header/Header.jsx b/app/components/shared/Header/Header.jsx index 02f966d..c58ab44 100644 --- a/app/components/shared/Header/Header.jsx +++ b/app/components/shared/Header/Header.jsx @@ -1,9 +1,11 @@ -import React, { PropTypes } from 'react' -import AppBar from 'material-ui/AppBar'; +import React, { PropTypes } from 'react'; +import _ from 'lodash'; +import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; import FlatButton from 'material-ui/FlatButton'; import IconButton from 'material-ui/IconButton'; import FontIcon from 'material-ui/FontIcon'; import { browserHistory } from 'react-router'; +import CountDown from './countdown.js' import styles from './header.css'; var logout = () => { @@ -12,20 +14,80 @@ var logout = () => { } class Header extends React.Component { - render () { + constructor(props) { + super(props); + this.state = { + serverAddr: window.localStorage.getItem('vaultUrl') + } + } + + static propTypes = { + tokenIdentity: PropTypes.object + }; + + render() { + + let renderTokenInfo = () => { + + let infoSectionItems = [] + + let username; + if (_.has(this.props.tokenIdentity, 'meta.username')) { + username = this.props.tokenIdentity.meta.username; + } else { + username = this.props.tokenIdentity.display_name + } + if (username) { + infoSectionItems.push( + + logged in as + {username} + + ) + } + + infoSectionItems.push( + + connected to + {this.state.serverAddr} + + ) + + if (this.props.tokenIdentity.ttl) { + infoSectionItems.push( + + token ttl + + + + + ) + } + + return infoSectionItems; + } + return (
- Vault} - onTitleTouchTap={() =>{ - window.localStorage.removeItem('vaultAccessTokenExpiration') - browserHistory.push('/'); - }} - iconElementLeft={} - iconElementRight={} - /> -
+ + + + + + { + browserHistory.push('/'); + }} + text="VAULT-UI" /> + + + {renderTokenInfo()} + + + + + +
) } } diff --git a/app/components/shared/Header/countdown.js b/app/components/shared/Header/countdown.js new file mode 100644 index 0000000..e9d92eb --- /dev/null +++ b/app/components/shared/Header/countdown.js @@ -0,0 +1,69 @@ +// Based on https://raw.githubusercontent.com/rogermarkussen/react.timer/master/src/countdown.js + +import React, { PropTypes, Component } from 'react' + +class CountDown extends Component { + + + static propTypes = { + startTime: PropTypes.number, + className: PropTypes.string + } + + constructor(props) { + super(props) + this.state = { time: props.startTime * 10 } + this.tick = this.tick.bind(this) + this.splitTimeComponents = this.splitTimeComponents.bind(this) + this.stopTime = Date.now() + (props.startTime * 1000) + } + + componentWillReceiveProps(nextProps) { + if(nextProps.startTime != this.props.startTime) { + clearInterval(this.time); + this.time = nextProps.startTime; + this.stopTime = Date.now() + (nextProps.startTime * 1000) + this.time = setInterval(this.tick, 100) + } + } + + componentDidMount() { + this.time = setInterval(this.tick, 100) + } + + componentWillUnmount() { + clearInterval(this.time) + } + + splitTimeComponents() { + const time = this.state.time / 10 + var delta = Math.floor(time) + var days = Math.floor(delta / 86400); + delta -= days * 86400; + var hours = Math.floor(delta / 3600) % 24; + delta -= hours * 3600; + var minutes = Math.floor(delta / 60) % 60; + delta -= minutes * 60; + var seconds = delta % 60; + + return `${days}d ${hours}h ${minutes}m ${seconds}s`; + } + + tick() { + const now = Date.now() + if (this.stopTime - now <= 0) { + this.setState({ time: 0 }) + clearInterval(this.time) + } else this.setState({ time: Math.round((this.stopTime - now) / 100) }) + } + + render() { + const time = this.state.time / 10 + const seconds = Math.floor(time) + return {this.splitTimeComponents()} + } +} +CountDown.propTypes = { + startTime: PropTypes.number.isRequired +} +export default CountDown diff --git a/app/components/shared/Header/header.css b/app/components/shared/Header/header.css index a391f69..436194b 100644 --- a/app/components/shared/Header/header.css +++ b/app/components/shared/Header/header.css @@ -1,5 +1,6 @@ -#title { +.title { cursor: pointer; + color: white !important; } #headerWrapper { @@ -8,3 +9,18 @@ top: 0; z-index: 1; } + +.infoSectionItem { + color: white; + padding: 0px 10px 0px 10px; +} + +.infoSectionItemKey { + font-variant: small-caps; + padding-right: 5px; + color: darkgrey; +} + +.infoSectionItemValue { + font-family: monospace; +} \ No newline at end of file diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index 39c6e2b..4d8a181 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -46,6 +46,13 @@ class Menu extends React.Component { ] }; + componentWillReceiveProps (nextProps) { + if(this.props.pathname != nextProps.pathname) { + this.setState({selectedPath: nextProps.pathname}); + } + } + + componentDidMount() { tokenHasCapabilities(['read'], 'sys/mounts') .then(() => { From 828964c7f09df011cc8fe0ed6cd16240c85dd8ea Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 16 Feb 2017 21:57:38 +1100 Subject: [PATCH 23/38] URL addressable policies --- app/App.jsx | 2 +- app/components/Policies/Manage.jsx | 84 ++++++++++++++++++++-------- app/components/shared/Menu/Menu.jsx | 2 +- app/components/shared/VaultUtils.jsx | 4 +- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/app/App.jsx b/app/App.jsx index 71c3bf6..71747b3 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -58,7 +58,7 @@ ReactDOM.render(( - + diff --git a/app/components/Policies/Manage.jsx b/app/components/Policies/Manage.jsx index 99773d4..46e5866 100644 --- a/app/components/Policies/Manage.jsx +++ b/app/components/Policies/Manage.jsx @@ -15,11 +15,12 @@ import FontIcon from 'material-ui/FontIcon'; import JsonEditor from '../shared/JsonEditor.jsx'; import hcltojson from 'hcl-to-json' import jsonschema from './vault-policy-schema.json' -import { callVaultApi } from '../shared/VaultUtils.jsx' +import { callVaultApi, tokenHasCapabilities } from '../shared/VaultUtils.jsx' import Avatar from 'material-ui/Avatar'; import HardwareSecurity from 'material-ui/svg-icons/hardware/security'; import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; import ActionDelete from 'material-ui/svg-icons/action/delete'; +import { browserHistory, Link } from 'react-router' function snackBarMessage(message) { let ev = new CustomEvent("snackbar", { detail: { message: message } }); @@ -27,6 +28,10 @@ function snackBarMessage(message) { } export default class PolicyManager extends React.Component { + static propTypes = { + params: PropTypes.object.isRequired, + }; + constructor(props) { super(props); this.state = { @@ -47,20 +52,34 @@ export default class PolicyManager extends React.Component { _.bindAll( this, 'updatePolicy', + 'displayPolicy', 'listPolicies', 'policyChangeSetState', 'renderEditDialog', 'renderNewPolicyDialog', 'renderDeleteConfirmationDialog', - 'clickPolicy', 'showDelete', 'renderPolicies', 'deletePolicy' ) } - componentWillMount() { - this.listPolicies(); + componentDidMount() { + if (this.props.params.splat) { + this.displayPolicy(); + } else { + this.listPolicies(); + } + } + + componentDidUpdate(prevProps, prevState) { + if (!_.isEqual(this.props.params, prevProps.params)) { + if (this.props.params.splat) { + this.displayPolicy(); + } else { + this.listPolicies(); + } + } } policyChangeSetState(v, syntaxCheckOk, schemaCheckOk) { @@ -73,8 +92,23 @@ export default class PolicyManager extends React.Component { renderEditDialog() { const actions = [ - this.setState({ openEditModal: false })} />, - this.updatePolicy(this.state.focusPolicy, false)} /> + { + this.setState({ openEditModal: false }) + browserHistory.push('/sys/policies'); + }} + />, + { + this.updatePolicy(this.state.focusPolicy, false) + browserHistory.push('/sys/policies'); + }} + /> ]; return ( @@ -85,14 +119,14 @@ export default class PolicyManager extends React.Component { open={this.state.openEditModal} onRequestClose={() => this.setState({ openEditModal: false })} autoScrollBodyContent={true} - > + > + /> ); } @@ -139,7 +173,7 @@ export default class PolicyManager extends React.Component { onRequestClose={() => this.setState({ openNewPolicyModal: false, newPolicyErrorMessage: '' })} autoScrollBodyContent={true} autoDetectWindowHeight={true} - > + > + /> + />
{this.state.newPolicyErrorMessage}
); @@ -173,7 +207,7 @@ export default class PolicyManager extends React.Component { actions={actions} open={this.state.openDeleteModal} onRequestClose={() => this.setState({ openDeleteModal: false, newPolicyErrorMessage: '' })} - > + >

You are about to permanently delete {this.state.deletingPolicy}. Are you sure?

To disable this prompt, visit the settings page. @@ -224,8 +258,8 @@ export default class PolicyManager extends React.Component { }); } - clickPolicy(policyName) { - callVaultApi('get', `sys/policy/${encodeURI(policyName)}`, null, null, null) + displayPolicy() { + callVaultApi('get', `sys/policy/${encodeURI(this.props.params.splat)}`, null, null, null) .then((resp) => { let rules = _.get(resp, 'data.data.rules', _.get(resp, 'data.rules', {})); let rules_obj; @@ -242,15 +276,13 @@ export default class PolicyManager extends React.Component { if (rules_obj) { this.setState({ openEditModal: true, - focusPolicy: policyName, + focusPolicy: this.props.params.splat, currentPolicy: rules_obj, disableSubmit: true }); } }) - .catch((err) => { - console.error(err.stack); - }); + .catch(snackBarMessage); } deletePolicy(policyName) { @@ -285,10 +317,10 @@ export default class PolicyManager extends React.Component { } else { this.setState({ deletingPolicy: policyName, openDeleteModal: true }) } - } } - > - { window.localStorage.getItem("showDeleteModal") === 'false' ? : } - + }} + > + {window.localStorage.getItem("showDeleteModal") === 'false' ? : } + ); } @@ -298,7 +330,13 @@ export default class PolicyManager extends React.Component { } />} - onTouchTap={() => { this.clickPolicy(policy.name) } } + onTouchTap={() => { + tokenHasCapabilities(['read'], 'sys/policy/' + policy.name).then(() => { + browserHistory.push(`/sys/policies/` + policy.name); + }).catch(() => { + snackBarMessage(new Error("Access denied")); + }) + }} primaryText={
{policy.name}
} rightIconButton={this.showDelete(policy.name)}>
diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index 4d8a181..60ddb9b 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -145,7 +145,7 @@ class Menu extends React.Component { primaryTogglesNestedList={true} initiallyOpen={true} nestedItems={[ - , + , ]} /> diff --git a/app/components/shared/VaultUtils.jsx b/app/components/shared/VaultUtils.jsx index c71d608..a152692 100644 --- a/app/components/shared/VaultUtils.jsx +++ b/app/components/shared/VaultUtils.jsx @@ -45,8 +45,7 @@ function callVaultApi(method, path, query = {}, data, headers = {}) { } function tokenHasCapabilities(capabilities, path) { - - if (window.localStorage.getItem('enableCapabilitiesCache')) { + if (window.localStorage.getItem('enableCapabilitiesCache') == "true") { try { var cached_capabilities = getCachedCapabilities(path); // At this point we have a result from the cache we can return the value in a form of a resolved promise @@ -71,7 +70,6 @@ function tokenHasCapabilities(capabilities, path) { let has_cap = _.indexOf(resp.data.capabilities, v) !== -1; return has_cap; }); - if (evaluation || _.indexOf(resp.data.capabilities, 'root') !== -1) { return Promise.resolve(true); } From 2d7745bf3246110c27df7e97ae4bc362ca09fb0f Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Thu, 16 Feb 2017 22:16:48 +1100 Subject: [PATCH 24/38] Restyle breadcrumb nav --- app/components/Secrets/Generic/Generic.jsx | 6 +++--- app/components/Secrets/Generic/generic.css | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index 94c8361..e2139b0 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -402,7 +402,7 @@ class GenericSecretBackend extends React.Component { let components = _.initial(this.getBaseDir(this.state.currentLogicalPath).split('/')); return _.map(components, (dir, index) => { var relativelink = [].concat(components).slice(0, index + 1).join('/') + '/'; - return ({dir}) + return (}>{dir}) }); } @@ -439,9 +439,9 @@ class GenericSecretBackend extends React.Component { } + connector={/} > {renderBreadcrumb()} diff --git a/app/components/Secrets/Generic/generic.css b/app/components/Secrets/Generic/generic.css index ad79d34..6b5fe88 100644 --- a/app/components/Secrets/Generic/generic.css +++ b/app/components/Secrets/Generic/generic.css @@ -1 +1,4 @@ -/*Place Holder*/ \ No newline at end of file +.breadCrumb { + justifyContent: 'flex-start'; + fontWeight: 600 +} \ No newline at end of file From de85678d2126ec71ce0908eb3eb09172cb2f11e0 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Fri, 17 Feb 2017 18:43:59 +1100 Subject: [PATCH 25/38] Better error handling --- app/components/Authentication/Token/Token.jsx | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/app/components/Authentication/Token/Token.jsx b/app/components/Authentication/Token/Token.jsx index ddf25cd..443f3dc 100644 --- a/app/components/Authentication/Token/Token.jsx +++ b/app/components/Authentication/Token/Token.jsx @@ -177,7 +177,7 @@ export default class TokenAuthBackend extends React.Component { this.setState({ roleDeleteDialogOpen: true, selectedRole: role }) } }).catch(() => { - snackBarMessage(new Error("Access denied").toString()); + snackBarMessage(new Error("Access denied")); }) } } > @@ -210,12 +210,9 @@ export default class TokenAuthBackend extends React.Component { newTokenCode: resp.data.auth.client_token }); }) - .catch((error) => { - // Despite our efforts, the request failed. show why - snackBarMessage(error.toString()); - }); - }).catch(() => { - snackBarMessage(new Error("Access denied").toString()); + .catch(snackBarMessage) + }).catch((err) => { + snackBarMessage(err || new Error("Access denied")); }) } } /> @@ -238,8 +235,8 @@ export default class TokenAuthBackend extends React.Component { }) .catch(snackBarMessage) }) - .catch(() => { - snackBarMessage(`No permissions to read content of role ${his.state.selectedRole}`); + .catch((err) => { + snackBarMessage(err || `No permissions to read content of role ${his.state.selectedRole}`); this.setState({ selectedRole: '' }); }) } @@ -306,12 +303,12 @@ export default class TokenAuthBackend extends React.Component { .catch((err) => { // This endpoint returns 404 when no roles are configured if (err.response.status != 404) { - snackBarMessage(err.toString()); + snackBarMessage(err); } }) }) - .catch(() => { - snackBarMessage('You don\' have enough permissions to list roles'); + .catch((err) => { + snackBarMessage(err || 'You don\' have enough permissions to list roles'); }); } @@ -326,8 +323,8 @@ export default class TokenAuthBackend extends React.Component { }); }); }) - .catch(() => { - snackBarMessage('You don\' have enough permissions to list accessors'); + .catch((err) => { + snackBarMessage(err || new Error('You don\' have enough permissions to list accessors')); }); } @@ -435,9 +432,7 @@ export default class TokenAuthBackend extends React.Component { this.reloadRoles() snackBarMessage(`Role ${rolename} deleted`); }) - .catch((err) => { - snackBarMessage(err.toString()); - }) + .catch(snackBarMessage) } renderRoleDeleteConfirmDialog() { @@ -480,12 +475,12 @@ export default class TokenAuthBackend extends React.Component { let handleSubmitAction = () => { if (_.indexOf(this.state.roleList, this.state.newRoleName) !== -1) { - snackBarMessage("A role with the same name already exists"); + snackBarMessage(new Error("A role with the same name already exists")); return; } if (!this.state.selectedRole && !this.state.newRoleName) { - snackBarMessage("Role name cannot be empty"); + snackBarMessage(new Error("Role name cannot be empty")); return; } @@ -522,7 +517,7 @@ export default class TokenAuthBackend extends React.Component { this.setState({ loading: false }); - snackBarMessage(error.toString()); + snackBarMessage(error); }); } @@ -700,7 +695,7 @@ export default class TokenAuthBackend extends React.Component { this.setState({ loading: false }); - snackBarMessage(error.toString()); + snackBarMessage(error); }); } From 3409b493d9ceaac0bb09ee9972deeacd5836a5ce Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Fri, 17 Feb 2017 18:44:31 +1100 Subject: [PATCH 26/38] earlier versions of vault return 400 instead of 403 --- app/components/App/App.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/components/App/App.jsx b/app/components/App/App.jsx index 4a3f321..9003654 100644 --- a/app/components/App/App.jsx +++ b/app/components/App/App.jsx @@ -70,7 +70,7 @@ export default class App extends React.Component { } }) .catch((err) => { - if (_.has(err, 'response.status') && err.response.status == 403) { + if (_.has(err, 'response.status') && err.response.status >= 400) { window.localStorage.removeItem('vaultAccessToken'); browserHistory.push('/login'); } else throw err; @@ -89,7 +89,6 @@ export default class App extends React.Component { let message = e.detail.message.toString(); if (e.detail.message instanceof Error) { // Handle logical erros from vault - //debugger; if (_.has(e.detail.message, 'response.data.errors')) if (e.detail.message.response.data.errors.length > 0) message = e.detail.message.response.data.errors.join(','); From 168adbb3862153d26d37c6e97625c8fbf5dcf22e Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Fri, 17 Feb 2017 21:51:33 +1100 Subject: [PATCH 27/38] Add babel eslint parser --- .eslintrc.json | 3 ++- package.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 5a63ac9..8bcbf19 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,5 @@ { + "parser": "babel-eslint", "env": { "browser": true, "commonjs": true, @@ -25,4 +26,4 @@ "react" ], "extends": ["eslint:recommended", "plugin:react/recommended"] -} \ No newline at end of file +} diff --git a/package.json b/package.json index d67b09e..795a47c 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "devDependencies": { "babel-cli": "^6.18.0", "babel-core": "^6.18.2", + "babel-eslint": "^7.1.1", "babel-loader": "^6.2.7", "babel-preset-es2015": "^6.18.0", "babel-preset-react": "^6.16.0", From 4b052dad5ae6d0e423a3b47fb7b3983c726a95fa Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sat, 18 Feb 2017 00:34:58 +1100 Subject: [PATCH 28/38] Redesign welcome screen When logging in with a token that doesnt have permissions to list auth/secret backends, a warning message is displayed on the welcome screen, informing the user of the missing permissions and how to fix it --- app/components/App/App.jsx | 104 +++++++++++++++++++-- app/components/App/app.css | 25 +++++ app/components/Policies/Manage.jsx | 2 + app/components/Secrets/Generic/Generic.jsx | 2 + app/components/Secrets/Generic/generic.css | 5 +- app/components/shared/Header/Header.jsx | 2 +- app/components/shared/JsonEditor.jsx | 4 +- app/components/shared/Menu/Menu.jsx | 34 ++----- 8 files changed, 140 insertions(+), 38 deletions(-) diff --git a/app/components/App/App.jsx b/app/components/App/App.jsx index 9003654..6f1dcac 100644 --- a/app/components/App/App.jsx +++ b/app/components/App/App.jsx @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react'; import _ from 'lodash'; +import { Tabs, Tab } from 'material-ui/Tabs'; import Menu from '../shared/Menu/Menu.jsx'; import Header from '../shared/Header/Header.jsx'; import Snackbar from 'material-ui/Snackbar'; @@ -7,9 +8,12 @@ import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import Paper from 'material-ui/Paper'; import { browserHistory } from 'react-router'; -import { green500, red500, yellow500 } from 'material-ui/styles/colors.js' +import Warning from 'material-ui/svg-icons/alert/warning'; +import { green500, red500 } from 'material-ui/styles/colors.js' import styles from './app.css'; -import { callVaultApi } from '../shared/VaultUtils.jsx'; +import JsonEditor from '../shared/JsonEditor.jsx'; +import { Card, CardHeader, CardText } from 'material-ui/Card'; +import { callVaultApi, tokenHasCapabilities } from '../shared/VaultUtils.jsx' let twoMinuteWarningTimeout; let logoutTimeout; @@ -20,6 +24,10 @@ function snackBarMessage(message) { } export default class App extends React.Component { + static propTypes = { + location: PropTypes.object.isRequired, + children: PropTypes.node + } constructor(props) { super(props); @@ -30,7 +38,9 @@ export default class App extends React.Component { snackbarStyle: {}, logoutOpen: false, logoutPromptSeen: false, - identity: {} + identity: {}, + tokenCanListSecretBackends: true, + tokenCanListAuthBackends: true, } _.bindAll( @@ -38,7 +48,9 @@ export default class App extends React.Component { 'reloadSessionIdentity', 'componentDidMount', 'componentWillUnmount', - 'renderSessionExpDialog' + 'renderSessionExpDialog', + 'renderWarningSecretBackends', + 'renderWarningAuthBackends' ); } @@ -57,6 +69,7 @@ export default class App extends React.Component { browserHistory.push('/login'); } + // Retrieve session identity information callVaultApi('get', 'auth/token/lookup-self') .then((resp) => { if (_.has(resp, 'data.data')) { @@ -102,7 +115,16 @@ export default class App extends React.Component { }); }); - this.reloadSessionIdentity() + this.reloadSessionIdentity(); + + // Check capabilities to list backends + tokenHasCapabilities(['read'], 'sys/mounts').catch(() => { + this.setState({ tokenCanListSecretBackends: false }); + }); + tokenHasCapabilities(['read'], 'sys/auth').catch(() => { + this.setState({ tokenCanListAuthBackends: false }); + }); + } componentWillUnmount() { @@ -141,12 +163,78 @@ export default class App extends React.Component { ); } + renderWarningAuthBackends() { + return ( + + + } + actAsExpander={true} + showExpandableButton={true} + /> + + Your token has been assigned the following policies: +
    + {_.map(this.state.identity.policies, (pol, idx) => { + return (
  • {pol}
  • ) + })} +
+ and none of them contains the following permissions: + +
+
+
+ ) + } + + renderWarningSecretBackends() { + return ( + + + } + actAsExpander={true} + showExpandableButton={true} + /> + + Your token has been assigned the following policies: +
    + {_.map(this.state.identity.policies, (pol, idx) => { + return (
  • {pol}
  • ) + })} +
+ and none of them contains the following permissions: + +
+
+
+ ) + } + render() { let welcome = (
-

Welcome to Vault UI.

-

From here you can manage your secrets, check the health of your Vault clusters, and more. - Use the menu on the left to navigate around.

+ + + + +

Get started by using the left menu to navigate your vault

+
+ {/*{ !this.state.tokenCanListSecretBackends ? + +

Your token doesn't have permissions to list secret backends

+
To correctly navigate the backends, Vault UI needs the following capabilities in
+
+ : null }*/} + {!this.state.tokenCanListSecretBackends ? this.renderWarningSecretBackends() : null} + {!this.state.tokenCanListAuthBackends ? this.renderWarningAuthBackends() : null} +
+
+
); return
diff --git a/app/components/App/app.css b/app/components/App/app.css index c6fd36d..796ead2 100644 --- a/app/components/App/app.css +++ b/app/components/App/app.css @@ -14,3 +14,28 @@ .snackbar { text-align: center; } + +.welcomeScreen { + padding-bottom: 21px; +} + +.welcomeTab > div > div { + font-weight: 800; + letter-spacing: 6px; + font-size: 25px; +} + +.welcomeHeader { + /*text-shadow: 0px 1px 1px #4d4d4d;*/ + text-align: center; +} + +.warningMsg { + border: 1px solid transparent; + border-radius: 4px !important; + box-shadow: 0 1px 1px rgba(0,0,0,0.05); + margin: 20px 15%; + border-color: #f39c12; + background-color: #fef5e6 !important; + /*text-align: center;*/ +} \ No newline at end of file diff --git a/app/components/Policies/Manage.jsx b/app/components/Policies/Manage.jsx index 46e5866..045d873 100644 --- a/app/components/Policies/Manage.jsx +++ b/app/components/Policies/Manage.jsx @@ -121,6 +121,7 @@ export default class PolicyManager extends React.Component { autoScrollBodyContent={true} > { this.editorEl = c; }} /> +
{ this.editorEl = c; }} /> ); } } diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index 60ddb9b..8e64e91 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -18,6 +18,11 @@ const supported_auth_backend_types = [ 'aws-ec2' ] +function snackBarMessage(message) { + let ev = new CustomEvent("snackbar", { detail: { message: message } }); + document.dispatchEvent(ev); +} + class Menu extends React.Component { static propTypes = { pathname: PropTypes.string.isRequired, @@ -30,20 +35,8 @@ class Menu extends React.Component { state = { selectedPath: this.props.pathname, - authBackends: [ - { - path: 'token/', - type: 'token', - description: 'token based credentials' - } - ], - secretBackends: [ - { - path: 'secret/', - type: 'generic', - description: 'generic secret storage' - } - ] + authBackends: [], + secretBackends: [] }; componentWillReceiveProps (nextProps) { @@ -71,13 +64,10 @@ class Menu extends React.Component { this.setState({secretBackends: discoveredSecretBackends}); }); }) - .catch((err) => { - // Not allowed to list secret backends, using default - console.log("unable to list: " + err); - }) + .catch((err) => {snackBarMessage(new Error("No permissions to list secret backends"))}) - tokenHasCapabilities(['read'], 'sys/auth/') + tokenHasCapabilities(['read'], 'sys/auth') .then(() => { return callVaultApi('get', 'sys/auth').then((resp) => { let entries = _.get(resp, 'data.data', _.get(resp, 'data', {})); @@ -93,11 +83,7 @@ class Menu extends React.Component { }).filter(Boolean); this.setState({authBackends: discoveredAuthBackends}); }); - }) - .catch((err) => { - // Not allowed to list secret backends, using default - console.log("unable to list: " + err); - }) + }).catch((err) => {snackBarMessage(new Error("No permissions to list auth backends"))}) } From 21616a400334976aedc851ae4bf5780a5a563787 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sat, 18 Feb 2017 01:42:27 +1100 Subject: [PATCH 29/38] set returnto querystring for post-login redirection --- app/App.jsx | 2 +- app/components/App/App.jsx | 2 +- app/components/Login/Login.jsx | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/App.jsx b/app/App.jsx index 71747b3..c7d1da6 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -36,7 +36,7 @@ injectTapEventPlugin(); const checkAccessToken = (nextState, replace, callback) => { let vaultAuthToken = window.localStorage.getItem('vaultAccessToken'); if (!vaultAuthToken) { - replace(`/login`) + replace(`/login?returnto=${encodeURI(nextState.location.pathname)}`) } callback(); diff --git a/app/components/App/App.jsx b/app/components/App/App.jsx index 6f1dcac..1e28e99 100644 --- a/app/components/App/App.jsx +++ b/app/components/App/App.jsx @@ -85,7 +85,7 @@ export default class App extends React.Component { .catch((err) => { if (_.has(err, 'response.status') && err.response.status >= 400) { window.localStorage.removeItem('vaultAccessToken'); - browserHistory.push('/login'); + browserHistory.push(`/login?returnto=${encodeURI(this.props.location.pathname)}`); } else throw err; }); } diff --git a/app/components/Login/Login.jsx b/app/components/Login/Login.jsx index 6a117c6..f9fd272 100644 --- a/app/components/Login/Login.jsx +++ b/app/components/Login/Login.jsx @@ -14,6 +14,10 @@ import _ from 'lodash'; import { callVaultApi } from '../shared/VaultUtils.jsx' export default class Login extends React.Component { + static propTypes = { + location: PropTypes.object.isRequired + } + constructor(props) { super(props); @@ -190,7 +194,10 @@ export default class Login extends React.Component { window.localStorage.setItem("vaultAccessToken", accessToken); window.localStorage.setItem('vaultUrl', this.getVaultUrl()); window.localStorage.setItem('loginMethodType', this.getVaultAuthMethod()); - window.location.href = '/'; + if (this.props.location.query.returnto && this.props.location.query.returnto.indexOf('/') === 0) + window.location.href = this.props.location.query.returnto; + else + window.location.href = '/'; } else { this.setState({ errorMessage: "Unable to obtain access token." }) } From ac71bb60830fa79cc298176e50fa9fcbbb2569b3 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sat, 18 Feb 2017 20:53:34 +1100 Subject: [PATCH 30/38] Integrate wrapping in generic secret backend --- app/App.jsx | 8 +- app/components/Authentication/Token/Token.jsx | 11 +-- app/components/Authentication/Token/token.css | 26 ------ app/components/Secrets/Generic/Generic.jsx | 9 ++ app/components/shared/JsonEditor.jsx | 1 - app/components/shared/VaultUtils.jsx | 6 +- app/components/shared/Wrapping/Unwrapper.jsx | 58 +++++++++++++ app/components/shared/Wrapping/Wrapper.jsx | 87 +++++++++++++++++++ app/components/shared/Wrapping/unwrapper.css | 42 +++++++++ app/components/shared/styles.css | 22 +++++ 10 files changed, 232 insertions(+), 38 deletions(-) create mode 100644 app/components/shared/Wrapping/Unwrapper.jsx create mode 100644 app/components/shared/Wrapping/Wrapper.jsx create mode 100644 app/components/shared/Wrapping/unwrapper.css diff --git a/app/App.jsx b/app/App.jsx index c7d1da6..edf24af 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -11,9 +11,10 @@ import Health from './components/Health/Health.jsx'; import PolicyManager from './components/Policies/Manage.jsx'; import Settings from './components/Settings/Settings.jsx'; import ResponseWrapper from './components/ResponseWrapper/ResponseWrapper.jsx'; -import TokenAuthBackend from './components/Authentication/Token/Token.jsx' -import AwsEc2AuthBackend from './components/Authentication/AwsEc2/AwsEc2.jsx' -import GithubAuthBackend from './components/Authentication/Github/Github.jsx' +import TokenAuthBackend from './components/Authentication/Token/Token.jsx'; +import AwsEc2AuthBackend from './components/Authentication/AwsEc2/AwsEc2.jsx'; +import GithubAuthBackend from './components/Authentication/Github/Github.jsx'; +import SecretUnwrapper from './components/shared/Wrapping/Unwrapper'; injectTapEventPlugin(); @@ -50,6 +51,7 @@ ReactDOM.render(( + diff --git a/app/components/Authentication/Token/Token.jsx b/app/components/Authentication/Token/Token.jsx index 443f3dc..c7ade34 100644 --- a/app/components/Authentication/Token/Token.jsx +++ b/app/components/Authentication/Token/Token.jsx @@ -1,6 +1,7 @@ import React from 'react' import _ from 'lodash'; import styles from './token.css'; +import sharedStyles from '../../shared/styles.css'; import { red500, orange500, green100, green400, red300, white } from 'material-ui/styles/colors.js' import RaisedButton from 'material-ui/RaisedButton'; import { Table, TableBody, TableFooter, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; @@ -802,7 +803,7 @@ export default class TokenAuthBackend extends React.Component { actions={NewTokenCodeDialogActions} > -
+
- + Here you can create new tokens and list active tokens.
Existing tokens are represented by their respective Accessor ID.
- +
- + Here you can create, list and edit token roles.
Roles can enforce specific behaviors when creating new tokens.
- + div > div > div { padding-right: 3%; } -.newTokenCodeEmitted input { - text-align: center; - font-family: monospace !important; - font-size: 150% !important; - color: black !important; - cursor: crosshair !important; -} - -.newTokenCodeEmitted { - text-align: center; -} - -.accessorListSection { - padding: 10px; -} - -.rolesListSection { - padding: 10px; -} - -.TabInfoSection { - padding: 10px; - text-align: center; - font-style: italic; -} - .classActionDelete { position: absolute !important; right: 4px; diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index 5600c7c..e1c3df8 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -24,6 +24,7 @@ import TextField from 'material-ui/TextField'; import { green500, green400, red500, red300, yellow500, white } from 'material-ui/styles/colors.js' import { callVaultApi, tokenHasCapabilities } from '../../shared/VaultUtils.jsx' import JsonEditor from '../../shared/JsonEditor.jsx'; +import SecretWrapper from '../../shared/Wrapping/Wrapper.jsx' import { browserHistory, Link } from 'react-router' @@ -51,6 +52,7 @@ class GenericSecretBackend extends React.Component { openEditObjectModal: false, openDeleteModal: false, deletingKey: '', + wrapPath: null, useRootKey: window.localStorage.getItem("useRootKey") === 'true' || false, rootKey: window.localStorage.getItem("secretsRootKey") || '', } @@ -265,6 +267,10 @@ class GenericSecretBackend extends React.Component { renderEditObjectDialog() { const actions = [ + { + this.setState({ wrapPath: this.state.currentLogicalPath }); + } + } />, { this.setState({ openEditObjectModal: false, secretContent: '' }); browserHistory.push(this.getBaseDir(this.props.location.pathname)); @@ -413,6 +419,9 @@ class GenericSecretBackend extends React.Component { {this.renderEditObjectDialog()} {this.renderNewObjectDialog()} {this.renderDeleteConfirmationDialog()} + { + this.setState({wrapPath: null}) + }}/> diff --git a/app/components/shared/JsonEditor.jsx b/app/components/shared/JsonEditor.jsx index f370337..560457e 100644 --- a/app/components/shared/JsonEditor.jsx +++ b/app/components/shared/JsonEditor.jsx @@ -64,7 +64,6 @@ class JsonEditor extends React.Component { mode: this.props.mode, modes: this.props.modes, schema: this.props.schema, - height: this.props.height, onChange: this.handleInputChange, }; diff --git a/app/components/shared/VaultUtils.jsx b/app/components/shared/VaultUtils.jsx index a152692..cbfdb9b 100644 --- a/app/components/shared/VaultUtils.jsx +++ b/app/components/shared/VaultUtils.jsx @@ -27,12 +27,12 @@ function getCachedCapabilities(path) { } } -function callVaultApi(method, path, query = {}, data, headers = {}) { +function callVaultApi(method, path, query = {}, data, headers = {}, vaultToken = null, vaultUrl = null) { var instance = axios.create({ baseURL: '/v1/', - params: { "vaultaddr": window.localStorage.getItem("vaultUrl") }, - headers: { "X-Vault-Token": window.localStorage.getItem("vaultAccessToken") } + params: { "vaultaddr": vaultUrl || window.localStorage.getItem("vaultUrl") }, + headers: { "X-Vault-Token": vaultToken || window.localStorage.getItem("vaultAccessToken") } }); return instance.request({ diff --git a/app/components/shared/Wrapping/Unwrapper.jsx b/app/components/shared/Wrapping/Unwrapper.jsx new file mode 100644 index 0000000..de3345d --- /dev/null +++ b/app/components/shared/Wrapping/Unwrapper.jsx @@ -0,0 +1,58 @@ +import React, { PropTypes, Component } from 'react' +import _ from 'lodash'; +import { callVaultApi } from '../VaultUtils.jsx' +import Dialog from 'material-ui/Dialog'; +import TextField from 'material-ui/TextField'; +import RaisedButton from 'material-ui/RaisedButton'; +import copy from 'copy-to-clipboard'; +import FontIcon from 'material-ui/FontIcon'; +import FlatButton from 'material-ui/FlatButton'; +import JsonEditor from '../JsonEditor.jsx'; +import styles from './unwrapper.css'; + +export default class SecretUnwrapper extends Component { + static propTypes = { + location: PropTypes.object, + }; + + constructor(props) { + super(props) + + this.state = { + headerMsg: 'Displaying data wrapped with token', + editorContent: null, + error: false, + }; + } + + + + componentDidMount() { + callVaultApi('post', 'sys/wrapping/unwrap', null, null, null, this.props.location.query.token, this.props.location.query.vaultUrl) + .then((resp) => { + this.setState({ + editorContent: resp.data.data + }) + }) + .catch((err) => { + this.setState({ + headerMsg: `Server returned error ${err.response.status} while unwrapping token`, + error: true, + }) + }) + } + + render() { + return ( +
+
+

{this.state.headerMsg}

+

{this.props.location.query.token}

+
+
+ {this.state.editorContent && } +
+
+ ) + } +} \ No newline at end of file diff --git a/app/components/shared/Wrapping/Wrapper.jsx b/app/components/shared/Wrapping/Wrapper.jsx new file mode 100644 index 0000000..ee07205 --- /dev/null +++ b/app/components/shared/Wrapping/Wrapper.jsx @@ -0,0 +1,87 @@ +import React, { PropTypes, Component } from 'react' +import _ from 'lodash'; +import { callVaultApi } from '../VaultUtils.jsx' +import Dialog from 'material-ui/Dialog'; +import TextField from 'material-ui/TextField'; +import RaisedButton from 'material-ui/RaisedButton'; +import copy from 'copy-to-clipboard'; +import FontIcon from 'material-ui/FontIcon'; +import FlatButton from 'material-ui/FlatButton'; +import sharedStyles from '../styles.css'; + +export default class SecretWrapper extends Component { + static propTypes = { + path: PropTypes.string, + onReceiveResponse: PropTypes.func, + onReceiveError: PropTypes.func, + onModalClose: PropTypes.func + } + + static defaultProps = { + path: null, + onReceiveResponse: () => { }, + onReceiveError: () => { }, + onModalClose: () => { } + } + + constructor(props) { + super(props) + } + + state = { + wrapInfo: {}, + }; + + componentDidUpdate(prevProps) { + if (!_.isEqual(prevProps.path, this.props.path) && this.props.path) { + callVaultApi('get', this.props.path, null, null, { 'X-Vault-Wrap-TTL': '10m' }) + .then((response) => { + this.setState({ wrapInfo: response.data.wrap_info }); + this.props.onReceiveResponse(response.data.wrap_info); + }) + .catch((err) => { + this.props.onReceiveError(err); + }) + } + } + + render() { + let vaultUrl = encodeURI(window.localStorage.getItem("vaultUrl")); + let tokenValue = ''; + let urlValue = ''; + if (this.state.wrapInfo) { + let loc = window.location; + tokenValue = this.state.wrapInfo.token; + urlValue = `${loc.protocol}//${loc.hostname}${(loc.port ? ":" + loc.port : "")}/unwrap?token=${tokenValue}&vaultUrl=${vaultUrl}`; + } + + return ( + {this.props.onModalClose(); this.setState({ wrapInfo: {} })}} />} + onRequestClose={this.props.onModalClose} + > +
+ + } label="Copy to Clipboard" onTouchTap={() => { copy(tokenValue) }} /> +
+
+ + } label="Copy to Clipboard" onTouchTap={() => { copy(urlValue) }} /> +
+
+ ) + } +} \ No newline at end of file diff --git a/app/components/shared/Wrapping/unwrapper.css b/app/components/shared/Wrapping/unwrapper.css new file mode 100644 index 0000000..c27d189 --- /dev/null +++ b/app/components/shared/Wrapping/unwrapper.css @@ -0,0 +1,42 @@ +#container { + position: relative; + height: 100%; + width: 100%; +} + +#cell { + text-align: center; + margin-bottom: 20px; + margin-top: 20px; + padding-top: 5px; + padding-bottom: 5px; +} + +.bwgradient { + background: radial-gradient(circle, black, black, white); +} + +.redgradient { + background: radial-gradient(circle, darkred, darkred, white); +} + +#cell h4 { + color: lightgray; + text-transform: full-width; +} + +#cell h2 { + font-family: monospace; + color: #c2daff; +} + +#content { + /*display: inline-block; + text-align: center; + align-content: center;*/ + + width: 80%; + margin: 0 auto; + + /*margin: auto;*/ +} \ No newline at end of file diff --git a/app/components/shared/styles.css b/app/components/shared/styles.css index 5ca091f..2c8a742 100644 --- a/app/components/shared/styles.css +++ b/app/components/shared/styles.css @@ -11,3 +11,25 @@ .listStyle span { font-family: monospace !important; } + +.newTokenCodeEmitted { + /*text-align: center;*/ +} + +.newTokenCodeEmitted input { + /*text-align: center;*/ + font-family: monospace !important; + font-size: 150% !important; + color: black !important; + cursor: crosshair !important; +} + +.newUrlEmitted { + /*text-align: center;*/ +} + +.newUrlEmitted input { + font-size: 15px !important; + color: black !important; + cursor: crosshair !important; +} \ No newline at end of file From 0af5e0b8408b27a358ba253df5a10769da8bdbec Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sat, 18 Feb 2017 21:14:39 +1100 Subject: [PATCH 31/38] eslint cleanups --- app/App.jsx | 44 +++++----- app/components/App/App.jsx | 7 +- app/components/Health/Health.jsx | 82 ------------------- app/components/Health/health.css | 19 ----- app/components/Secrets/Generic/Generic.jsx | 17 ++-- app/components/Settings/Settings.jsx | 2 +- app/components/shared/Header/countdown.js | 2 - app/components/shared/JsonEditor.jsx | 3 +- app/components/shared/Menu/Menu.jsx | 55 ++++++------- .../shared/PolicyPicker/PolicyPicker.jsx | 19 ++--- app/components/shared/Wrapping/Unwrapper.jsx | 9 -- 11 files changed, 64 insertions(+), 195 deletions(-) delete mode 100644 app/components/Health/Health.jsx delete mode 100644 app/components/Health/health.css diff --git a/app/App.jsx b/app/App.jsx index edf24af..21e425f 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -1,7 +1,7 @@ -import React, { PropTypes } from 'react' +import React from 'react' import ReactDOM from 'react-dom'; import Login from './components/Login/Login.jsx'; -import { Router, Route, Link, browserHistory } from 'react-router' +import { Router, Route, browserHistory } from 'react-router' import injectTapEventPlugin from 'react-tap-event-plugin'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import getMuiTheme from 'material-ui/styles/getMuiTheme'; @@ -20,18 +20,18 @@ injectTapEventPlugin(); (function () { - if ( typeof window.CustomEvent === "function" ) return false; + if (typeof window.CustomEvent === "function") return false; - function CustomEvent ( event, params ) { - params = params || { bubbles: false, cancelable: false, detail: undefined }; - var evt = document.createEvent( 'CustomEvent' ); - evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); - return evt; - } + function CustomEvent(event, params) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + } - CustomEvent.prototype = window.Event.prototype; + CustomEvent.prototype = window.Event.prototype; - window.CustomEvent = CustomEvent; + window.CustomEvent = CustomEvent; })(); const checkAccessToken = (nextState, replace, callback) => { @@ -44,23 +44,23 @@ const checkAccessToken = (nextState, replace, callback) => { } const muiTheme = getMuiTheme({ - fontFamily: 'Source Sans Pro, sans-serif', + fontFamily: 'Source Sans Pro, sans-serif', }); ReactDOM.render(( - - + + - - - - - - - - + + + + + + + + diff --git a/app/components/App/App.jsx b/app/components/App/App.jsx index 1e28e99..f5d2dee 100644 --- a/app/components/App/App.jsx +++ b/app/components/App/App.jsx @@ -182,7 +182,7 @@ export default class App extends React.Component { })} and none of them contains the following permissions: - +
@@ -208,7 +208,7 @@ export default class App extends React.Component { })} and none of them contains the following permissions: - +
@@ -255,7 +255,6 @@ export default class App extends React.Component { {this.props.children || welcome}
- -
; +
} } diff --git a/app/components/Health/Health.jsx b/app/components/Health/Health.jsx deleted file mode 100644 index b238da0..0000000 --- a/app/components/Health/Health.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { PropTypes } from 'react' -import styles from './health.css'; -import Paper from 'material-ui/Paper'; -import { green500, red500, yellow500 } from 'material-ui/styles/colors.js' -import _ from 'lodash'; - -class Health extends React.Component { - constructor(props) { - super(props); - this.renderCluster = this.renderCluster.bind(this); - this.state = { - cluster : [ - { - id: "c9abceea-4f46-4dab-a688-5ce55f89e227", - name: "vault-cluster-5515c810", - version: "0.6.1-dev", - level: 0, - message: 'blah' - }, { - id: "c9abceea-4f46-4dab-a688-5ce55f89e228", - name: "vault-cluster-5515c810", - version: "0.6.1-dev", - level: 1, - message: 'boh' - }, { - id: "c9abceea-4f46-4dab-a688-5ce55f89e229", - name: "vault-cluster-5515c810", - version: "0.6.1-dev", - level: 2, - message: 'argh' - }, { - id: "c9abceea-4f46-4dab-a688-5ce55f89e230", - name: "vault-cluster-5515c810", - version: "0.6.1-dev", - level: 0, - message: 'la' - } - ] - } - } - - renderCluster() { - let chooseColor = (level) => { - switch (level) { - case 1: - return yellow500; - case 2: - return red500; - default: - return green500; - } - } - return _.map(this.state.cluster, box => { - return ( -
- -
-
-
{box.id}
-
{box.name}
-
{box.version}
-
{box.message}
-
-
-
- ); - }) - - } - - render () { - return ( -
-

Health

-

Here you can view the health of your Vault cluster.

-
{this.renderCluster()}
-
- ) - } -} - -export default Health; diff --git a/app/components/Health/health.css b/app/components/Health/health.css deleted file mode 100644 index f5d5def..0000000 --- a/app/components/Health/health.css +++ /dev/null @@ -1,19 +0,0 @@ -#welcomeHeadline { - font-size: 60px; - font-weight: 200; -} - -.cluster { - padding: 20px; - position: relative; - margin-bottom: 10px; -} - -.status { - height: 10px; - width: 10px; - position: absolute; - top: 10px; - left: 10px; - border-radius: 50%; -} diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index e1c3df8..4e9b12f 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import { Tabs, Tab } from 'material-ui/Tabs'; -import { Toolbar, ToolbarGroup, ToolbarSeparator } from 'material-ui/Toolbar'; +import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; import Subheader from 'material-ui/Subheader'; import Paper from 'material-ui/Paper'; import Avatar from 'material-ui/Avatar'; @@ -8,20 +8,16 @@ import FileFolder from 'material-ui/svg-icons/file/folder'; import ActionAssignment from 'material-ui/svg-icons/action/assignment'; import ActionDelete from 'material-ui/svg-icons/action/delete'; import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; -import ArrowForwardIcon from 'material-ui/svg-icons/navigation/arrow-forward'; import IconButton from 'material-ui/IconButton'; -import FontIcon from 'material-ui/FontIcon'; import Divider from 'material-ui/Divider'; import { List, ListItem } from 'material-ui/List'; import { Step, Stepper, StepLabel } from 'material-ui/Stepper'; -import Checkbox from 'material-ui/Checkbox'; -import styles from './generic.css'; import sharedStyles from '../../shared/styles.css'; import _ from 'lodash'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import TextField from 'material-ui/TextField'; -import { green500, green400, red500, red300, yellow500, white } from 'material-ui/styles/colors.js' +import { green500, green400, red500, red300, white } from 'material-ui/styles/colors.js' import { callVaultApi, tokenHasCapabilities } from '../../shared/VaultUtils.jsx' import JsonEditor from '../../shared/JsonEditor.jsx'; import SecretWrapper from '../../shared/Wrapping/Wrapper.jsx' @@ -36,6 +32,7 @@ function snackBarMessage(message) { class GenericSecretBackend extends React.Component { static propTypes = { params: PropTypes.object.isRequired, + location: PropTypes.object.isRequired }; constructor(props) { @@ -145,7 +142,7 @@ class GenericSecretBackend extends React.Component { } } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps) { if (!_.isEqual(this.props.params, prevProps.params)) { if (this.isPathDirectory(this.props.params.splat)) { this.loadSecretsList(); @@ -174,7 +171,7 @@ class GenericSecretBackend extends React.Component { let secret = this.state.secretContent; let fullpath = this.state.currentLogicalPath + this.state.newSecretName; callVaultApi('post', fullpath, null, secret, null) - .then((resp) => { + .then(() => { if (this.state.newSecretName) { this.loadSecretsList(); snackBarMessage(`Secret ${fullpath} added`); @@ -188,7 +185,7 @@ class GenericSecretBackend extends React.Component { DeleteObject(key) { let fullpath = this.state.currentLogicalPath + key; callVaultApi('delete', fullpath, null, null, null) - .then((resp) => { + .then(() => { let secrets = this.state.secretList; let secretToDelete = _.find(secrets, (secretToDelete) => { return secretToDelete == key; }); secrets = _.pull(secrets, secretToDelete); @@ -205,7 +202,7 @@ class GenericSecretBackend extends React.Component { const MISSING_KEY_ERROR = "Key cannot be empty."; const DUPLICATE_KEY_ERROR = `Key '${this.state.currentLogicalPath}${this.state.newSecretName}' already exists.`; - let validateAndSubmit = (e, v) => { + let validateAndSubmit = () => { if (this.state.newSecretName === '') { snackBarMessage(new Error(MISSING_KEY_ERROR)); return; diff --git a/app/components/Settings/Settings.jsx b/app/components/Settings/Settings.jsx index fd64a93..6e029fd 100644 --- a/app/components/Settings/Settings.jsx +++ b/app/components/Settings/Settings.jsx @@ -1,4 +1,4 @@ -import React, { PropTypes } from 'react'; +import React from 'react'; import TextField from 'material-ui/TextField'; import Checkbox from 'material-ui/Checkbox'; import styles from './settings.css'; diff --git a/app/components/shared/Header/countdown.js b/app/components/shared/Header/countdown.js index e9d92eb..a60104f 100644 --- a/app/components/shared/Header/countdown.js +++ b/app/components/shared/Header/countdown.js @@ -58,8 +58,6 @@ class CountDown extends Component { } render() { - const time = this.state.time / 10 - const seconds = Math.floor(time) return {this.splitTimeComponents()} } } diff --git a/app/components/shared/JsonEditor.jsx b/app/components/shared/JsonEditor.jsx index 560457e..f4725f0 100644 --- a/app/components/shared/JsonEditor.jsx +++ b/app/components/shared/JsonEditor.jsx @@ -1,5 +1,4 @@ import React, { PropTypes } from 'react'; -import { browserHistory } from 'react-router'; import JSONEditor from 'jsoneditor'; import 'jsoneditor/src/css/reset.css'; import 'jsoneditor/src/css/jsoneditor.css'; @@ -9,7 +8,7 @@ import 'jsoneditor/src/css/contextmenu.css'; function isValid(value) { return value !== '' && value !== undefined && value !== null; -}; +} class JsonEditor extends React.Component { static propTypes = { diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index 8e64e91..1144d9c 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -1,13 +1,13 @@ -import React, {PropTypes} from 'react'; +import React, { PropTypes } from 'react'; +import _ from 'lodash'; import styles from './menu.css'; import Drawer from 'material-ui/Drawer'; import { browserHistory } from 'react-router'; import { List, ListItem, makeSelectable } from 'material-ui/List'; -import {tokenHasCapabilities, callVaultApi} from '../VaultUtils.jsx' +import { tokenHasCapabilities, callVaultApi } from '../VaultUtils.jsx' const SelectableList = makeSelectable(List); - const supported_secret_backend_types = [ 'generic' ] @@ -26,25 +26,24 @@ function snackBarMessage(message) { class Menu extends React.Component { static propTypes = { pathname: PropTypes.string.isRequired, - }; + } constructor(props) { super(props); - } - state = { - selectedPath: this.props.pathname, - - authBackends: [], - secretBackends: [] - }; + this.state = { + selectedPath: this.props.pathname, + authBackends: [], + secretBackends: [] + }; + } - componentWillReceiveProps (nextProps) { - if(this.props.pathname != nextProps.pathname) { - this.setState({selectedPath: nextProps.pathname}); + componentWillReceiveProps(nextProps) { + if (this.props.pathname != nextProps.pathname) { + this.setState({ selectedPath: nextProps.pathname }); } } - + componentDidMount() { tokenHasCapabilities(['read'], 'sys/mounts') @@ -52,7 +51,7 @@ class Menu extends React.Component { return callVaultApi('get', 'sys/mounts').then((resp) => { let entries = _.get(resp, 'data.data', _.get(resp, 'data', {})); let discoveredSecretBackends = _.map(entries, (v, k) => { - if ( _.indexOf(supported_secret_backend_types, v.type) != -1 ) { + if (_.indexOf(supported_secret_backend_types, v.type) != -1) { let entry = { path: k, type: v.type, @@ -61,10 +60,10 @@ class Menu extends React.Component { return entry; } }).filter(Boolean); - this.setState({secretBackends: discoveredSecretBackends}); + this.setState({ secretBackends: discoveredSecretBackends }); }); }) - .catch((err) => {snackBarMessage(new Error("No permissions to list secret backends"))}) + .catch(() => { snackBarMessage(new Error("No permissions to list secret backends")) }) tokenHasCapabilities(['read'], 'sys/auth') @@ -72,7 +71,7 @@ class Menu extends React.Component { return callVaultApi('get', 'sys/auth').then((resp) => { let entries = _.get(resp, 'data.data', _.get(resp, 'data', {})); let discoveredAuthBackends = _.map(entries, (v, k) => { - if ( _.indexOf(supported_auth_backend_types, v.type) != -1 ) { + if (_.indexOf(supported_auth_backend_types, v.type) != -1) { let entry = { path: k, type: v.type, @@ -81,18 +80,16 @@ class Menu extends React.Component { return entry; } }).filter(Boolean); - this.setState({authBackends: discoveredAuthBackends}); + this.setState({ authBackends: discoveredAuthBackends }); }); - }).catch((err) => {snackBarMessage(new Error("No permissions to list auth backends"))}) + }).catch(() => { snackBarMessage(new Error("No permissions to list auth backends")) }) } - render() { - let renderSecretBackendList = () => { return _.map(this.state.secretBackends, (backend, idx) => { return ( - + ) }) } @@ -100,17 +97,16 @@ class Menu extends React.Component { let renderAuthBackendList = () => { return _.map(this.state.authBackends, (backend, idx) => { return ( - + ) }) } let handleMenuChange = (e, v) => { - this.setState({selectedPath: v}); - browserHistory.push(v) + this.setState({ selectedPath: v }); + browserHistory.push(v) } - return ( @@ -144,8 +140,7 @@ class Menu extends React.Component { - - ); + ) } } diff --git a/app/components/shared/PolicyPicker/PolicyPicker.jsx b/app/components/shared/PolicyPicker/PolicyPicker.jsx index 5e64517..ef755d3 100644 --- a/app/components/shared/PolicyPicker/PolicyPicker.jsx +++ b/app/components/shared/PolicyPicker/PolicyPicker.jsx @@ -1,20 +1,12 @@ import React, { PropTypes } from 'react'; import { callVaultApi, tokenHasCapabilities } from '../VaultUtils.jsx' import _ from 'lodash'; -import { browserHistory } from 'react-router'; import { List, ListItem } from 'material-ui/List'; -import Subheader from 'material-ui/Subheader'; import styles from './policypicker.css'; -import { lightBlue50, indigo400 } from 'material-ui/styles/colors.js' import KeyboardArrowRight from 'material-ui/svg-icons/hardware/keyboard-arrow-right'; -import KeyboardArrowLeft from 'material-ui/svg-icons/hardware/keyboard-arrow-left'; import AutoComplete from 'material-ui/AutoComplete'; -import Search from 'material-ui/svg-icons/action/search'; -import Paper from 'material-ui/Paper'; import Clear from 'material-ui/svg-icons/content/clear'; -import { Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle } from 'material-ui/Toolbar'; -import TextField from 'material-ui/TextField'; -import { Menu, MenuItem } from 'material-ui/Menu'; +import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; class PolicyPicker extends React.Component { static propTypes = { @@ -27,7 +19,7 @@ class PolicyPicker extends React.Component { static defaultProps = { onError: (err) => { console.error(err) }, - onSelectedChange: (selectedPolicies) => {}, + onSelectedChange: () => {}, excludePolicies: ['default'], title: "Policy Picker", height: "300px", @@ -51,7 +43,7 @@ class PolicyPicker extends React.Component { 'selectedPolicyAdd', 'selectedPolicyRemove' ) - }; + } reloadPolicyList() { tokenHasCapabilities(['read'], 'sys/policy') @@ -189,7 +181,7 @@ class PolicyPicker extends React.Component { searchText: searchText }); } } - onNewRequest={(chosenRequest, index) => { + onNewRequest={(chosenRequest) => { if ( (!_.includes(this.props.excludePolicies, chosenRequest)) && (chosenRequest !== 'root') @@ -211,8 +203,7 @@ class PolicyPicker extends React.Component {
) - }; - + } } export default PolicyPicker; \ No newline at end of file diff --git a/app/components/shared/Wrapping/Unwrapper.jsx b/app/components/shared/Wrapping/Unwrapper.jsx index de3345d..c8ac045 100644 --- a/app/components/shared/Wrapping/Unwrapper.jsx +++ b/app/components/shared/Wrapping/Unwrapper.jsx @@ -1,12 +1,5 @@ import React, { PropTypes, Component } from 'react' -import _ from 'lodash'; import { callVaultApi } from '../VaultUtils.jsx' -import Dialog from 'material-ui/Dialog'; -import TextField from 'material-ui/TextField'; -import RaisedButton from 'material-ui/RaisedButton'; -import copy from 'copy-to-clipboard'; -import FontIcon from 'material-ui/FontIcon'; -import FlatButton from 'material-ui/FlatButton'; import JsonEditor from '../JsonEditor.jsx'; import styles from './unwrapper.css'; @@ -25,8 +18,6 @@ export default class SecretUnwrapper extends Component { }; } - - componentDidMount() { callVaultApi('post', 'sys/wrapping/unwrap', null, null, null, this.props.location.query.token, this.props.location.query.vaultUrl) .then((resp) => { From 8f6fde521a7fe39f439e800a484aa7435f2bd4c5 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sun, 19 Feb 2017 02:18:59 +1100 Subject: [PATCH 32/38] ResponseWrapper integration with shared Wrapper component --- .../ResponseWrapper/ResponseWrapper.jsx | 269 +++--------------- app/components/Secrets/Generic/Generic.jsx | 53 ++-- app/components/shared/Menu/Menu.jsx | 2 +- app/components/shared/Wrapping/Wrapper.jsx | 132 +++++++-- app/components/shared/Wrapping/unwrapper.css | 11 +- 5 files changed, 178 insertions(+), 289 deletions(-) diff --git a/app/components/ResponseWrapper/ResponseWrapper.jsx b/app/components/ResponseWrapper/ResponseWrapper.jsx index 4d74719..5dd6b73 100644 --- a/app/components/ResponseWrapper/ResponseWrapper.jsx +++ b/app/components/ResponseWrapper/ResponseWrapper.jsx @@ -1,253 +1,58 @@ import React from 'react'; +import { Tabs, Tab } from 'material-ui/Tabs'; +import Paper from 'material-ui/Paper'; +import sharedStyles from '../shared/styles.css'; +import JsonEditor from '../shared/JsonEditor.jsx'; +import SecretWrapper from '../shared/Wrapping/Wrapper.jsx' -import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; -import SelectField from 'material-ui/SelectField'; -import MenuItem from 'material-ui/MenuItem'; -import TextField from 'material-ui/TextField'; -import FlatButton from 'material-ui/FlatButton'; -import { green500, green400, red500, red300, yellow500, white } from 'material-ui/styles/colors.js'; -import Dialog from 'material-ui/Dialog'; - -import _ from 'lodash'; -import axios from 'axios'; - -import styles from './responseWrapper.css'; -import copy from 'copy-to-clipboard'; +function snackBarMessage(message) { + let ev = new CustomEvent("snackbar", { detail: { message: message } }); + document.dispatchEvent(ev); +} export default class ResponseWrapper extends React.Component { constructor(props) { super(props); this.state = { - WrapType: '', - WrapValue: '', - WrappedToken: '', - WrapTokenDialogValue: '', - WrappedSecretKey: '', - WrapTTL: '', - submitBtnColor: 'lightgrey', - submitBtnDisabled: true, - openWrapTokenDialog: false, - errorMessage: '' + wrapEditorValue: {} }; - _.bindAll( - this, - 'checkTTLValue', - 'submitBtnClick', - 'checkValue', - 'checkWrappedToken', - 'checkSecretKey', - 'showWrappedToken' - ); - } - - restoreStateDefaults() { - this.setState({ - WrapValue: '', - WrappedToken: '', - WrappedSecretKey: '', - WrapTTL: '', - submitBtnColor: 'lightgrey', - WrapTokenDialogValue: '', - submitBtnDisabled: true, - errorMessage: '' - }); - } - - checkTTLValue(e, v) { - //Try to parse as an int, if failed, return error - if (!isNaN(v) && v.indexOf('.') === -1) { - let buttonColor = (v && (this.state.WrapValue || this.state.WrappedSecretKey) && v > 0) ? green500 : 'lightgrey'; - this.setState({ - WrapTTL: v, - submitBtnColor: buttonColor, - submitBtnDisabled: !(v && (this.state.WrapValue || this.state.WrappedSecretKey)) - }); - } - } - - checkValue(e, v) { - let buttonColor = (v && !isNaN(this.state.WrapTTL) && this.state.WrapTTL > 0) ? green500 : 'lightgrey'; - this.setState({ - WrapValue: v, - submitBtnColor: buttonColor, - submitBtnDisabled: !(v && this.state.WrapValue) - }); - } - - checkWrappedToken(e, v) { - let buttonColor = v ? green500 : 'lightgrey'; - this.setState({ - WrappedToken: v, - submitBtnColor: buttonColor, - submitBtnDisabled: !v - }); - } - - checkSecretKey(e, v) { - let buttonColor = v ? green500 : 'lightgrey'; - this.setState({ - WrappedSecretKey: v, - submitBtnColor: buttonColor, - submitBtnDisabled: !v - }); - } - - submitBtnClick() { - switch (this.state.WrapType) { - case "WRAPVALUE": - axios.post(`/wrap?vaultaddr=${encodeURI(window.localStorage.getItem("vaultUrl"))}&token=${encodeURI(window.localStorage.getItem("vaultAccessToken"))}`, { "value": this.state.WrapValue, "ttl": this.state.WrapTTL }) - .then((resp) => { - this.state.WrapTokenDialogValue = _.get(resp, "data.token"); - this.setState({ - openWrapTokenDialog: true - }); - }) - .catch((err) => { - console.error(err.stack); - }); - break; - case "WRAPSECRET": - break; - case "UNWRAP": - axios.post(`/unwrap?vaultaddr=${encodeURI(window.localStorage.getItem("vaultUrl"))}&token=${this.state.WrappedToken}`) - .then((resp) => { - this.setState({ - WrapTokenDialogValue: resp.data.value, - openWrapTokenDialog: true, - errorMessage: '' - }) - }) - .catch((err) => { - this.setState({ - errorMessage: 'Token is invalid' - }); - }); - break; - } - } - - showWrappedToken() { - const actions = [ -
- this.setState({ openWrapTokenDialog: false })} /> - { copy(this.state.WrapTokenDialogValue), this.setState({ openWrapTokenDialog: false }) } } /> -
- ]; - return ( - -
-

{this.state.WrapTokenDialogValue}

-
- -
- ); - } - - renderWrapFunction() { - switch (this.state.WrapType) { - case "WRAPVALUE": - return ( -
-
- -
-
- -
- -
- ) - case "WRAPSECRET": - return ( -
-
- -
- -
- ) - case "UNWRAP": - return ( -
- -
- ) - default: - return ( -
- ) - } } render() { - let handleSelectFieldChange = (e, i, v) => { - this.restoreStateDefaults(); - this.setState({ - WrapType: v, - }); + let secretChangedJsonEditor = (v, syntaxCheckOk) => { + if (syntaxCheckOk && v) { + this.setState({ wrapEditorValue: v }); + } else { + this.setState({ wrapEditorValue: null }); + } } return (
{this.state.openWrapTokenDialog && this.showWrappedToken()} -
-

Response Wrapping

- - - - - -
- {this.renderWrapFunction()} -
- this.submitBtnClick()} /> -
-
{this.state.errorMessage}
+ + + + Here you can store data inside vault and collect a temporary, single-use token to display the initial data + + + +
+ +
+
+
+
) } diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index 4e9b12f..87c2861 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -218,7 +218,7 @@ class GenericSecretBackend extends React.Component { const actions = [ this.setState({ openNewObjectModal: false, secretContent: '' })} />, - + ]; var rootKeyInfo; @@ -234,7 +234,7 @@ class GenericSecretBackend extends React.Component { hintText="Value" autoFocus onChange={this.secretChangedTextEditor} - /> + /> ); } else { content = ( @@ -243,18 +243,19 @@ class GenericSecretBackend extends React.Component { rootName={`${this.state.currentLogicalPath}${this.state.newSecretName}`} mode={'tree'} onChange={this.secretChangedJsonEditor} - /> + /> ); } return ( { this.setState({ openNewObjectModal: false, secretContent: '' }) }} actions={actions} open={this.state.openNewObjectModal} autoScrollBodyContent={true} - > + > this.setState({ newSecretName: v })} /> {content}
{rootKeyInfo}
@@ -264,16 +265,13 @@ class GenericSecretBackend extends React.Component { renderEditObjectDialog() { const actions = [ - { - this.setState({ wrapPath: this.state.currentLogicalPath }); - } - } />, + , { this.setState({ openEditObjectModal: false, secretContent: '' }); browserHistory.push(this.getBaseDir(this.props.location.pathname)); } } />, - submitUpdate()} /> + submitUpdate()} /> ]; let submitUpdate = () => { @@ -296,7 +294,7 @@ class GenericSecretBackend extends React.Component { multiLine={true} defaultValue={this.state.secretContent[this.state.rootKey]} fullWidth={true} - /> + /> ); } else { title = `Editing ${this.state.currentLogicalPath}`; @@ -307,17 +305,21 @@ class GenericSecretBackend extends React.Component { value={this.state.secretContent} mode={'tree'} onChange={this.secretChangedJsonEditor} - /> + /> ); } return ( + onRequestClose={() => { + this.setState({ openEditObjectModal: false, secretContent: '' }) + browserHistory.push(this.getBaseDir(this.props.location.pathname)) + }} + > {content} ); @@ -340,7 +342,7 @@ class GenericSecretBackend extends React.Component { modal={false} actions={actions} open={this.state.openDeleteModal} - > + >

You are about to permanently delete {this.state.currentLogicalPath}{this.state.deletingKey}. Are you sure?

To disable this prompt, visit the settings page. @@ -367,8 +369,8 @@ class GenericSecretBackend extends React.Component { }).catch(() => { snackBarMessage(new Error("Access denied")); }) - } } - > + }} + > {window.localStorage.getItem("showDeleteModal") === 'false' ? : }
); @@ -395,8 +397,8 @@ class GenericSecretBackend extends React.Component { snackBarMessage(new Error("Access denied")); }) - } } - /> + }} + /> ) if (this.isPathDirectory(key) && returndirs) { return item } if (!this.isPathDirectory(key) && returnobjs) { return item } @@ -407,7 +409,7 @@ class GenericSecretBackend extends React.Component { let components = _.initial(this.getBaseDir(this.state.currentLogicalPath).split('/')); return _.map(components, (dir, index) => { var relativelink = [].concat(components).slice(0, index + 1).join('/') + '/'; - return (}>{dir}) + return (}>{dir}) }); } @@ -416,9 +418,6 @@ class GenericSecretBackend extends React.Component { {this.renderEditObjectDialog()} {this.renderNewObjectDialog()} {this.renderDeleteConfirmationDialog()} - { - this.setState({wrapPath: null}) - }}/> @@ -440,17 +439,17 @@ class GenericSecretBackend extends React.Component { newSecretName: '', secretContent: '' }) - } } - /> + }} + /> /} - > + > {renderBreadcrumb()} diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index 1144d9c..6a7b3b4 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -128,7 +128,7 @@ class Menu extends React.Component { initiallyOpen={true} nestedItems={[ , - + ]} /> { }, onReceiveError: () => { }, onModalClose: () => { } @@ -30,13 +42,37 @@ export default class SecretWrapper extends Component { state = { wrapInfo: {}, + openPopover: false, + ttl: '5m', + data: null, + path: null }; - componentDidUpdate(prevProps) { - if (!_.isEqual(prevProps.path, this.props.path) && this.props.path) { - callVaultApi('get', this.props.path, null, null, { 'X-Vault-Wrap-TTL': '10m' }) + componentWillReceiveProps (nextProps) { + // Trigger automatically on props change if the builtin button is not used + if(!this.props.showButton) { + if (!_.isEqual(nextProps.path, this.props.path) && this.props.path) { + this.setState({ path: nextProps.path}) + } else if (!_.isEqual(nextProps.data, this.props.data) && this.props.data) { + this.setState({ data: nextProps.data}) + } + } + } + + componentDidUpdate(prevProps, prevState) { + if (!_.isEqual(prevState.path, this.state.path) && this.state.path) { + callVaultApi('get', this.state.path, null, null, { 'X-Vault-Wrap-TTL': this.state.ttl }) + .then((response) => { + this.setState({ wrapInfo: response.data.wrap_info, path: null }); + this.props.onReceiveResponse(response.data.wrap_info); + }) + .catch((err) => { + this.props.onReceiveError(err); + }) + } else if (!_.isEqual(prevState.data, this.state.data) && this.state.data) { + callVaultApi('post', 'sys/wrapping/wrap', null, this.state.data, { 'X-Vault-Wrap-TTL': this.state.ttl }) .then((response) => { - this.setState({ wrapInfo: response.data.wrap_info }); + this.setState({ wrapInfo: response.data.wrap_info, data: null }); this.props.onReceiveResponse(response.data.wrap_info); }) .catch((err) => { @@ -45,6 +81,30 @@ export default class SecretWrapper extends Component { } } + handleTouchTap = (event) => { + event.preventDefault(); + + this.setState({ + anchorEl: event.currentTarget, + openPopover: true + }); + }; + + handleRequestClose = () => { + this.setState({ + openPopover: false + }); + }; + + handleItemTouchTap = (event, menuItem) => { + this.setState({ + openPopover: false, + ttl: menuItem.props.secondaryText, + data: this.props.data, + path: this.props.path + }); + }; + render() { let vaultUrl = encodeURI(window.localStorage.getItem("vaultUrl")); let tokenValue = ''; @@ -56,14 +116,39 @@ export default class SecretWrapper extends Component { } return ( - {this.props.onModalClose(); this.setState({ wrapInfo: {} })}} />} - onRequestClose={this.props.onModalClose} - > -
+
+ {this.props.showButton && +
+ + + + + + + + + + + + + + +
+ } + { this.props.onModalClose(); this.setState({ wrapInfo: {} }) }} />} + onRequestClose={this.props.onModalClose} + > +
} label="Copy to Clipboard" onTouchTap={() => { copy(tokenValue) }} /> -
-
- - } label="Copy to Clipboard" onTouchTap={() => { copy(urlValue) }} /> -
-
+
+
+ + } label="Copy to Clipboard" onTouchTap={() => { copy(urlValue) }} /> +
+
+ ) } } \ No newline at end of file diff --git a/app/components/shared/Wrapping/unwrapper.css b/app/components/shared/Wrapping/unwrapper.css index c27d189..d4b4ac3 100644 --- a/app/components/shared/Wrapping/unwrapper.css +++ b/app/components/shared/Wrapping/unwrapper.css @@ -31,12 +31,11 @@ } #content { - /*display: inline-block; - text-align: center; - align-content: center;*/ - width: 80%; margin: 0 auto; - - /*margin: auto;*/ +} + +.ttlList { + line-height: 24px !important; + min-height: 24px !important; } \ No newline at end of file From f52b176f137cd3efa544d7e20e4c1edbcf780596 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sun, 19 Feb 2017 02:31:54 +1100 Subject: [PATCH 33/38] Consolidate layout for settings and github components --- .../Authentication/Github/Github.jsx | 76 +++++++++---------- app/components/Settings/Settings.jsx | 67 ++++++++-------- app/components/shared/Menu/Menu.jsx | 1 - 3 files changed, 75 insertions(+), 69 deletions(-) diff --git a/app/components/Authentication/Github/Github.jsx b/app/components/Authentication/Github/Github.jsx index 12e7f91..aeac78c 100644 --- a/app/components/Authentication/Github/Github.jsx +++ b/app/components/Authentication/Github/Github.jsx @@ -8,9 +8,11 @@ import Dialog from 'material-ui/Dialog'; import TextField from 'material-ui/TextField'; import IconButton from 'material-ui/IconButton'; import FontIcon from 'material-ui/FontIcon'; +import { Tabs, Tab } from 'material-ui/Tabs'; +import Paper from 'material-ui/Paper'; +import sharedStyles from '../../shared/styles.css'; import Checkbox from 'material-ui/Checkbox'; import { callVaultApi } from '../../shared/VaultUtils.jsx' -import Snackbar from 'material-ui/Snackbar'; export default class GithubAuthBackend extends React.Component { constructor(props) { @@ -24,8 +26,7 @@ export default class GithubAuthBackend extends React.Component { submitBtnColor: 'lightgrey', submitBtnDisabled: true, errorMessage: '', - selected: props.selected === 'Github', - snackBarMsg: '' + selected: props.selected === 'Github' }; this.processTeamNameDebounced = _.debounce(this.processTeamName, 400); @@ -84,14 +85,14 @@ export default class GithubAuthBackend extends React.Component { actions={actions} modal={true} open={this.state.requestOrganization} - > + > this.setState({ tmpOrganization: v })} - /> + />
{this.state.errorMessage}
) @@ -140,7 +141,7 @@ export default class GithubAuthBackend extends React.Component { }); }) .catch((err) => { - this.setState({errorMessage: `${err} - URI: ${decodeURI(uri)}`}); + this.setState({ errorMessage: `${err} - URI: ${decodeURI(uri)}` }); }); } @@ -198,11 +199,11 @@ export default class GithubAuthBackend extends React.Component { leftCheckbox={ this.policyChecked(policy.name, e, v)} checked={policy.checked} - />} + />} style={{ marginLeft: -17 }} key={policy.name} primaryText={
{policy.name}
} - > + >
); }); @@ -211,37 +212,36 @@ export default class GithubAuthBackend extends React.Component { render() { return (
-

Github

{this.renderOrganizationDialog()} -

Here you can view, update, and delete policies assigned to teams in your Github org.

-
-

Current Organization: {this.state.organization ? this.setState({ requestOrganization: true })} /> : ""}

- -
-
- -
- {this.renderPolicies()} - {this.state.policies.length > 0 && this.submitGithubPolicy()} />} - this.setState({ snackBarMsg: '' })} - autoHideDuration={4000} - onRequestClose={() => this.setState({ snackBarMsg: '' })} - /> + + + + Here you can view, update, and delete policies assigned to teams in your Github org. + + +
+

Current Organization: {this.state.organization ? this.setState({ requestOrganization: true })} /> : ""}

+ +
+
+ +
+ {this.renderPolicies()} + {this.state.policies.length > 0 && this.submitGithubPolicy()} />} +
+
+
); } diff --git a/app/components/Settings/Settings.jsx b/app/components/Settings/Settings.jsx index 6e029fd..33e9b54 100644 --- a/app/components/Settings/Settings.jsx +++ b/app/components/Settings/Settings.jsx @@ -1,6 +1,9 @@ import React from 'react'; import TextField from 'material-ui/TextField'; import Checkbox from 'material-ui/Checkbox'; +import { Tabs, Tab } from 'material-ui/Tabs'; +import sharedStyles from '../shared/styles.css'; +import Paper from 'material-ui/Paper'; import styles from './settings.css'; import _ from 'lodash'; @@ -42,36 +45,40 @@ class Settings extends React.Component { render() { return (
-

Settings

-

Customize your settings here.

-

You are currently connected to the Vault cluster on - {window.localStorage.getItem('vaultUrl')}. - To switch this, you will need to logout.

-
-

General

- - -
-
-

Secrets

- - -
+ + + + Here you can customize your Vault UI settings. + + +
+

General

+ + +
+
+

Secrets

+ + +
+
+
+
) } diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index 6a7b3b4..e2b3a20 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -15,7 +15,6 @@ const supported_secret_backend_types = [ const supported_auth_backend_types = [ 'token', 'github', - 'aws-ec2' ] function snackBarMessage(message) { From b119d5da9bb6872f04bb0291b302c5226eb683b7 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sun, 19 Feb 2017 03:17:57 +1100 Subject: [PATCH 34/38] Integrate policypicker in token backend component --- app/components/Authentication/Token/Token.jsx | 201 ++++++------------ .../shared/PolicyPicker/PolicyPicker.jsx | 16 +- app/components/shared/Wrapping/Unwrapper.jsx | 2 +- app/components/shared/Wrapping/Wrapper.jsx | 2 +- .../Wrapping/{unwrapper.css => wrapping.css} | 0 5 files changed, 82 insertions(+), 139 deletions(-) rename app/components/shared/Wrapping/{unwrapper.css => wrapping.css} (100%) diff --git a/app/components/Authentication/Token/Token.jsx b/app/components/Authentication/Token/Token.jsx index c7ade34..2fff960 100644 --- a/app/components/Authentication/Token/Token.jsx +++ b/app/components/Authentication/Token/Token.jsx @@ -2,7 +2,7 @@ import React from 'react' import _ from 'lodash'; import styles from './token.css'; import sharedStyles from '../../shared/styles.css'; -import { red500, orange500, green100, green400, red300, white } from 'material-ui/styles/colors.js' +import { red500, orange500, green100, red300, white } from 'material-ui/styles/colors.js' import RaisedButton from 'material-ui/RaisedButton'; import { Table, TableBody, TableFooter, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; import { Toolbar, ToolbarGroup, ToolbarSeparator } from 'material-ui/Toolbar'; @@ -15,20 +15,20 @@ import Subheader from 'material-ui/Subheader'; import Divider from 'material-ui/Divider'; import LinearProgress from 'material-ui/LinearProgress'; import { Tabs, Tab } from 'material-ui/Tabs'; -import Checkbox from 'material-ui/Checkbox'; import Toggle from 'material-ui/Toggle'; import Paper from 'material-ui/Paper'; import { List, ListItem } from 'material-ui/List'; import TextField from 'material-ui/TextField'; import FlatButton from 'material-ui/FlatButton'; import FontIcon from 'material-ui/FontIcon'; -import {tokenHasCapabilities, callVaultApi} from '../../shared/VaultUtils.jsx' +import { tokenHasCapabilities, callVaultApi } from '../../shared/VaultUtils.jsx' import JsonEditor from '../../shared/JsonEditor.jsx'; import UltimatePagination from 'react-ultimate-pagination-material-ui' import Avatar from 'material-ui/Avatar'; import ActionClass from 'material-ui/svg-icons/action/class'; import ActionDelete from 'material-ui/svg-icons/action/delete'; import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; +import PolicyPicker from '../../shared/PolicyPicker/PolicyPicker.jsx' function snackBarMessage(message) { let ev = new CustomEvent("snackbar", { detail: { message: message } }); @@ -180,8 +180,8 @@ export default class TokenAuthBackend extends React.Component { }).catch(() => { snackBarMessage(new Error("Access denied")); }) - } } - > + }} + > {window.localStorage.getItem("showDeleteModal") === 'false' ? : } ) @@ -192,10 +192,10 @@ export default class TokenAuthBackend extends React.Component { primaryText={role} leftAvatar={} />} rightIconButton={action} - onTouchTap={(e, v) => { + onTouchTap={() => { this.setState({ selectedRole: role, newRoleName: '' }); - } } - > + }} + >
{ snackBarMessage(err || new Error("Access denied")); }) - } } - /> + }} + />
) @@ -237,7 +237,7 @@ export default class TokenAuthBackend extends React.Component { .catch(snackBarMessage) }) .catch((err) => { - snackBarMessage(err || `No permissions to read content of role ${his.state.selectedRole}`); + snackBarMessage(err || `No permissions to read content of role ${this.state.selectedRole}`); this.setState({ selectedRole: '' }); }) } @@ -340,15 +340,9 @@ export default class TokenAuthBackend extends React.Component { .then(() => { // sudo users can use the `no_parent` attribute to create orphan tokens this.setState({ 'canCreateOrphan': 'no_parent' }); - // sudo users can assign any policy to a token. load the full list, if possible - return tokenHasCapabilities(['read'], 'sys/policy').then(() => { - return callVaultApi('get', 'sys/policy').then((resp) => { - this.setState({ newTokenAvailablePolicies: resp.data.data.keys }); - }); - }); }) .catch(() => { - // User doesnt have sudo or policy list failed, either way use user assigned policies + // User doesnt have sudo, use user assigned policies let p1 = callVaultApi('get', 'auth/token/lookup-self').then((resp) => { this.setState({ newTokenAvailablePolicies: resp.data.data.policies }); }).catch(); // <- This shouldnt have failed @@ -397,7 +391,7 @@ export default class TokenAuthBackend extends React.Component { actions={actions} open={this.state.revokeConfirmDialog} onRequestClose={() => this.setState({ revokeConfirmDialog: false })} - > + >

You are about to permanently delete {this.state.revokeAccessorId}. Are you sure?

To disable this prompt, visit the settings page. @@ -416,20 +410,20 @@ export default class TokenAuthBackend extends React.Component { actions={actions} open={this.state.accessorInfoDialog} onRequestClose={() => this.setState({ accessorInfoDialog: false })} - > + > + /> ) } DeleteRole(rolename) { callVaultApi('delete', 'auth/token/roles/' + rolename, null, null, null) - .then((resp) => { + .then(() => { this.reloadRoles() snackBarMessage(`Role ${rolename} deleted`); }) @@ -454,7 +448,7 @@ export default class TokenAuthBackend extends React.Component { actions={actions} open={this.state.roleDeleteDialogOpen} onRequestClose={() => this.setState({ roleDeleteDialogOpen: false })} - > + >

You are about to permanently delete {this.state.selectedRole}. Are you sure?

To disable this prompt, visit the settings page. @@ -462,17 +456,6 @@ export default class TokenAuthBackend extends React.Component { } renderRoleDialog() { - - let handlePoliciesCheckUncheck = (policy, isInputChecked) => { - let role = this.state.roleAttributes - if (isInputChecked) { - role.allowed_policies = _.union(role.allowed_policies, [policy]); - } else { - role.allowed_policies = _.without(role.allowed_policies, policy); - } - this.setState({ roleAttributes: role }); - }; - let handleSubmitAction = () => { if (_.indexOf(this.state.roleList, this.state.newRoleName) !== -1) { @@ -503,7 +486,7 @@ export default class TokenAuthBackend extends React.Component { callVaultApi('post', vault_endpoint, {}, role) - .then((resp) => { + .then(() => { this.setState({ loading: false, selectedRole: '', @@ -528,19 +511,6 @@ export default class TokenAuthBackend extends React.Component { ]; - - let policiesItems = this.state.newTokenAvailablePolicies.map((policy, idx) => { - if (policy != "default" && policy != "root") { - return ( - { handlePoliciesCheckUncheck(policy, iic) } } />} - primaryText={policy} - /> - ) - } - }); - return (
this.setState({ roleDialogOpen: false })} - > + > {this.state.selectedRole == '' ? { this.setState({ newRoleName: e.target.value }); - } } - /> + }} + /> : ''} + }} + /> + }} + /> Settings + }} + /> } primaryText="Orphan Token" - /> - { - let role = this.state.roleAttributes; - role.disallowed_policies = v ? [] : ['default']; - this.setState({ roleAttributes: role }); - } } - /> - } - primaryText="Allow Default Policy" - /> + /> + }} + /> } primaryText="Renewable" - /> + /> Allowed Policies - {policiesItems} + { + let role = this.state.roleAttributes; + role.allowed_policies = policies; + this.setState({ roleAttributes: role }); + }} + /> -
) @@ -646,14 +610,6 @@ export default class TokenAuthBackend extends React.Component { renderNewTokenDialog() { - let handlePoliciesCheckUncheck = (policy, isInputChecked) => { - if (isInputChecked) { - this.setState({ newTokenSelectedPolicies: _.union(this.state.newTokenSelectedPolicies, [policy]) }) - } else { - this.setState({ newTokenSelectedPolicies: _.without(this.state.newTokenSelectedPolicies, policy) }) - } - }; - let handleCreateAction = () => { this.setState({ loading: true }); @@ -710,18 +666,6 @@ export default class TokenAuthBackend extends React.Component { this.setState({ newTokenCode: '', newTokenDialog: false })} /> ]; - let policiesItems = this.state.newTokenAvailablePolicies.map((policy, idx) => { - if (policy != "default" && policy != "root") { - return ( - { handlePoliciesCheckUncheck(policy, iic) } } />} - primaryText={policy} - /> - ) - } - }); - return (
this.setState({ newTokenDialog: false })} - > + > { this.setState({ newTokenDisplayName: e.target.value }) } } + onChange={(e) => { this.setState({ newTokenDisplayName: e.target.value }) }} autoFocus - /> + /> { this.setState({ newTokenMaxUses: Math.max(0, Number(e.target.value)) }) } } - /> + onChange={(e) => { this.setState({ newTokenMaxUses: Math.max(0, Number(e.target.value)) }) }} + /> { this.setState({ newTokenOverrideTTL: Math.max(0, Number(e.target.value)) }) } } - /> + onChange={(e) => { this.setState({ newTokenOverrideTTL: Math.max(0, Number(e.target.value)) }) }} + /> Settings this.setState({ newTokenIsOrphan: v })} - /> + /> } primaryText="Orphan Token" - /> - handlePoliciesCheckUncheck('default', v)} - /> - } - primaryText="Default Policy" - /> + /> this.setState({ newTokenIsRenewable: v })} - /> + /> } primaryText="Renewable" - /> + /> - Assign Additional Policies - {policiesItems} + Assign Policies + { + this.setState({ newTokenSelectedPolicies: policies }); + }} + /> - - + >
- } label="Copy to Clipboard" onTouchTap={() => { copy(this.state.newTokenCode) } } /> + /> + } label="Copy to Clipboard" onTouchTap={() => { copy(this.state.newTokenCode) }} />
@@ -852,8 +791,8 @@ export default class TokenAuthBackend extends React.Component { newTokenMaxUses: 0, newTokenOverrideTTL: 0 }) - } } - /> + }} + /> @@ -862,15 +801,15 @@ export default class TokenAuthBackend extends React.Component { primaryText="Show details" disabled={!this.state.selectedAccessor} onTouchTap={() => this.setState({ accessorInfoDialog: true })} - /> + /> { this.setState({ revokeConfirmDialog: true, revokeAccessorId: this.state.selectedAccessor }) - } } - /> + }} + /> @@ -894,7 +833,7 @@ export default class TokenAuthBackend extends React.Component { currentPage={this.state.currentPage} totalPages={this.state.totalPages} onChange={this.onPageChangeFromPagination} - /> + /> @@ -921,8 +860,8 @@ export default class TokenAuthBackend extends React.Component { roleAttributes: _.clone(this.defaultRoleAttributes), roleDialogOpen: true }) - } } - /> + }} + /> diff --git a/app/components/shared/PolicyPicker/PolicyPicker.jsx b/app/components/shared/PolicyPicker/PolicyPicker.jsx index ef755d3..b44d857 100644 --- a/app/components/shared/PolicyPicker/PolicyPicker.jsx +++ b/app/components/shared/PolicyPicker/PolicyPicker.jsx @@ -13,15 +13,17 @@ class PolicyPicker extends React.Component { onError: PropTypes.func, onSelectedChange: PropTypes.func, excludePolicies: PropTypes.array, - title: PropTypes.string, + selectedPolicies: PropTypes.array, + fixedPolicyList: PropTypes.array, height: PropTypes.string, }; static defaultProps = { + fixedPolicyList: [], + selectedPolicies: [], onError: (err) => { console.error(err) }, onSelectedChange: () => {}, - excludePolicies: ['default'], - title: "Policy Picker", + excludePolicies: [], height: "300px", }; @@ -29,9 +31,9 @@ class PolicyPicker extends React.Component { super(props); this.state = { - availablePolicies: [], + availablePolicies: this.props.fixedPolicyList, displayedAvailPolicies: [], - selectedPolicies: [], + selectedPolicies: this.props.selectedPolicies, manualPolicies: [], policyListAvailable: true, searchText: '' @@ -68,7 +70,9 @@ class PolicyPicker extends React.Component { } componentDidMount() { - this.reloadPolicyList(); + if(_.isEmpty(this.props.fixedPolicyList)) { + this.reloadPolicyList(); + } } componentWillUpdate(nextProps, nextState) { diff --git a/app/components/shared/Wrapping/Unwrapper.jsx b/app/components/shared/Wrapping/Unwrapper.jsx index c8ac045..d131df2 100644 --- a/app/components/shared/Wrapping/Unwrapper.jsx +++ b/app/components/shared/Wrapping/Unwrapper.jsx @@ -1,7 +1,7 @@ import React, { PropTypes, Component } from 'react' import { callVaultApi } from '../VaultUtils.jsx' import JsonEditor from '../JsonEditor.jsx'; -import styles from './unwrapper.css'; +import styles from './wrapping.css'; export default class SecretUnwrapper extends Component { static propTypes = { diff --git a/app/components/shared/Wrapping/Wrapper.jsx b/app/components/shared/Wrapping/Wrapper.jsx index 524d6f3..9fac3d5 100644 --- a/app/components/shared/Wrapping/Wrapper.jsx +++ b/app/components/shared/Wrapping/Wrapper.jsx @@ -10,7 +10,7 @@ import FlatButton from 'material-ui/FlatButton'; import Popover from 'material-ui/Popover'; import Menu from 'material-ui/Menu'; import MenuItem from 'material-ui/MenuItem'; -import styles from './Unwrapper.css' +import styles from './wrapping.css' import sharedStyles from '../styles.css'; export default class SecretWrapper extends Component { diff --git a/app/components/shared/Wrapping/unwrapper.css b/app/components/shared/Wrapping/wrapping.css similarity index 100% rename from app/components/shared/Wrapping/unwrapper.css rename to app/components/shared/Wrapping/wrapping.css From c108baa298524baac8ebc78885981c23ea20afc0 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sun, 19 Feb 2017 03:26:55 +1100 Subject: [PATCH 35/38] add test unprivileged user --- app/App.jsx | 2 -- run-docker-compose-dev | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/App.jsx b/app/App.jsx index 21e425f..413eeaf 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -7,7 +7,6 @@ import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import getMuiTheme from 'material-ui/styles/getMuiTheme'; import App from './components/App/App.jsx'; import SecretsGeneric from './components/Secrets/Generic/Generic.jsx'; -import Health from './components/Health/Health.jsx'; import PolicyManager from './components/Policies/Manage.jsx'; import Settings from './components/Settings/Settings.jsx'; import ResponseWrapper from './components/ResponseWrapper/ResponseWrapper.jsx'; @@ -57,7 +56,6 @@ ReactDOM.render(( - diff --git a/run-docker-compose-dev b/run-docker-compose-dev index 9baae26..2893de2 100755 --- a/run-docker-compose-dev +++ b/run-docker-compose-dev @@ -21,6 +21,7 @@ exec_in_vault vault auth-enable github exec_in_vault vault auth-enable -path=awsaccount1 aws-ec2 exec_in_vault vault policy-write admin /app/admin.hcl exec_in_vault vault write auth/userpass/users/test password=test policies=admin +exec_in_vault vault write auth/userpass/users/lame password=lame policies=default exec_in_vault vault write secret/test somekey=somedata exec_in_vault vault mount -path=ultrasecret generic exec_in_vault vault write ultrasecret/moretest somekey=somedata From 6b5ec8101329574cf5523c24d516a62bc37c838a Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Sun, 19 Feb 2017 15:45:04 +1100 Subject: [PATCH 36/38] small layout fixes --- app/components/App/App.jsx | 2 +- app/components/App/app.css | 4 ++-- app/components/shared/Menu/Menu.jsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/components/App/App.jsx b/app/components/App/App.jsx index f5d2dee..ebfc70d 100644 --- a/app/components/App/App.jsx +++ b/app/components/App/App.jsx @@ -251,7 +251,7 @@ export default class App extends React.Component {
- + {this.props.children || welcome}
diff --git a/app/components/App/app.css b/app/components/App/app.css index 796ead2..8f87e2a 100644 --- a/app/components/App/app.css +++ b/app/components/App/app.css @@ -1,6 +1,6 @@ #content { - padding-left: 50px; - width: calc(100vw - 305px - 50px); + padding-left: 30px; + width: calc(100vw - 305px); display: inline-block; margin-left: 250px; margin-top: 80px; diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index e2b3a20..1f3bb4a 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -131,8 +131,8 @@ class Menu extends React.Component { ]} /> From c4b3fddf11d5bf5c66d08c50a01d0f5c4599f25f Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Tue, 21 Feb 2017 10:44:11 +1100 Subject: [PATCH 37/38] Remove unused nodejs code --- src/respwrapping.js | 48 --------------------------------------------- src/routeHandler.js | 3 --- 2 files changed, 51 deletions(-) delete mode 100644 src/respwrapping.js diff --git a/src/respwrapping.js b/src/respwrapping.js deleted file mode 100644 index 6b3a145..0000000 --- a/src/respwrapping.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -var axios = require('axios'); -var _ = require('lodash'); - -exports.wrapResponse = function (req, res) { - let endpoint = `/v1/sys/wrapping/wrap`; - let vaultAddr = decodeURI(req.query['vaultaddr']); - let config = { - headers: { - 'X-Vault-Token': req.query['token'], - 'X-Vault-Wrap-TTL': `${_.get(req, "body.ttl")}s` - } - }; - let dataValue = _.get(req, "body.value"); - try { - dataValue = JSON.parse(dataValue) - } catch (e) { } - - let data = { 'value': dataValue }; - - axios.post(`${vaultAddr}${endpoint}`, data, config) - .then((resp) => { - res.json(resp.data.wrap_info); - }) - .catch((err) => { - res.status(err.response.status).send(err.response); - }); -} - -exports.unwrapResponse = function (req, res) { - let endpoint = `/v1/sys/wrapping/unwrap`; - let vaultAddr = decodeURI(req.query['vaultaddr']); - let config = { - headers: { - 'X-Vault-Token': decodeURI(req.query['token']) - } - }; - - axios.post(`${vaultAddr}${endpoint}`, {}, config) - .then((resp) => { - res.json(resp.data.data); - }) - .catch((err) => { - let error = _.get(err, "response.data.errors[0]"); - res.status(err.response.status).send(error); - }); -} \ No newline at end of file diff --git a/src/routeHandler.js b/src/routeHandler.js index 47d085a..6eabc35 100644 --- a/src/routeHandler.js +++ b/src/routeHandler.js @@ -1,12 +1,9 @@ 'use strict'; -var respwrapping = require('./respwrapping'); var vaultapi = require('./vaultapi'); module.exports = (function () { return { - wrapValue: respwrapping.wrapResponse, - unwrapValue: respwrapping.unwrapResponse, vaultapi: vaultapi.callMethod }; })(); From fcf5a5d3ed31d650201ccf5370e04daee6f5eaf9 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Tue, 21 Feb 2017 10:46:20 +1100 Subject: [PATCH 38/38] Fix tokens component layout --- app/components/Authentication/Token/Token.jsx | 194 +++++++++--------- 1 file changed, 96 insertions(+), 98 deletions(-) diff --git a/app/components/Authentication/Token/Token.jsx b/app/components/Authentication/Token/Token.jsx index 2fff960..4d9e5b9 100644 --- a/app/components/Authentication/Token/Token.jsx +++ b/app/components/Authentication/Token/Token.jsx @@ -766,111 +766,109 @@ export default class TokenAuthBackend extends React.Component { {this.renderNewTokenDialog()} {this.renderRoleDialog()} {this.renderRoleDeleteConfirmDialog()} - - - - - Here you can create new tokens and list active tokens.
- Existing tokens are represented by their respective Accessor ID. + + + + Here you can create new tokens and list active tokens.
+ Existing tokens are represented by their respective Accessor ID.
- - - - + + + { + this.setState({ + newTokenDialog: true, + newTokenCodeDialog: false, + newTokenCode: '', + newTokenSelectedPolicies: ['default'], + newTokenIsOrphan: false, + newTokenIsRenewable: true, + newTokenMaxUses: 0, + newTokenOverrideTTL: 0 + }) + }} + /> + + + + }> + this.setState({ accessorInfoDialog: true })} + /> + + { - this.setState({ - newTokenDialog: true, - newTokenCodeDialog: false, - newTokenCode: '', - newTokenSelectedPolicies: ['default'], - newTokenIsOrphan: false, - newTokenIsRenewable: true, - newTokenMaxUses: 0, - newTokenOverrideTTL: 0 - }) + this.setState({ revokeConfirmDialog: true, revokeAccessorId: this.state.selectedAccessor }) }} /> - - - - }> - this.setState({ accessorInfoDialog: true })} - /> - - { - this.setState({ revokeConfirmDialog: true, revokeAccessorId: this.state.selectedAccessor }) - }} + + + + + + + Accessor ID + Display Name + Additional Policies + Created + Orphan + + + + {this.renderAccessorTableItems()} + + + + + - - - -
- - - Accessor ID - Display Name - Additional Policies - Created - Orphan - - - - {this.renderAccessorTableItems()} - - - - - - - - -
+ + + + +
+
+ + + Here you can create, list and edit token roles.
+ Roles can enforce specific behaviors when creating new tokens.
-
- - - Here you can create, list and edit token roles.
- Roles can enforce specific behaviors when creating new tokens. -
- - - - { + + + + { - this.setState({ - selectedRole: '', - newRoleName: '', - roleAttributes: _.clone(this.defaultRoleAttributes), - roleDialogOpen: true - }) - }} - /> - - - - {this.renderRoleListItems()} - - -
-
-
+ this.setState({ + selectedRole: '', + newRoleName: '', + roleAttributes: _.clone(this.defaultRoleAttributes), + roleDialogOpen: true + }) + }} + /> + + + + {this.renderRoleListItems()} + +
+ + ); }