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
- })}}
- />
-
-
-
- }>
-
-
-
-
-
- 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 (
);
}
- renderDeleteConfirmationDialog() {
+ renderEditObjectDialog() {
const actions = [
- this.setState({ openDeleteModal: false, deletingKey: '' })} />,
- this.deleteKey(this.state.deletingKey)} />
- ];
-
- return (
-
- )
- }
-
- 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 (
);
}
- 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 (
+
+ )
}
- 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 (
-
- )
- }
-
- 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 (
+
+
+
+ )
+ }
+
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 (
-
-
+
- } 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 (
+
+
+
+
+
+ {
+ 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()}
-
-
-
-
-
-
-
-
- {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()}
+
+
+
+
);
}