No difference detected.
;
93 | } else {
94 | return (
95 |
107 |
{ this.editorEl = c; }} />
108 |
109 |
this.setState({showDiff: isChecked})}
113 | />
114 | {this.state.showDiff && this.renderDiff()}
115 |
116 | );
117 | }
118 | }
119 |
120 | export default JsonEditor;
121 |
--------------------------------------------------------------------------------
/app/components/shared/Menu/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import _ from 'lodash';
4 | import styles from './menu.css';
5 | import Drawer from 'material-ui/Drawer';
6 | import { List, ListItem, makeSelectable } from 'material-ui/List';
7 | import IconButton from 'material-ui/IconButton';
8 | import Build from 'material-ui/svg-icons/action/build';
9 | import ContentAdd from 'material-ui/svg-icons/content/add'
10 | import MountTuneDeleteDialog from '../MountUtils/MountTuneDelete.jsx'
11 | import NewMountDialog from '../MountUtils/NewMount.jsx'
12 | import { tokenHasCapabilities, callVaultApi, history } from '../VaultUtils.jsx'
13 |
14 | const SelectableList = makeSelectable(List);
15 |
16 | const supported_secret_backend_types = [
17 | 'generic',
18 | 'kv'
19 | ]
20 |
21 | const supported_auth_backend_types = [
22 | 'token',
23 | 'github',
24 | 'radius',
25 | 'aws-ec2',
26 | 'userpass',
27 | 'aws',
28 | 'kubernetes',
29 | 'okta',
30 | 'approle'
31 | ]
32 |
33 | function snackBarMessage(message) {
34 | let ev = new CustomEvent("snackbar", { detail: { message: message } });
35 | document.dispatchEvent(ev);
36 | }
37 |
38 | class Menu extends React.Component {
39 | static propTypes = {
40 | pathname: PropTypes.string.isRequired,
41 | }
42 |
43 | constructor(props) {
44 | super(props);
45 |
46 | this.state = {
47 | tuneMountObj: null,
48 | openNewAuthMountDialog: false,
49 | openNewSecretMountDialog: false,
50 | selectedPath: this.props.pathname,
51 | authBackends: [],
52 | secretBackends: []
53 | };
54 | }
55 |
56 | getCurrentMenuItemFromPath(path) {
57 | if (path.startsWith('/secret')) {
58 | let res = _.find(this.state.secretBackends, (backend) => {
59 | return path.startsWith(`/secrets/${backend.type}/${backend.path}`)
60 | });
61 | if (res) {
62 | return `/secrets/${res.type}/${res.path}`;
63 | }
64 | }
65 | else if (path.startsWith('/auth')) {
66 | let res = _.find(this.state.authBackends, (backend) => {
67 | return path.startsWith(`/auth/${backend.type}/${backend.path}`)
68 | });
69 | if (res) {
70 | return `/auth/${res.type}/${res.path}`;
71 | }
72 | } else {
73 | return path;
74 | }
75 | }
76 |
77 | componentWillReceiveProps(nextProps) {
78 | if (this.props.pathname != nextProps.pathname) {
79 | this.setState({ selectedPath: this.getCurrentMenuItemFromPath(nextProps.pathname) });
80 | }
81 | }
82 |
83 |
84 | loadSecretBackends() {
85 | tokenHasCapabilities(['read'], 'sys/mounts')
86 | .then(() => {
87 | return callVaultApi('get', 'sys/mounts').then((resp) => {
88 | let entries = _.get(resp, 'data.data', _.get(resp, 'data', {}));
89 | let discoveredSecretBackends = _.map(entries, (v, k) => {
90 | if (_.indexOf(supported_secret_backend_types, v.type) != -1) {
91 | let entry = {
92 | path: k,
93 | type: v.type,
94 | description: v.description,
95 | config: v.config
96 | }
97 | return entry;
98 | }
99 | }).filter(Boolean);
100 | this.setState({ secretBackends: discoveredSecretBackends }, () => this.getCurrentMenuItemFromPath(this.props.pathname));
101 | }).catch(snackBarMessage)
102 | }).catch(() => { snackBarMessage(new Error("No permissions to list secret backends")) })
103 | }
104 |
105 | loadAuthBackends() {
106 | tokenHasCapabilities(['read'], 'sys/auth')
107 | .then(() => {
108 | return callVaultApi('get', 'sys/auth').then((resp) => {
109 | let entries = _.get(resp, 'data.data', _.get(resp, 'data', {}));
110 | let discoveredAuthBackends = _.map(entries, (v, k) => {
111 | if (_.indexOf(supported_auth_backend_types, v.type) != -1) {
112 | let entry = {
113 | path: k,
114 | type: v.type,
115 | description: v.description,
116 | config: v.config
117 | }
118 | return entry;
119 | }
120 | }).filter(Boolean);
121 | this.setState({ authBackends: discoveredAuthBackends }, () => this.getCurrentMenuItemFromPath(this.props.pathname));
122 | }).catch(snackBarMessage)
123 | }).catch(() => { snackBarMessage(new Error("No permissions to list auth backends")) })
124 | }
125 |
126 | componentDidMount() {
127 | this.loadAuthBackends();
128 | this.loadSecretBackends();
129 | }
130 |
131 | render() {
132 | let renderSecretBackendList = () => {
133 | return _.map(this.state.secretBackends, (backend, idx) => {
134 | let tuneObj = {
135 | path: `sys/mounts/${backend.path}`,
136 | config: backend.config
137 | }
138 |
139 | return (
140 |
this.setState({ tuneMountObj: tuneObj })}
150 | >
151 |
152 |
153 | }
154 | />
155 | )
156 | })
157 | }
158 |
159 | let renderAuthBackendList = () => {
160 | return _.map(this.state.authBackends, (backend, idx) => {
161 | let tuneObj = {
162 | path: `sys/auth/${backend.path}`,
163 | uipath: `/auth/${backend.type}/${backend.path}`,
164 | config: backend.config
165 | }
166 |
167 | return (
168 | this.setState({ tuneMountObj: tuneObj })}
178 | >
179 |
180 |
181 | }
182 | />
183 | )
184 | })
185 | }
186 |
187 | let handleMenuChange = (e, v) => {
188 | history.push(v)
189 | }
190 |
191 | return (
192 |
193 | {
198 | snackBarMessage(`New authentication backend ${type} mounted at ${path}`)
199 | this.loadAuthBackends();
200 | }}
201 | onActionError={snackBarMessage}
202 | onClose={() => this.setState({ openNewAuthMountDialog: false })}
203 | />
204 | {
209 | snackBarMessage(`New secret backend ${type} mounted at ${path}`)
210 | this.loadSecretBackends();
211 | }}
212 | onActionError={snackBarMessage}
213 | onClose={() => this.setState({ openNewSecretMountDialog: false })}
214 | />
215 | {
219 | snackBarMessage(`Mountpoint ${path} tuned`)
220 | this.loadAuthBackends();
221 | this.loadSecretBackends();
222 | }}
223 | onActionDeleteSuccess={(path, uipath) => {
224 | snackBarMessage(`Mountpoint ${path} deleted`)
225 | if (this.props.pathname.startsWith(uipath)) {
226 | history.push('/');
227 | }
228 | this.loadAuthBackends();
229 | this.loadSecretBackends();
230 | }}
231 | onClose={() => this.setState({ tuneMountObj: null })}
232 | />
233 |
234 |
235 | this.setState({openNewSecretMountDialog: true})}
245 | >
246 |
247 |
248 | }
249 | />
250 | this.setState({openNewAuthMountDialog: true})}
260 | >
261 |
262 |
263 | }
264 |
265 | />
266 | ,
272 |
273 | ]}
274 | />
275 |
281 |
282 |
283 |
284 |
285 | )
286 | }
287 | }
288 |
289 | export default Menu;
290 |
--------------------------------------------------------------------------------
/app/components/shared/Menu/menu.css:
--------------------------------------------------------------------------------
1 | .root {
2 | padding-left: 16px;
3 | margin-top: 64px;
4 | height: calc(100% - 60px) !important;
5 | }
6 |
7 | .root span {
8 | font-size: 20px !important;
9 | font-weight: 200 !important;
10 | }
11 |
12 | .link {
13 | cursor: pointer;
14 | user-select: none;
15 | }
16 |
17 | .activeLink {
18 | color: #00ABE0;
19 | font-weight: bold;
20 | }
21 |
22 | .link:hover {
23 | font-weight: bold;
24 | }
25 |
26 | .sublink {
27 | font-size: 16px;
28 | margin-left: 10px;
29 | user-select: none;
30 | }
31 |
32 | .disabled {
33 | display: none;
34 | }
--------------------------------------------------------------------------------
/app/components/shared/MountUtils/MountTuneDelete.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types';
3 | import _ from 'lodash';
4 | import { callVaultApi } from '../VaultUtils.jsx'
5 | import Dialog from 'material-ui/Dialog';
6 | import FlatButton from 'material-ui/FlatButton';
7 | import TextField from 'material-ui/TextField';
8 | import update from 'immutability-helper';
9 | import VaultObjectDeleter from '../DeleteObject/DeleteObject.jsx'
10 |
11 | export default class MountTuneDeleteDialog extends Component {
12 | static propTypes = {
13 | mountpointObject: PropTypes.object,
14 | onActionTuneSuccess: PropTypes.func,
15 | onActionDeleteSuccess: PropTypes.func,
16 | onActionError: PropTypes.func,
17 | onClose: PropTypes.func
18 | }
19 |
20 | static defaultProps = {
21 | mountpointObject: null,
22 | onActionTuneSuccess: () => { },
23 | onActionDeleteSuccess: () => { },
24 | onActionError: () => { },
25 | onClose: () => { }
26 | }
27 |
28 | constructor(props) {
29 | super(props)
30 | }
31 |
32 | state = {
33 | mountpointObject: {},
34 | unmountPath: '',
35 | openDialog: false
36 | };
37 |
38 | componentWillReceiveProps(nextProps) {
39 | if (!_.isEqual(nextProps.mountpointObject, this.props.mountpointObject)) {
40 | this.setState({ mountpointObject: nextProps.mountpointObject })
41 | }
42 | }
43 |
44 | componentDidUpdate(prevProps, prevState) {
45 | if (this.state.mountpointObject && !_.isEqual(prevState.mountpointObject, this.state.mountpointObject)) {
46 | this.setState({ openDialog: true })
47 | }
48 | }
49 |
50 | tuneMountpoint() {
51 | let mountCfg = _.clone(this.state.mountpointObject.config);
52 | if (mountCfg.default_lease_ttl != this.props.mountpointObject.config.default_lease_ttl) {
53 | if (!mountCfg.default_lease_ttl)
54 | mountCfg.default_lease_ttl = "system"
55 | } else {
56 | mountCfg.default_lease_ttl = "";
57 | }
58 |
59 | if (mountCfg.max_lease_ttl != this.props.mountpointObject.config.max_lease_ttl) {
60 | if (!mountCfg.max_lease_ttl)
61 | mountCfg.max_lease_ttl = "system"
62 | } else {
63 | mountCfg.max_lease_ttl = "";
64 | }
65 |
66 | if (mountCfg) {
67 | callVaultApi('post', `${this.state.mountpointObject.path}tune`, null, mountCfg)
68 | .then(() => {
69 | this.props.onActionTuneSuccess(this.state.mountpointObject.path, this.state.mountpointObject.uipath);
70 | this.setState({ mountpointObject: null, openDialog: false }, () => this.props.onClose());
71 | })
72 | .catch((err) => {
73 | this.props.onActionError(err);
74 | })
75 | } else {
76 | this.setState({ mountpointObject: null, openDialog: false }, () => this.props.onClose());
77 | }
78 | }
79 |
80 | render() {
81 | const actions = [
82 | this.setState({ unmountPath: this.state.mountpointObject.path })}
84 | label="Delete Mountpoint"
85 | secondary={true}
86 | />,
87 | this.setState({ mountpointObject: null, openDialog: false }, () => this.props.onClose())}
89 | label="Cancel"
90 | />,
91 | this.tuneMountpoint()}
93 | label="Save Settings"
94 | primary={true}
95 | />
96 | ];
97 |
98 | return (
99 |
100 | {this.state.openDialog &&
101 |
102 |
{
107 | this.setState({
108 | openDialog: false,
109 | mountpointObject: null
110 | });
111 | this.props.onClose();
112 | }}
113 | actions={actions}
114 | >
115 |
116 |
117 | {
124 | this.setState({ mountpointObject: update(this.state.mountpointObject, { config: { default_lease_ttl: { $set: e.target.value } } }) });
125 | }}
126 | />
127 |
128 |
129 | {
136 | this.setState({ mountpointObject: update(this.state.mountpointObject, { config: { max_lease_ttl: { $set: e.target.value } } }) });
137 | }}
138 | />
139 |
140 |
141 |
142 |
{
146 | this.props.onActionDeleteSuccess(this.state.mountpointObject.path, this.state.mountpointObject.uipath);
147 | this.setState({ unmountPath: '', mountpointObject: null, openDialog: false }, () => this.props.onClose());
148 | }}
149 | onReceiveError={(err) => {
150 | this.setState({ unmountPath: '' });
151 | this.props.onActionError(err);
152 | }}
153 | />
154 |
155 | }
156 |
157 | )
158 | }
159 | }
--------------------------------------------------------------------------------
/app/components/shared/MountUtils/NewMount.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types';
3 | import _ from 'lodash';
4 | import { callVaultApi } from '../VaultUtils.jsx'
5 | import Dialog from 'material-ui/Dialog';
6 | import FlatButton from 'material-ui/FlatButton';
7 | import TextField from 'material-ui/TextField';
8 | import SelectField from 'material-ui/SelectField';
9 | import MenuItem from 'material-ui/MenuItem';
10 |
11 | export default class NewMountDialog extends Component {
12 | static propTypes = {
13 | mountType: PropTypes.oneOf(["auth", "secret"]).isRequired,
14 | supportedBackendTypes: PropTypes.array.isRequired,
15 | openDialog: PropTypes.bool,
16 | onActionSuccess: PropTypes.func,
17 | onActionError: PropTypes.func,
18 | onClose: PropTypes.func,
19 | }
20 |
21 | static defaultProps = {
22 | mountType: "auth",
23 | supportedBackendTypes: [],
24 | openDialog: false,
25 | onActionSuccess: () => { },
26 | onActionError: () => { },
27 | onClose: () => { }
28 | }
29 |
30 | constructor(props) {
31 | super(props)
32 | }
33 |
34 | state = {
35 | openDialog: false,
36 | backendType: '',
37 | backendDescription: '',
38 | backendPath: ''
39 |
40 | };
41 |
42 | componentWillReceiveProps(nextProps) {
43 | if (nextProps.openDialog && nextProps.openDialog != this.props.openDialog) {
44 | // Reset
45 | this.setState({
46 | openDialog: true,
47 | backendType: '',
48 | backendDescription: '',
49 | backendPath: ''
50 | })
51 | }
52 | }
53 |
54 | BackendMount() {
55 | let fullpath;
56 | if (this.props.mountType == 'auth')
57 | fullpath = `sys/auth/${this.state.backendPath}`;
58 | else
59 | fullpath = `sys/mounts/${this.state.backendPath}`;
60 |
61 | let data = { type: this.state.backendType, description: this.state.backendDescription }
62 |
63 | callVaultApi('post', fullpath, null, data)
64 | .then(() => {
65 | this.props.onActionSuccess(this.state.backendType, fullpath);
66 | this.setState({ openDialog: false }, () => this.props.onClose());
67 | })
68 | .catch((err) => {
69 | this.props.onActionError(err);
70 | })
71 | }
72 |
73 | render() {
74 | const actions = [
75 | this.setState({ openDialog: false }, () => this.props.onClose())}
77 | label="Cancel"
78 | />,
79 | this.BackendMount()}
81 | label="Mount Backend"
82 | primary={true}
83 | />
84 | ];
85 |
86 | let title = this.props.mountType == "auth" ? "Add new authentication backend" : "Add new secret backend";
87 |
88 | return (
89 |
90 | {this.state.openDialog &&
91 |
{
96 | this.setState({
97 | openDialog: false
98 | });
99 | this.props.onClose();
100 | }}
101 | actions={actions}
102 | >
103 |
104 |
105 | this.setState({backendType: v, backendPath: v})}
111 | >
112 | {_.map(this.props.supportedBackendTypes, (b) => {
113 | return ( )
114 | })}
115 |
116 |
117 |
118 | this.setState({ backendPath: e.target.value }) }
124 | />
125 |
126 |
127 |
128 | }
129 |
130 | )
131 | }
132 | }
--------------------------------------------------------------------------------
/app/components/shared/VaultUtils.jsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import _ from 'lodash';
3 | import { browserHistory, hashHistory } from 'react-router'
4 |
5 | var history;
6 | if(WEBPACK_DEF_TARGET_WEB) {
7 | history = browserHistory;
8 | } else {
9 | history = hashHistory;
10 | }
11 |
12 | function resetCapabilityCache() {
13 | window.localStorage.setItem('capability_cache', JSON.stringify({}));
14 | return {};
15 | }
16 |
17 | function setCachedCapabilities(path, result) {
18 | var cache = JSON.parse(window.localStorage.getItem('capability_cache'));
19 | if (!cache) {
20 | cache = resetCapabilityCache();
21 | }
22 | cache[path] = result;
23 | window.localStorage.setItem('capability_cache', JSON.stringify(cache));
24 | }
25 |
26 | function getCachedCapabilities(path) {
27 | var cache = JSON.parse(window.localStorage.getItem('capability_cache'));
28 | if (!cache) {
29 | cache = resetCapabilityCache();
30 | }
31 | if (path in cache) {
32 | return cache[path];
33 | } else {
34 | throw new Error('cache miss');
35 | }
36 | }
37 |
38 | function callVaultApi(method, path, query = {}, data, headers = {}, vaultToken = null, vaultUrl = null) {
39 |
40 | var instance;
41 |
42 | // Normalize vault address by removing trailing slashes
43 | let normVaultAddr = vaultUrl || window.localStorage.getItem("vaultUrl");
44 | normVaultAddr = normVaultAddr.replace(/\/*$/g, "");
45 |
46 | if(WEBPACK_DEF_TARGET_WEB) {
47 | instance = axios.create({
48 | baseURL: '/v1/',
49 | params: { "vaultaddr": normVaultAddr },
50 | headers: { "X-Vault-Token": vaultToken || window.localStorage.getItem("vaultAccessToken") }
51 | });
52 | } else {
53 | instance = axios.create({
54 | baseURL: `${normVaultAddr}/v1/`,
55 | headers: { "X-Vault-Token": vaultToken || window.localStorage.getItem("vaultAccessToken") }
56 | });
57 | }
58 |
59 | return instance.request({
60 | url: encodeURI(path),
61 | method: method,
62 | data: data,
63 | params: query,
64 | headers: headers
65 | });
66 | }
67 |
68 | function tokenHasCapabilities(capabilities, path) {
69 | if (window.localStorage.getItem('enableCapabilitiesCache') == "true") {
70 | try {
71 | var cached_capabilities = getCachedCapabilities(path);
72 | // At this point we have a result from the cache we can return the value in a form of a resolved promise
73 | if (cached_capabilities) {
74 | var evaluation = _.every(capabilities, function (v) {
75 | return _.indexOf(cached_capabilities, v) !== -1;
76 | });
77 | if (evaluation || _.indexOf(cached_capabilities, 'root') !== -1) {
78 | return Promise.resolve(true);
79 | }
80 | }
81 | return Promise.reject(false);
82 | } catch (e) {
83 | // That was a cache miss, let's continue and ask vault
84 | }
85 | }
86 |
87 | return callVaultApi('post', 'sys/capabilities-self', {}, { path: path })
88 | .then((resp) => {
89 | setCachedCapabilities(path, resp.data.capabilities);
90 | var evaluation = _.every(capabilities, function (v) {
91 | let has_cap = _.indexOf(resp.data.capabilities, v) !== -1;
92 | return has_cap;
93 | });
94 | if (evaluation || _.indexOf(resp.data.capabilities, 'root') !== -1) {
95 | return Promise.resolve(true);
96 | }
97 | return Promise.reject(false)
98 | })
99 | .catch((err) => {
100 | return Promise.reject(err)
101 | });
102 | }
103 |
104 | module.exports = {
105 | history: history,
106 | callVaultApi: callVaultApi,
107 | tokenHasCapabilities: tokenHasCapabilities,
108 | resetCapabilityCache: resetCapabilityCache
109 | };
--------------------------------------------------------------------------------
/app/components/shared/Wrapping/Unwrapper.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types';
3 | import { callVaultApi } from '../VaultUtils.jsx'
4 | import JsonEditor from '../JsonEditor.jsx';
5 | import styles from './wrapping.css';
6 |
7 | export default class SecretUnwrapper extends Component {
8 | static propTypes = {
9 | location: PropTypes.object,
10 | };
11 |
12 | constructor(props) {
13 | super(props)
14 |
15 | this.state = {
16 | headerMsg: 'Displaying data wrapped with token',
17 | editorContent: null,
18 | error: false,
19 | };
20 | }
21 |
22 | componentDidMount() {
23 | callVaultApi('post', 'sys/wrapping/unwrap', null, null, null, this.props.location.query.token, this.props.location.query.vaultUrl)
24 | .then((resp) => {
25 | this.setState({
26 | editorContent: resp.data.data
27 | })
28 | })
29 | .catch((err) => {
30 | this.setState({
31 | headerMsg: `Server returned error ${err.response.status} while unwrapping token`,
32 | error: true,
33 | })
34 | })
35 | }
36 |
37 | render() {
38 | return (
39 |
40 |
41 |
{this.state.headerMsg}
42 | {this.props.location.query.token}
43 |
44 |
45 | {this.state.editorContent && }
46 |
47 |
48 | )
49 | }
50 | }
--------------------------------------------------------------------------------
/app/components/shared/Wrapping/Wrapper.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types';
3 | import _ from 'lodash';
4 | import { callVaultApi } from '../VaultUtils.jsx'
5 | import Dialog from 'material-ui/Dialog';
6 | import TextField from 'material-ui/TextField';
7 | import RaisedButton from 'material-ui/RaisedButton';
8 | import copy from 'copy-to-clipboard';
9 | import FlatButton from 'material-ui/FlatButton';
10 | import Divider from 'material-ui/Divider';
11 | import Popover from 'material-ui/Popover';
12 | import Menu from 'material-ui/Menu';
13 | import MenuItem from 'material-ui/MenuItem';
14 | import ContentContentCopy from 'material-ui/svg-icons/content/content-copy';
15 | import styles from './wrapping.css'
16 | import sharedStyles from '../styles.css';
17 |
18 | const RETURN_KEY = 13;
19 |
20 | export default class SecretWrapper extends Component {
21 | static propTypes = {
22 | buttonLabel: PropTypes.string,
23 | showButton: PropTypes.bool,
24 | onButtonTouchTap: PropTypes.func,
25 | path: PropTypes.string,
26 | data: PropTypes.object,
27 | onReceiveResponse: PropTypes.func,
28 | onReceiveError: PropTypes.func,
29 | onModalClose: PropTypes.func
30 | }
31 |
32 | static defaultProps = {
33 | buttonLabel: 'Wrap',
34 | showButton: true,
35 | onButtonTouchTap: null,
36 | path: null,
37 | data: null,
38 | onReceiveResponse: () => { },
39 | onReceiveError: () => { },
40 | onModalClose: () => { }
41 | }
42 |
43 | constructor(props) {
44 | super(props)
45 | }
46 |
47 | state = {
48 | wrapInfo: {},
49 | openPopover: false,
50 | customTtl: '',
51 | ttl: '5m',
52 | data: null,
53 | path: null
54 | };
55 |
56 | componentWillReceiveProps (nextProps) {
57 | // Trigger automatically on props change if the builtin button is not used
58 | if(!this.props.showButton) {
59 | if (!_.isEqual(nextProps.path, this.props.path) && this.props.path) {
60 | this.setState({ path: nextProps.path})
61 | } else if (!_.isEqual(nextProps.data, this.props.data) && this.props.data) {
62 | this.setState({ data: nextProps.data})
63 | }
64 | }
65 | }
66 |
67 | componentDidUpdate(prevProps, prevState) {
68 | if (!_.isEqual(prevState.path, this.state.path) && this.state.path) {
69 | callVaultApi('get', this.state.path, null, null, { 'X-Vault-Wrap-TTL': this.state.ttl })
70 | .then((response) => {
71 | this.setState({ wrapInfo: response.data.wrap_info, path: null });
72 | this.props.onReceiveResponse(response.data.wrap_info);
73 | })
74 | .catch((err) => {
75 | this.props.onReceiveError(err);
76 | })
77 | } else if (!_.isEqual(prevState.data, this.state.data) && this.state.data) {
78 | callVaultApi('post', 'sys/wrapping/wrap', null, this.state.data, { 'X-Vault-Wrap-TTL': this.state.ttl })
79 | .then((response) => {
80 | this.setState({ wrapInfo: response.data.wrap_info, data: null });
81 | this.props.onReceiveResponse(response.data.wrap_info);
82 | })
83 | .catch((err) => {
84 | this.props.onReceiveError(err);
85 | })
86 | }
87 | }
88 |
89 | handleTouchTap = (event) => {
90 | event.preventDefault();
91 |
92 | this.setState({
93 | anchorEl: event.currentTarget,
94 | openPopover: true
95 | });
96 | };
97 |
98 | handleRequestClose = () => {
99 | this.setState({
100 | openPopover: false
101 | });
102 | };
103 |
104 | handleItemTouchTap = (event, menuItem) => {
105 | this.setState({
106 | openPopover: false,
107 | ttl: menuItem.props.secondaryText,
108 | data: this.props.data,
109 | path: this.props.path
110 | });
111 | };
112 |
113 | handleCustomTtl = (e, v) => {
114 | if (e.keyCode === RETURN_KEY) {
115 | let customTtl = this.state.customTtl;
116 | this.setState({
117 | openPopover: false,
118 | ttl: customTtl,
119 | customTtl: '',
120 | data: this.props.data,
121 | path: this.props.path
122 | });
123 | }
124 | }
125 |
126 | render() {
127 | let vaultUrl = encodeURI(window.localStorage.getItem("vaultUrl"));
128 | let tokenValue = '';
129 | let urlValue = '';
130 | if (this.state.wrapInfo) {
131 | let loc = window.location;
132 | tokenValue = this.state.wrapInfo.token;
133 | if(WEBPACK_DEF_TARGET_WEB) {
134 | urlValue = `${loc.protocol}//${loc.hostname}${(loc.port ? ":" + loc.port : "")}/unwrap?token=${tokenValue}&vaultUrl=${vaultUrl}`;
135 | } else {
136 | urlValue = `vaultui://#/unwrap~token=${tokenValue}&vaultUrl=${vaultUrl}`;
137 | }
138 | }
139 |
140 | return (
141 |
142 | {this.props.showButton &&
143 |
144 |
145 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | this.setState({ customTtl: v})}
167 | onKeyDown={this.handleCustomTtl}
168 | name="custom"
169 | hintText="Custom lifetime"
170 | floatingLabelText="Custom lifetime"
171 | />
172 |
173 |
174 | }
175 |
{ this.props.onModalClose(); this.setState({ wrapInfo: {} }) }} />}
180 | onRequestClose={this.props.onModalClose}
181 | >
182 |
183 |
189 | } label="Copy to Clipboard" onTouchTap={() => { copy(tokenValue) }} />
190 |
191 |
192 |
198 | } label="Copy to Clipboard" onTouchTap={() => { copy(urlValue) }} />
199 |
200 |
201 |
202 | )
203 | }
204 | }
--------------------------------------------------------------------------------
/app/components/shared/Wrapping/wrapping.css:
--------------------------------------------------------------------------------
1 | #container {
2 | position: relative;
3 | height: 100%;
4 | width: 100%;
5 | }
6 |
7 | #cell {
8 | text-align: center;
9 | margin-bottom: 20px;
10 | margin-top: 20px;
11 | padding-top: 5px;
12 | padding-bottom: 5px;
13 | }
14 |
15 | .bwgradient {
16 | background: radial-gradient(circle, black, black, white);
17 | }
18 |
19 | .redgradient {
20 | background: radial-gradient(circle, darkred, darkred, white);
21 | }
22 |
23 | #cell h4 {
24 | color: lightgray;
25 | text-transform: full-width;
26 | }
27 |
28 | #cell h2 {
29 | font-family: monospace;
30 | color: #c2daff;
31 | }
32 |
33 | #content {
34 | width: 80%;
35 | margin: 0 auto;
36 | }
37 |
38 | .ttlList {
39 | line-height: 24px !important;
40 | min-height: 24px !important;
41 | }
--------------------------------------------------------------------------------
/app/components/shared/styles.css:
--------------------------------------------------------------------------------
1 | .TabInfoSection {
2 | padding: 10px;
3 | text-align: center;
4 | font-style: italic;
5 | }
6 |
7 | .centered {
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | }
12 |
13 | .TabContentSection {
14 | padding: 10px;
15 | }
16 |
17 | .listStyle span {
18 | font-family: monospace !important;
19 | }
20 |
21 | .newTokenCodeEmitted {
22 | /*text-align: center;*/
23 | }
24 |
25 | .newTokenCodeEmitted input {
26 | /*text-align: center;*/
27 | font-family: monospace !important;
28 | font-size: 150% !important;
29 | color: black !important;
30 | cursor: crosshair !important;
31 | }
32 |
33 | .newUrlEmitted {
34 | /*text-align: center;*/
35 | }
36 |
37 | .newUrlEmitted input {
38 | font-size: 15px !important;
39 | color: black !important;
40 | cursor: crosshair !important;
41 | }
--------------------------------------------------------------------------------
/bin/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [[ ! -z "$CUSTOM_CA_CERT" ]]; then
4 | echo "$CUSTOM_CA_CERT" > misc/custom_ca.crt
5 | export NODE_EXTRA_CA_CERTS=misc/custom_ca.crt
6 | fi
7 |
8 | if [ "$1" = 'start_app' ]; then
9 | exec yarn run serve "$@"
10 | elif [ "$1" = 'dev' ]; then
11 | exec yarn run dev "$@"
12 | fi
13 |
14 | exec "$@"
15 |
--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djenriquez/vault-ui/36266eaaa82049bfc3059d522df26973cc49b72a/build/icon.icns
--------------------------------------------------------------------------------
/build/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djenriquez/vault-ui/36266eaaa82049bfc3059d522df26973cc49b72a/build/icon.ico
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: '2'
3 | services:
4 | vault:
5 | image: vault
6 | privileged: true
7 | volumes:
8 | - ./misc:/misc
9 | ports:
10 | - "8200:8200"
11 | - "8201:8201"
12 | environment:
13 | VAULT_ADDR: http://127.0.0.1:8200
14 | VAULT_LOCAL_CONFIG: '{"listener" : { "tcp" : {"address" : "0.0.0.0:8201", "tls_cert_file" : "/misc/devserver.crt", "tls_key_file" : "/misc/devserver.key" } } }'
15 |
16 | vault-ui:
17 | build: .
18 | ports:
19 | - "8000:8000"
20 | links:
21 | - vault
22 | volumes:
23 | - .:/app
24 | environment:
25 | NODE_TLS_REJECT_UNAUTHORIZED: 0
26 | VAULT_URL_DEFAULT: http://vault:8200
27 | VAULT_AUTH_DEFAULT: USERNAMEPASSWORD
28 | command: ["dev"]
29 | # VAULT_SUPPLIED_TOKEN_HEADER: 'X-Remote-User'
30 |
31 | webpack:
32 | build: .
33 | volumes:
34 | - .:/app
35 | command: yarn run dev-pack
36 |
--------------------------------------------------------------------------------
/index.desktop.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Vault-UI
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/index.web.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Vault-UI
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/kubernetes/chart/vault-ui/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *~
18 | # Various IDEs
19 | .project
20 | .idea/
21 | *.tmproj
22 |
--------------------------------------------------------------------------------
/kubernetes/chart/vault-ui/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | description: A Helm chart for Vault-ui
3 | name: vault-ui
4 | version: 0.1.0
5 |
--------------------------------------------------------------------------------
/kubernetes/chart/vault-ui/README.md:
--------------------------------------------------------------------------------
1 | # Helm chart
2 |
3 | [Helm](chart) to deploy `vault-ui` in a kubernetes cluster. To run this chart you need to have a kubernetes cluster and helm installed and configured properly. To install `vault-ui` you just need to execute the following `helm` command:
4 |
5 | ```
6 | helm install ./chart/vault-ui
7 | ```
8 |
9 | To run this chart you need 2 settings:
10 |
11 | * VAULT_URL_DEFAULT: http://vault-service-name:8200
12 | * VAULT_AUTH_DEFAULT: by default is token, but you can use any of the 4 options provided.
13 |
14 |
15 | ```
16 | helm install ./chart/vault-ui --set vault.url=http://MY_RELEASE-vault:8200"
17 | ```
18 |
19 | The `vault.url` parameter is the value of your kubernetes `vault` service.
20 |
--------------------------------------------------------------------------------
/kubernetes/chart/vault-ui/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | 1. Get the application URL by running these commands:
2 | {{- if .Values.ingress.hostname }}
3 | http://{{- .Values.ingress.hostname }}
4 | {{- else if contains "NodePort" .Values.service.type }}
5 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "fullname" . }})
6 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
7 | echo http://$NODE_IP:$NODE_PORT
8 | {{- else if contains "LoadBalancer" .Values.service.type }}
9 | NOTE: It may take a few minutes for the LoadBalancer IP to be available.
10 | You can watch the status of by running 'kubectl get svc -w {{ template "fullname" . }}'
11 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
12 | echo http://$SERVICE_IP:{{ .Values.service.externalPort }}
13 | {{- else if contains "ClusterIP" .Values.service.type }}
14 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
15 | echo "Visit http://127.0.0.1:8080 to use your application"
16 | kubectl port-forward $POD_NAME 8080:{{ .Values.service.externalPort }}
17 | {{- end }}
18 |
--------------------------------------------------------------------------------
/kubernetes/chart/vault-ui/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/* vim: set filetype=mustache: */}}
2 | {{/*
3 | Expand the name of the chart.
4 | */}}
5 | {{- define "name" -}}
6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
7 | {{- end -}}
8 |
9 | {{/*
10 | Create a default fully qualified app name.
11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
12 | */}}
13 | {{- define "fullname" -}}
14 | {{- $name := default .Chart.Name .Values.nameOverride -}}
15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
16 | {{- end -}}
17 |
--------------------------------------------------------------------------------
/kubernetes/chart/vault-ui/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: extensions/v1beta1
2 | kind: Deployment
3 | metadata:
4 | name: {{ template "fullname" . }}
5 | labels:
6 | app: {{ template "name" . }}
7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
8 | release: {{ .Release.Name }}
9 | heritage: {{ .Release.Service }}
10 | spec:
11 | replicas: {{ .Values.replicaCount }}
12 | template:
13 | metadata:
14 | labels:
15 | app: {{ template "name" . }}
16 | release: {{ .Release.Name }}
17 | spec:
18 | containers:
19 | - name: {{ .Chart.Name }}
20 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
21 | imagePullPolicy: {{ .Values.image.pullPolicy }}
22 | env:
23 | - name: VAULT_URL_DEFAULT
24 | value: {{ .Values.vault.url }}
25 | - name: VAULT_AUTH_DEFAULT
26 | value: {{ .Values.vault.auth }}
27 |
28 | ports:
29 | - containerPort: {{ .Values.service.internalPort }}
30 | livenessProbe:
31 | httpGet:
32 | path: /
33 | port: {{ .Values.service.internalPort }}
34 | readinessProbe:
35 | httpGet:
36 | path: /
37 | port: {{ .Values.service.internalPort }}
38 | resources:
39 | {{ toYaml .Values.resources | indent 12 }}
40 | {{- if .Values.nodeSelector }}
41 | nodeSelector:
42 | {{ toYaml .Values.nodeSelector | indent 8 }}
43 | {{- end }}
44 |
--------------------------------------------------------------------------------
/kubernetes/chart/vault-ui/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.ingress.enabled -}}
2 | {{- $serviceName := include "fullname" . -}}
3 | {{- $servicePort := .Values.service.externalPort -}}
4 | apiVersion: extensions/v1beta1
5 | kind: Ingress
6 | metadata:
7 | name: {{ template "fullname" . }}
8 | labels:
9 | app: {{ template "name" . }}
10 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
11 | release: {{ .Release.Name }}
12 | heritage: {{ .Release.Service }}
13 | annotations:
14 | {{- range $key, $value := .Values.ingress.annotations }}
15 | {{ $key }}: {{ $value | quote }}
16 | {{- end }}
17 | spec:
18 | rules:
19 | {{- range $host := .Values.ingress.hosts }}
20 | - host: {{ $host }}
21 | http:
22 | paths:
23 | - path: /
24 | backend:
25 | serviceName: {{ $serviceName }}
26 | servicePort: {{ $servicePort }}
27 | {{- end -}}
28 | {{- if .Values.ingress.tls }}
29 | tls:
30 | {{ toYaml .Values.ingress.tls | indent 4 }}
31 | {{- end -}}
32 | {{- end -}}
33 |
--------------------------------------------------------------------------------
/kubernetes/chart/vault-ui/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ template "fullname" . }}
5 | labels:
6 | app: {{ template "name" . }}
7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
8 | release: {{ .Release.Name }}
9 | heritage: {{ .Release.Service }}
10 | spec:
11 | type: {{ .Values.service.type }}
12 | ports:
13 | - port: {{ .Values.service.externalPort }}
14 | targetPort: {{ .Values.service.internalPort }}
15 | protocol: TCP
16 | name: {{ .Values.service.name }}
17 | selector:
18 | app: {{ template "name" . }}
19 | release: {{ .Release.Name }}
20 |
--------------------------------------------------------------------------------
/kubernetes/chart/vault-ui/values.yaml:
--------------------------------------------------------------------------------
1 | # Default values for vault-ui.
2 | # This is a YAML-formatted file.
3 | # Declare variables to be passed into your templates.
4 | replicaCount: 1
5 | image:
6 | repository: djenriquez/vault-ui
7 | tag: latest
8 | pullPolicy: IfNotPresent
9 | service:
10 | name: vault-ui
11 | type: ClusterIP
12 | externalPort: 8000
13 | internalPort: 8000
14 | ingress:
15 | enabled: true
16 | # Used to create Ingress record (should used with service.type: ClusterIP).
17 | hosts:
18 | - vault-ui.example.com
19 | annotations:
20 | # AWS --> redirect http to https
21 | kubernetes.io/ingress.class: nginx
22 | ingress.kubernetes.io/force-ssl-redirect: "true"
23 | tls:
24 | # Secrets must be manually created in the namespace.
25 | # - secretName: chart-example-tls
26 | # hosts:
27 | # - chart-example.local
28 | resources: {}
29 | # We usually recommend not to specify default resources and to leave this as a conscious
30 | # choice for the user. This also increases chances charts run on environments with little
31 | # resources, such as Minikube. If you do want to specify resources, uncomment the following
32 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
33 | # limits:
34 | # cpu: 100m
35 | # memory: 128Mi
36 | #requests:
37 | # cpu: 100m
38 | # memory: 128Mi
39 |
40 | vault:
41 | auth: TOKEN
42 | url: http://vault:8200
43 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const { app, protocol, BrowserWindow, Menu, dialog } = require('electron')
2 |
3 | const path = require('path')
4 | const url = require('url')
5 |
6 | // Keep a global reference of the window object, if you don't, the window will
7 | // be closed automatically when the JavaScript object is garbage collected.
8 | let mainWindow
9 | let initialPath;
10 |
11 | // Handle commandline
12 | if (process.argv && process.argv.length > 1) {
13 | setInitialPath(process.argv[1]);
14 | }
15 |
16 | function setInitialPath(urlloc) {
17 |
18 | let l = url.parse(urlloc, true);
19 | if (l && l.protocol == 'vaultui:' && l.hash) {
20 |
21 | // Windows Protocol Handler bug workaround
22 | initialPath = l.hash.replace('~', '?');
23 | }
24 | if (mainWindow) {
25 | mainWindow.loadURL(url.format({
26 | pathname: path.join(__dirname, 'index.desktop.html'),
27 | protocol: 'file:',
28 | hash: initialPath,
29 | slashes: true
30 | }))
31 | }
32 | }
33 |
34 | const shouldQuit = app.makeSingleInstance((commandLine) => {
35 | // Someone tried to run a second instance, we should focus our window.
36 | if (mainWindow) {
37 | if (mainWindow.isMinimized()) mainWindow.restore()
38 | mainWindow.focus()
39 | }
40 |
41 | if (commandLine && commandLine.length > 1) {
42 | setInitialPath(commandLine[1]);
43 | }
44 | })
45 |
46 | if (shouldQuit) {
47 | app.quit()
48 | }
49 |
50 | app.setAsDefaultProtocolClient('vaultui')
51 | app.on('open-url', function (event, openurl) {
52 | setInitialPath(openurl);
53 | })
54 |
55 | function createWindow() {
56 | // Create the browser window.
57 | mainWindow = new BrowserWindow({ width: 1024, height: 768 })
58 |
59 | // and load the index.html of the app.
60 | mainWindow.loadURL(url.format({
61 | pathname: path.join(__dirname, 'index.desktop.html'),
62 | protocol: 'file:',
63 | hash: initialPath,
64 | slashes: true
65 | }))
66 |
67 | // Open the DevTools.
68 | // mainWindow.webContents.openDevTools()
69 |
70 | // Emitted when the window is closed.
71 | mainWindow.on('closed', function () {
72 | // Dereference the window object, usually you would store windows
73 | // in an array if your app supports multi windows, this is the time
74 | // when you should delete the corresponding element.
75 | mainWindow = null
76 | })
77 |
78 | // Create the Application's main menu
79 | var template = [{
80 | label: "Application",
81 | submenu: [
82 | { label: "About Application", selector: "orderFrontStandardAboutPanel:" },
83 | { type: "separator" },
84 | { label: "Quit", accelerator: "Command+Q", click: function () { app.quit(); } }
85 | ]
86 | }, {
87 | label: "Edit",
88 | submenu: [
89 | { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
90 | { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
91 | { type: "separator" },
92 | { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
93 | { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
94 | { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
95 | { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
96 | ]
97 | }, {
98 | label: "Tools",
99 | submenu: [
100 | { label: "Open Development Tools", click: function () { mainWindow.webContents.openDevTools(); } }
101 | ]
102 | }
103 | ];
104 |
105 | Menu.setApplicationMenu(Menu.buildFromTemplate(template));
106 |
107 | protocol.registerFileProtocol('vaultui', (request, callback) => {
108 | const url = request.url.substr(7)
109 | callback({ path: path.normalize(`${__dirname}/${url}`) })
110 | }, (error) => {
111 | if (error) console.error('Failed to register protocol')
112 | })
113 |
114 | }
115 |
116 | // This method will be called when Electron has finished
117 | // initialization and is ready to create browser windows.
118 | // Some APIs can only be used after this event occurs.
119 | app.on('ready', createWindow)
120 |
121 | // Quit when all windows are closed.
122 | app.on('window-all-closed', function () {
123 | // On OS X it is common for applications and their menu bar
124 | // to stay active until the user quits explicitly with Cmd + Q
125 | if (process.platform !== 'darwin') {
126 | app.quit()
127 | }
128 | })
129 |
130 | app.on('activate', function () {
131 | // On OS X it's common to re-create a window in the app when the
132 | // dock icon is clicked and there are no other windows open.
133 | if (mainWindow === null) {
134 | createWindow()
135 | }
136 | })
137 |
138 | app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
139 | var result = dialog.showMessageBox({
140 | type: 'warning',
141 | title: 'Server TLS Certificate Error',
142 | message: 'The validity of the TLS connection with the remote server cannot be verified. Would you like to proceed anyway?',
143 | detail: error,
144 | buttons: ['Yes', 'No']
145 | });
146 | if (result == 0) {
147 | event.preventDefault()
148 | callback(true)
149 | } else {
150 | callback(false)
151 | }
152 | })
153 |
--------------------------------------------------------------------------------
/misc/admin.hcl:
--------------------------------------------------------------------------------
1 | {
2 | "path": {
3 | "*": {
4 | "capabilities": [
5 | "create",
6 | "read",
7 | "update",
8 | "delete",
9 | "list",
10 | "sudo"
11 | ]
12 | },
13 | "ultrasecret/admincantlistthis/": {
14 | "capabilities": [
15 | "deny"
16 | ]
17 | },
18 | "ultrasecret/admincantreadthis": {
19 | "capabilities": [
20 | "deny"
21 | ]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/misc/devserver.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICyDCCAbACCQDp7ZXbcZ7BujANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDExtW
3 | YXVsdC1VSSBEZXZlbG9wbWVudCBTZXJ2ZXIwHhcNMTcwNDAzMDUxMDIzWhcNMjcw
4 | NDAxMDUxMDIzWjAmMSQwIgYDVQQDExtWYXVsdC1VSSBEZXZlbG9wbWVudCBTZXJ2
5 | ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2kja9oBgcWKmrgqce
6 | FHz2Ta3iMrJClBh2laC/zKqOwljj3YZHf2RlA8d0zJPm77E/HHcKCVeqRMjgO9+P
7 | 7ephq3Oz0KpcLnaCImJavosqfI8Jj4pFeoTt1zjE/idO5SKSw+/YblSqKEzigXNE
8 | NxZ9d8aFZ3J+A6QySW30gdANOB3A0I8lJy2A2em6xXAOMP8gSfIfSOGL5voZ+FpY
9 | At8G7A/wNztBR2bnoTPSq18KWCxZW0M412HhJruzQ57j/joPRDDmBQrE3JhOUNuI
10 | pg1nmbNg3jYP/EsTCwYEHklxoyVtUPm3eQIHYQXNEg0r3TAEt+o/z7prI9SZnTwP
11 | kefZAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEhWjO7W/ysoSClXt7IFCdAzWZBH
12 | 8hNH/yFVvKW1DbVJEnQj5u6WqXPAKWd9j+xrU3D1d38i4lz7MUsIm+HMuLgIYONa
13 | aVvFruHsFb9FJhyinIiF2IaKUncaAHZ9r3xI4pGRWRPNrgNIkAct6kKZXxp/2qsQ
14 | oBNOI5rtJA73/bzxH7kevXOrryI3M8/NM+VlddgKphm+K6HS+vSd+hD4oyupi/Q6
15 | FNhun8igTpErzf7ZW7ZSPIJ6CU15+pX1gUNYOJIzcS763YmcRMXzjioqjIkyYfPb
16 | Z9tra/9W/mu7NeBWQ9G/m2vyqX00T+LpGD8gwsgJgtNeZbmtarsi+lJbx14=
17 | -----END CERTIFICATE-----
18 |
--------------------------------------------------------------------------------
/misc/devserver.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEogIBAAKCAQEAtpI2vaAYHFipq4KnHhR89k2t4jKyQpQYdpWgv8yqjsJY492G
3 | R39kZQPHdMyT5u+xPxx3CglXqkTI4Dvfj+3qYatzs9CqXC52giJiWr6LKnyPCY+K
4 | RXqE7dc4xP4nTuUiksPv2G5UqihM4oFzRDcWfXfGhWdyfgOkMklt9IHQDTgdwNCP
5 | JSctgNnpusVwDjD/IEnyH0jhi+b6GfhaWALfBuwP8Dc7QUdm56Ez0qtfClgsWVtD
6 | ONdh4Sa7s0Oe4/46D0Qw5gUKxNyYTlDbiKYNZ5mzYN42D/xLEwsGBB5JcaMlbVD5
7 | t3kCB2EFzRINK90wBLfqP8+6ayPUmZ08D5Hn2QIDAQABAoIBACdm5umF4646dGPP
8 | jsGvKkj9+skWp+I2lBEDue2q/iRRTV3gMVq8463pYuKSRFlS4a39NrOz0Heu4KuE
9 | QHuPnUX2+sGUBzBd1rW/NfrfpKlGuJgXon/cMVQjXt0k/NbKHOwP3XOYXC1dBTrd
10 | NUNDoFbzwqSH7u3DW2x+7HwYiA5R8Lr2pmjzZCK3btEYXaLmY+Ee2WvsIwiJEcas
11 | ixKBEG8z3vbtKmsR1B4QV5KKvM4Pz/lHT0Ue/YxY3U2oBJ1JznDD/L7nDrtNpr+8
12 | Fh/WAivqmFJYLtIMYMV1TrnT4NRaqOxtFuEwuypAYEF8aQsYEDJ9P10yL6BBofFM
13 | 1FQmpQECgYEA37PwIamLhlRL+cmIWtaKxBIO7bUOq0PVP5Wzj0e6izqg5Q8DujvX
14 | 9rRQCeVxX2F75Jg1OfZjWL683zo2cKT+VE7l4Bm2pTRgmb2XYdOXAJN8URZdpj3K
15 | e2rQfX4aZ41gBY3lqbRx5p/9IPSkUHyFhSUolhv2oh2L9S8pDzpNLKsCgYEA0O4L
16 | 13skGpxq3mmURfRkFLGC716LBv5LK8CI0N9pSahOYt2Bq3E7U4hgaQF0zD4P3QUS
17 | MGtRPHoLR4EQUzfJ0ihJzLEEA6OptoEJAePA0uAL7oJibE46KIdlBxdX9gOer2Hq
18 | rGxNQSRJD6vkR426yx3eWpyUhNsua9CzyJ3B9YsCgYB005IK4nJ9WrS65KcTWYvq
19 | zcuCFNZuVuSdal716u3fHGU+etLlha9Jpe1O3caRm2WKgnr5pFVJ2YLlyY740RIJ
20 | kZK3sHYUXQA+Cidu7YOkx2FbL6UE1qxSO/xaLWs4vTpybCKOuC/r043skhbl+cH5
21 | QOirTDtHesrG5zQ4QahgNQKBgGqxFTT9uksok15utfwfODhlCcMpGYABvetiz7sy
22 | S3cEzrqn+P7OvQgEPY+B4d4m1zz7yPUW6I4kmLv0CZ0lgRej4UP5JV6iZhk/vZTM
23 | dHx7UzyCMrayH/rwYUQExLNp19AiBY/1YmIgoHqzQcjUdI4i+5h0G1fZAdSm6BhL
24 | j2/PAoGAM7YLrWNdpyViWjDLeOBV0zj2p635i/WYZPOLhvyjR1AjTju+13PJZs1o
25 | n0JleS6eD3tw9Rz+9lK84KJ+YXEfZ1nGoCxKTKYGSUUHovZjE6uUTxpxzMvNUH8G
26 | HUMwI0s/aW9e3FbwmV3WyfdYyIjDsRQT0BLaujBsClLmTsGPf2E=
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vault-ui",
3 | "version": "2.4.0-rc3",
4 | "description": "Graphical interface for Hashicorp Vault",
5 | "main": "main.js",
6 | "browserslist": [
7 | "> 1%",
8 | "last 2 versions"
9 | ],
10 | "scripts": {
11 | "lint": "eslint .",
12 | "dist": "build",
13 | "serve": "node ./server.js",
14 | "dev": "nodemon ./server.js",
15 | "dev-pack": "webpack -d --env.target=web --hide-modules -w",
16 | "build-desktop": "webpack -p --env.target=electron --hide-modules",
17 | "build-web": "webpack -p --env.target=web --hide-modules",
18 | "desktop": "yarn run build-desktop && electron .",
19 | "package-mac": "yarn run build-desktop && build --mac=zip --ia32 --x64 --publish never",
20 | "package-win32": "yarn run build-desktop && build --win=nsis --ia32 --x64 --publish never",
21 | "package-linux": "yarn run build-desktop && build --linux=appimage --ia32 --x64 --publish never",
22 | "publish": "yarn run build-desktop && build --mac zip --win nsis --linux appimage --ia32 --x64 --publish onTag",
23 | "cleanup": "mop -v"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/djenriquez/vault-ui.git"
28 | },
29 | "keywords": [],
30 | "author": {
31 | "name": "Vault UI Contributors",
32 | "email": "no-reply@vault-ui.djenriquez.github.com"
33 | },
34 | "license": "ISC",
35 | "bugs": {
36 | "url": "https://github.com/djenriquez/vault-ui/issues"
37 | },
38 | "babel": {
39 | "presets": [
40 | "es2015",
41 | "react",
42 | "stage-2",
43 | "stage-0"
44 | ]
45 | },
46 | "homepage": "https://github.com/djenriquez/vault-ui#readme",
47 | "devDependencies": {
48 | "autoprefixer": "^6.5.3",
49 | "babel-core": "^6.26.0",
50 | "babel-eslint": "^7.1.1",
51 | "babel-loader": "^7.1.2",
52 | "babel-preset-es2015": "^6.18.0",
53 | "babel-preset-react": "^6.16.0",
54 | "babel-preset-stage-0": "^6.22.0",
55 | "babel-preset-stage-2": "^6.18.0",
56 | "copy-to-clipboard": "^3.0.5",
57 | "cross-env": "^3.1.4",
58 | "css-loader": "^0.28.0",
59 | "electron": "^1.6.14",
60 | "electron-builder": "^19.33.0",
61 | "eslint": "^3.14.0",
62 | "eslint-plugin-react": "^6.10.3",
63 | "extract-text-webpack-plugin": "^3.0.0",
64 | "extract-zip": "1.6.0",
65 | "file-loader": "^0.11.1",
66 | "flexboxgrid": "^6.3.1",
67 | "gopher-hcl": "^0.1.0",
68 | "immutability-helper": "^2.1.2",
69 | "jsondiffpatch": "^0.2.4",
70 | "jsondiffpatch-for-react": "^1.0.1",
71 | "jsoneditor": "^5.9.6",
72 | "lodash": "^4.17.4",
73 | "material-ui": "^0.19.2",
74 | "mui-icons": "^1.2.1",
75 | "postcss-loader": "^1.3.3",
76 | "prop-types": "^15.6.0",
77 | "react": "^15.6.2",
78 | "react-dom": "^15.6.2",
79 | "react-router": "^3.0.0",
80 | "react-tap-event-plugin": "^2.0.0",
81 | "react-ultimate-pagination-material-ui": "^1.0.3",
82 | "style-loader": "^0.16.1",
83 | "url-loader": "^0.5.8",
84 | "webpack": "^3.6.0"
85 | },
86 | "dependencies": {
87 | "axios": "^0.16.1",
88 | "body-parser": "^1.15.2",
89 | "compression": "^1.6.2",
90 | "express": "^4.14.0",
91 | "nodemon": "^1.11.0"
92 | },
93 | "build": {
94 | "productName": "Vault-UI",
95 | "appId": "com.github.djenriquez.vault-ui",
96 | "artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
97 | "mac": {
98 | "category": "public.app-category.tools"
99 | },
100 | "protocols": [
101 | {
102 | "name": "Vault-UI Desktop URL Protocol Handler",
103 | "schemes": [
104 | "vaultui"
105 | ]
106 | }
107 | ],
108 | "dmg": {
109 | "icon": "build/icon.icns",
110 | "contents": [
111 | {
112 | "x": 410,
113 | "y": 150,
114 | "type": "link",
115 | "path": "/Applications"
116 | },
117 | {
118 | "x": 130,
119 | "y": 150,
120 | "type": "file"
121 | }
122 | ]
123 | },
124 | "files": [
125 | "dist/",
126 | "index.desktop.html",
127 | "main.js",
128 | "package.json"
129 | ],
130 | "win": {
131 | "icon": "build/icon.ico",
132 | "target": "nsis"
133 | },
134 | "linux": {
135 | "target": [
136 | "deb",
137 | "AppImage"
138 | ]
139 | },
140 | "directories": {
141 | "buildResources": "build",
142 | "output": "release"
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/run-docker-compose-dev:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo "------------- yarn install -------------"
3 | yarn install
4 |
5 | echo "------------- docker-compose up -d -------------"
6 | docker-compose up -d
7 | echo
8 |
9 | echo "------------- docker-compose ps -------------"
10 | docker-compose ps
11 | echo
12 |
13 | exec_in_vault() {
14 | echo "------------- $@ -------------"
15 | docker-compose exec -T vault "$@"
16 | echo
17 | }
18 |
19 | exec_in_vault vault login "$(docker-compose logs vault | awk '/Root Token:/ {print $NF;}' | tail -n 1 | sed 's/\x1b\[[0-9;]*m//g')"
20 | exec_in_vault vault status
21 | exec_in_vault vault auth enable userpass
22 | exec_in_vault vault auth enable -path=userpass2 userpass
23 | exec_in_vault vault auth enable github
24 | exec_in_vault vault auth enable radius
25 | exec_in_vault vault auth enable -path=awsaccount1 aws-ec2
26 | exec_in_vault vault auth enable okta
27 | exec_in_vault vault auth enable approle
28 | exec_in_vault vault auth enable kubernetes
29 | exec_in_vault vault policy write admin /misc/admin.hcl
30 | exec_in_vault vault write auth/userpass/users/test password=test policies=admin
31 | exec_in_vault vault write auth/userpass2/users/john password=doe policies=admin
32 | exec_in_vault vault write auth/userpass/users/lame password=lame policies=default
33 | exec_in_vault vault write auth/radius/users/test password=test policies=admin
34 | exec_in_vault vault write secret/test somekey=somedata
35 | exec_in_vault vault secrets enable -path=ultrasecret generic
36 | exec_in_vault vault write ultrasecret/moretest somekey=somedata
37 | exec_in_vault vault write ultrasecret/dir1/secret somekey=somedata
38 | exec_in_vault vault write ultrasecret/dir2/secret somekey=somedata
39 | exec_in_vault vault write ultrasecret/dir2/secret2 somekey=somedata
40 | exec_in_vault vault write ultrasecret/admincantlistthis/butcanreadthis somekey=somedata
41 | exec_in_vault vault write ultrasecret/admincantreadthis somekey=somedata
42 |
43 | echo "------------- Vault Root Token -------------"
44 | docker-compose logs vault | grep 'Root Token:' | tail -n 1
45 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var express = require('express');
4 | var bodyParser = require('body-parser');
5 | var path = require('path');
6 | var routeHandler = require('./src/routeHandler');
7 | var compression = require('compression');
8 |
9 | var PORT = process.env.PORT || 8000;
10 |
11 | var app = express();
12 | app.set('view engine', 'html');
13 | // app.engine('html', require('hbs').__express);
14 | app.use('/dist', compression(), express.static('dist'));
15 |
16 | // parse application/x-www-form-urlencoded
17 | app.use(bodyParser.urlencoded({ extended: false }));
18 |
19 | // parse application/json
20 | app.use(bodyParser.json());
21 |
22 | app.use(function (req, res, next) {
23 | res.header('Access-Control-Allow-Origin', '*');
24 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
25 | next();
26 | });
27 |
28 | app.listen(PORT, function () {
29 | console.log('Vault UI listening on: ' + PORT);
30 | });
31 |
32 | app.get('/vaultui', function(req,res) {
33 | routeHandler.vaultuiHello(req, res);
34 | });
35 |
36 | app.all('/v1/*', function(req, res) {
37 | routeHandler.vaultapi(req, res);
38 | })
39 |
40 | app.get('/');
41 |
42 | app.get('*', function (req, res) {
43 | res.sendFile(path.join(__dirname, '/index.web.html'));
44 | });
45 |
--------------------------------------------------------------------------------
/shippable.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - 5.10
5 |
6 | env:
7 | global:
8 | - PROJECT=${REPO_NAME}
9 |
10 | build:
11 | ci:
12 | - docker build -t ${PROJECT}:ci .
13 | # run tests here
--------------------------------------------------------------------------------
/src/routeHandler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var vaultapi = require('./vaultapi');
4 | var vaultui = require('./vaultui');
5 |
6 | module.exports = (function () {
7 | return {
8 | vaultapi: vaultapi.callMethod,
9 | vaultuiHello: vaultui.vaultuiHello
10 | };
11 | })();
12 |
--------------------------------------------------------------------------------
/src/vaultapi.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var axios = require('axios');
4 |
5 | exports.callMethod = function (req, res) {
6 | let vaultAddr = req.query.vaultaddr;
7 | if (!vaultAddr) {
8 | res.status(400).send("missing vaultaddr parameter");
9 | return;
10 | }
11 | delete req.query.vaultaddr;
12 | delete req.headers.host;
13 | let config = {
14 | method: req.method,
15 | baseURL: decodeURI(vaultAddr),
16 | url: req.path,
17 | params: req.query,
18 | headers: req.headers,
19 | data: req.body
20 | }
21 |
22 | axios.request(config)
23 | .then(function (resp) {
24 | res.json(resp.data);
25 | })
26 | .catch(function (err) {
27 | if(err.response) {
28 | res.status(err.response.status).send(err.response.data);
29 | } else {
30 | res.status(500).send({errors: [err.toString()]});
31 | }
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/src/vaultui.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var VAULT_URL_DEFAULT = process.env.VAULT_URL_DEFAULT || "";
4 | var VAULT_URL_DEFAULT_FORCE = process.env.VAULT_URL_DEFAULT_FORCE ? true : false;
5 | var VAULT_AUTH_DEFAULT = process.env.VAULT_AUTH_DEFAULT || "GITHUB";
6 | var VAULT_AUTH_DEFAULT_FORCE = process.env.VAULT_AUTH_DEFAULT_FORCE ? true : false;
7 | var VAULT_AUTH_BACKEND_PATH = process.env.VAULT_AUTH_BACKEND_PATH
8 | var VAULT_AUTH_BACKEND_PATH_FORCE = process.env.VAULT_AUTH_BACKEND_PATH_FORCE ? true : false;
9 | var VAULT_SUPPLIED_TOKEN_HEADER = process.env.VAULT_SUPPLIED_TOKEN_HEADER
10 |
11 | exports.vaultuiHello = function (req, res) {
12 | let response = {
13 | defaultVaultUrl: VAULT_URL_DEFAULT,
14 | defaultVaultUrlForce: VAULT_URL_DEFAULT_FORCE,
15 | defaultAuthMethod: VAULT_AUTH_DEFAULT,
16 | defaultAuthMethodForce: VAULT_AUTH_DEFAULT_FORCE,
17 | suppliedAuthToken: VAULT_SUPPLIED_TOKEN_HEADER ? req.header(VAULT_SUPPLIED_TOKEN_HEADER) : VAULT_SUPPLIED_TOKEN_HEADER,
18 | defaultBackendPath: VAULT_AUTH_BACKEND_PATH,
19 | defaultBackendPathForce: VAULT_AUTH_BACKEND_PATH_FORCE
20 | }
21 |
22 | res.status(200).send(response);
23 | };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var ExtractTextPlugin = require("extract-text-webpack-plugin");
3 | var path = require('path');
4 |
5 | module.exports = function (env) {
6 | let buildfor = (env && env.target) ? env.target : "web";
7 |
8 | return {
9 | target: buildfor,
10 | entry: {
11 | common: './app/App.jsx'
12 | },
13 | resolve: {
14 | modules: [
15 | "node_modules"
16 | ],
17 | extensions: ['.js', '.jsx']
18 | },
19 | output: {
20 | path: path.resolve(__dirname, './dist'),
21 | publicPath: env.target == 'electron' ? 'dist/' : '/dist/',
22 | filename: buildfor + '-bundle.js'
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.jsx?$/,
28 | use: ['babel-loader?cacheDirectory=true']
29 | },
30 | {
31 | test: /\.css$/,
32 | use: ExtractTextPlugin.extract({
33 | fallback: 'style-loader',
34 | use: ['css-loader?modules=true&localIdentName=[path][name]__[local]--[hash:base64:5]', 'postcss-loader']
35 | }),
36 | include: path.resolve(__dirname, './app')
37 | },
38 | {
39 | test: /\.css$/,
40 | use: ExtractTextPlugin.extract({
41 | fallback: 'style-loader',
42 | use: ['css-loader', 'postcss-loader']
43 | }),
44 | include: path.resolve(__dirname, './node_modules')
45 | },
46 | {
47 | test: /\.ico$/,
48 | use: ['file-loader?name=[name].[ext]']
49 | },
50 | {
51 | test: /\.(svg|png)$/,
52 | use: ['file-loader']
53 | },
54 | {
55 | test: /\.(ttf|eot|woff(2)?)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
56 | use: ["file-loader"]
57 | },
58 | ]
59 | },
60 | plugins: [
61 | new ExtractTextPlugin("styles.css"),
62 | new webpack.IgnorePlugin(/regenerator|nodent|js-beautify/, /ajv/),
63 | new webpack.DefinePlugin({ WEBPACK_DEF_TARGET_WEB: (buildfor == "web") })
64 | ]
65 | }
66 | };
--------------------------------------------------------------------------------