diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ef370c2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +node_modules/ +release/ diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a261f29 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +dist/* diff --git a/Dockerfile b/Dockerfile index 1cd1e52..41a925f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,10 @@ -FROM node:slim +FROM node:alpine MAINTAINER Vault-UI Contributors -ADD package.json /tmp/package.json -RUN cd /tmp && npm install --silent && mkdir -p /app/ && mv /tmp/node_modules /app/ - -RUN npm install --silent -g webpack - ADD . /app WORKDIR /app - -RUN npm run build +RUN npm install --silent && npm run build-web && npm prune --silent --production EXPOSE 8000 diff --git a/README.md b/README.md index e9f1fe1..5baf568 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,88 @@ -# Vault-UI + + Vault-UI Logo + + [![](https://images.microbadger.com/badges/image/djenriquez/vault-ui.svg)](https://microbadger.com/images/djenriquez/vault-ui) [![Run Status](https://api.shippable.com/projects/581e7826fbc68c0f00deb0ca/badge?branch=master)](https://app.shippable.com/projects/581e7826fbc68c0f00deb0ca) -A beautiful way to manage your secrets in Vault -![Landing Page](images/Landing.png) +# Vault-UI + +A beautiful way to manage your Hashicorp Vault + +![](http://i.imgur.com/COBxk3m.gif) + +## Features + +- Easy to deploy as Web App +- Desktop version works on Mac, Linux and Windows +- Material UI Design +- Integrated JSON Editor +- Written in React + +## Installation + +### Desktop Version + +Vault-UI Desktop is available for the following operating systems: +- Windows +- MacOS +- Linux (32bit and 64bit AppImage) + +Download the latest version from the release page and install/run the software + +### Web Version + +Vault-UI can be deployed as a shared web app for your organization -## Configuration -Configuration is accessed by clicking on the configuration cog on the login page. +Docker images are automatically built using an [automated build on Docker Hub](https://hub.docker.com/r/djenriquez/vault-ui/builds/). +We encourage that versioned images are used for production. - +To run Vault-UI using the latest Docker image: +```bash +docker run -d \ +-p 8000:8000 \ +--name vault-ui \ +djenriquez/vault-ui +``` -### Vault Endpoint -Users can enter in the full endpoint to Vault, including scheme. When running the docker image, it is possible to -set the following environment variables to pre-configure authentication settings: +#### Advanced configuration options + +By default, connection and authentication parameters must be configured by clicking on the configuration cog on the login page. +Using environment variables (via docker), an administrator can pre-configure those parameters. + +Example command to pre-configure the Vault server URL and authentication method +```bash +docker run -d \ +-p 8000:8000 \ +-e VAULT_URL_DEFAULT=http://vault.server.org:8200 +-e VAULT_AUTH_DEFAULT=GITHUB +--name vault-ui \ +djenriquez/vault-ui +``` + +Supported environment variables: +- `NODE_TLS_REJECT_UNAUTHORIZED` disable TLS server side validation (ex. vault deployed with self-signed certificate) - `VAULT_URL_DEFAULT` will set the default vault endpoint. - `VAULT_AUTH_DEFAULT` will set the default authentication method type. See below for supported authentication methods. - `VAULT_AUTH_BACKEND_PATH` will set the default backend path. Useful when multiple backends of the same type are mounted on the vault file system. +- `VAULT_SUPPLIED_TOKEN_HEADER` will instruct Vault-UI to attempt authentication using a token provided by the client in the specified HTTP request header. This defaults can be overridden if the user fills out the endpoint and auth method manually. -## Authentication + Currently supported authentication methods: - `GITHUB` : When using the [GitHub](https://www.vaultproject.io/docs/auth/github.html) backend - `USERNAMEPASSWORD` : When using the [Username & Password](https://www.vaultproject.io/docs/auth/userpass.html) or [RADIUS](https://www.vaultproject.io/docs/auth/radius.html) backends - `LDAP` : When using the [LDAP](https://www.vaultproject.io/docs/auth/ldap.html) backend - `TOKEN` : When using the [Tokens](https://www.vaultproject.io/docs/auth/token.html) backend -### Token authentication by header (SSO) + In some cases, users might want to use middleware to authenticate into Vault-UI for purposes like SSO. In this case, the `VAULT_SUPPLIED_TOKEN_HEADER` may be populated with the name of the header that contains a token to be used for authentication. + +## Usage + ### Basic policy for Vault-UI users A user/token accessing Vault-UI requires a basic set of capabilities in order to correctly discover and display the various mounted backends. Please make sure your user is granted a policy with at least the following permissions: @@ -80,51 +134,24 @@ path "sys/auth" { } ``` -## Secrets -![Secrets Management](images/Home.png) - +### Secrets Secrets are now managed using the graphical [josdejong/jsoneditor](https://github.com/josdejong/jsoneditor) JSON editor. Schema validation is enforced on policies to aid the operator in writing correct syntax. - -Secrets also are accessible directly by key from a browser by navigating to the URI `/secrets///key`. For example, if you have a generic secret key of /hello/world/vault using the generic mount `secret`, one can navigate to this directly through http://vault-ui.myorg.com/secrets/secret/hello/world/vault. +Secrets also are accessible directly by key from a browser by navigating to the URI `/secrets///key`. For example, if you have a generic secret key of /hello/world/vault using the _generic_ mount `secret/`, one can navigate to this directly through http://vault-ui.myorg.com/secrets/secret/hello/world/vault. -### Root key bias +#### Root key bias By default, secrets will display as their raw JSON value represented by the `data` field in the HTTP GET response metadata. However, users can apply a "Root Key" bias to the secrets through the settings page. The "Root Key" will be used when reading, creating and updating secrets such that the value displayed in the UI is the value stored at the "Root Key". For example, if the secret at `secret/hello` is `{ "value": "world" }`, setting the "Root Key" to `value` will update the UI such that the secret will display as simply "world" instead of `{ "value": "world" }`. - -## Policies +### Policies Policies are managed also using the [josdejong/jsoneditor](https://github.com/josdejong/jsoneditor) JSON editor. Currently, GitHub and raw Tokens are the only supported authentication backends for associated policies. -## Token Management - +### Token Management +Users have the ability to create and revoke tokens, manage token roles and list accessors. -Users now have the ability to create and revoke tokens. - +### Response Wrapping +Vault-UI supports response-wrapping of secrets in _generic_ backends. Wrapping custom JSON data is also supported. -## Response Wrapping -Vault-UI supports response-wrapping raw values. It currently does not support wrapping of existing secrets. - - -## Run -Vault-UI Docker images are automatically built using an [automated build on Docker Hub](https://hub.docker.com/r/djenriquez/vault-ui/builds/). We encourage that versioned images are used for production. -To run Vault-UI using the latest Docker image: -```bash -docker run -d \ --p 8000:8000 \ ---name vault-ui \ -djenriquez/vault-ui -``` - -### Skip TLS Verification -In the case that you need to skip TLS verification, say for self-signed certs, you can run Vault-UI with the environment variable `NODE_TLS_REJECT_UNAUTHORIZED=0`: -``` -docker run -d \ --p 8000:8000 \ --e NODE_TLS_REJECT_UNAUTHORIZED=0 \ ---name vault-ui \ -djenriquez/vault-ui -``` ## Development @@ -143,12 +170,8 @@ The following will spin up a Vault UI server only. It will not set up Vault for you: ```sh npm install - -# If you do not have webpack installed globally -npm install -g webpack - +npm run dev-pack & npm start -webpack -w ``` # Licensing diff --git a/app/App.jsx b/app/App.jsx index 5ca72ef..4e851c7 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -1,10 +1,12 @@ import React from 'react' +import axios from 'axios'; import ReactDOM from 'react-dom'; import Login from './components/Login/Login.jsx'; -import { Router, Route, browserHistory } from 'react-router' +import { Router, Route } 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 { history } from './components/shared/VaultUtils.jsx'; import App from './components/App/App.jsx'; import SecretsGeneric from './components/Secrets/Generic/Generic.jsx'; import PolicyManager from './components/Policies/Manage.jsx'; @@ -16,6 +18,10 @@ import GithubAuthBackend from './components/Authentication/Github/Github.jsx'; import RadiusAuthBackend from './components/Authentication/Radius/Radius.jsx'; import SecretUnwrapper from './components/shared/Wrapping/Unwrapper'; +// Load here to signal webpack +import 'flexboxgrid/dist/flexboxgrid.min.css'; +import './assets/favicon.ico'; + injectTapEventPlugin(); (function () { @@ -34,6 +40,22 @@ injectTapEventPlugin(); window.CustomEvent = CustomEvent; })(); +const checkVaultUiServer = (nextState, replace, callback) => { + // If it's a web deployment, query the server for default connection parameters + // Those can be set using environment variables in the nodejs process + if (WEBPACK_DEF_TARGET_WEB) { + axios.get('/vaultui').then((resp) => { + window.defaultVaultUrl = resp.data.defaultVaultUrl; + window.defaultAuthMethod = resp.data.defaultAuthMethod; + window.defaultBackendPath = resp.data.defaultBackendPath; + window.suppliedAuthToken = resp.data.suppliedAuthToken; + callback(); + }).catch((err) => callback()) + } else { + callback(); + } +} + const checkAccessToken = (nextState, replace, callback) => { let vaultAuthToken = window.localStorage.getItem('vaultAccessToken'); if (!vaultAuthToken) { @@ -49,8 +71,8 @@ const muiTheme = getMuiTheme({ ReactDOM.render(( - - + + diff --git a/app/assets/favicon.ico b/app/assets/favicon.ico new file mode 100644 index 0000000..a623824 Binary files /dev/null and b/app/assets/favicon.ico differ diff --git a/app/assets/vault-ui-logo.svg b/app/assets/vault-ui-logo.svg new file mode 100644 index 0000000..65bdb0a --- /dev/null +++ b/app/assets/vault-ui-logo.svg @@ -0,0 +1,236 @@ + + + + + + + + + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/components/App/App.jsx b/app/components/App/App.jsx index 1189f43..af147dc 100644 --- a/app/components/App/App.jsx +++ b/app/components/App/App.jsx @@ -7,16 +7,15 @@ 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 Warning from 'material-ui/svg-icons/alert/warning'; import { green500, red500 } from 'material-ui/styles/colors.js' import styles from './app.css'; import JsonEditor from '../shared/JsonEditor.jsx'; import { Card, CardHeader, CardText } from 'material-ui/Card'; -import { callVaultApi, tokenHasCapabilities } from '../shared/VaultUtils.jsx' +import { callVaultApi, tokenHasCapabilities, history } from '../shared/VaultUtils.jsx' -let twoMinuteWarningTimeout; -let logoutTimeout; +let twoMinuteWarningTimeoutId; +let logoutTimeoutId; function snackBarMessage(message) { let ev = new CustomEvent("snackbar", { detail: { message: message } }); @@ -67,7 +66,7 @@ export default class App extends React.Component { } let logoutTimeout = () => { - browserHistory.push('/login'); + history.push('/login'); } // Retrieve session identity information @@ -78,15 +77,17 @@ export default class App extends React.Component { 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); + clearTimeout(logoutTimeoutId); + clearTimeout(twoMinuteWarningTimeoutId); + logoutTimeoutId = setTimeout(logoutTimeout, ttl); + twoMinuteWarningTimeoutId = setTimeout(twoMinuteWarningTimeout, ttl - TWO_MINUTES); } } }) .catch((err) => { if (_.has(err, 'response.status') && err.response.status >= 400) { window.localStorage.removeItem('vaultAccessToken'); - browserHistory.push(`/login?returnto=${encodeURI(this.props.location.pathname)}`); + history.push(`/login?returnto=${encodeURI(this.props.location.pathname)}`); } else throw err; }); } @@ -119,11 +120,11 @@ export default class App extends React.Component { this.reloadSessionIdentity(); // Check access to the sys/capabilities-self path - callVaultApi('post', 'sys/capabilities-self', null, {path: '/'}) - .then(() =>{ + callVaultApi('post', 'sys/capabilities-self', null, { path: '/' }) + .then(() => { this.setState({ tokenCanQueryCapabilities: true }); }) - .catch(() =>{ + .catch(() => { this.setState({ tokenCanQueryCapabilities: false }); }) @@ -138,8 +139,8 @@ export default class App extends React.Component { } componentWillUnmount() { - clearTimeout(logoutTimeout); - clearTimeout(twoMinuteWarningTimeout); + clearTimeout(logoutTimeoutId); + clearTimeout(twoMinuteWarningTimeoutId); } renderSessionExpDialog() { @@ -258,7 +259,7 @@ export default class App extends React.Component { -

Get started by using the left menu to navigate your Vault

+

Get started by using the left menu to navigate your Vault

{!this.state.tokenCanQueryCapabilities ? this.renderWarningCapabilities() : null} {!this.state.tokenCanListSecretBackends ? this.renderWarningSecretBackends() : null} diff --git a/app/components/App/app.css b/app/components/App/app.css index 8f87e2a..f4b79dc 100644 --- a/app/components/App/app.css +++ b/app/components/App/app.css @@ -6,11 +6,6 @@ margin-top: 80px; } -#welcomeHeadline { - font-size: 60px; - font-weight: 200; -} - .snackbar { text-align: center; } @@ -26,7 +21,6 @@ } .welcomeHeader { - /*text-shadow: 0px 1px 1px #4d4d4d;*/ text-align: center; } @@ -37,5 +31,4 @@ margin: 20px 15%; border-color: #f39c12; background-color: #fef5e6 !important; - /*text-align: center;*/ } \ No newline at end of file diff --git a/app/components/Authentication/AwsEc2/AwsEc2.jsx b/app/components/Authentication/AwsEc2/AwsEc2.jsx index bc584e5..7523490 100644 --- a/app/components/Authentication/AwsEc2/AwsEc2.jsx +++ b/app/components/Authentication/AwsEc2/AwsEc2.jsx @@ -19,12 +19,11 @@ import styles from './awsec2.css'; import sharedStyles from '../../shared/styles.css'; import { green500, green400, red500, red300, yellow500, white } from 'material-ui/styles/colors.js'; import Checkbox from 'material-ui/Checkbox'; -import { callVaultApi, tokenHasCapabilities } from '../../shared/VaultUtils.jsx'; +import { callVaultApi, tokenHasCapabilities, history } from '../../shared/VaultUtils.jsx'; // Misc import _ from 'lodash'; import update from 'immutability-helper'; import Avatar from 'material-ui/Avatar'; -import { browserHistory } from 'react-router' import PolicyPicker from '../../shared/PolicyPicker/PolicyPicker.jsx' import VaultObjectDeleter from '../../shared/DeleteObject/DeleteObject.jsx' @@ -124,7 +123,7 @@ export default class AwsEc2AuthBackend extends React.Component { snackBarMessage(error); } else { error.message = `This backend has not yet been configured`; - browserHistory.push(`${this.state.baseUrl}backend`); + history.push(`${this.state.baseUrl}backend`); this.setState({ selectedTab: 'backend', isBackendConfigured: false }); snackBarMessage(error); } @@ -160,7 +159,7 @@ export default class AwsEc2AuthBackend extends React.Component { snackBarMessage(`Role ${roleId} has been updated`); this.listEc2Roles(); this.setState({ openNewRoleDialog: false, openEditRoleDialog: false, newRoleConfig: _.clone(this.roleConfigSchema), selectedRoleId: '', newRoleId: '' }); - browserHistory.push(`${this.state.baseUrl}roles`); + history.push(`${this.state.baseUrl}roles`); }) .catch(snackBarMessage); }) @@ -190,7 +189,7 @@ export default class AwsEc2AuthBackend extends React.Component { componentWillMount() { let tab = this.props.location.pathname.split(this.state.baseUrl)[1]; if (!tab) { - browserHistory.push(`${this.state.baseUrl}${this.state.selectedTab}/`); + history.push(`${this.state.baseUrl}${this.state.selectedTab}/`); } else { this.state.selectedTab = tab.includes('/') ? tab.split('/')[0] : tab; } @@ -257,7 +256,7 @@ export default class AwsEc2AuthBackend extends React.Component { tokenHasCapabilities(['read'], `${this.state.baseVaultPath}/role/${role}`) .then(() => { this.setState({ selectedRoleId: role }); - browserHistory.push(`${this.state.baseUrl}roles/${role}`); + history.push(`${this.state.baseUrl}roles/${role}`); }).catch(() => { snackBarMessage(new Error('Access denied')); }) @@ -452,7 +451,7 @@ export default class AwsEc2AuthBackend extends React.Component { label='Cancel' onTouchTap={() => { this.setState({ openEditRoleDialog: false, selectedRoleId: '' }) - browserHistory.push(`${this.state.baseUrl}roles/`); + history.push(`${this.state.baseUrl}roles/`); }} />, { this.setState({ openEditRoleDialog: false, selectedRoleId: '' }); - browserHistory.push(`${this.state.baseUrl}roles/`); + history.push(`${this.state.baseUrl}roles/`); }} autoScrollBodyContent={true} > @@ -621,7 +620,7 @@ export default class AwsEc2AuthBackend extends React.Component { /> { - browserHistory.push(`${this.state.baseUrl}${e}/`); + history.push(`${this.state.baseUrl}${e}/`); this.setState({ newConfigObj: _.clone(this.state.configObj) }); }} value={this.state.selectedTab} diff --git a/app/components/Authentication/Github/Github.jsx b/app/components/Authentication/Github/Github.jsx index c9f86f8..870667e 100644 --- a/app/components/Authentication/Github/Github.jsx +++ b/app/components/Authentication/Github/Github.jsx @@ -24,10 +24,9 @@ import sharedStyles from '../../shared/styles.css'; // Misc import _ from 'lodash'; import update from 'immutability-helper'; -import { browserHistory } from 'react-router' import PolicyPicker from '../../shared/PolicyPicker/PolicyPicker.jsx' import VaultObjectDeleter from '../../shared/DeleteObject/DeleteObject.jsx' -import { callVaultApi, tokenHasCapabilities } from '../../shared/VaultUtils.jsx'; +import { callVaultApi, tokenHasCapabilities, history } from '../../shared/VaultUtils.jsx'; function snackBarMessage(message) { document.dispatchEvent(new CustomEvent('snackbar', { detail: { message: message } })); @@ -122,7 +121,7 @@ export default class GithubAuthBackend extends React.Component { .then((resp) => { let config = _.get(resp, 'data.data', this.backendConfigSchema); if (!config.organization) { - browserHistory.push(`${this.state.baseUrl}backend`); + history.push(`${this.state.baseUrl}backend`); this.setState({ selectedTab: 'backend', isBackendConfigured: false, newConfig: this.backendConfigSchema }); snackBarMessage(new Error(`This backend has not yet been configured`)); } else { @@ -138,7 +137,7 @@ export default class GithubAuthBackend extends React.Component { snackBarMessage(error); } else { error.message = `This backend has not yet been configured`; - browserHistory.push(`${this.state.baseUrl}backend`); + history.push(`${this.state.baseUrl}backend`); snackBarMessage(error); } }); @@ -191,7 +190,7 @@ export default class GithubAuthBackend extends React.Component { this.listGithubTeams(); this.listGithubUsers(); this.setState({ openItemDialog: false, openNewItemDialog: false, itemConfig: _.clone(this.itemConfigSchema), selectedItemId: '' }); - browserHistory.push(this.state.baseUrl); + history.push(this.state.baseUrl); }) .catch(snackBarMessage); }) @@ -203,7 +202,7 @@ export default class GithubAuthBackend extends React.Component { componentWillMount() { let tab = this.props.location.pathname.split(this.state.baseUrl)[1]; if (!tab) { - browserHistory.push(`${this.state.baseUrl}${this.state.selectedTab}/`); + history.push(`${this.state.baseUrl}${this.state.selectedTab}/`); } else { this.state.selectedTab = tab.includes('/') ? tab.split('/')[0] : tab; } @@ -245,7 +244,7 @@ export default class GithubAuthBackend extends React.Component { selectedTab: 'teams', isBackendConfigured: false }, () => { - browserHistory.push(`${this.state.baseUrl}teams`); + history.push(`${this.state.baseUrl}teams`); this.listGithubTeams(); this.listGithubUsers(); this.getOrgConfig(); @@ -278,7 +277,7 @@ export default class GithubAuthBackend extends React.Component { tokenHasCapabilities(['read'], `${this.state.baseVaultPath}/${this.state.selectedTab}/${item}`) .then(() => { this.setState({ selectedItemId: `${this.state.selectedTab}/${item}` }); - browserHistory.push(`${this.state.baseUrl}${this.state.selectedTab}/${item}`); + history.push(`${this.state.baseUrl}${this.state.selectedTab}/${item}`); }).catch(() => { snackBarMessage(new Error('Access denied')); }) @@ -296,7 +295,7 @@ export default class GithubAuthBackend extends React.Component { label='Cancel' onTouchTap={() => { this.setState({ openItemDialog: false, selectedItemId: '' }); - browserHistory.push(this.state.baseUrl); + history.push(this.state.baseUrl); }} />, { this.setState({ openItemDialog: false, selectedItemId: '' }); - browserHistory.push(this.state.baseUrl); + history.push(this.state.baseUrl); }} autoScrollBodyContent={true} > @@ -340,7 +339,7 @@ export default class GithubAuthBackend extends React.Component { label='Cancel' onTouchTap={() => { this.setState({ openNewItemDialog: false, newItemId: '' }); - browserHistory.push(this.state.baseUrl); + history.push(this.state.baseUrl); }} />, { this.setState({ openNewItemDialog: false, newItemId: '' }); - browserHistory.push(this.state.baseUrl); + history.push(this.state.baseUrl); }} autoScrollBodyContent={true} > @@ -405,7 +404,7 @@ export default class GithubAuthBackend extends React.Component { /> { - browserHistory.push(`${this.state.baseUrl}${e}/`); + history.push(`${this.state.baseUrl}${e}/`); this.setState({ newConfig: _.clone(this.state.config) }); }} value={this.state.selectedTab} diff --git a/app/components/Authentication/Radius/Radius.jsx b/app/components/Authentication/Radius/Radius.jsx index de03e59..72316e5 100644 --- a/app/components/Authentication/Radius/Radius.jsx +++ b/app/components/Authentication/Radius/Radius.jsx @@ -16,13 +16,11 @@ import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import TextField from 'material-ui/TextField'; import { red500 } from 'material-ui/styles/colors.js' -import { callVaultApi, tokenHasCapabilities } from '../../shared/VaultUtils.jsx' +import { callVaultApi, tokenHasCapabilities, history } from '../../shared/VaultUtils.jsx' import PolicyPicker from '../../shared/PolicyPicker/PolicyPicker.jsx' import VaultObjectDeleter from '../../shared/DeleteObject/DeleteObject.jsx' -import { browserHistory } from 'react-router' import update from 'immutability-helper'; - function snackBarMessage(message) { let ev = new CustomEvent("snackbar", { detail: { message: message } }); document.dispatchEvent(ev); @@ -178,7 +176,7 @@ class RadiusAuthBackend extends React.Component { this.setState({ openNewUserDialog: false, newUserId: '' }); snackBarMessage(`User ${userid} has been registered`); } else { - browserHistory.push(this.state.baseUrl); + history.push(this.state.baseUrl); this.setState({ openEditUserDialog: false, selectedUserId: '' }); snackBarMessage(`User ${userid} has been updated`); } @@ -226,7 +224,7 @@ class RadiusAuthBackend extends React.Component { this.setState({ newUserId: '' }); tokenHasCapabilities(['read'], userobj.path).then(() => { this.setState({ selectedUserId: userobj.id }); - browserHistory.push(`${this.state.baseUrl}${userobj.id}`); + history.push(`${this.state.baseUrl}${userobj.id}`); }).catch(() => { snackBarMessage(new Error("Access denied")); }) @@ -244,7 +242,7 @@ class RadiusAuthBackend extends React.Component { label="Cancel" onTouchTap={() => { this.setState({ openEditUserDialog: false, selectedUserId: '' }) - browserHistory.push(this.state.baseUrl); + history.push(this.state.baseUrl); }} />, {policies} {getDateStr(this.state.accessorDetails[acc_id].creation_time)} {this.state.accessorDetails[acc_id].orphan && - + } ) @@ -751,7 +753,7 @@ export default class TokenAuthBackend extends React.Component { errorStyle={{ color: orange500, }} defaultValue={this.state.newTokenCode} /> - } label="Copy to Clipboard" onTouchTap={() => { copy(this.state.newTokenCode) }} /> + } label="Copy to Clipboard" onTouchTap={() => { copy(this.state.newTokenCode) }} /> @@ -795,7 +797,7 @@ export default class TokenAuthBackend extends React.Component { - }> + }> {this.state.openSettings && this.renderSettingsDialog()} -
-
AULT - UI
+
+
AULT - UI
{this.renderSelectedLoginOption()} diff --git a/app/components/Policies/Manage.jsx b/app/components/Policies/Manage.jsx index 2c36eac..a2fc5b3 100644 --- a/app/components/Policies/Manage.jsx +++ b/app/components/Policies/Manage.jsx @@ -15,12 +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, tokenHasCapabilities } from '../shared/VaultUtils.jsx' +import { callVaultApi, tokenHasCapabilities, history } 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' +import { Link } from 'react-router' function snackBarMessage(message) { let ev = new CustomEvent("snackbar", { detail: { message: message } }); @@ -97,7 +97,7 @@ export default class PolicyManager extends React.Component { primary={true} onTouchTap={() => { this.setState({ openEditModal: false }) - browserHistory.push('/sys/policies'); + history.push('/sys/policies'); }} />, { this.updatePolicy(this.state.focusPolicy, false) - browserHistory.push('/sys/policies'); + history.push('/sys/policies'); }} /> ]; @@ -334,7 +334,7 @@ export default class PolicyManager extends React.Component { leftAvatar={} />} onTouchTap={() => { tokenHasCapabilities(['read'], 'sys/policy/' + policy.name).then(() => { - browserHistory.push(`/sys/policies/` + policy.name); + history.push(`/sys/policies/` + policy.name); }).catch(() => { snackBarMessage(new Error("Access denied")); }) diff --git a/app/components/Secrets/Generic/Generic.jsx b/app/components/Secrets/Generic/Generic.jsx index d988ce1..6cebaf9 100644 --- a/app/components/Secrets/Generic/Generic.jsx +++ b/app/components/Secrets/Generic/Generic.jsx @@ -20,10 +20,10 @@ import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import TextField from 'material-ui/TextField'; import { green500, green400, red500, red300, white } from 'material-ui/styles/colors.js' -import { callVaultApi, tokenHasCapabilities } from '../../shared/VaultUtils.jsx' +import { callVaultApi, tokenHasCapabilities, history } from '../../shared/VaultUtils.jsx' import JsonEditor from '../../shared/JsonEditor.jsx'; import SecretWrapper from '../../shared/Wrapping/Wrapper.jsx' -import { browserHistory, Link } from 'react-router' +import { Link } from 'react-router' const SORT_DIR = { ASC: 'asc', @@ -279,7 +279,7 @@ class GenericSecretBackend extends React.Component { , { this.setState({ openEditObjectModal: false, secretContent: '' }); - browserHistory.push(this.getBaseDir(this.props.location.pathname)); + history.push(this.getBaseDir(this.props.location.pathname)); } } />, submitUpdate()} /> @@ -288,7 +288,7 @@ class GenericSecretBackend extends React.Component { let submitUpdate = () => { this.CreateUpdateObject(); this.setState({ openEditObjectModal: false, secretContent: '' }); - browserHistory.push(this.getBaseDir(this.props.location.pathname)); + history.push(this.getBaseDir(this.props.location.pathname)); } var objectIsBasicRootKey = _.size(this.state.secretContent) == 1 && this.state.secretContent.hasOwnProperty(this.state.rootKey); @@ -328,7 +328,7 @@ class GenericSecretBackend extends React.Component { autoScrollBodyContent={true} onRequestClose={() => { this.setState({ openEditObjectModal: false, secretContent: '' }) - browserHistory.push(this.getBaseDir(this.props.location.pathname)) + history.push(this.getBaseDir(this.props.location.pathname)) }} > {content} @@ -404,7 +404,7 @@ class GenericSecretBackend extends React.Component { onTouchTap={() => { this.setState({ newSecretName: '' }); tokenHasCapabilities([capability], this.state.currentLogicalPath + key).then(() => { - browserHistory.push(`/secrets/generic/${this.state.currentLogicalPath}${key}`); + history.push(`/secrets/generic/${this.state.currentLogicalPath}${key}`); }).catch(() => { snackBarMessage(new Error("Access denied")); }) diff --git a/app/components/shared/Header/Header.jsx b/app/components/shared/Header/Header.jsx index 223af52..91d72e7 100644 --- a/app/components/shared/Header/Header.jsx +++ b/app/components/shared/Header/Header.jsx @@ -3,17 +3,14 @@ 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 Github from 'mui-icons/fontawesome/github'; import CountDown from './countdown.js' import styles from './header.css'; -import { callVaultApi } from '../../shared/VaultUtils.jsx'; -import Snackbar from 'material-ui/Snackbar'; - +import { callVaultApi, history } from '../../shared/VaultUtils.jsx'; var logout = () => { window.localStorage.removeItem('vaultAccessToken'); - browserHistory.push('/login'); + history.push('/login'); } function snackBarMessage(message) { @@ -85,7 +82,7 @@ class Header extends React.Component { token ttl - + ) @@ -107,12 +104,21 @@ class Header extends React.Component {
- - + { + if(WEBPACK_DEF_TARGET_WEB) { + window.open('https://github.com/djenriquez/vault-ui', '_blank'); + } else { + event.preventDefault(); + require('electron').shell.openExternal('https://github.com/djenriquez/vault-ui') + } + }} + > + { - browserHistory.push('/'); + history.push('/'); }} text="VAULT - UI" /> diff --git a/app/components/shared/Header/countdown.js b/app/components/shared/Header/countdown.js index a60104f..c6ba77f 100644 --- a/app/components/shared/Header/countdown.js +++ b/app/components/shared/Header/countdown.js @@ -6,23 +6,24 @@ class CountDown extends Component { static propTypes = { - startTime: PropTypes.number, + countDown: PropTypes.number, + retrigger: PropTypes.number, className: PropTypes.string } constructor(props) { super(props) - this.state = { time: props.startTime * 10 } + this.state = { time: props.countDown * 10 } this.tick = this.tick.bind(this) this.splitTimeComponents = this.splitTimeComponents.bind(this) - this.stopTime = Date.now() + (props.startTime * 1000) + this.stopTime = Date.now() + (props.countDown * 1000) } componentWillReceiveProps(nextProps) { - if(nextProps.startTime != this.props.startTime) { + if(nextProps.retrigger !== this.props.retrigger) { clearInterval(this.time); - this.time = nextProps.startTime; - this.stopTime = Date.now() + (nextProps.startTime * 1000) + this.time = nextProps.countDown; + this.stopTime = Date.now() + (nextProps.countDown * 1000) this.time = setInterval(this.tick, 100) } } @@ -62,6 +63,6 @@ class CountDown extends Component { } } CountDown.propTypes = { - startTime: PropTypes.number.isRequired + countDown: PropTypes.number.isRequired } export default CountDown diff --git a/app/components/shared/Menu/Menu.jsx b/app/components/shared/Menu/Menu.jsx index 5d5757d..034c99e 100644 --- a/app/components/shared/Menu/Menu.jsx +++ b/app/components/shared/Menu/Menu.jsx @@ -2,14 +2,13 @@ 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 IconButton from 'material-ui/IconButton'; import Build from 'material-ui/svg-icons/action/build'; import ContentAdd from 'material-ui/svg-icons/content/add' import MountTuneDeleteDialog from '../MountUtils/MountTuneDelete.jsx' import NewMountDialog from '../MountUtils/NewMount.jsx' -import { tokenHasCapabilities, callVaultApi } from '../VaultUtils.jsx' +import { tokenHasCapabilities, callVaultApi, history } from '../VaultUtils.jsx' const SelectableList = makeSelectable(List); @@ -179,7 +178,7 @@ class Menu extends React.Component { } let handleMenuChange = (e, v) => { - browserHistory.push(v) + history.push(v) } return ( @@ -217,7 +216,7 @@ class Menu extends React.Component { onActionDeleteSuccess={(path, uipath) => { snackBarMessage(`Mountpoint ${path} deleted`) if (this.props.pathname.startsWith(uipath)) { - browserHistory.push('/'); + history.push('/'); } this.loadAuthBackends(); this.loadSecretBackends(); diff --git a/app/components/shared/VaultUtils.jsx b/app/components/shared/VaultUtils.jsx index cbfdb9b..bfd309b 100644 --- a/app/components/shared/VaultUtils.jsx +++ b/app/components/shared/VaultUtils.jsx @@ -1,5 +1,13 @@ import axios from 'axios'; import _ from 'lodash'; +import { browserHistory, hashHistory } from 'react-router' + +var history; +if(WEBPACK_DEF_TARGET_WEB) { + history = browserHistory; +} else { + history = hashHistory; +} function resetCapabilityCache() { window.localStorage.setItem('capability_cache', JSON.stringify({})); @@ -29,11 +37,20 @@ function getCachedCapabilities(path) { function callVaultApi(method, path, query = {}, data, headers = {}, vaultToken = null, vaultUrl = null) { - var instance = axios.create({ - baseURL: '/v1/', - params: { "vaultaddr": vaultUrl || window.localStorage.getItem("vaultUrl") }, - headers: { "X-Vault-Token": vaultToken || window.localStorage.getItem("vaultAccessToken") } - }); + var instance; + + if(WEBPACK_DEF_TARGET_WEB) { + instance = axios.create({ + baseURL: '/v1/', + params: { "vaultaddr": vaultUrl || window.localStorage.getItem("vaultUrl") }, + headers: { "X-Vault-Token": vaultToken || window.localStorage.getItem("vaultAccessToken") } + }); + } else { + instance = axios.create({ + baseURL: `${window.localStorage.getItem("vaultUrl")}/v1/`, + headers: { "X-Vault-Token": vaultToken || window.localStorage.getItem("vaultAccessToken") } + }); + } return instance.request({ url: encodeURI(path), @@ -81,6 +98,7 @@ function tokenHasCapabilities(capabilities, path) { } module.exports = { + history: history, callVaultApi: callVaultApi, tokenHasCapabilities: tokenHasCapabilities, resetCapabilityCache: resetCapabilityCache diff --git a/app/components/shared/Wrapping/Wrapper.jsx b/app/components/shared/Wrapping/Wrapper.jsx index d960b2d..37918dc 100644 --- a/app/components/shared/Wrapping/Wrapper.jsx +++ b/app/components/shared/Wrapping/Wrapper.jsx @@ -5,12 +5,12 @@ 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 Divider from 'material-ui/Divider'; import Popover from 'material-ui/Popover'; import Menu from 'material-ui/Menu'; import MenuItem from 'material-ui/MenuItem'; +import ContentContentCopy from 'material-ui/svg-icons/content/content-copy'; import styles from './wrapping.css' import sharedStyles from '../styles.css'; @@ -129,7 +129,11 @@ export default class SecretWrapper extends Component { 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}`; + if(WEBPACK_DEF_TARGET_WEB) { + urlValue = `${loc.protocol}//${loc.hostname}${(loc.port ? ":" + loc.port : "")}/unwrap?token=${tokenValue}&vaultUrl=${vaultUrl}`; + } else { + urlValue = `vaultui://#/unwrap~token=${tokenValue}&vaultUrl=${vaultUrl}`; + } } return ( @@ -181,7 +185,7 @@ export default class SecretWrapper extends Component { floatingLabelText="This is a single-use unwrap token to read the wrapped data" defaultValue={tokenValue} /> - } label="Copy to Clipboard" onTouchTap={() => { copy(tokenValue) }} /> + } label="Copy to Clipboard" onTouchTap={() => { copy(tokenValue) }} />
- } label="Copy to Clipboard" onTouchTap={() => { copy(urlValue) }} /> + } label="Copy to Clipboard" onTouchTap={() => { copy(urlValue) }} />
diff --git a/build/icon.icns b/build/icon.icns new file mode 100644 index 0000000..7c22b69 Binary files /dev/null and b/build/icon.icns differ diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000..a623824 Binary files /dev/null and b/build/icon.ico differ diff --git a/docker-compose.yaml b/docker-compose.yaml index 3ebaad9..0d9ea9c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,11 +5,13 @@ services: image: vault privileged: true volumes: - - .:/app + - ./misc:/misc ports: - "8200:8200" + - "8201:8201" environment: VAULT_ADDR: http://127.0.0.1:8200 + VAULT_LOCAL_CONFIG: '{"listener" : { "tcp" : {"address" : "0.0.0.0:8201", "tls_cert_file" : "/misc/devserver.crt", "tls_key_file" : "/misc/devserver.key" } } }' vault-ui: build: . @@ -19,15 +21,14 @@ services: - vault volumes: - .:/app - - /app/node_modules environment: + NODE_TLS_REJECT_UNAUTHORIZED: 0 VAULT_URL_DEFAULT: http://vault:8200 VAULT_AUTH_DEFAULT: USERNAMEPASSWORD - VAULT_SUPPLIED_TOKEN_HEADER: 'X-Remote-User' +# VAULT_SUPPLIED_TOKEN_HEADER: 'X-Remote-User' webpack: build: . volumes: - .:/app - - /app/node_modules - command: webpack -w + command: npm run dev-pack diff --git a/images/AuthConfig.png b/images/AuthConfig.png deleted file mode 100644 index a460290..0000000 Binary files a/images/AuthConfig.png and /dev/null differ diff --git a/images/Config.png b/images/Config.png deleted file mode 100644 index 8bbe28a..0000000 Binary files a/images/Config.png and /dev/null differ diff --git a/images/Home.png b/images/Home.png deleted file mode 100644 index c77c427..0000000 Binary files a/images/Home.png and /dev/null differ diff --git a/images/Landing.png b/images/Landing.png deleted file mode 100644 index 11dd2af..0000000 Binary files a/images/Landing.png and /dev/null differ diff --git a/images/NewSecret.png b/images/NewSecret.png deleted file mode 100644 index 7f0afab..0000000 Binary files a/images/NewSecret.png and /dev/null differ diff --git a/images/NewToken.png b/images/NewToken.png deleted file mode 100644 index 1be5688..0000000 Binary files a/images/NewToken.png and /dev/null differ diff --git a/images/ResponseWrapping.png b/images/ResponseWrapping.png deleted file mode 100644 index cf475b7..0000000 Binary files a/images/ResponseWrapping.png and /dev/null differ diff --git a/images/RootKey.png b/images/RootKey.png deleted file mode 100644 index 0a1364a..0000000 Binary files a/images/RootKey.png and /dev/null differ diff --git a/images/Secrets.png b/images/Secrets.png deleted file mode 100644 index e8da2c8..0000000 Binary files a/images/Secrets.png and /dev/null differ diff --git a/images/TokenManagement.png b/images/TokenManagement.png deleted file mode 100644 index 03c8f2a..0000000 Binary files a/images/TokenManagement.png and /dev/null differ diff --git a/index.desktop.html b/index.desktop.html new file mode 100644 index 0000000..c735696 --- /dev/null +++ b/index.desktop.html @@ -0,0 +1,20 @@ + + + + + Vault-UI + + + + + + +
+ + + diff --git a/index.html b/index.html deleted file mode 100644 index 667b748..0000000 --- a/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Vault-UI - - - - - - - - -
- - - - - - - diff --git a/index.web.html b/index.web.html new file mode 100644 index 0000000..76e0295 --- /dev/null +++ b/index.web.html @@ -0,0 +1,20 @@ + + + + + Vault-UI + + + + + + +
+ + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..d249f91 --- /dev/null +++ b/main.js @@ -0,0 +1,152 @@ +const { app, protocol, BrowserWindow, Menu, dialog } = require('electron') + +const path = require('path') +const url = require('url') + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow +let initialPath; + +// Handle commandline +if (process.argv && process.argv.length > 1) { + setInitialPath(process.argv[1]); +} + +function setInitialPath(urlloc) { + + let l = url.parse(urlloc, true); + if (l && l.protocol == 'vaultui:' && l.hash) { + + // Windows Protocol Handler bug workaround + initialPath = l.hash.replace('~', '?'); + } + if (mainWindow) { + mainWindow.loadURL(url.format({ + pathname: path.join(__dirname, 'index.desktop.html'), + protocol: 'file:', + hash: initialPath, + slashes: true + })) + } +} + +const shouldQuit = app.makeSingleInstance((commandLine) => { + // Someone tried to run a second instance, we should focus our window. + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + } + + if (commandLine && commandLine.length > 1) { + setInitialPath(commandLine[1]); + } +}) + +if (shouldQuit) { + app.quit() +} + +app.setAsDefaultProtocolClient('vaultui') +app.on('open-url', function (event, openurl) { + setInitialPath(openurl); +}) + +function createWindow() { + // Create the browser window. + mainWindow = new BrowserWindow({ width: 1024, height: 768 }) + + // and load the index.html of the app. + mainWindow.loadURL(url.format({ + pathname: path.join(__dirname, 'index.desktop.html'), + protocol: 'file:', + hash: initialPath, + slashes: true + })) + + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) + + // Create the Application's main menu + var template = [{ + label: "Application", + submenu: [ + { label: "About Application", selector: "orderFrontStandardAboutPanel:" }, + { type: "separator" }, + { label: "Quit", accelerator: "Command+Q", click: function () { app.quit(); } } + ] + }, { + label: "Edit", + submenu: [ + { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" }, + { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" }, + { type: "separator" }, + { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, + { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, + { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, + { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" } + ] + }, { + label: "Tools", + submenu: [ + { label: "Open Development Tools", click: function () { mainWindow.webContents.openDevTools(); } } + ] + } + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + + protocol.registerFileProtocol('vaultui', (request, callback) => { + const url = request.url.substr(7) + callback({ path: path.normalize(`${__dirname}/${url}`) }) + }, (error) => { + if (error) console.error('Failed to register protocol') + }) + +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow() + } +}) + +app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { + var result = dialog.showMessageBox({ + type: 'warning', + title: 'Server TLS Certificate Error', + message: 'The validity of the TLS connection with the remote server cannot be verified. Would you like to proceed anyway?', + detail: error, + buttons: ['Yes', 'No'] + }); + if (result == 0) { + event.preventDefault() + callback(true) + } else { + callback(false) + } +}) diff --git a/admin.hcl b/misc/admin.hcl similarity index 100% rename from admin.hcl rename to misc/admin.hcl diff --git a/misc/devserver.crt b/misc/devserver.crt new file mode 100644 index 0000000..dd4033b --- /dev/null +++ b/misc/devserver.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICyDCCAbACCQDp7ZXbcZ7BujANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDExtW +YXVsdC1VSSBEZXZlbG9wbWVudCBTZXJ2ZXIwHhcNMTcwNDAzMDUxMDIzWhcNMjcw +NDAxMDUxMDIzWjAmMSQwIgYDVQQDExtWYXVsdC1VSSBEZXZlbG9wbWVudCBTZXJ2 +ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2kja9oBgcWKmrgqce +FHz2Ta3iMrJClBh2laC/zKqOwljj3YZHf2RlA8d0zJPm77E/HHcKCVeqRMjgO9+P +7ephq3Oz0KpcLnaCImJavosqfI8Jj4pFeoTt1zjE/idO5SKSw+/YblSqKEzigXNE +NxZ9d8aFZ3J+A6QySW30gdANOB3A0I8lJy2A2em6xXAOMP8gSfIfSOGL5voZ+FpY +At8G7A/wNztBR2bnoTPSq18KWCxZW0M412HhJruzQ57j/joPRDDmBQrE3JhOUNuI +pg1nmbNg3jYP/EsTCwYEHklxoyVtUPm3eQIHYQXNEg0r3TAEt+o/z7prI9SZnTwP +kefZAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEhWjO7W/ysoSClXt7IFCdAzWZBH +8hNH/yFVvKW1DbVJEnQj5u6WqXPAKWd9j+xrU3D1d38i4lz7MUsIm+HMuLgIYONa +aVvFruHsFb9FJhyinIiF2IaKUncaAHZ9r3xI4pGRWRPNrgNIkAct6kKZXxp/2qsQ +oBNOI5rtJA73/bzxH7kevXOrryI3M8/NM+VlddgKphm+K6HS+vSd+hD4oyupi/Q6 +FNhun8igTpErzf7ZW7ZSPIJ6CU15+pX1gUNYOJIzcS763YmcRMXzjioqjIkyYfPb +Z9tra/9W/mu7NeBWQ9G/m2vyqX00T+LpGD8gwsgJgtNeZbmtarsi+lJbx14= +-----END CERTIFICATE----- diff --git a/misc/devserver.key b/misc/devserver.key new file mode 100644 index 0000000..474e627 --- /dev/null +++ b/misc/devserver.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAtpI2vaAYHFipq4KnHhR89k2t4jKyQpQYdpWgv8yqjsJY492G +R39kZQPHdMyT5u+xPxx3CglXqkTI4Dvfj+3qYatzs9CqXC52giJiWr6LKnyPCY+K +RXqE7dc4xP4nTuUiksPv2G5UqihM4oFzRDcWfXfGhWdyfgOkMklt9IHQDTgdwNCP +JSctgNnpusVwDjD/IEnyH0jhi+b6GfhaWALfBuwP8Dc7QUdm56Ez0qtfClgsWVtD +ONdh4Sa7s0Oe4/46D0Qw5gUKxNyYTlDbiKYNZ5mzYN42D/xLEwsGBB5JcaMlbVD5 +t3kCB2EFzRINK90wBLfqP8+6ayPUmZ08D5Hn2QIDAQABAoIBACdm5umF4646dGPP +jsGvKkj9+skWp+I2lBEDue2q/iRRTV3gMVq8463pYuKSRFlS4a39NrOz0Heu4KuE +QHuPnUX2+sGUBzBd1rW/NfrfpKlGuJgXon/cMVQjXt0k/NbKHOwP3XOYXC1dBTrd +NUNDoFbzwqSH7u3DW2x+7HwYiA5R8Lr2pmjzZCK3btEYXaLmY+Ee2WvsIwiJEcas +ixKBEG8z3vbtKmsR1B4QV5KKvM4Pz/lHT0Ue/YxY3U2oBJ1JznDD/L7nDrtNpr+8 +Fh/WAivqmFJYLtIMYMV1TrnT4NRaqOxtFuEwuypAYEF8aQsYEDJ9P10yL6BBofFM +1FQmpQECgYEA37PwIamLhlRL+cmIWtaKxBIO7bUOq0PVP5Wzj0e6izqg5Q8DujvX +9rRQCeVxX2F75Jg1OfZjWL683zo2cKT+VE7l4Bm2pTRgmb2XYdOXAJN8URZdpj3K +e2rQfX4aZ41gBY3lqbRx5p/9IPSkUHyFhSUolhv2oh2L9S8pDzpNLKsCgYEA0O4L +13skGpxq3mmURfRkFLGC716LBv5LK8CI0N9pSahOYt2Bq3E7U4hgaQF0zD4P3QUS +MGtRPHoLR4EQUzfJ0ihJzLEEA6OptoEJAePA0uAL7oJibE46KIdlBxdX9gOer2Hq +rGxNQSRJD6vkR426yx3eWpyUhNsua9CzyJ3B9YsCgYB005IK4nJ9WrS65KcTWYvq +zcuCFNZuVuSdal716u3fHGU+etLlha9Jpe1O3caRm2WKgnr5pFVJ2YLlyY740RIJ +kZK3sHYUXQA+Cidu7YOkx2FbL6UE1qxSO/xaLWs4vTpybCKOuC/r043skhbl+cH5 +QOirTDtHesrG5zQ4QahgNQKBgGqxFTT9uksok15utfwfODhlCcMpGYABvetiz7sy +S3cEzrqn+P7OvQgEPY+B4d4m1zz7yPUW6I4kmLv0CZ0lgRej4UP5JV6iZhk/vZTM +dHx7UzyCMrayH/rwYUQExLNp19AiBY/1YmIgoHqzQcjUdI4i+5h0G1fZAdSm6BhL +j2/PAoGAM7YLrWNdpyViWjDLeOBV0zj2p635i/WYZPOLhvyjR1AjTju+13PJZs1o +n0JleS6eD3tw9Rz+9lK84KJ+YXEfZ1nGoCxKTKYGSUUHovZjE6uUTxpxzMvNUH8G +HUMwI0s/aW9e3FbwmV3WyfdYyIjDsRQT0BLaujBsClLmTsGPf2E= +-----END RSA PRIVATE KEY----- diff --git a/package.json b/package.json index cf71a95..9f960ec 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,31 @@ { - "name": "vault_ui", - "version": "1.0.0", - "description": "", - "main": "server.js", + "name": "Vault-UI", + "version": "2.1.0", + "description": "Graphical interface for Hashicorp Vault", + "main": "main.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon ./server.js --exec babel-node --presets es2015,stage-2", - "build": "webpack --config=webpack.min.config.js", - "serve": "nodemon ./server.js" + "lint": "eslint .", + "dist": "build", + "serve": "nodemon ./server.js", + "dev-pack": "webpack --config webpack/webpack.dev.config.js -w", + "build-desktop": "cross-env NODE_ENV=production webpack --config webpack/webpack.desktop.config.js --profile", + "build-web": "cross-env NODE_ENV=production webpack --config webpack/webpack.web.config.js --profile", + "desktop": "npm run build-desktop && electron .", + "package-mac": "npm run build-desktop && build --mac=zip --ia32 --x64 --publish never", + "package-win32": "npm run build-desktop && build --win=nsis --ia32 --x64 --publish never", + "package-linux": "npm run build-desktop && build --linux=appimage --ia32 --x64 --publish never", + "publish": "npm run build-desktop && build --mac=zip --win=nsis --linux=appimage --ia32 --x64 --publish always", + "cleanup": "mop -v" }, "repository": { "type": "git", "url": "git+https://github.com/djenriquez/vault-ui.git" }, "keywords": [], - "author": "", + "author": { + "name": "Vault UI Contributors", + "email": "no-reply@vault-ui.djenriquez.github.com" + }, "license": "ISC", "bugs": { "url": "https://github.com/djenriquez/vault-ui/issues" @@ -28,44 +39,96 @@ }, "homepage": "https://github.com/djenriquez/vault-ui#readme", "devDependencies": { - "babel-cli": "^6.18.0", + "autoprefixer": "^6.5.3", "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", "babel-preset-stage-2": "^6.18.0", + "copy-to-clipboard": "^3.0.5", + "cross-env": "^3.1.4", "css-loader": "^0.25.0", + "electron": "^1.4.1", + "electron-builder": "^13.3.2", "eslint": "^3.14.0", - "eslint-plugin-react": "^6.9.0", + "eslint-plugin-react": "^6.10.3", "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.9.0", - "json-loader": "^0.5.4", - "material-ui": "^0.16.4", - "nodemon": "^1.11.0", - "postcss-loader": "^1.1.0", - "style-loader": "^0.13.1", - "url-loader": "^0.5.7", - "webpack": "^1.13.3", - "webpack-dev-server": "^1.16.2" - }, - "dependencies": { - "autoprefixer": "^6.5.3", - "axios": "^0.15.2", - "body-parser": "^1.15.2", - "compression": "^1.6.2", - "copy-to-clipboard": "^3.0.5", - "express": "^4.14.0", - "hbs": "^4.0.1", + "flexboxgrid": "^6.3.1", "hcl-to-json": "0.0.4", "immutability-helper": "^2.1.2", + "json-loader": "^0.5.4", "jsoneditor": "^5.5.11", "lodash": "^4.16.6", "material-ui": "^0.16.7", + "mui-icons": "^1.2.1", + "postcss-loader": "^1.1.0", "react": "^15.4.0", "react-dom": "^15.4.0", "react-router": "^3.0.0", "react-tap-event-plugin": "^2.0.0", - "react-ultimate-pagination-material-ui": "^0.5.0" + "react-ultimate-pagination-material-ui": "^0.5.0", + "url-loader": "^0.5.7", + "webpack": "^1.13.3" + }, + "dependencies": { + "axios": "^0.15.2", + "body-parser": "^1.15.2", + "compression": "^1.6.2", + "express": "^4.14.0", + "nodemon": "^1.11.0" + }, + "build": { + "productName": "Vault-UI", + "appId": "com.github.djenriquez.vault-ui", + "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", + "mac": { + "category": "public.app-category.tools" + }, + "protocols": [ + { + "name": "Vault-UI Desktop URL Protocol Handler", + "schemes": [ + "vaultui" + ] + } + ], + "dmg": { + "icon": "build/icon.icns", + "contents": [ + { + "x": 410, + "y": 150, + "type": "link", + "path": "/Applications" + }, + { + "x": 130, + "y": 150, + "type": "file" + } + ] + }, + "files": [ + "dist/", + "index.desktop.html", + "main.js", + "package.json" + ], + "win": { + "icon": "build/icon.ico", + "target": "nsis" + }, + "linux": { + "target": [ + "deb", + "AppImage" + ] + }, + "directories": { + "buildResources": "build", + "output": "release" + } } } diff --git a/run-docker-compose-dev b/run-docker-compose-dev index daa6ebe..461bf88 100755 --- a/run-docker-compose-dev +++ b/run-docker-compose-dev @@ -1,4 +1,6 @@ #!/bin/sh +echo "------------- npm install -------------" +npm install echo "------------- docker-compose up -d -------------" docker-compose up -d @@ -21,7 +23,7 @@ exec_in_vault vault auth-enable -path=userpass2 userpass exec_in_vault vault auth-enable github exec_in_vault vault auth-enable radius exec_in_vault vault auth-enable -path=awsaccount1 aws-ec2 -exec_in_vault vault policy-write admin /app/admin.hcl +exec_in_vault vault policy-write admin /misc/admin.hcl exec_in_vault vault write auth/userpass/users/test password=test policies=admin exec_in_vault vault write auth/userpass2/users/john password=doe policies=admin exec_in_vault vault write auth/userpass/users/lame password=lame policies=default diff --git a/server.js b/server.js index 335db00..3886151 100644 --- a/server.js +++ b/server.js @@ -3,21 +3,15 @@ var express = require('express'); var bodyParser = require('body-parser'); var path = require('path'); -var axios = require('axios'); -var _ = require('lodash'); var routeHandler = require('./src/routeHandler'); var compression = require('compression'); var PORT = 8000; -var VAULT_URL_DEFAULT = process.env.VAULT_URL_DEFAULT || ""; -var VAULT_AUTH_DEFAULT = process.env.VAULT_AUTH_DEFAULT || "GITHUB"; -var VAULT_SUPPLIED_TOKEN_HEADER = process.env.VAULT_SUPPLIED_TOKEN_HEADER -var VAULT_AUTH_BACKEND_PATH = process.env.VAULT_AUTH_BACKEND_PATH var app = express(); app.set('view engine', 'html'); -app.engine('html', require('hbs').__express); -app.use('/assets', compression(), express.static('dist')); +// app.engine('html', require('hbs').__express); +app.use('/dist', compression(), express.static('dist')); // parse application/x-www-form-urlencoded app.use(bodyParser.urlencoded({ extended: false })); @@ -35,25 +29,16 @@ app.listen(PORT, function () { console.log('Vault UI listening on: ' + PORT); }); -app.post('/wrap', function(req,res) { - routeHandler.wrapValue(req, res); +app.get('/vaultui', function(req,res) { + routeHandler.vaultuiHello(req, res); }); -app.post('/unwrap', function(req, res) { - routeHandler.unwrapValue(req, res); -}) - -app.all('/v1/*', function(req, res, next) { +app.all('/v1/*', function(req, res) { routeHandler.vaultapi(req, res); }) app.get('/'); app.get('*', function (req, res) { - res.render(path.join(__dirname, '/index.html'),{ - defaultUrl: VAULT_URL_DEFAULT, - defaultAuth: VAULT_AUTH_DEFAULT, - suppliedAuthToken: VAULT_SUPPLIED_TOKEN_HEADER ? req.header(VAULT_SUPPLIED_TOKEN_HEADER) : "", - defaultBackendPath: VAULT_AUTH_BACKEND_PATH - }); + res.sendFile(path.join(__dirname, '/index.web.html')); }); diff --git a/src/routeHandler.js b/src/routeHandler.js index 6eabc35..2357e52 100644 --- a/src/routeHandler.js +++ b/src/routeHandler.js @@ -1,9 +1,11 @@ 'use strict'; var vaultapi = require('./vaultapi'); +var vaultui = require('./vaultui'); module.exports = (function () { return { - vaultapi: vaultapi.callMethod + vaultapi: vaultapi.callMethod, + vaultuiHello: vaultui.vaultuiHello }; })(); diff --git a/src/vaultapi.js b/src/vaultapi.js index e4337ef..49ebc43 100644 --- a/src/vaultapi.js +++ b/src/vaultapi.js @@ -24,6 +24,10 @@ exports.callMethod = function (req, res) { res.json(resp.data); }) .catch(function (err) { - res.status(err.response.status).send(err.response.data); + if(err.response) { + res.status(err.response.status).send(err.response.data); + } else { + res.status(500).send({errors: [err.toString()]}); + } }); -}; \ No newline at end of file +}; diff --git a/src/vaultui.js b/src/vaultui.js new file mode 100644 index 0000000..974f0b0 --- /dev/null +++ b/src/vaultui.js @@ -0,0 +1,23 @@ +'use strict'; + +var VAULT_URL_DEFAULT = process.env.VAULT_URL_DEFAULT || ""; +var VAULT_URL_DEFAULT_FORCE = process.env.VAULT_URL_DEFAULT_FORCE ? true : false; +var VAULT_AUTH_DEFAULT = process.env.VAULT_AUTH_DEFAULT || "GITHUB"; +var VAULT_AUTH_DEFAULT_FORCE = process.env.VAULT_AUTH_DEFAULT_FORCE ? true : false; +var VAULT_AUTH_BACKEND_PATH = process.env.VAULT_AUTH_BACKEND_PATH +var VAULT_AUTH_BACKEND_PATH_FORCE = process.env.VAULT_AUTH_BACKEND_PATH_FORCE ? true : false; +var VAULT_SUPPLIED_TOKEN_HEADER = process.env.VAULT_SUPPLIED_TOKEN_HEADER + +exports.vaultuiHello = function (req, res) { + let response = { + defaultVaultUrl: VAULT_URL_DEFAULT, + defaultVaultUrlForce: VAULT_URL_DEFAULT_FORCE, + defaultAuthMethod: VAULT_AUTH_DEFAULT, + defaultAuthMethodForce: VAULT_AUTH_DEFAULT_FORCE, + suppliedAuthToken: VAULT_SUPPLIED_TOKEN_HEADER, + defaultBackendPath: VAULT_AUTH_BACKEND_PATH, + defaultBackendPathForce: VAULT_AUTH_BACKEND_PATH_FORCE + } + + res.status(200).send(response); +}; \ No newline at end of file diff --git a/webpack.config.js b/webpack/webpack.config.js similarity index 80% rename from webpack.config.js rename to webpack/webpack.config.js index df67513..b497276 100644 --- a/webpack.config.js +++ b/webpack/webpack.config.js @@ -11,7 +11,7 @@ module.exports = { }, output: { path: './dist', - publicPath: '/assets/', + publicPath: 'dist/', filename: 'bundle.js' }, devtool: 'eval', @@ -25,19 +25,22 @@ module.exports = { loader: 'json-loader', exclude: 'node_modules' }, { - test: /\.svg$/, - loader: 'url' + test: /\.(svg|png)$/, + loader: 'url-loader' }, { test: /\.css$/, loader: ExtractTextPlugin.extract('css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader'), exclude: /node_modules/ + }, { + test: /\.ico$/, + loader: 'file-loader?name=[name].[ext]' // <-- retain original file name }, { test: /\.css$/, loader: ExtractTextPlugin.extract('css!postcss-loader'), include: /node_modules/ }] }, - postcss: [ autoprefixer({ browsers: ['last 2 versions'] }) ], + postcss: [autoprefixer({ browsers: ['last 2 versions'] })], plugins: [ new ExtractTextPlugin("styles.css"), new webpack.IgnorePlugin(/regenerator|nodent|js-beautify/, /ajv/) diff --git a/webpack/webpack.desktop.config.js b/webpack/webpack.desktop.config.js new file mode 100644 index 0000000..d3d5ac2 --- /dev/null +++ b/webpack/webpack.desktop.config.js @@ -0,0 +1,8 @@ +var webpack = require('webpack'); +var config = require('./webpack.min.config'); + +config.target = "electron"; +config.output.filename = "desktopbundle.js"; +config.plugins.push(new webpack.DefinePlugin({WEBPACK_DEF_TARGET_WEB: false})); + +module.exports = config; diff --git a/webpack/webpack.dev.config.js b/webpack/webpack.dev.config.js new file mode 100644 index 0000000..c02793b --- /dev/null +++ b/webpack/webpack.dev.config.js @@ -0,0 +1,7 @@ +var webpack = require('webpack'); +var config = require('./webpack.config'); + +config.target = "web"; +config.plugins.push(new webpack.DefinePlugin({WEBPACK_DEF_TARGET_WEB: true})); + +module.exports = config; diff --git a/webpack.min.config.js b/webpack/webpack.min.config.js similarity index 100% rename from webpack.min.config.js rename to webpack/webpack.min.config.js diff --git a/webpack/webpack.web.config.js b/webpack/webpack.web.config.js new file mode 100644 index 0000000..4bc9117 --- /dev/null +++ b/webpack/webpack.web.config.js @@ -0,0 +1,7 @@ +var webpack = require('webpack'); +var config = require('./webpack.min.config'); + +config.target = "web"; +config.plugins.push(new webpack.DefinePlugin({WEBPACK_DEF_TARGET_WEB: true})); + +module.exports = config;