diff --git a/app/components/Policies/vault-policy-schema.json b/app/components/Policies/vault-policy-schema.json
new file mode 100644
index 0000000..068142a
--- /dev/null
+++ b/app/components/Policies/vault-policy-schema.json
@@ -0,0 +1,50 @@
+{
+ "type": "object",
+ "required": [ "path" ],
+ "properties": {
+ "path": {
+ "type": "object",
+ "minProperties": 1,
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[^\/].*$": {
+ "type": "object",
+ "additionalProperties": false,
+ "anyOf": [
+ {"required": ["capabilities"]},
+ {"required": ["policy"]}
+ ],
+ "properties": {
+ "capabilities" : {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "enum": [
+ "create",
+ "read",
+ "update",
+ "delete",
+ "list",
+ "sudo",
+ "deny"
+ ]
+ }
+ },
+ "policy" : {
+ "type": "string",
+ "enum": [
+ "read",
+ "write",
+ "sudo",
+ "deny"
+ ]
+ }
+ }
+
+ }
+ }
+ }
+ }
+}
diff --git a/app/components/Secrets/Secrets.jsx b/app/components/Secrets/Secrets.jsx
index a0562fb..0c91cfb 100644
--- a/app/components/Secrets/Secrets.jsx
+++ b/app/components/Secrets/Secrets.jsx
@@ -13,6 +13,7 @@ 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 JsonEditor from '../shared/JsonEditor.jsx';
const copyEvent = new CustomEvent("snackbar", {
detail: {
@@ -49,7 +50,8 @@ class Secrets extends React.Component {
'renderList',
'renderNamespace',
'clickSecret',
- 'secretChanged',
+ 'secretChangedTextEditor',
+ 'secretChangedJsonEditor',
'updateSecret',
'renderEditDialog',
'renderNewKeyDialog',
@@ -115,35 +117,19 @@ class Secrets extends React.Component {
})
}
- secretChanged(e, v) {
- if (this.state.useRootKey) {
- //If root key is in place, set as json object to properly escape special characters
- let tmp = {};
- tmp = _.set(tmp, `${this.state.rootKey}`, v);
- this.state.focusSecret = tmp;
+ secretChangedJsonEditor(v, syntaxCheckOk) {
+ if (syntaxCheckOk && v) {
+ this.setState({disableSubmit: false, focusSecret: v});
} else {
- this.state.focusSecret = v;
-
+ this.setState({disableSubmit: true});
}
}
- checkValidJson() {
- try {
- if (this.state.useRootKey) {
- JSON.parse(JSON.stringify(this.state.focusSecret));
- } else {
- JSON.parse(this.state.focusSecret);
- }
- this.setState({
- errorMessage: ''
- })
- return true;
- } catch (e) {
- this.setState({
- errorMessage: `Invalid JSON`
- })
- return false;
- }
+ secretChangedTextEditor(e, v) {
+ this.setState({disableSubmit: false});
+ let tmp = {};
+ _.set(tmp, `${this.state.rootKey}`, v);
+ this.state.focusSecret = tmp;
}
renderEditDialog() {
@@ -153,28 +139,47 @@ class Secrets extends React.Component {
];
let submitUpdate = () => {
- if (!this.checkValidJson()) return;
this.updateSecret(false);
this.setState({ openEditModal: false });
}
+ var objectIsBasicRootKey = _.size(this.state.focusSecret) == 1 && this.state.focusSecret.hasOwnProperty(this.state.rootKey);
+ var content;
+
+ if (objectIsBasicRootKey && this.state.useRootKey) {
+ var title = `Editing ${this.state.namespace}${this.state.focusKey} with specified root key`;
+ content = (
+
+ );
+ } else {
+ var title = `Editing ${this.state.namespace}${this.state.focusKey}`;
+ content = (
+
+ );
+ }
return (
);
@@ -219,40 +224,52 @@ class Secrets extends React.Component {
});
return;
}
- if (!this.checkValidJson()) return;
+ console.log(this.state.focusSecret);
this.updateSecret(true);
this.setState({ openNewKeyModal: false, errorMessage: '' });
}
const actions = [
this.setState({ openNewKeyModal: false, errorMessage: '' })} />,
-
+
];
var rootKeyInfo;
+ var content;
if (this.state.useRootKey) {
rootKeyInfo = "Current Root Key: " + this.state.rootKey;
+ var content = (
+
+ );
} else {
- rootKeyInfo = "No Root Key set. Value must be JSON.";
+ content = (
+
+ );
}
return (
@@ -322,28 +339,15 @@ class Secrets extends React.Component {
let fullKey = `${this.state.namespace}${key}`;
axios.get(`/secret?vaultaddr=${encodeURI(window.localStorage.getItem("vaultUrl"))}&secret=${encodeURI(fullKey)}&token=${encodeURI(window.localStorage.getItem("vaultAccessToken"))}`)
.then((resp) => {
- let val = this.state.useRootKey ? _.get(resp, `data.${this.state.rootKey}`) : resp.data;
- if (val === undefined) {
- this.setState({
- errorMessage: `No value exists under the root key '${this.state.rootKey}'.`,
- focusSecret: '',
- disableSubmit: true,
- openEditModal: true,
- disableTextField: true,
- listBackends: false
- });
- } else {
- val = typeof val == 'object' ? JSON.stringify(val) : val;
- this.setState({
- errorMessage: '',
- disableSubmit: false,
- disableTextField: false,
- openEditModal: true,
- focusKey: key,
- focusSecret: val,
- listBackends: false
- });
- }
+ this.setState({
+ errorMessage: '',
+ disableSubmit: false,
+ disableTextField: false,
+ openEditModal: true,
+ focusKey: key,
+ focusSecret: resp.data,
+ listBackends: false
+ });
})
.catch((err) => {
console.error(err.stack);
@@ -449,12 +453,18 @@ class Secrets extends React.Component {
Secrets
Here you can view, update, and delete keys stored in your Vault. Just remember, deleting keys cannot be undone!
this.setState({ openNewKeyModal: true, focusKey: '', focusSecret: '', errorMessage: '' })} />
+ onTouchTap={() => this.setState({
+ disableSubmit: true,
+ openNewKeyModal: true,
+ focusKey: '',
+ focusSecret: '',
+ errorMessage: ''
+ })} />
{this.renderNamespace()}
{this.renderList()}
diff --git a/app/components/shared/JsonEditor.jsx b/app/components/shared/JsonEditor.jsx
new file mode 100644
index 0000000..f141d45
--- /dev/null
+++ b/app/components/shared/JsonEditor.jsx
@@ -0,0 +1,89 @@
+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';
+import 'jsoneditor/src/css/menu.css';
+import 'jsoneditor/src/css/searchbox.css';
+import 'jsoneditor/src/css/contextmenu.css';
+
+function isValid(value) {
+ return value !== '' && value !== undefined && value !== null;
+};
+
+class JsonEditor extends React.Component {
+ static propTypes = {
+ rootName: PropTypes.string,
+ value: PropTypes.any,
+ mode: PropTypes.oneOf(['tree', 'code']),
+ schema: PropTypes.object,
+ onChange: PropTypes.func,
+ };
+
+ static defaultProps = {
+ rootName: '',
+ value: '',
+ mode: 'tree',
+ schema: null,
+ onChange: () => {}
+ };
+
+ state = {
+ hasValue: false,
+ };
+
+ constructor(props) {
+ super(props);
+ if (typeof JSONEditor === undefined) {
+ throw new Error('JSONEditor is undefined!');
+ }
+ }
+
+ handleInputChange = () => {
+ try {
+ this.setState({hasValue: isValid(this._jsoneditor.get())});
+ if (this.props.onChange) {
+ let schemaCheck = true;
+ if (this.props.schema) {
+ schemaCheck = this._jsoneditor.validateSchema(this._jsoneditor.get());
+ }
+ this.props.onChange(this._jsoneditor.get(), this.state.hasValue, schemaCheck);
+ }
+ } catch (e) {
+ this.props.onChange(null, false, false);
+ }
+ }
+
+ componentDidMount() {
+ var container = this.editorEl;//ReactDOM.findDOMNode(this);
+ var options = {
+ name: this.props.rootName,
+ mode: this.props.mode,
+ modes: ['tree', 'code'],
+ schema: this.props.schema,
+ onChange: this.handleInputChange,
+ };
+
+ this._jsoneditor = new JSONEditor(container, options, this.props.value);
+ this.setState({hasValue: true});
+ this._jsoneditor.focus();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.rootName !== this.props.rootName) {
+ this._jsoneditor.setName(nextProps.rootName);
+ }
+ }
+
+ componentWillUnmount() {
+ this._jsoneditor.destroy();
+ }
+
+ render() {
+ return (
+ { this.editorEl = c; }} />
+ );
+ }
+}
+
+export default JsonEditor;
diff --git a/docker-compose.yaml b/docker-compose.yaml
index d40f432..0671c6f 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -19,6 +19,9 @@ services:
volumes:
- .:/app
- /app/node_modules
+ environment:
+ VAULT_URL_DEFAULT: http://vault:8200
+ VAULT_AUTH_DEFAULT: USERNAMEPASSWORD
webpack:
build: .
diff --git a/package.json b/package.json
index 3619de0..e10339c 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,8 @@
"babel-preset-react": "^6.16.0",
"babel-preset-stage-2": "^6.18.0",
"css-loader": "^0.25.0",
+ "url-loader": "^0.5.7",
+ "json-loader": "^0.5.4",
"extract-text-webpack-plugin": "^1.0.1",
"material-ui": "^0.16.4",
"postcss-loader": "^1.1.0",
@@ -53,6 +55,7 @@
"hcl-to-json": "0.0.4",
"lodash": "^4.16.6",
"material-ui": "^0.16.1",
+ "jsoneditor": "^5.5.11",
"react": "^15.4.0",
"react-dom": "^15.4.0",
"react-router": "^3.0.0",
diff --git a/run-docker-compose-dev b/run-docker-compose-dev
index f97af15..1b74f02 100755
--- a/run-docker-compose-dev
+++ b/run-docker-compose-dev
@@ -19,6 +19,7 @@ exec_in_vault vault status
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
echo "------------- Vault Root Token -------------"
docker-compose logs vault | grep 'Root Token:' | tail -n 1
diff --git a/src/policies.js b/src/policies.js
index c0e14d1..418b155 100644
--- a/src/policies.js
+++ b/src/policies.js
@@ -48,16 +48,8 @@ exports.updatePolicy = function (req, res) {
//API requires an escaped JSON
let policy = _.get(req, "body.Policy");
- // Attempt to parse into JSON incase a stringified JSON was sent
- try {
- policy = JSON.parse(policy)
- } catch (e) { }
-
- //If the user passed in an HCL document, convert to stringified JSON as required by the API
- let rules = typeof policy == 'object' ? JSON.stringify(policy) : JSON.stringify(hcltojson(policy));
-
let body = {
- rules: rules
+ rules: JSON.stringify(policy)
};
axios.put(`${vaultAddr}${endpoint}`, body, config)
@@ -65,8 +57,7 @@ exports.updatePolicy = function (req, res) {
res.json(resp.data);
})
.catch((err) => {
- console.error(err.stack);
- res.status(err.response.status).send(err.response);
+ res.status(err.response.status).send(err.response.data);
});
}
diff --git a/webpack.config.js b/webpack.config.js
index 2508c48..edc7b30 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -21,11 +21,19 @@ module.exports = {
exclude: 'node_modules'
}, {
test: /\.json$/i,
- loader: 'json',
+ loader: 'json-loader',
exclude: 'node_modules'
+ }, {
+ test: /\.svg$/,
+ loader: 'url'
+ }, {
+ test: /\.css$/,
+ loader: ExtractTextPlugin.extract('css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader'),
+ exclude: /node_modules/
}, {
test: /\.css$/,
- loader: ExtractTextPlugin.extract('css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader')
+ loader: ExtractTextPlugin.extract('css!postcss-loader'),
+ include: /node_modules/
}]
},
postcss: [ autoprefixer({ browsers: ['last 2 versions'] }) ],
@@ -43,6 +51,7 @@ module.exports = {
// comments: false,
// sourceMap: false
// }),
- new ExtractTextPlugin("styles.css")
+ new ExtractTextPlugin("styles.css"),
+ new webpack.IgnorePlugin(/regenerator|nodent|js-beautify/, /ajv/)
]
};