Skip to content
This repository has been archived by the owner on Mar 27, 2019. It is now read-only.

Commit

Permalink
Merge pull request #38 from cotdsa/json_editor
Browse files Browse the repository at this point in the history
Graphical JSON editor for secrets and policies.

Awesome work @msessa-cotd!
  • Loading branch information
djenriquez authored Jan 13, 2017
2 parents 78e81fd + 1740081 commit a9ce841
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 156 deletions.
160 changes: 89 additions & 71 deletions app/components/Policies/Manage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ 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 JsonEditor from '../shared/JsonEditor.jsx';
import hcltojson from 'hcl-to-json'
import jsonschema from './vault-policy-schema.json'

export default class Manage extends React.Component {
constructor(props) {
Expand All @@ -17,11 +20,13 @@ export default class Manage extends React.Component {
openEditModal: false,
openNewPolicyModal: false,
newPolicyErrorMessage: '',
newPolicyNameErrorMessage: '',
openDeleteModal: false,
editingPolicy: -1,
focusPolicy: -1,
deletingPolicy: '',
policies: [],
currentPolicy: '',
disableSubmit: false,
errorMessage: '',
forbidden: false,
buttonColor: 'lightgrey'
Expand All @@ -31,6 +36,7 @@ export default class Manage extends React.Component {
this,
'updatePolicy',
'listPolicies',
'policyChangeSetState',
'renderEditDialog',
'renderNewPolicyDialog',
'renderDeleteConfirmationDialog',
Expand All @@ -45,23 +51,27 @@ export default class Manage extends React.Component {
this.listPolicies();
}

updatePolicy() {
let policyName = this.state.editingPolicy;
updatePolicy(policyName, isNewPolicy) {

axios.put(`/policy?vaultaddr=${encodeURI(window.localStorage.getItem("vaultUrl"))}&policy=${encodeURI(policyName)}&token=${encodeURI(window.localStorage.getItem("vaultAccessToken"))}`, { "Policy": this.state.currentPolicy })
.then((resp) => {
// Custom future logic on success
this.setState({
errorMessage: ''
});
if (isNewPolicy) {
let policies = this.state.policies;
policies.push({ name: policyName });
this.setState({
policies: policies
});
}
})
.catch((err) => {
console.error(err.stack);
this.setState({
errorMessage: err.response.data
});
if(err.response.data.errors){
this.setState({
errorMessage: err.response.data.errors.join('<br />')
});
}
})

this.setState({ openNewPolicyModal: false });
this.setState({ openEditModal: false });
}

Expand Down Expand Up @@ -90,89 +100,74 @@ export default class Manage extends React.Component {
});
}

policyChangeSetState(v, syntaxCheckOk, schemaCheckOk) {
if (syntaxCheckOk && schemaCheckOk && v) {
this.setState({disableSubmit: false, currentPolicy: v});
} else {
this.setState({disableSubmit: true});
}
}

renderEditDialog() {
const actions = [
<FlatButton label="Cancel" primary={true} onTouchTap={() => this.setState({ openEditModal: false })} />,
<FlatButton label="Submit" primary={true} onTouchTap={() => this.updatePolicy()} />
<FlatButton label="Submit" disabled={this.state.disableSubmit} primary={true} onTouchTap={() => this.updatePolicy(this.state.focusPolicy, false)} />
];

let policyChanged = (e, v, ) => {
this.state.currentPolicy = e.target.value;
};

return (
<Dialog
title={`Editing ${this.state.editingPolicy}`}
title={`Editing ${this.state.focusPolicy}`}
modal={false}
actions={actions}
open={this.state.openEditModal}
onRequestClose={() => this.setState({ openEditModal: false })}
autoScrollBodyContent={true}
>
<TextField
style={{ height: '5000px' }}
onChange={(e, v) => policyChanged(e, v)}
name="editingText" multiLine={true}
autoFocus
defaultValue={this.state.currentPolicy}
fullWidth={true} />
<JsonEditor
rootName={this.state.focusPolicy}
value={this.state.currentPolicy}
mode={'code'}
schema={jsonschema}
onChange={this.policyChangeSetState}
/>
</Dialog>
);
}

renderNewPolicyDialog() {
const MISSING_POLICY_ERROR = "Policy cannot be empty.";
const DUPLICATE_POLICY_ERROR = `Policy ${this.state.newPolicy.name} already exists.`;
const DUPLICATE_POLICY_ERROR = `Policy ${this.state.focusPolicy} already exists.`;

let validateAndSubmit = () => {
if (this.state.newPolicy.name === '') {
if (this.state.focusPolicy === '') {
this.setState({
newPolicyErrorMessage: MISSING_POLICY_ERROR
});
return;
}

if (_.filter(this.state.policies, x => x.name === this.state.newPolicy.name).length > 0) {
if (_.filter(this.state.policies, x => x.name === this.state.focusPolicy).length > 0) {
this.setState({
newPolicyErrorMessage: DUPLICATE_POLICY_ERROR
});
return;
}

axios.put(`/policy?vaultaddr=${encodeURI(window.localStorage.getItem("vaultUrl"))}&policy=${encodeURI(this.state.newPolicy.name)}&token=${encodeURI(window.localStorage.getItem("vaultAccessToken"))}`, { "Policy": this.state.currentPolicy })
.then((resp) => {
let policies = this.state.policies;
policies.push({ name: this.state.newPolicy.name });
this.setState({
policies: policies,
errorMessage: ''
});
})
.catch((err) => {
console.error(err.stack);
this.setState({
errorMessage: err.response.data
});
})

this.setState({ openNewPolicyModal: false });
this.updatePolicy(this.state.focusPolicy, true);
}

const actions = [
<FlatButton label="Cancel" primary={true} onTouchTap={() => this.setState({ openNewPolicyModal: false, newPolicyErrorMessage: '' })} />,
<FlatButton label="Submit" primary={true} onTouchTap={validateAndSubmit} />
<FlatButton label="Submit" disabled={this.state.disableSubmit} primary={true} onTouchTap={validateAndSubmit} />
];

let setNewPolicy = (e, v) => {
let currentPolicy = this.state.newPolicy;
if (e.target.name === "newName") {
currentPolicy.name = v;
} else if (e.target.name === "newRules") {
currentPolicy.rules = v;
let validatePolicyName = (event, v) => {
var pattern = /^[^\/&]+$/;
v = v.toLowerCase();
if (v.match(pattern)) {
this.setState({newPolicyNameErrorMessage: '', focusPolicy: v});
} else {
this.setState({newPolicyNameErrorMessage: 'Policy name contains illegal characters'});
}
this.setState({
newPolicy: currentPolicy
});
}


Expand All @@ -186,14 +181,21 @@ export default class Manage extends React.Component {
autoScrollBodyContent={true}
autoDetectWindowHeight={true}
>
<TextField name="newName" autoFocus fullWidth={true} hintText="Name" onChange={(e, v) => setNewPolicy(e, v)} />
<TextField
name="newRules"
multiLine={true}
style={{ height: '5000px' }}
name="newName"
autoFocus
fullWidth={true}
hintText="Rules"
onChange={(e, v) => setNewPolicy(e, v)} />
hintText="Name"
errorText={this.state.newPolicyNameErrorMessage}
onChange={validatePolicyName}
/>
<JsonEditor
rootName={this.state.focusPolicy || null}
value={this.state.currentPolicy}
mode={'code'}
schema={jsonschema}
onChange={this.policyChangeSetState}
/>
<div className={styles.error}>{this.state.newPolicyErrorMessage}</div>
</Dialog>
);
Expand Down Expand Up @@ -224,18 +226,26 @@ export default class Manage extends React.Component {
axios.get(`/policy?vaultaddr=${encodeURI(window.localStorage.getItem("vaultUrl"))}&policy=${encodeURI(policyName)}&token=${encodeURI(window.localStorage.getItem("vaultAccessToken"))}`)
.then((resp) => {
let rules = resp.data.data.rules;
let rules_obj;
// Attempt to parse into JSON incase a stringified JSON was sent
try {
rules = JSON.parse(rules)
rules_obj = JSON.parse(rules);
} catch (e) { }

let val = typeof rules == 'object' ? JSON.stringify(rules, null, 4) : rules;
if (!rules_obj) {
// Previous parse failed, attempt HCL to JSON conversion
rules_obj = hcltojson(rules);
}

this.setState({
openEditModal: true,
editingPolicy: policyName,
currentPolicy: val
});
if(rules_obj) {
this.setState({
openEditModal: true,
focusPolicy: policyName,
currentPolicy: rules_obj,
disableSubmit: true,
errorMessage: '',
});
}
})
.catch((err) => {
console.error(err.stack);
Expand All @@ -252,10 +262,10 @@ export default class Manage extends React.Component {
});
} else {
let policies = this.state.policies;
let policyToDelete = _.find(policies, (policyToDelete) => { return policyToDelete.name === policyName }); å
let policyToDelete = _.find(policies, (policyToDelete) => { return policyToDelete.name === policyName });
policies = _.pull(policies, policyToDelete);
this.setState({
secrets: policies,
policies: policies,
errorMessage: ''
});
}
Expand Down Expand Up @@ -317,7 +327,15 @@ export default class Manage extends React.Component {
backgroundColor={this.state.buttonColor}
hoverColor={green400}
labelStyle={{ color: white }}
onTouchTap={() => this.setState({ openNewPolicyModal: true, newPolicy: { name: '', value: '' } })} />}
onTouchTap={() => this.setState({
openNewPolicyModal: true,
errorMessage: '',
newPolicyErrorMessage: '',
newPolicyNameErrorMessage: '',
disableSubmit: true,
focusPolicy: '',
currentPolicy: { path: { 'sample/path' : { capabilities: ['read'] }} }
})} />}
{this.state.errorMessage &&
<div className={styles.error}>
<FontIcon className="fa fa-exclamation-triangle" color={red500} style={{ marginRight: 10 }} />
Expand Down
50 changes: 50 additions & 0 deletions app/components/Policies/vault-policy-schema.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}

}
}
}
}
}
Loading

0 comments on commit a9ce841

Please sign in to comment.