├── .babelrc ├── .dockerignore ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── App.jsx ├── assets │ ├── favicon.ico │ └── vault-ui-logo.svg └── components │ ├── App │ ├── App.jsx │ └── app.css │ ├── Authentication │ ├── AppRole │ │ ├── AppRole.jsx │ │ └── approle.css │ ├── Aws │ │ ├── Aws.jsx │ │ └── aws.css │ ├── AwsEc2 │ │ ├── AwsEc2.jsx │ │ └── awsec2.css │ ├── Github │ │ ├── Github.jsx │ │ └── github.css │ ├── Kubernetes │ │ ├── Kubernetes.jsx │ │ └── kubernetes.css │ ├── Okta │ │ ├── Okta.jsx │ │ └── okta.css │ ├── Radius │ │ ├── Radius.jsx │ │ └── radius.css │ ├── Token │ │ ├── Token.jsx │ │ └── token.css │ └── UserPass │ │ ├── UserPass.jsx │ │ └── userpass.css │ ├── Login │ ├── Login.jsx │ └── login.css │ ├── Policies │ ├── Manage.jsx │ ├── policies.css │ └── vault-policy-schema.json │ ├── ResponseWrapper │ ├── ResponseWrapper.jsx │ └── responseWrapper.css │ ├── Secrets │ └── Generic │ │ ├── Generic.jsx │ │ └── generic.css │ ├── Settings │ ├── Settings.jsx │ └── settings.css │ └── shared │ ├── DeleteObject │ └── DeleteObject.jsx │ ├── Header │ ├── Header.jsx │ ├── countdown.js │ └── header.css │ ├── ItemList │ └── ItemList.jsx │ ├── ItemPicker │ ├── ItemPicker.jsx │ └── itempicker.css │ ├── JsonEditor.jsx │ ├── Menu │ ├── Menu.jsx │ └── menu.css │ ├── MountUtils │ ├── MountTuneDelete.jsx │ └── NewMount.jsx │ ├── VaultUtils.jsx │ ├── Wrapping │ ├── Unwrapper.jsx │ ├── Wrapper.jsx │ └── wrapping.css │ └── styles.css ├── bin └── entrypoint.sh ├── build ├── icon.icns └── icon.ico ├── docker-compose.yaml ├── index.desktop.html ├── index.web.html ├── kubernetes └── chart │ └── vault-ui │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ └── service.yaml │ └── values.yaml ├── main.js ├── misc ├── admin.hcl ├── devserver.crt └── devserver.key ├── package.json ├── postcss.config.js ├── run-docker-compose-dev ├── server.js ├── shippable.yml ├── src ├── routeHandler.js ├── vaultapi.js └── vaultui.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-2", 6 | "stage-0" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules/ 3 | release/ 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "jsx": true 12 | }, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "no-const-assign": "warn", 17 | "no-this-before-super": "warn", 18 | "no-undef": "warn", 19 | "no-unreachable": "warn", 20 | "no-unused-vars": "warn", 21 | "constructor-super": "warn", 22 | "valid-typeof": "warn", 23 | "no-console": 0 24 | }, 25 | "plugins": [ 26 | "react" 27 | ], 28 | "extends": ["eslint:recommended", "plugin:react/recommended"] 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | release/ 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # build folder 41 | dist/* 42 | 43 | # Visual studio code 44 | .vscode/ 45 | 46 | # OSX 47 | .DS_Store 48 | 49 | # Intellij 50 | .idea/* 51 | *.iml 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.4.0 (unreleased) 2 | # Features 3 | - Add KV compatibility #198 4 | - Upgrade dependencies #200 5 | 6 | # Bug fixes 7 | - Fix desktop client icons #199 - #157 8 | - Fix issue with favicon #188 9 | 10 | # 2.3.0 11 | ## Features: 12 | - Refactor PolicyPicker to ItemPicker for general use - #175 13 | - Refactor item listing to use a centralized class /w filtering, pagination and sorting by default - #175 + #187 14 | - Add backend mount description field - #105 15 | - Allow self-signed CA certifications - #140 16 | - Support Okta Authentication Backend - #156 17 | 18 | # 2.2.0 19 | ## Features 20 | - Add filtering to all lists - https://github.com/djenriquez/vault-ui/pull/106 21 | - Add pagination to secrets list - https://github.com/djenriquez/vault-ui/pull/110 22 | - Add JSON diff view to compare updates - https://github.com/djenriquez/vault-ui/pull/84 23 | - Add ability to renew token - https://github.com/djenriquez/vault-ui/pull/114 24 | - Add AWS auth backend with IAM - https://github.com/djenriquez/vault-ui/pull/126 25 | 26 | ## Enhancements 27 | - Optimize Docker image size + caching - https://github.com/djenriquez/vault-ui/pull/122 28 | 29 | ## Bug fixes 30 | - Fix issue with secrets loading causing UI to be unresponsive - https://github.com/djenriquez/vault-ui/pull/110 31 | - Fix docker build electron dependency - https://github.com/djenriquez/vault-ui/pull/112 32 | - Fix issue with sorting/pagination of secrets not affecting the entire secret namespace - https://github.com/djenriquez/vault-ui/pull/127 & https://github.com/djenriquez/vault-ui/pull/134 33 | - Fix issue with trailing slashes being sent to Vault requests - https://github.com/djenriquez/vault-ui/pull/104 34 | 35 | # 2.1.0 36 | ## Features 37 | - Support AWS EC2 authentication backend - https://github.com/djenriquez/vault-ui/pull/76 38 | - Support User/Pass authentication backend - https://github.com/djenriquez/vault-ui/pull/94 39 | - Improve GitHub authentication backend - https://github.com/djenriquez/vault-ui/pull/78 40 | - Provide ability to sort secrets - https://github.com/djenriquez/vault-ui/pull/82 41 | - Improvements to backend code + upgrade React components - https://github.com/djenriquez/vault-ui/pull/93 42 | - Support use with new Desktop App - https://github.com/djenriquez/vault-ui/pull/85 43 | 44 | ## Bug Fixes 45 | - Fix 307 redirects 46 | 47 | # 2.0.1 48 | - Fix reference to Vault icon 49 | 50 | # 2.0.0 51 | - Improved the UI as a whole - https://github.com/djenriquez/vault-ui/pull/47 52 | - Added a dynamic navigation menu bar 53 | - Added ability to renew token before expiration 54 | - Improved wrapping experience - https://github.com/djenriquez/vault-ui/pull/47/commits/ac71bb60830fa79cc298176e50fa9fcbbb2569b3 55 | - Added ability to wrap secrets - https://github.com/djenriquez/vault-ui/pull/47/commits/8f6fde521a7fe39f439e800a484aa7435f2bd4c5 56 | - Consolidated error reporting - https://github.com/djenriquez/vault-ui/pull/47/commits/de85678d2126ec71ce0908eb3eb09172cb2f11e0 57 | - Support Radius auth backend - https://github.com/djenriquez/vault-ui/pull/59 58 | - Reduced the footprint of the Docker image - https://github.com/djenriquez/vault-ui/pull/53 59 | - Added ability to manage secret and authentication backends - https://github.com/djenriquez/vault-ui/pull/62 60 | - Support custom login mountpoint paths - https://github.com/djenriquez/vault-ui/pull/60 61 | - Support mount management for generic, github and radius- https://github.com/djenriquez/vault-ui/pull/62 62 | 63 | # 1.0.1 64 | - Fixed slight token management issues - https://github.com/djenriquez/vault-ui/pull/48 65 | 66 | # 1.0.0 67 | - React best-practices inspired refactor - https://github.com/djenriquez/vault-ui/pull/32 68 | - Update backend API to match Vault API by using the express server as a passthrough - https://github.com/djenriquez/vault-ui/pull/46 69 | - Added ability to set Default URL + Auth with environment variables - https://github.com/djenriquez/vault-ui/pull/36 70 | - Added `VAULT_SUPPLIED_TOKEN_HEADER` header to enable SSO functionality - [Feature request #39](https://github.com/djenriquez/vault-ui/issues/39) + #40 71 | - Support multiple generic backends - https://github.com/djenriquez/vault-ui/pull/31 72 | - Fixed bug with auto-logout where large values caused logout to happen immediately - https://github.com/djenriquez/vault-ui/pull/35 73 | - Improved value editting with [josdejong/jsoneditor](josdejong/jsoneditor) - https://github.com/djenriquez/vault-ui/pull/38 (Note: HCL is no longer supported as a format for managing secrets or policies) 74 | - Add `Token management` - [Feature request #41](https://github.com/djenriquez/vault-ui/issues/41) 75 | 76 | # 0.1.0 77 | - Supports [Github, Username and Password, Token](https://github.com/djenriquez/vault-ui/pull/3), and [LDAP](https://github.com/djenriquez/vault-ui/pull/16) authentication 78 | - Full [generic secrets management](https://github.com/djenriquez/vault-ui/pull/2) 79 | - Full [policies management](https://github.com/djenriquez/vault-ui/pull/4) 80 | - Full [response wrapping support](https://github.com/djenriquez/vault-ui/pull/18) 81 | - Full [GitHub auth backend team/org policy management](https://github.com/djenriquez/vault-ui/pull/13) 82 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | LABEL maintainer="Vault-UI Contributors" 4 | 5 | WORKDIR /app 6 | COPY . . 7 | 8 | RUN yarn install --pure-lockfile --silent && \ 9 | yarn run build-web && \ 10 | yarn install --silent --production && \ 11 | yarn check --verify-tree --production && \ 12 | yarn global add nodemon && \ 13 | yarn cache clean && \ 14 | rm -f /root/.electron/* 15 | 16 | EXPOSE 8000 17 | 18 | ENTRYPOINT ["./bin/entrypoint.sh"] 19 | CMD ["start_app"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | All contributions by Vault-UI Admins: 4 | Copyright (c) 2016, DJ Enriquez, Matteo Sessa, Christopher Pauley, Robert Lippens, Alex Unwin 5 | All rights reserved. 6 | 7 | All other contributions: 8 | Copyright (c) 2016, Vault-UI contributors 9 | All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions are met: 13 | 14 | * Redistributions of source code must retain the above copyright notice, this 15 | list of conditions and the following disclaimer. 16 | 17 | * Redistributions in binary form must reproduce the above copyright notice, 18 | this list of conditions and the following disclaimer in the documentation 19 | and/or other materials provided with the distribution. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Vault-UI Logo 4 | 5 | 6 | [![Run Status](https://api.shippable.com/projects/581e7826fbc68c0f00deb0ca/badge?branch=master)](https://app.shippable.com/projects/581e7826fbc68c0f00deb0ca) 7 | [![](https://images.microbadger.com/badges/image/djenriquez/vault-ui.svg)](https://microbadger.com/images/djenriquez/vault-ui) 8 | [![Join the chat at https://gitter.im/vault-ui/Lobby](https://badges.gitter.im/vault-ui/Lobby.svg)](https://gitter.im/vault-ui/Lobby) 9 | 10 | # Vault-UI 11 | 12 | A beautiful way to manage your Hashicorp Vault 13 | 14 | ![](http://i.imgur.com/COBxk3m.gif) 15 | 16 | ## Features 17 | 18 | - Easy to deploy as Web App 19 | - Desktop version works on Mac, Linux and Windows 20 | - Material UI Design 21 | - Integrated JSON Editor 22 | - Written in React 23 | 24 | ## Installation 25 | 26 | ### Desktop Version 27 | 28 | Vault-UI Desktop is available for the following operating systems: 29 | - Windows 30 | - MacOS 31 | - Linux (32bit and 64bit AppImage) 32 | 33 | Download the latest version from the release page and install/run the software 34 | 35 | ### Web Version 36 | 37 | Vault-UI can be deployed as a shared web app for your organization 38 | 39 | Docker images are automatically built using an [automated build on Docker Hub](https://hub.docker.com/r/djenriquez/vault-ui/builds/). 40 | We encourage that versioned images are used for production. 41 | 42 | To run Vault-UI using the latest Docker image: 43 | ```bash 44 | docker run -d \ 45 | -p 8000:8000 \ 46 | --name vault-ui \ 47 | djenriquez/vault-ui 48 | ``` 49 | 50 | #### Advanced configuration options 51 | 52 | By default, connection and authentication parameters must be configured by clicking on the configuration cog on the login page. 53 | Using environment variables (via docker), an administrator can pre-configure those parameters. 54 | 55 | Example command to pre-configure the Vault server URL and authentication method 56 | ```bash 57 | docker run -d \ 58 | -p 8000:8000 \ 59 | -e VAULT_URL_DEFAULT=http://vault.server.org:8200 \ 60 | -e VAULT_AUTH_DEFAULT=GITHUB \ 61 | --name vault-ui \ 62 | djenriquez/vault-ui 63 | ``` 64 | 65 | Supported environment variables: 66 | - `PORT` Sets the port for Vault-UI to listen on. (Default 8000) 67 | - `CUSTOM_CA_CERT` Pass a self-signed certificate that the system should trust. 68 | - `NODE_TLS_REJECT_UNAUTHORIZED` Disable TLS server side validation. (ex. vault deployed with self-signed certificate). Set to `0` to disable. 69 | - `VAULT_URL_DEFAULT` Sets the default vault endpoint. Note: protocol part of the url is mandatory. Example: http://10.0.0.1:8200 70 | - `VAULT_AUTH_DEFAULT` Sets the default authentication method type. See below for supported authentication methods. 71 | - `VAULT_AUTH_BACKEND_PATH` Sets the default backend path. Useful when multiple backends of the same type are mounted on the vault file system. 72 | - `VAULT_SUPPLIED_TOKEN_HEADER` Instructs Vault-UI to attempt authentication using a token provided by the client in the specified HTTP request header. 73 | 74 | This defaults can be overridden if the user fills out the endpoint and auth method manually. 75 | 76 | 77 | Current supported login methods: 78 | - `GITHUB` : When using the [GitHub](https://www.vaultproject.io/docs/auth/github.html) backend 79 | - `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 80 | - `LDAP` : When using the [LDAP](https://www.vaultproject.io/docs/auth/ldap.html) backend 81 | - `TOKEN` : When using the [Tokens](https://www.vaultproject.io/docs/auth/token.html) backend 82 | 83 | Current supported management of backend auth methods: 84 | - [GitHub](https://www.vaultproject.io/docs/auth/github.html) 85 | - [RADIUS](https://www.vaultproject.io/docs/auth/radius.html) 86 | - [AWS-EC2](https://www.vaultproject.io/docs/auth/aws-ec2.html) 87 | - [Username & Password](https://www.vaultproject.io/docs/auth/userpass.html) 88 | - [Token](https://www.vaultproject.io/docs/auth/token.html) 89 | - [AppRole](https://www.vaultproject.io/docs/auth/approle.html) 90 | - [Kubernetes](https://www.vaultproject.io/docs/auth/kubernetes.html) 91 | 92 | 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. 93 | 94 | 95 | ## Usage 96 | 97 | ### Basic policy for Vault-UI users 98 | A user/token accessing Vault-UI requires a basic set of capabilities in order to correctly discover and display the various mounted backends. 99 | Please make sure your user is granted a policy with at least the following permissions: 100 | 101 | #### JSON 102 | ```json 103 | { 104 | "path": { 105 | "auth/token/lookup-self": { 106 | "capabilities": [ 107 | "read" 108 | ] 109 | }, 110 | "sys/capabilities-self": { 111 | "capabilities": [ 112 | "update" 113 | ] 114 | }, 115 | "sys/mounts": { 116 | "capabilities": [ 117 | "read" 118 | ] 119 | }, 120 | "sys/auth": { 121 | "capabilities": [ 122 | "read" 123 | ] 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | #### HCL 130 | ``` 131 | path "auth/token/lookup-self" { 132 | capabilities = [ "read" ] 133 | } 134 | 135 | path "sys/capabilities-self" { 136 | capabilities = [ "update" ] 137 | } 138 | 139 | path "sys/mounts" { 140 | capabilities = [ "read" ] 141 | } 142 | 143 | path "sys/auth" { 144 | capabilities = [ "read" ] 145 | } 146 | ``` 147 | 148 | ### Secrets 149 | 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. 150 | 151 | 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. 152 | 153 | #### Root key bias 154 | 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" }`. 155 | 156 | 157 | ### Policies 158 | 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. 159 | 160 | ### Token Management 161 | Users have the ability to create and revoke tokens, manage token roles and list accessors. The following permissions are needed at minimum for this feature: 162 | 163 | #### JSON: 164 | ```json 165 | { 166 | "path": { 167 | "auth/token/accessors": { 168 | "capabilities": [ 169 | "sudo", 170 | "list" 171 | ] 172 | }, 173 | "auth/token/lookup-accessor/*": { 174 | "capabilities": [ 175 | "read" 176 | ] 177 | } 178 | } 179 | } 180 | ``` 181 | #### HCL 182 | ```hcl 183 | path "auth/token/accessors" { 184 | capabilities = [ "sudo", "list" ] 185 | } 186 | 187 | path "auth/token/lookup-accessor/*" { 188 | capabilities = [ "read" ] 189 | } 190 | ``` 191 | 192 | ### Response Wrapping 193 | Vault-UI supports response-wrapping of secrets in _generic_ backends. Wrapping custom JSON data is also supported. 194 | 195 | 196 | ## Development 197 | Install the [yarn](https://yarnpkg.com/en/docs/install) package manager 198 | 199 | ### With Docker 200 | The command below will use [Docker Compose](https://docs.docker.com/compose/) 201 | to spin up a Vault dev server and a Vault UI server that you can log 202 | into with username "test" and password "test": 203 | ```sh 204 | ./run-docker-compose-dev 205 | ``` 206 | 207 | If major changes are made, be sure to run `docker-compose build` to rebuild dependencies. 208 | 209 | ### Without Docker 210 | The following will spin up a Vault UI server only. It will not set up 211 | Vault for you: 212 | ```sh 213 | yarn run dev-pack & 214 | yarn start 215 | ``` 216 | 217 | # Licensing 218 | Vault-UI is licensed under BSD 2-Clause. See [LICENSE](https://github.com/djenriquez/vault-ui/blob/master/LICENSE) for the full license text. 219 | 220 | # Donations 221 | Vault-UI maintainers are humbly accepting [donations](https://github.com/djenriquez/vault-ui/wiki/Donations) as a way of saying thank you! 222 | -------------------------------------------------------------------------------- /app/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios'; 3 | import ReactDOM from 'react-dom'; 4 | import Login from './components/Login/Login.jsx'; 5 | import { Router, Route } from 'react-router' 6 | import injectTapEventPlugin from 'react-tap-event-plugin'; 7 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 8 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 9 | import { history } from './components/shared/VaultUtils.jsx'; 10 | import App from './components/App/App.jsx'; 11 | import SecretsGeneric from './components/Secrets/Generic/Generic.jsx'; 12 | import PolicyManager from './components/Policies/Manage.jsx'; 13 | import Settings from './components/Settings/Settings.jsx'; 14 | import ResponseWrapper from './components/ResponseWrapper/ResponseWrapper.jsx'; 15 | import TokenAuthBackend from './components/Authentication/Token/Token.jsx'; 16 | import AwsEc2AuthBackend from './components/Authentication/AwsEc2/AwsEc2.jsx'; 17 | import AwsAuthBackend from './components/Authentication/Aws/Aws.jsx'; 18 | import KubernetesAuthBackend from './components/Authentication/Kubernetes/Kubernetes.jsx'; 19 | import GithubAuthBackend from './components/Authentication/Github/Github.jsx'; 20 | import RadiusAuthBackend from './components/Authentication/Radius/Radius.jsx'; 21 | import UserPassAuthBackend from './components/Authentication/UserPass/UserPass.jsx'; 22 | import SecretUnwrapper from './components/shared/Wrapping/Unwrapper'; 23 | import OktaAuthBackend from './components/Authentication/Okta/Okta.jsx'; 24 | import AppRoleAuthBackend from './components/Authentication/AppRole/AppRole.jsx' 25 | 26 | // Load here to signal webpack 27 | import 'flexboxgrid/dist/flexboxgrid.min.css'; 28 | import './assets/favicon.ico'; 29 | 30 | injectTapEventPlugin(); 31 | 32 | (function () { 33 | 34 | if (typeof window.CustomEvent === "function") return false; 35 | 36 | function CustomEvent(event, params) { 37 | params = params || { bubbles: false, cancelable: false, detail: undefined }; 38 | var evt = document.createEvent('CustomEvent'); 39 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); 40 | return evt; 41 | } 42 | 43 | CustomEvent.prototype = window.Event.prototype; 44 | 45 | window.CustomEvent = CustomEvent; 46 | })(); 47 | 48 | const checkVaultUiServer = (nextState, replace, callback) => { 49 | // If it's a web deployment, query the server for default connection parameters 50 | // Those can be set using environment variables in the nodejs process 51 | if (WEBPACK_DEF_TARGET_WEB) { 52 | axios.get('/vaultui').then((resp) => { 53 | window.defaultVaultUrl = resp.data.defaultVaultUrl; 54 | window.defaultAuthMethod = resp.data.defaultAuthMethod; 55 | window.defaultBackendPath = resp.data.defaultBackendPath; 56 | window.suppliedAuthToken = resp.data.suppliedAuthToken; 57 | callback(); 58 | }).catch(() => callback()) 59 | } else { 60 | callback(); 61 | } 62 | } 63 | 64 | const checkAccessToken = (nextState, replace, callback) => { 65 | let vaultAuthToken = window.localStorage.getItem('vaultAccessToken'); 66 | if (!vaultAuthToken) { 67 | replace(`/login?returnto=${encodeURI(nextState.location.pathname)}`) 68 | } 69 | 70 | callback(); 71 | } 72 | 73 | const muiTheme = getMuiTheme({ 74 | fontFamily: 'Source Sans Pro, sans-serif', 75 | }); 76 | 77 | ReactDOM.render(( 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | ), document.getElementById('app')) -------------------------------------------------------------------------------- /app/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djenriquez/vault-ui/36266eaaa82049bfc3059d522df26973cc49b72a/app/assets/favicon.ico -------------------------------------------------------------------------------- /app/assets/vault-ui-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 42 | 46 | 50 | 54 | 58 | 62 | 66 | 67 | 69 | Created by potrace 1.11, written by Peter Selinger 2001-2013 70 | 71 | 73 | image/svg+xml 74 | 76 | 77 | 78 | 79 | 80 | 85 | 88 | 89 | 95 | 101 | 107 | 113 | 119 | 125 | 131 | 137 | 143 | 149 | 155 | 161 | 167 | 173 | 179 | 185 | 191 | 197 | 202 | 208 | 215 | 222 | 229 | 236 | 237 | -------------------------------------------------------------------------------- /app/components/App/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import _ from 'lodash'; 4 | import { Tabs, Tab } from 'material-ui/Tabs'; 5 | import Menu from '../shared/Menu/Menu.jsx'; 6 | import Header from '../shared/Header/Header.jsx'; 7 | import Snackbar from 'material-ui/Snackbar'; 8 | import Dialog from 'material-ui/Dialog'; 9 | import FlatButton from 'material-ui/FlatButton'; 10 | import Paper from 'material-ui/Paper'; 11 | import Warning from 'material-ui/svg-icons/alert/warning'; 12 | import { green500, red500 } from 'material-ui/styles/colors.js' 13 | import styles from './app.css'; 14 | import JsonEditor from '../shared/JsonEditor.jsx'; 15 | import { Card, CardHeader, CardText } from 'material-ui/Card'; 16 | import { callVaultApi, tokenHasCapabilities, history } from '../shared/VaultUtils.jsx' 17 | 18 | let twoMinuteWarningTimeoutId; 19 | let logoutTimeoutId; 20 | 21 | function snackBarMessage(message) { 22 | let ev = new CustomEvent("snackbar", { detail: { message: message } }); 23 | document.dispatchEvent(ev); 24 | } 25 | 26 | export default class App extends React.Component { 27 | static propTypes = { 28 | location: PropTypes.object.isRequired, 29 | children: PropTypes.node 30 | } 31 | 32 | constructor(props) { 33 | super(props); 34 | 35 | this.state = { 36 | snackbarMessage: '', 37 | snackbarType: 'OK', 38 | snackbarStyle: {}, 39 | logoutOpen: false, 40 | logoutPromptSeen: false, 41 | identity: {}, 42 | tokenCanQueryCapabilities: true, 43 | tokenCanListSecretBackends: true, 44 | tokenCanListAuthBackends: true, 45 | } 46 | 47 | _.bindAll( 48 | this, 49 | 'reloadSessionIdentity', 50 | 'componentDidMount', 51 | 'componentWillUnmount', 52 | 'renderSessionExpDialog', 53 | 'renderWarningSecretBackends', 54 | 'renderWarningAuthBackends' 55 | ); 56 | } 57 | 58 | reloadSessionIdentity() { 59 | let TWO_MINUTES = 1000 * 60 * 2; 60 | 61 | let twoMinuteWarningTimeout = () => { 62 | if (!this.state.logoutPromptSeen) { 63 | this.setState({ 64 | logoutOpen: true 65 | }); 66 | } 67 | } 68 | 69 | let logoutTimeout = () => { 70 | history.push('/login'); 71 | } 72 | 73 | // Retrieve session identity information 74 | callVaultApi('get', 'auth/token/lookup-self') 75 | .then((resp) => { 76 | if (_.has(resp, 'data.data')) { 77 | this.setState({ identity: resp.data.data }) 78 | let ttl = resp.data.data.ttl * 1000; 79 | // The upper limit of setTimeout is 0x7FFFFFFF (or 2147483647 in decimal) 80 | if (ttl > 0 && ttl < 2147483648) { 81 | clearTimeout(logoutTimeoutId); 82 | clearTimeout(twoMinuteWarningTimeoutId); 83 | logoutTimeoutId = setTimeout(logoutTimeout, ttl); 84 | twoMinuteWarningTimeoutId = setTimeout(twoMinuteWarningTimeout, ttl - TWO_MINUTES); 85 | } 86 | } 87 | }) 88 | .catch((err) => { 89 | if (_.has(err, 'response.status') && err.response.status >= 400) { 90 | window.localStorage.removeItem('vaultAccessToken'); 91 | history.push(`/login?returnto=${encodeURI(this.props.location.pathname)}`); 92 | } else throw err; 93 | }); 94 | } 95 | 96 | componentDidMount() { 97 | if (!window.localStorage.getItem('showDeleteModal')) { 98 | window.localStorage.setItem('showDeleteModal', 'true'); 99 | } 100 | if (!window.localStorage.getItem('enableCapabilitiesCache')) { 101 | window.localStorage.setItem('enableCapabilitiesCache', 'true'); 102 | } 103 | if (!window.localStorage.getItem('enableDiffAnnotations')) { 104 | window.localStorage.setItem('enableDiffAnnotations', 'false'); 105 | } 106 | document.addEventListener("snackbar", (e) => { 107 | let messageStyle = { backgroundColor: green500 }; 108 | let message = e.detail.message.toString(); 109 | if (e.detail.message instanceof Error) { 110 | // Handle logical erros from vault 111 | if (_.has(e.detail.message, 'response.data.errors')) 112 | if (e.detail.message.response.data.errors.length > 0) 113 | message = e.detail.message.response.data.errors.join(','); 114 | messageStyle = { backgroundColor: red500 }; 115 | } 116 | 117 | this.setState({ 118 | snackbarMessage: message, 119 | snackbarType: e.detail.type || 'OK', 120 | snackbarStyle: messageStyle 121 | }); 122 | }); 123 | 124 | this.reloadSessionIdentity(); 125 | 126 | // Check access to the sys/capabilities-self path 127 | callVaultApi('post', 'sys/capabilities-self', null, { path: '/' }) 128 | .then(() => { 129 | this.setState({ tokenCanQueryCapabilities: true }); 130 | }) 131 | .catch(() => { 132 | this.setState({ tokenCanQueryCapabilities: false }); 133 | }) 134 | 135 | // Check capabilities to list backends 136 | tokenHasCapabilities(['read'], 'sys/mounts').catch(() => { 137 | this.setState({ tokenCanListSecretBackends: false }); 138 | }); 139 | tokenHasCapabilities(['read'], 'sys/auth').catch(() => { 140 | this.setState({ tokenCanListAuthBackends: false }); 141 | }); 142 | 143 | } 144 | 145 | componentWillUnmount() { 146 | clearTimeout(logoutTimeoutId); 147 | clearTimeout(twoMinuteWarningTimeoutId); 148 | } 149 | 150 | renderSessionExpDialog() { 151 | const actions = [ 152 | { 156 | callVaultApi('post', 'auth/token/renew-self') 157 | .then(() => { 158 | this.reloadSessionIdentity(); 159 | snackBarMessage("Session renewed"); 160 | }) 161 | .catch(snackBarMessage) 162 | this.setState({ logoutOpen: false }) 163 | }} 164 | />, 165 | this.setState({ logoutOpen: false, logoutPromptSeen: true })} /> 166 | ]; 167 | 168 | return ( 169 | this.setState({ logoutOpen: false, logoutPromptSeen: true })} 175 | > 176 |
Your session token will expire soon. Use the renew button to request a lease extension
177 |
178 | ); 179 | } 180 | 181 | renderWarningCapabilities() { 182 | return ( 183 | 184 | 185 | } 189 | actAsExpander={true} 190 | showExpandableButton={true} 191 | /> 192 | 193 | Your token has been assigned the following policies: 194 |
    195 | {_.map(this.state.identity.policies, (pol, idx) => { 196 | return (
  • {pol}
  • ) 197 | })} 198 |
199 | and none of them contains the following permissions: 200 | 201 |
202 |
203 |
204 | ) 205 | } 206 | 207 | renderWarningAuthBackends() { 208 | return ( 209 | 210 | 211 | } 215 | actAsExpander={true} 216 | showExpandableButton={true} 217 | /> 218 | 219 | Your token has been assigned the following policies: 220 |
    221 | {_.map(this.state.identity.policies, (pol, idx) => { 222 | return (
  • {pol}
  • ) 223 | })} 224 |
225 | and none of them contains the following permissions: 226 | 227 |
228 |
229 |
230 | ) 231 | } 232 | 233 | renderWarningSecretBackends() { 234 | return ( 235 | 236 | 237 | } 241 | actAsExpander={true} 242 | showExpandableButton={true} 243 | /> 244 | 245 | Your token has been assigned the following policies: 246 |
    247 | {_.map(this.state.identity.policies, (pol, idx) => { 248 | return (
  • {pol}
  • ) 249 | })} 250 |
251 | and none of them contains the following permissions: 252 | 253 |
254 |
255 |
256 | ) 257 | } 258 | 259 | render() { 260 | let welcome = ( 261 |
262 | 263 | 264 | 265 | 266 |

Get started by using the left menu to navigate your Vault

267 |
268 | {!this.state.tokenCanQueryCapabilities ? this.renderWarningCapabilities() : null} 269 | {!this.state.tokenCanListSecretBackends ? this.renderWarningSecretBackends() : null} 270 | {!this.state.tokenCanListAuthBackends ? this.renderWarningAuthBackends() : null} 271 |
272 |
273 |
274 |
275 | ); 276 | return ( 277 |
278 | this.setState({ snackbarMessage: '' })} 285 | onActionTouchTap={() => this.setState({ snackbarMessage: '' })} 286 | /> 287 | {this.state.logoutOpen && this.renderSessionExpDialog()} 288 |
289 | 290 |
291 | 292 | {this.props.children || welcome} 293 | 294 |
295 |
296 | ) 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /app/components/App/app.css: -------------------------------------------------------------------------------- 1 | #content { 2 | padding-left: 30px; 3 | width: calc(100vw - 270px); 4 | margin-left: 235px; 5 | margin-top: 70px; 6 | height: calc(100% - 70px); 7 | overflow-y: scroll; 8 | position: fixed; 9 | } 10 | 11 | .snackbar { 12 | text-align: center; 13 | } 14 | 15 | .welcomeScreen { 16 | padding-bottom: 21px; 17 | } 18 | 19 | .welcomeTab > div > div { 20 | font-weight: 800; 21 | letter-spacing: 6px; 22 | font-size: 25px; 23 | } 24 | 25 | .welcomeHeader { 26 | text-align: center; 27 | } 28 | 29 | .warningMsg { 30 | border: 1px solid transparent; 31 | border-radius: 4px !important; 32 | box-shadow: 0 1px 1px rgba(0,0,0,0.05); 33 | margin: 20px 15%; 34 | border-color: #f39c12; 35 | background-color: #fef5e6 !important; 36 | } -------------------------------------------------------------------------------- /app/components/Authentication/AppRole/approle.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: #f44336; 3 | margin: 20px 0; 4 | } 5 | 6 | .orgName { 7 | color: dodgerblue; 8 | font-weight: 1000; 9 | } -------------------------------------------------------------------------------- /app/components/Authentication/Aws/aws.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: #f44336; 3 | margin: 20px 0; 4 | } -------------------------------------------------------------------------------- /app/components/Authentication/AwsEc2/awsec2.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: #f44336; 3 | margin: 20px 0; 4 | } -------------------------------------------------------------------------------- /app/components/Authentication/Github/github.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: #f44336; 3 | margin: 20px 0; 4 | } 5 | 6 | .orgName { 7 | color: dodgerblue; 8 | font-weight: 1000; 9 | } -------------------------------------------------------------------------------- /app/components/Authentication/Kubernetes/kubernetes.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: #f44336; 3 | margin: 20px 0; 4 | } -------------------------------------------------------------------------------- /app/components/Authentication/Okta/okta.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: #f44336; 3 | margin: 20px 0; 4 | } -------------------------------------------------------------------------------- /app/components/Authentication/Radius/radius.css: -------------------------------------------------------------------------------- 1 | .textFieldStyle { 2 | width: 30% !important; 3 | padding-right: 3%; 4 | } -------------------------------------------------------------------------------- /app/components/Authentication/Token/token.css: -------------------------------------------------------------------------------- 1 | #welcomeHeadline { 2 | font-size: 60px; 3 | font-weight: 200; 4 | } 5 | 6 | span.policiesList > div > div { 7 | padding: 2px !important; 8 | font-size: 90%; 9 | text-align: center; 10 | vertical-align: middle; 11 | border: dashed 1px black; 12 | } 13 | 14 | span.policiesList > div > div > div { 15 | margin: 2px 0px 2px !important; 16 | } 17 | 18 | .textFieldStyle { 19 | width: 30% !important; 20 | padding-right: 3%; 21 | } 22 | 23 | .classActionDelete { 24 | position: absolute !important; 25 | right: 4px; 26 | top: 0px; 27 | } 28 | 29 | .TokenFromRoleBtn { 30 | position: absolute; 31 | right: 64px; 32 | bottom: 10px; 33 | } -------------------------------------------------------------------------------- /app/components/Authentication/UserPass/UserPass.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | // Material UI 4 | import Dialog from 'material-ui/Dialog'; 5 | import TextField from 'material-ui/TextField'; 6 | import { Tabs, Tab } from 'material-ui/Tabs'; 7 | import Paper from 'material-ui/Paper'; 8 | import { List } from 'material-ui/List'; 9 | import FlatButton from 'material-ui/FlatButton'; 10 | import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; 11 | import Subheader from 'material-ui/Subheader'; 12 | // Styles 13 | import styles from './userpass.css'; 14 | import sharedStyles from '../../shared/styles.css'; 15 | import { callVaultApi, tokenHasCapabilities, history } from '../../shared/VaultUtils.jsx'; 16 | // Misc 17 | import _ from 'lodash'; 18 | import update from 'immutability-helper'; 19 | import ItemPicker from '../../shared/ItemPicker/ItemPicker.jsx' 20 | import ItemList from '../../shared/ItemList/ItemList.jsx'; 21 | 22 | function snackBarMessage(message) { 23 | document.dispatchEvent(new CustomEvent('snackbar', { detail: { message: message } })); 24 | } 25 | 26 | export default class UserPassAuthBackend extends React.Component { 27 | static propTypes = { 28 | params: PropTypes.object.isRequired, 29 | location: PropTypes.object.isRequired 30 | }; 31 | 32 | userPassConfigSchema = { 33 | id: undefined, 34 | max_ttl: undefined, 35 | ttl: undefined, 36 | policies: undefined 37 | }; 38 | 39 | constructor(props) { 40 | super(props); 41 | this.state = { 42 | baseUrl: `/auth/userpass/${this.props.params.namespace}/`, 43 | baseVaultPath: `auth/${this.props.params.namespace}`, 44 | users: [], 45 | config: this.userPassConfigSchema, 46 | selectedUserId: '', 47 | newUserId: '', 48 | newUserPassword: '', 49 | newUserPassword2: '', 50 | openItemDialog: false, 51 | openNewItemDialog: false, 52 | } 53 | } 54 | 55 | listUsers() { 56 | tokenHasCapabilities(['list'], `${this.state.baseVaultPath}/users`) 57 | .then(() => { 58 | callVaultApi('get', `${this.state.baseVaultPath}/users`, { list: true }, null) 59 | .then((resp) => { 60 | let users = _.get(resp, 'data.data.keys', []); 61 | this.setState({ users: _.valuesIn(users) }); 62 | }) 63 | .catch((error) => { 64 | if (error.response.status !== 404) { 65 | snackBarMessage(error); 66 | } else { 67 | this.setState({ users: [] }); 68 | } 69 | }); 70 | }) 71 | .catch(() => { 72 | snackBarMessage(new Error('Access denied')); 73 | }) 74 | } 75 | 76 | displayItem() { 77 | tokenHasCapabilities(['read'], `${this.state.baseVaultPath}/users/${this.state.selectedUserId}`) 78 | .then(() => { 79 | callVaultApi('get', `${this.state.baseVaultPath}/users/${this.state.selectedUserId}`, null, null) 80 | .then((resp) => { 81 | let user = _.get(resp, 'data.data', {}); 82 | 83 | let policies = _.get(user, 'policies', undefined); 84 | 85 | this.setState({ config: user, openItemDialog: true }); 86 | }) 87 | .catch(snackBarMessage) 88 | }) 89 | .catch(() => { 90 | snackBarMessage(new Error('Access denied')); 91 | }) 92 | } 93 | 94 | createUpdateUser(user, newUser = null) { 95 | tokenHasCapabilities(['create', 'update'], `${this.state.baseVaultPath}/users/${user}`) 96 | .then(() => { 97 | let updateObj = _.clone(this.state.config); 98 | if (newUser) updateObj.password = this.state.newUserPassword; 99 | updateObj.policies = updateObj.policies.join(','); 100 | callVaultApi('post', `${this.state.baseVaultPath}/users/${user}`, null, updateObj) 101 | .then(() => { 102 | snackBarMessage(`User ${user} has been updated`); 103 | this.listUsers(); 104 | this.setState({ openNewItemDialog: false, openItemDialog: false, config: _.clone(this.userPassConfigSchema), selectedUserId: '', newUserId: '' }); 105 | history.push(`${this.state.baseUrl}users/`); 106 | }) 107 | .catch(snackBarMessage); 108 | }) 109 | .catch(() => { 110 | this.setState({ selectedRoleId: '' }) 111 | snackBarMessage(new Error(`No permissions to display properties for role ${user}`)); 112 | }); 113 | } 114 | 115 | componentDidMount() { 116 | this.listUsers(); 117 | let user = this.props.location.pathname.split(this.state.baseUrl)[1]; 118 | if (user) { 119 | this.setState({ selectedUserId: user }); 120 | this.displayItem(); 121 | } 122 | } 123 | 124 | componentDidUpdate(prevProps, prevState) { 125 | if (this.state.selectedUserId != prevState.selectedUserId) { 126 | this.listUsers(); 127 | if (this.state.selectedUserId) { 128 | this.displayItem(); 129 | } 130 | } 131 | } 132 | 133 | componentWillReceiveProps(nextProps) { 134 | if (!_.isEqual(this.props.params.namespace, nextProps.params.namespace)) { 135 | // Reset 136 | this.setState({ 137 | baseUrl: `/auth/userpass/${nextProps.params.namespace}/`, 138 | baseVaultPath: `auth/${nextProps.params.namespace}`, 139 | users: [], 140 | config: this.userPassConfigSchema, 141 | selectedUserId: '', 142 | newUserId: '', 143 | openItemDialog: false, 144 | openNewItemDialog: false, 145 | }, () => { 146 | this.listUsers(); 147 | }); 148 | } 149 | } 150 | 151 | render() { 152 | let renderPolicyDialog = () => { 153 | const actions = [ 154 | { 157 | this.setState({ openItemDialog: false, selectedUserId: '' }); 158 | history.push(this.state.baseUrl); 159 | }} 160 | />, 161 | { 165 | this.createUpdateUser(this.state.selectedUserId); 166 | }} 167 | /> 168 | ]; 169 | 170 | return ( 171 | { 177 | this.setState({ openItemDialog: false, selectedUserId: '' }); 178 | history.push(this.state.baseUrl); 179 | }} 180 | autoScrollBodyContent={true} 181 | > 182 | 183 | Assigned Policies 184 | { 188 | this.setState({ config: update(this.state.config, { policies: { $set: newPolicies } }) }); 189 | }} 190 | /> 191 | 192 | 193 | ); 194 | }; 195 | 196 | let renderNewPolicyDialog = () => { 197 | const actions = [ 198 | { 201 | this.setState({ openNewItemDialog: false, newUserId: '' }); 202 | history.push(this.state.baseUrl); 203 | }} 204 | />, 205 | { 210 | this.createUpdateUser(`${this.state.newUserId}`, true); 211 | }} 212 | /> 213 | ]; 214 | 215 | return ( 216 | { 222 | this.setState({ openNewItemDialog: false, newUserId: '' }); 223 | history.push(this.state.baseUrl); 224 | }} 225 | autoScrollBodyContent={true} 226 | > 227 | 228 | { 236 | this.setState({ newUserId: e.target.value }); 237 | }} 238 | />
239 | { 248 | this.setState({ newUserPassword: e.target.value }); 249 | }} 250 | />
251 | { 260 | this.setState({ newUserPassword2: e.target.value }); 261 | }} 262 | />
263 | Assigned Policies 264 | { 268 | this.setState({ config: update(this.state.config, { policies: { $set: newPolicies } }) }); 269 | }} 270 | /> 271 |
272 |
273 | ); 274 | }; 275 | return ( 276 |
277 | {this.state.openItemDialog && renderPolicyDialog()} 278 | {this.state.openNewItemDialog && renderNewPolicyDialog()} 279 | 280 | 284 | 285 | Here you can configure Users. 286 | 287 | 288 | 289 | 290 | { 294 | this.setState({ 295 | newUserId: '', 296 | openNewItemDialog: true, 297 | config: _.clone(this.userPassConfigSchema) 298 | }) 299 | }} 300 | /> 301 | 302 | 303 | { 308 | snackBarMessage(`User '${deletedItem}' deleted`) 309 | this.listUsers(); 310 | }} 311 | onTouchTap={(item) => { 312 | tokenHasCapabilities(['read'], `${this.state.baseVaultPath}/${item}`) 313 | .then(() => { 314 | this.setState({ selectedUserId: `${item}` }); 315 | history.push(`${this.state.baseUrl}${item}`); 316 | }).catch(() => { 317 | snackBarMessage(new Error('Access denied')); 318 | }) 319 | 320 | }} 321 | /> 322 | 323 | 324 | 325 |
326 | ); 327 | } 328 | 329 | } -------------------------------------------------------------------------------- /app/components/Authentication/UserPass/userpass.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: #f44336; 3 | margin: 20px 0; 4 | } -------------------------------------------------------------------------------- /app/components/Login/login.css: -------------------------------------------------------------------------------- 1 | #title { 2 | font-size: 60px; 3 | font-weight: 300; 4 | } 5 | 6 | .show { 7 | opacity: 1; 8 | transition: all 1.5s ease-in; 9 | } 10 | 11 | .hide { 12 | opacity: 0; 13 | } 14 | 15 | #root { 16 | height: calc(100vh - 20px); 17 | } 18 | 19 | .error { 20 | color: rgb(244, 67, 54); 21 | } 22 | -------------------------------------------------------------------------------- /app/components/Policies/Manage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types'; 3 | import _ from 'lodash'; 4 | import { Tabs, Tab } from 'material-ui/Tabs'; 5 | import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; 6 | import Paper from 'material-ui/Paper'; 7 | import styles from './policies.css'; 8 | import sharedStyles from '../shared/styles.css'; 9 | import FlatButton from 'material-ui/FlatButton'; 10 | import { green500, green400, red500, red300, white } from 'material-ui/styles/colors.js' 11 | import { List, ListItem } from 'material-ui/List'; 12 | import Dialog from 'material-ui/Dialog'; 13 | import TextField from 'material-ui/TextField'; 14 | import IconButton from 'material-ui/IconButton'; 15 | import JsonEditor from '../shared/JsonEditor.jsx'; 16 | import ghcl from 'gopher-hcl'; 17 | import jsonschema from './vault-policy-schema.json' 18 | import { callVaultApi, tokenHasCapabilities, history } from '../shared/VaultUtils.jsx' 19 | import Avatar from 'material-ui/Avatar'; 20 | import HardwareSecurity from 'material-ui/svg-icons/hardware/security'; 21 | import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; 22 | import ActionDelete from 'material-ui/svg-icons/action/delete'; 23 | 24 | import ItemList from '../shared/ItemList/ItemList.jsx'; 25 | 26 | function snackBarMessage(message) { 27 | let ev = new CustomEvent("snackbar", { detail: { message: message } }); 28 | document.dispatchEvent(ev); 29 | } 30 | 31 | export default class PolicyManager extends React.Component { 32 | static propTypes = { 33 | params: PropTypes.object.isRequired, 34 | }; 35 | 36 | constructor(props) { 37 | super(props); 38 | 39 | this.baseUrl = `/sys/policies/`; 40 | this.baseVaultPath = `sys/policy`; 41 | 42 | this.state = { 43 | openEditModal: false, 44 | openNewPolicyModal: false, 45 | newPolicyErrorMessage: '', 46 | newPolicyNameErrorMessage: '', 47 | openDeleteModal: false, 48 | focusPolicy: -1, 49 | deletingPolicy: '', 50 | policies: [], 51 | currentPolicy: '', 52 | disableSubmit: false, 53 | forbidden: false, 54 | buttonColor: 'lightgrey' 55 | }; 56 | 57 | _.bindAll( 58 | this, 59 | 'updatePolicy', 60 | 'displayPolicy', 61 | 'listPolicies', 62 | 'policyChangeSetState', 63 | 'renderEditDialog', 64 | 'renderNewPolicyDialog' 65 | ) 66 | } 67 | 68 | componentDidMount() { 69 | if (this.props.params.splat) { 70 | this.displayPolicy(); 71 | } else { 72 | this.listPolicies(); 73 | } 74 | } 75 | 76 | componentDidUpdate(prevProps) { 77 | if (!_.isEqual(this.props.params, prevProps.params)) { 78 | if (this.props.params.splat) { 79 | this.displayPolicy(); 80 | } else { 81 | this.listPolicies(); 82 | } 83 | } 84 | } 85 | 86 | policyChangeSetState(v, syntaxCheckOk, schemaCheckOk) { 87 | if (syntaxCheckOk && schemaCheckOk && v) { 88 | this.setState({ disableSubmit: false, currentPolicy: v }); 89 | } else { 90 | this.setState({ disableSubmit: true }); 91 | } 92 | } 93 | 94 | renderEditDialog() { 95 | const actions = [ 96 | { 100 | this.setState({ openEditModal: false }) 101 | history.push(this.baseUrl); 102 | }} 103 | />, 104 | { 109 | this.updatePolicy(this.state.focusPolicy, false) 110 | history.push(this.baseUrl); 111 | }} 112 | /> 113 | ]; 114 | 115 | return ( 116 | this.setState({ openEditModal: false })} 122 | autoScrollBodyContent={true} 123 | > 124 | 132 | 133 | ); 134 | } 135 | 136 | renderNewPolicyDialog() { 137 | const MISSING_POLICY_ERROR = "Policy cannot be empty."; 138 | const DUPLICATE_POLICY_ERROR = `Policy ${this.state.focusPolicy} already exists.`; 139 | 140 | let validateAndSubmit = () => { 141 | if (this.state.focusPolicy === '') { 142 | snackBarMessage(new Error(MISSING_POLICY_ERROR)); 143 | return; 144 | } 145 | 146 | if (_.filter(this.state.policies, x => x === this.state.focusPolicy).length > 0) { 147 | snackBarMessage(new Error(DUPLICATE_POLICY_ERROR)); 148 | return; 149 | } 150 | this.updatePolicy(this.state.focusPolicy, true); 151 | } 152 | 153 | const actions = [ 154 | this.setState({ openNewPolicyModal: false, newPolicyErrorMessage: '' })} />, 155 | 156 | ]; 157 | 158 | let validatePolicyName = (event, v) => { 159 | var pattern = /^[^\/&]+$/; 160 | v = v.toLowerCase(); 161 | if (v.match(pattern)) { 162 | this.setState({ newPolicyNameErrorMessage: '', focusPolicy: v }); 163 | } else { 164 | this.setState({ newPolicyNameErrorMessage: 'Policy name contains illegal characters' }); 165 | } 166 | } 167 | 168 | 169 | return ( 170 | this.setState({ openNewPolicyModal: false, newPolicyErrorMessage: '' })} 176 | autoScrollBodyContent={true} 177 | autoDetectWindowHeight={true} 178 | > 179 | 187 | 195 |
{this.state.newPolicyErrorMessage}
196 |
197 | ); 198 | } 199 | 200 | updatePolicy(policyName, isNewPolicy) { 201 | let stringifiedPolicy = JSON.stringify(this.state.currentPolicy); 202 | callVaultApi('put', `${this.baseVaultPath}/${policyName}`, null, { rules: stringifiedPolicy }, null) 203 | .then(() => { 204 | if (isNewPolicy) { 205 | let policies = this.state.policies; 206 | policies.push(policyName); 207 | this.setState({ 208 | policies: policies 209 | }); 210 | snackBarMessage(`Policy '${policyName}' added`); 211 | } else { 212 | snackBarMessage(`Policy '${policyName}' updated`); 213 | } 214 | }) 215 | .catch((err) => { 216 | console.error(err.stack); 217 | snackBarMessage(err); 218 | }) 219 | this.setState({ openNewPolicyModal: false }); 220 | this.setState({ openEditModal: false }); 221 | } 222 | 223 | listPolicies() { 224 | callVaultApi('get', this.baseVaultPath, null, null, null) 225 | .then((resp) => { 226 | this.setState({ 227 | policies: resp.data.policies, 228 | buttonColor: green500 229 | }); 230 | }) 231 | .catch((err) => { 232 | console.error(err.response.data); 233 | snackBarMessage(err); 234 | }); 235 | } 236 | 237 | displayPolicy() { 238 | callVaultApi('get', `${this.baseVaultPath}/${this.props.params.splat}`, null, null, null) 239 | .then((resp) => { 240 | let rules = _.get(resp, 'data.data.rules', _.get(resp, 'data.rules', {})); 241 | let rules_obj; 242 | // Attempt to parse into JSON incase a stringified JSON was sent 243 | try { 244 | rules_obj = JSON.parse(rules); 245 | } 246 | // Previous parse failed, attempt HCL to JSON conversion 247 | catch (e) { 248 | rules_obj = ghcl.parse(rules); 249 | } 250 | 251 | if (rules_obj) { 252 | this.setState({ 253 | openEditModal: true, 254 | focusPolicy: this.props.params.splat, 255 | currentPolicy: rules_obj, 256 | disableSubmit: true 257 | }); 258 | } 259 | }) 260 | .catch(snackBarMessage); 261 | } 262 | 263 | render() { 264 | return ( 265 |
266 | {this.state.openEditModal && this.renderEditDialog()} 267 | {this.state.openNewPolicyModal && this.renderNewPolicyDialog()} 268 | {this.state.openDeleteModal && this.renderDeleteConfirmationDialog()} 269 | 270 | 271 | 272 | Here you can view, update, and delete policies stored in your Vault. Just remember, deleting policies cannot be undone! 273 | 274 | 275 | 276 | 277 | this.setState({ 284 | openNewPolicyModal: true, 285 | newPolicyErrorMessage: '', 286 | newPolicyNameErrorMessage: '', 287 | disableSubmit: true, 288 | focusPolicy: '', 289 | currentPolicy: { path: { 'sample/path': { capabilities: ['read'] } } } 290 | })} 291 | /> 292 | 293 | 294 | { 299 | snackBarMessage(`Object '${deletedItem}' deleted`) 300 | this.listPolicies(); 301 | }} 302 | onTouchTap={(item) => { 303 | tokenHasCapabilities(['read'], `${this.baseVaultPath}/${item}`).then(() => { 304 | history.push(`${this.baseUrl}${item}`); 305 | }).catch(() => { 306 | snackBarMessage(new Error("Access denied")); 307 | }) 308 | }} 309 | /> 310 | 311 | 312 | 313 |
314 | ); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /app/components/Policies/policies.css: -------------------------------------------------------------------------------- 1 | /* Copied from secrets.css */ 2 | #welcomeHeadline { 3 | font-size: 60px; 4 | font-weight: 200; 5 | } 6 | 7 | .actionButtons { 8 | position: absolute; 9 | right: 0; 10 | } 11 | 12 | .key{ 13 | max-width: 600px; 14 | word-wrap: break-word; 15 | } 16 | 17 | .error { 18 | color: #f44336; 19 | margin: 20px 0; 20 | } 21 | 22 | .orgName { 23 | color: dodgerblue; 24 | font-weight: 1000; 25 | } -------------------------------------------------------------------------------- /app/components/Policies/vault-policy-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": [ 4 | "path" 5 | ], 6 | "properties": { 7 | "path": { 8 | "type": "object", 9 | "minProperties": 1, 10 | "additionalProperties": false, 11 | "patternProperties": { 12 | "^[^\/].*$": { 13 | "type": "object", 14 | "additionalProperties": false, 15 | "anyOf": [ 16 | { 17 | "required": [ 18 | "capabilities" 19 | ] 20 | }, 21 | { 22 | "required": [ 23 | "policy" 24 | ] 25 | }, 26 | { 27 | "optional": [ 28 | "required_parameters" 29 | ] 30 | }, 31 | { 32 | "optional": [ 33 | "allowed_parameters" 34 | ] 35 | }, 36 | { 37 | "optional": [ 38 | "denied_parameters" 39 | ] 40 | } 41 | ], 42 | "properties": { 43 | "capabilities": { 44 | "type": "array", 45 | "minItems": 1, 46 | "uniqueItems": true, 47 | "items": { 48 | "type": "string", 49 | "enum": [ 50 | "create", 51 | "read", 52 | "update", 53 | "delete", 54 | "list", 55 | "sudo", 56 | "deny" 57 | ] 58 | } 59 | }, 60 | "required_parameters": { 61 | "type": "array", 62 | "minItems": 0, 63 | "uniqueItems": true, 64 | "items": { 65 | "type": "string" 66 | } 67 | }, 68 | "allowed_parameters": { 69 | "type": "object", 70 | "minProperties": 0, 71 | "additionalProperties": true, 72 | "patternProperties": { 73 | "^[^\/].*$": { 74 | "type": "array", 75 | "minItems": 0, 76 | "uniqueItems": true, 77 | "items": { 78 | "type": "string" 79 | } 80 | } 81 | } 82 | }, 83 | "denied_parameters": { 84 | "type": "object", 85 | "minProperties": 0, 86 | "additionalProperties": true, 87 | "patternProperties": { 88 | "^[^\/].*$": { 89 | "type": "array", 90 | "minItems": 0, 91 | "uniqueItems": true, 92 | "items": { 93 | "type": "string" 94 | } 95 | } 96 | } 97 | }, 98 | "policy": { 99 | "type": "string", 100 | "enum": [ 101 | "read", 102 | "write", 103 | "sudo", 104 | "deny" 105 | ] 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /app/components/ResponseWrapper/ResponseWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tabs, Tab } from 'material-ui/Tabs'; 3 | import Paper from 'material-ui/Paper'; 4 | import sharedStyles from '../shared/styles.css'; 5 | import JsonEditor from '../shared/JsonEditor.jsx'; 6 | import SecretWrapper from '../shared/Wrapping/Wrapper.jsx' 7 | 8 | function snackBarMessage(message) { 9 | let ev = new CustomEvent("snackbar", { detail: { message: message } }); 10 | document.dispatchEvent(ev); 11 | } 12 | 13 | export default class ResponseWrapper extends React.Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | wrapEditorValue: {} 19 | }; 20 | 21 | } 22 | 23 | render() { 24 | let secretChangedJsonEditor = (v, syntaxCheckOk) => { 25 | if (syntaxCheckOk && v) { 26 | this.setState({ wrapEditorValue: v }); 27 | } else { 28 | this.setState({ wrapEditorValue: null }); 29 | } 30 | } 31 | 32 | return ( 33 |
34 | {this.state.openWrapTokenDialog && this.showWrappedToken()} 35 | 36 | 37 | 38 | Here you can store data inside vault and collect a temporary, single-use token to display the initial data 39 | 40 | 41 | 47 |
48 | 52 |
53 |
54 |
55 |
56 |
57 | ) 58 | } 59 | } -------------------------------------------------------------------------------- /app/components/ResponseWrapper/responseWrapper.css: -------------------------------------------------------------------------------- 1 | #pageHeader { 2 | font-size: 60px; 3 | font-weight: 200; 4 | } 5 | 6 | .error { 7 | color: rgb(244, 67, 54); 8 | } -------------------------------------------------------------------------------- /app/components/Secrets/Generic/generic.css: -------------------------------------------------------------------------------- 1 | /*Placeholder*/ -------------------------------------------------------------------------------- /app/components/Settings/Settings.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import Checkbox from 'material-ui/Checkbox'; 4 | import { Tabs, Tab } from 'material-ui/Tabs'; 5 | import sharedStyles from '../shared/styles.css'; 6 | import Paper from 'material-ui/Paper'; 7 | import styles from './settings.css'; 8 | import _ from 'lodash'; 9 | 10 | class Settings extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | rootKey: window.localStorage.getItem('secretsRootKey') || '', 16 | useRootKey: window.localStorage.getItem('useRootKey') === 'true' || false 17 | } 18 | _.bindAll(this, 19 | 'setDeleteDialogPreference', 20 | 'setRootKeyPreference', 21 | 'setRootKey' 22 | ); 23 | } 24 | 25 | setDeleteDialogPreference(e, isChecked) { 26 | window.localStorage.setItem('showDeleteModal', isChecked); 27 | } 28 | 29 | setRootKeyPreference(e, isChecked) { 30 | window.localStorage.setItem('useRootKey', isChecked); 31 | this.setState({ 32 | useRootKey: isChecked 33 | }); 34 | } 35 | 36 | setDiffAnnotationsPreference(e, isChecked) { 37 | window.localStorage.setItem('enableDiffAnnotations', isChecked); 38 | } 39 | 40 | setCapCachePreference(e, isChecked) { 41 | window.localStorage.setItem('enableCapabilitiesCache', isChecked); 42 | } 43 | 44 | setRootKey(e, rootKey) { 45 | window.localStorage.setItem('secretsRootKey', rootKey) 46 | this.setState({ rootKey: rootKey }); 47 | } 48 | 49 | render() { 50 | return ( 51 |
52 | 53 | 54 | 55 | Here you can customize your Vault UI settings. 56 | 57 | 58 |
59 |

General

60 | 64 | 68 | 72 |
73 |
74 |

Secrets

75 | 79 | 86 |
87 |
88 |
89 |
90 |
91 | ) 92 | } 93 | } 94 | 95 | export default Settings; 96 | -------------------------------------------------------------------------------- /app/components/Settings/settings.css: -------------------------------------------------------------------------------- 1 | #welcomeHeadline { 2 | font-size: 60px; 3 | font-weight: 200; 4 | } 5 | 6 | .code { 7 | padding: 2px 4px; 8 | font-size: 90%; 9 | color: #c7254e; 10 | background-color: #f9f2f4; 11 | border-radius: 4px; 12 | } 13 | -------------------------------------------------------------------------------- /app/components/shared/DeleteObject/DeleteObject.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 | 8 | export default class VaultObjectDeleter extends Component { 9 | static propTypes = { 10 | path: PropTypes.string, 11 | forceShowDialog: PropTypes.bool, 12 | onReceiveResponse: PropTypes.func, 13 | onReceiveError: PropTypes.func, 14 | onModalClose: PropTypes.func 15 | } 16 | 17 | static defaultProps = { 18 | path: '', 19 | forceShowDialog: false, 20 | onReceiveResponse: () => { }, 21 | onReceiveError: () => { }, 22 | onModalClose: () => { } 23 | } 24 | 25 | constructor(props) { 26 | super(props) 27 | 28 | this.state = { 29 | openDeleteModal: false, 30 | path: this.props.path 31 | }; 32 | } 33 | 34 | 35 | 36 | componentWillReceiveProps(nextProps) { 37 | // Trigger automatically on props change 38 | if (nextProps.path && !_.isEqual(nextProps.path, this.props.path)) { 39 | this.setState({ path: nextProps.path }) 40 | } 41 | } 42 | 43 | componentDidUpdate(prevProps, prevState) { 44 | if (!_.isEqual(prevState.path, this.state.path) && this.state.path) { 45 | if (window.localStorage.getItem("showDeleteModal") === 'false' && !this.props.forceShowDialog) { 46 | this.DeleteObject(this.state.path); 47 | } else { 48 | this.setState({ openDeleteModal: true }) 49 | } 50 | } 51 | } 52 | 53 | DeleteObject(fullpath) { 54 | callVaultApi('delete', fullpath) 55 | .then((response) => { 56 | this.setState({ openDeleteModal: false }); 57 | this.props.onReceiveResponse(response.data); 58 | }) 59 | .catch((err) => { 60 | this.setState({ openDeleteModal: false }); 61 | this.props.onReceiveError(err); 62 | }) 63 | } 64 | 65 | render() { 66 | const actions = [ 67 | { this.setState({ openDeleteModal: false }); this.props.onModalClose(); }} />, 68 | this.DeleteObject(this.state.path)} /> 69 | ]; 70 | 71 | const style_objpath = { 72 | color: 'red', 73 | fontFamily: 'monospace', 74 | fontSize: '16px', 75 | paddingLeft: '5px' 76 | } 77 | 78 | return ( 79 | 85 |

You are about to permanently delete the object at path:

86 |

{this.state.path}

87 |

Are you sure?

88 | {!this.props.forceShowDialog ? To disable this prompt, visit the settings page. : null} 89 |
90 | ) 91 | } 92 | } -------------------------------------------------------------------------------- /app/components/shared/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import _ from 'lodash'; 4 | import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; 5 | import FlatButton from 'material-ui/FlatButton'; 6 | import IconButton from 'material-ui/IconButton'; 7 | import Github from 'mui-icons/fontawesome/github'; 8 | import CountDown from './countdown.js' 9 | import styles from './header.css'; 10 | import { callVaultApi, history } from '../../shared/VaultUtils.jsx'; 11 | import IconMenu from 'material-ui/IconMenu'; 12 | import MenuItem from 'material-ui/MenuItem'; 13 | import NavigationMenu from 'material-ui/svg-icons/navigation/menu'; 14 | import Divider from 'material-ui/Divider'; 15 | import Dialog from 'material-ui/Dialog'; 16 | import TextField from 'material-ui/TextField'; 17 | import sharedStyles from '../styles.css'; 18 | import RaisedButton from 'material-ui/RaisedButton'; 19 | import ContentContentCopy from 'material-ui/svg-icons/content/content-copy'; 20 | import copy from 'copy-to-clipboard'; 21 | 22 | var logout = () => { 23 | window.localStorage.removeItem('vaultAccessToken'); 24 | history.push('/login'); 25 | } 26 | 27 | function snackBarMessage(message) { 28 | let ev = new CustomEvent("snackbar", { detail: { message: message } }); 29 | document.dispatchEvent(ev); 30 | } 31 | 32 | class Header extends React.Component { 33 | constructor(props) { 34 | super(props); 35 | this.state = { 36 | serverAddr: window.localStorage.getItem('vaultUrl'), 37 | version: '', 38 | tokenDialogOpened: false, 39 | tokenRenewed: false, 40 | ttl: 0 41 | } 42 | _.bindAll( 43 | this, 44 | 'renewTokenLease' 45 | ); 46 | } 47 | 48 | static propTypes = { 49 | tokenIdentity: PropTypes.object 50 | } 51 | 52 | componentWillMount() { 53 | callVaultApi('get', 'sys/health', null, null, null) 54 | .then((resp) => { 55 | this.setState({ 56 | version: resp.data.version, 57 | }); 58 | }) 59 | .catch((error) => { 60 | if (error.response.status === 429) { 61 | this.setState({ 62 | version: error.response.data.version, 63 | }); 64 | } else { 65 | snackBarMessage(error); 66 | } 67 | }); 68 | } 69 | 70 | renewTokenLease() { 71 | callVaultApi('post', 'auth/token/renew-self') 72 | .then((resp) => { 73 | snackBarMessage("Session renewed"); 74 | this.setState({ ttl: resp.data.auth.lease_duration, tokenRenewed: true }); 75 | }) 76 | .catch(snackBarMessage) 77 | } 78 | 79 | 80 | render() { 81 | 82 | let tokenDialogOptions = [ 83 | this.setState({ tokenDialogOpened: false })} /> 84 | ]; 85 | 86 | let showToken = () => { 87 | return ( 88 | 94 |
95 | 101 | } label="Copy to Clipboard" onTouchTap={() => { copy(window.localStorage.getItem("vaultAccessToken")) }} /> 102 |
103 |
104 | ) 105 | } 106 | 107 | let renderTokenInfo = () => { 108 | 109 | let infoSectionItems = [] 110 | 111 | let username; 112 | if (_.has(this.props.tokenIdentity, 'meta.username')) { 113 | username = this.props.tokenIdentity.meta.username; 114 | } else { 115 | username = this.props.tokenIdentity.display_name 116 | } 117 | if (username) { 118 | infoSectionItems.push( 119 | 120 | logged in as 121 | {username} 122 | 123 | ) 124 | } 125 | 126 | infoSectionItems.push( 127 | 128 | connected to 129 | {this.state.serverAddr} 130 | 131 | ) 132 | 133 | if (this.state.tokenRenewed) { 134 | infoSectionItems.push( 135 | 136 | token ttl 137 | 138 | 139 | 140 | 141 | ); 142 | } else if (this.props.tokenIdentity.ttl) { 143 | infoSectionItems.push( 144 | 145 | token ttl 146 | 147 | 148 | 149 | 150 | ) 151 | } 152 | 153 | if (this.state.version) { 154 | infoSectionItems.push( 155 | 156 | vault version 157 | {this.state.version} 158 | 159 | ) 160 | } 161 | 162 | return infoSectionItems; 163 | } 164 | 165 | return ( 166 |
167 | {showToken()} 168 |
169 | 170 | 171 | { 173 | if (WEBPACK_DEF_TARGET_WEB) { 174 | window.open('https://github.com/djenriquez/vault-ui', '_blank'); 175 | } else { 176 | event.preventDefault(); 177 | require('electron').shell.openExternal('https://github.com/djenriquez/vault-ui') 178 | } 179 | }} 180 | > 181 | 182 | 183 | { 185 | history.push('/'); 186 | }} 187 | text="VAULT - UI" /> 188 | 189 | 190 | {renderTokenInfo()} 191 | 192 | 193 | }> 194 | this.setState({ tokenDialogOpened: true })} 197 | /> 198 | 199 | 203 | 204 | 208 | 209 | 210 | 211 |
212 |
213 | ) 214 | } 215 | } 216 | 217 | export default Header; 218 | -------------------------------------------------------------------------------- /app/components/shared/Header/countdown.js: -------------------------------------------------------------------------------- 1 | // Based on https://raw.githubusercontent.com/rogermarkussen/react.timer/master/src/countdown.js 2 | 3 | import React, { Component } from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | class CountDown extends Component { 7 | 8 | 9 | static propTypes = { 10 | countDown: PropTypes.number, 11 | retrigger: PropTypes.number, 12 | className: PropTypes.string 13 | } 14 | 15 | constructor(props) { 16 | super(props) 17 | this.state = { time: props.countDown * 10 } 18 | this.tick = this.tick.bind(this) 19 | this.splitTimeComponents = this.splitTimeComponents.bind(this) 20 | this.stopTime = Date.now() + (props.countDown * 1000) 21 | } 22 | 23 | componentWillReceiveProps(nextProps) { 24 | if (nextProps !== this.props) { 25 | clearInterval(this.time); 26 | this.time = nextProps.countDown; 27 | this.stopTime = Date.now() + (nextProps.countDown * 1000) 28 | this.time = setInterval(this.tick, 100) 29 | } 30 | } 31 | 32 | componentDidMount() { 33 | this.time = setInterval(this.tick, 100) 34 | } 35 | 36 | componentWillUnmount() { 37 | clearInterval(this.time) 38 | } 39 | 40 | splitTimeComponents() { 41 | const time = this.state.time / 10 42 | var delta = Math.floor(time) 43 | var days = Math.floor(delta / 86400); 44 | delta -= days * 86400; 45 | var hours = Math.floor(delta / 3600) % 24; 46 | delta -= hours * 3600; 47 | var minutes = Math.floor(delta / 60) % 60; 48 | delta -= minutes * 60; 49 | var seconds = delta % 60; 50 | 51 | return `${days}d ${hours}h ${minutes}m ${seconds}s`; 52 | } 53 | 54 | tick() { 55 | const now = Date.now() 56 | if (this.stopTime - now <= 0) { 57 | this.setState({ time: 0 }) 58 | clearInterval(this.time) 59 | } else this.setState({ time: Math.round((this.stopTime - now) / 100) }) 60 | } 61 | 62 | render() { 63 | return {this.splitTimeComponents()} 64 | } 65 | } 66 | CountDown.propTypes = { 67 | countDown: PropTypes.number.isRequired 68 | } 69 | export default CountDown 70 | -------------------------------------------------------------------------------- /app/components/shared/Header/header.css: -------------------------------------------------------------------------------- 1 | .title { 2 | cursor: pointer; 3 | color: white !important; 4 | } 5 | 6 | #headerWrapper { 7 | position: fixed; 8 | width: 100%; 9 | top: 0; 10 | z-index: 100; 11 | } 12 | 13 | .infoSectionItem { 14 | color: white; 15 | padding: 0px 10px 0px 10px; 16 | } 17 | 18 | .infoSectionItemKey { 19 | font-variant: small-caps; 20 | padding-right: 5px; 21 | color: darkgrey; 22 | } 23 | 24 | .infoSectionItemValue { 25 | font-family: monospace; 26 | } -------------------------------------------------------------------------------- /app/components/shared/ItemList/ItemList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Avatar from 'material-ui/Avatar'; 4 | 5 | import FileFolder from 'material-ui/svg-icons/file/folder'; 6 | import ActionDelete from 'material-ui/svg-icons/action/delete'; 7 | import ActionDeleteForever from 'material-ui/svg-icons/action/delete-forever'; 8 | import ActionAccountBox from 'material-ui/svg-icons/action/account-box'; 9 | import IconButton from 'material-ui/IconButton'; 10 | import Divider from 'material-ui/Divider'; 11 | 12 | import { List, ListItem } from 'material-ui/List'; 13 | import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; 14 | import TextField from 'material-ui/TextField'; 15 | import SelectField from 'material-ui/SelectField'; 16 | import MenuItem from 'material-ui/MenuItem'; 17 | 18 | import sharedStyles from '../../shared/styles.css'; 19 | import VaultObjectDeleter from '../../shared/DeleteObject/DeleteObject.jsx' 20 | import UltimatePagination from 'react-ultimate-pagination-material-ui' 21 | 22 | import { red500 } from 'material-ui/styles/colors.js'; 23 | 24 | const SORT_DIR = { 25 | ASC: 'asc', 26 | DESC: 'desc' 27 | }; 28 | 29 | function snackBarMessage(message) { 30 | let ev = new CustomEvent("snackbar", { detail: { message: message } }); 31 | document.dispatchEvent(ev); 32 | } 33 | 34 | export default class ItemList extends React.Component { 35 | 36 | static propTypes = { 37 | itemList: PropTypes.array.isRequired, 38 | itemUri: PropTypes.string.isRequired, 39 | maxItemsPerPage: PropTypes.number, 40 | onTouchTap: PropTypes.func, 41 | onDeleteTap: PropTypes.func.isRequired, 42 | onCustomListRender: PropTypes.func 43 | }; 44 | 45 | constructor(props) { 46 | super(props); 47 | 48 | this.itemListFull = []; 49 | this.filteredItemList = []; 50 | this.lastMaxItemsPerPage = 25; 51 | 52 | this.state = { 53 | // itemListFull: this.props.itemList, 54 | // filteredItemList: this.props.itemList, 55 | maxItemsPerPage: this.props.maxItemsPerPage ? this.props.maxItemsPerPage : 25, 56 | pageItems: [], 57 | parsedItems: [], 58 | filterString: '', 59 | sortDirection: SORT_DIR.ASC, 60 | currentPage: 1, 61 | totalPages: 1, 62 | deletePath: '', 63 | openDelete: false 64 | }; 65 | 66 | _.bindAll( 67 | this, 68 | 'renderItemList', 69 | 'setPage' 70 | ); 71 | } 72 | 73 | 74 | renderItemList() { 75 | let directories = _.filter(this.state.pageItems, (item) => { 76 | return this.isPathDirectory(item); 77 | }); 78 | var isFolder = directories.length > 0 ? true : false; 79 | 80 | return _.map(_.sortBy(this.state.pageItems, (item) => { 81 | if (this.isPathDirectory(item)) return 0; 82 | }), (item) => { 83 | var avatar = this.isPathDirectory(item) ? (} />) : (} />); 84 | var action = this.isPathDirectory(item) ? () : ( 85 | { e.stopPropagation(); this.setState({ deletePath: `${this.props.itemUri}/${item}`, openDelete: true }); } } 88 | > 89 | {window.localStorage.getItem('showDeleteModal') === 'false' ? : } 90 | 91 | ); 92 | 93 | if (!this.isPathDirectory(item) && isFolder) { 94 | isFolder = false; 95 | return ([ 96 | , 97 | 105 | ]) 106 | } else return ( 107 | 115 | ) 116 | }); 117 | } 118 | 119 | isPathDirectory(key) { 120 | if (!key) key = '/'; 121 | return (key[key.length - 1] === '/'); 122 | } 123 | 124 | filterItems(filter) { 125 | if (filter) { 126 | this.filteredItemList = _.filter(this.itemListFull, (item) => { 127 | return item.toLowerCase().includes(filter.toLowerCase()); 128 | }) 129 | } else { 130 | this.filteredItemList = this.itemListFull; 131 | } 132 | } 133 | 134 | setPage(page = null, sortDirection = null, maxItemsPerPage = null) { 135 | // Defaults 136 | page = page ? page : this.state.currentPage; 137 | sortDirection = sortDirection ? sortDirection : this.state.sortDirection; 138 | maxItemsPerPage = maxItemsPerPage ? maxItemsPerPage : this.state.maxItemsPerPage; 139 | 140 | let maxPage = Math.ceil(this.filteredItemList.length / maxItemsPerPage); 141 | // Never allow to set to higher page than max 142 | page = page > maxPage ? maxPage : page 143 | // Never allow a 0th or negative page 144 | page = page <= 0 ? 1 : page; 145 | 146 | let sortedItems = _.orderBy(this.filteredItemList, _.identity, sortDirection); 147 | let parsedItems = _.chunk(sortedItems, maxItemsPerPage); 148 | let totalPages = Math.ceil(sortedItems.length / maxItemsPerPage); 149 | this.setState( 150 | { 151 | currentPage: page, 152 | totalPages: 1 > totalPages ? 1 : totalPages, 153 | parsedItems: parsedItems, 154 | pageItems: parsedItems[page - 1] 155 | }); 156 | } 157 | 158 | resetPage() { 159 | this.setState({ 160 | currentPage: 1, 161 | totalPages: 1 162 | }); 163 | } 164 | 165 | // Events 166 | componentDidMount() { 167 | this.setPage(1); 168 | } 169 | 170 | componentWillUpdate(nextProps, nextState) { 171 | if (!nextProps.maxItemsPerPage) { 172 | this.lastMaxItemsPerPage = this.state.maxItemsPerPage; 173 | } 174 | } 175 | 176 | componentWillReceiveProps(nextProps) { 177 | this.itemListFull = nextProps.itemList; 178 | this.filterItems(this.state.filterString); 179 | this.setPage(); 180 | } 181 | 182 | render() { 183 | return ( 184 |
185 | snackBarMessage(err)} 190 | /> 191 | 192 | 193 | { 200 | this.setState({ filterString: v }); 201 | this.filterItems(v); 202 | this.setPage(this.state.currentPage, this.state.sortDirection) 203 | }} 204 | /> 205 | { 212 | if (!this.state.maxItemsPerPage) { 213 | this.setState({ maxItemsPerPage: this.lastMaxItemsPerPage }); 214 | } 215 | }} 216 | onChange={(e, v) => { 217 | this.setState({ maxItemsPerPage: v }); 218 | this.setPage(this.state.currentPage, this.state.sortDirection, v) 219 | }} 220 | /> 221 | { 228 | this.setPage(this.state.currentPage, v); 229 | }} 230 | > 231 | 232 | 233 | 234 | 235 | 236 | 237 | {(this.props.onCustomListRender && this.props.onCustomListRender()) || this.renderItemList()} 238 | 239 |
240 | { 244 | this.setPage(e, this.state.sortDirection) 245 | }} 246 | /> 247 |
248 |
249 | ); 250 | } 251 | } -------------------------------------------------------------------------------- /app/components/shared/ItemPicker/ItemPicker.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { callVaultApi, tokenHasCapabilities } from '../VaultUtils.jsx' 4 | import _ from 'lodash'; 5 | import { List, ListItem } from 'material-ui/List'; 6 | import styles from './itempicker.css'; 7 | import KeyboardArrowRight from 'material-ui/svg-icons/hardware/keyboard-arrow-right'; 8 | import AutoComplete from 'material-ui/AutoComplete'; 9 | import Clear from 'material-ui/svg-icons/content/clear'; 10 | import { Toolbar, ToolbarGroup, ToolbarTitle } from 'material-ui/Toolbar'; 11 | import UltimatePagination from 'react-ultimate-pagination-material-ui' 12 | import update from 'immutability-helper'; 13 | import sharedStyles from '../styles.css'; 14 | 15 | const LIST_TYPE = { 16 | SELECTED: 'selected', 17 | AVAILABLE: 'available' 18 | }; 19 | 20 | function snackBarMessage(message) { 21 | document.dispatchEvent(new CustomEvent('snackbar', { detail: { message: message } })); 22 | } 23 | 24 | export default class ItemPicker extends React.Component { 25 | static propTypes = { 26 | onError: PropTypes.func, 27 | onSelectedChange: PropTypes.func, 28 | excludePolicies: PropTypes.array, 29 | selectedPolicies: PropTypes.array, 30 | height: PropTypes.string, 31 | type: PropTypes.string, 32 | item: PropTypes.string, 33 | vaultPath: PropTypes.string 34 | }; 35 | 36 | static defaultProps = { 37 | selectedPolicies: [], 38 | onError: (err) => { console.error(err) }, 39 | onSelectedChange: () => { }, 40 | excludePolicies: [], 41 | height: "300px", 42 | item: "policies", 43 | type: "policy" 44 | }; 45 | 46 | constructor(props) { 47 | super(props); 48 | 49 | this.state = { 50 | 51 | available: { 52 | maxItemsPerPage: 7, 53 | sortDirection: 'asc', 54 | currentPage: 1, 55 | totalPages: 1, 56 | pageItems: [], 57 | pagedItems: [], 58 | items: [], 59 | searchText: '' 60 | }, 61 | selected: { 62 | maxItemsPerPage: 7, 63 | sortDirection: 'asc', 64 | currentPage: 1, 65 | totalPages: 1, 66 | pageItems: [], 67 | pagedItems: [], 68 | items: [], 69 | searchText: '' 70 | } 71 | }; 72 | 73 | _.bindAll( 74 | this, 75 | 'loadItemList', 76 | 'selectedItemAdd', 77 | 'selectedItemRemove', 78 | 'setPage' 79 | ) 80 | 81 | this.loadItemList(this.props.type); 82 | } 83 | 84 | loadItemList(type) { 85 | let allowed_methods = [''] 86 | let http_method = '' 87 | let path = '' 88 | let params = null 89 | 90 | // Determine item type 91 | switch (type) { 92 | case "okta/users": 93 | allowed_methods = ['list'] 94 | http_method = 'get' 95 | params = { list: true } 96 | path = this.props.vaultPath 97 | break; 98 | default: 99 | allowed_methods = ['read'] 100 | http_method = 'get' 101 | path = 'sys/policy' 102 | break; 103 | } 104 | 105 | // Make Vault API call 106 | tokenHasCapabilities(allowed_methods, path) 107 | .then(() => { 108 | callVaultApi(http_method, path, params, null, null) 109 | .then((resp) => { 110 | let itemList = [] 111 | 112 | if (this.props.type === 'policy') { 113 | itemList = _.filter(resp.data.data.keys, (item) => { 114 | return (!_.includes(this.props.excludePolicies, item)) && (item !== 'root'); 115 | }) 116 | } else { 117 | itemList = _.filter(resp.data.data.keys, (item) => { 118 | return (!_.includes(this.props.excludePolicies, item)); 119 | }) 120 | } 121 | this.setState({ 122 | available: update(this.state.available, { 123 | items: { $set: itemList } 124 | }) 125 | }); 126 | }) 127 | .catch(this.props.onError) 128 | }) 129 | .catch(snackBarMessage) 130 | } 131 | 132 | componentDidMount() { 133 | this.setState({ selected: update(this.state.selected, { items: { $set: this.props.selectedPolicies } }) }); 134 | } 135 | 136 | componentWillReceiveProps(nextProps) { 137 | // Updates the module when reselected 138 | if (!_.isEqual(this.props.selectedPolicies.sort(), nextProps.selectedPolicies.sort())) { 139 | this.setState({ 140 | selected: update(this.state.selected, { 141 | items: { $set: nextProps.selectedPolicies } 142 | }) 143 | }); 144 | } 145 | } 146 | 147 | componentWillUpdate(nextProps, nextState) { 148 | // Throw event when selected itemlist changes 149 | if (!_.isEqual(this.state.selected.items.sort(), nextState.selected.items.sort())) { 150 | this.props.onSelectedChange(nextState.selected.items); 151 | } 152 | } 153 | 154 | componentDidUpdate(prevProps, prevState) { 155 | // Update available or selected if items change 156 | if ( 157 | !_.isEqual(this.state.selected.items.sort(), prevState.selected.items.sort()) || 158 | this.state.selected.searchText !== prevState.selected.searchText 159 | ) { 160 | this.setPage(LIST_TYPE.SELECTED, 1); 161 | } 162 | if ( 163 | !_.isEqual(this.state.available.items.sort(), prevState.available.items.sort()) || 164 | this.state.available.searchText !== prevState.available.searchText 165 | ) { 166 | this.setPage(LIST_TYPE.AVAILABLE, 1); 167 | } 168 | } 169 | 170 | 171 | selectedItemAdd(v) { 172 | let available = update(this.state.available, { 173 | pageItems: { $set: _(this.state.available.pageItems).without(v).value() } 174 | }); 175 | let selected = update(this.state.selected, { 176 | pageItems: { $set: _(this.state.selected.pageItems).concat(v).value() }, 177 | items: { $set: _(this.state.selected.items).concat(v).value() } 178 | }); 179 | 180 | this.setState({ 181 | selected: selected, 182 | available: available 183 | }); 184 | } 185 | 186 | selectedItemRemove(v) { 187 | let available = update(this.state.available, { 188 | pageItems: { $set: _(this.state.available.pageItems).concat(v).value() } 189 | }); 190 | let selected = update(this.state.selected, { 191 | pageItems: { $set: _(this.state.selected.pageItems).without(v).value() }, 192 | items: { $set: _(this.state.selected.items).without(v).value() } 193 | }) 194 | 195 | // If items exists in the available items (not added in), do not add into available items 196 | if (_.indexOf(this.state.available.items, v) >= 0) { 197 | this.setState({ 198 | selected: selected, 199 | available: available 200 | }); 201 | } else { 202 | this.setState({ 203 | selected: selected 204 | }); 205 | } 206 | 207 | } 208 | 209 | setPage(listType, page = null, sortDirection = null, maxItemsPerPage = null) { 210 | // Defaults 211 | var list = listType == LIST_TYPE.SELECTED ? _.clone(this.state.selected) : _.clone(this.state.available); 212 | page = page ? page : list.currentPage; 213 | sortDirection = sortDirection ? sortDirection : list.sortDirection; 214 | maxItemsPerPage = maxItemsPerPage ? maxItemsPerPage : list.maxItemsPerPage; 215 | 216 | let maxPage = Math.ceil(list.items.length / maxItemsPerPage); 217 | // Never allow to set to higher page than max 218 | page = page > maxPage ? maxPage : page 219 | // Never allow a 0th or negative page 220 | page = page <= 0 ? 1 : page; 221 | 222 | // Filter 223 | if (listType === LIST_TYPE.AVAILABLE) { 224 | let selectedAvailableItems = _(this.state.available.items).difference(this.state.selected.items).value(); 225 | list.items = _.filter(selectedAvailableItems, (item) => { 226 | return _.includes(item, list.searchText); 227 | }); 228 | } 229 | 230 | // Sort 231 | let sortedItems = _.orderBy(list.items, _.identity, sortDirection); 232 | let pagedItems = _.chunk(sortedItems, maxItemsPerPage); 233 | 234 | if (listType === LIST_TYPE.AVAILABLE) { 235 | this.setState( 236 | { 237 | available: update(this.state.available, { 238 | currentPage: { $set: page }, 239 | totalPages: { $set: Math.ceil(list.items.length / maxItemsPerPage) }, 240 | pagedItems: { $set: pagedItems }, 241 | pageItems: { $set: pagedItems[page - 1] ? pagedItems[page - 1].filter(Boolean) : [] } 242 | }) 243 | }); 244 | } else if (listType === LIST_TYPE.SELECTED) { 245 | this.setState( 246 | { 247 | selected: update(this.state.selected, { 248 | currentPage: { $set: page }, 249 | totalPages: { $set: Math.ceil(sortedItems.length / maxItemsPerPage) }, 250 | pagedItems: { $set: pagedItems }, 251 | pageItems: { $set: pagedItems[page - 1] ? pagedItems[page - 1].filter(Boolean) : [] } 252 | }) 253 | }); 254 | } 255 | } 256 | 257 | 258 | render() { 259 | 260 | let renderAvailableListItems = () => { 261 | return _.map(this.state.available.pageItems, (key) => { 262 | return ( 263 | { this.selectedItemAdd(key) }} 266 | key={key} 267 | rightIcon={} 268 | primaryText={key} 269 | /> 270 | ) 271 | }); 272 | }; 273 | 274 | let renderSelectedListItems = () => { 275 | return _.map(this.state.selected.pageItems, (key) => { 276 | let style = {}; 277 | 278 | if (!_(this.state.available.items).includes(key)) { 279 | style = { color: "#FF7043" } 280 | } 281 | return ( 282 | { this.selectedItemRemove(key) }} 285 | style={style} 286 | key={key} 287 | rightIcon={} 288 | primaryText={key} 289 | /> 290 | ) 291 | }); 292 | }; 293 | 294 | return ( 295 |
296 |
297 | 298 | 299 | 300 | 301 | 302 | 303 | { 308 | this.setState({ available: update(this.state.available, { searchText: { $set: searchText } }) }); 309 | }} 310 | onNewRequest={(chosenRequest) => { 311 | if ( 312 | (!_.includes(this.props.excludePolicies, chosenRequest)) && 313 | (chosenRequest !== 'root') 314 | ) { 315 | this.selectedItemAdd(chosenRequest); 316 | this.setState({ available: update(this.state.available, { searchText: { $set: '' } }) }); 317 | } 318 | }} 319 | /> 320 | 321 | 322 |
323 | 324 | {renderAvailableListItems()} 325 | 326 |
327 |
328 | { 332 | this.setPage(LIST_TYPE.AVAILABLE, e) 333 | }} 334 | /> 335 |
336 |
337 | 338 |
339 | 340 | 341 | 342 | 343 | 344 |
345 | 346 | {renderSelectedListItems()} 347 | 348 |
349 |
350 |
351 | ) 352 | } 353 | } -------------------------------------------------------------------------------- /app/components/shared/ItemPicker/itempicker.css: -------------------------------------------------------------------------------- 1 | .ppOverlay { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | opacity: 0.5; 8 | background-color: red; 9 | } 10 | 11 | .ppTitle { 12 | /*background-color: #ECEFF1; 13 | color: #0C0C0C; 14 | -moz-border-radius: 5px; 15 | -webkit-border-radius: 5px; 16 | margin: 5px;*/ 17 | text-align: center; 18 | } 19 | 20 | .ppColumn { 21 | display: inline-block; 22 | vertical-align: top; 23 | width: 49%; 24 | -webkit-box-sizing: border-box; 25 | -moz-box-sizing: border-box; 26 | box-sizing: border-box; 27 | background-color: #E1F5FE; 28 | color: #fff; 29 | /*padding: 10px;*/ 30 | -moz-border-radius: 5px; 31 | -webkit-border-radius: 5px; 32 | overflow: auto; 33 | } 34 | 35 | .ppToolbar span { 36 | font-size: 12px !important; 37 | color: black !important; 38 | font-weight: bold !important; 39 | } 40 | 41 | 42 | .ppListContainer { 43 | overflow-y: auto; 44 | margin: 5px; 45 | } 46 | 47 | .ppListheader { 48 | 49 | } 50 | 51 | .ppList > div > div{ 52 | font-family: monospace; 53 | line-height: 1px; 54 | } 55 | 56 | .ppList svg{ 57 | font-family: monospace; 58 | line-height: 1px; 59 | margin: 0px !important; 60 | top: 5px !important; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /app/components/shared/JsonEditor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import JSONEditor from 'jsoneditor'; 4 | import JsonDiffReact from 'jsondiffpatch-for-react'; 5 | import Checkbox from 'material-ui/Checkbox'; 6 | import Divider from 'material-ui/Divider'; 7 | import 'jsoneditor/src/css/reset.css'; 8 | import 'jsoneditor/src/css/jsoneditor.css'; 9 | import 'jsoneditor/src/css/menu.css'; 10 | import 'jsoneditor/src/css/searchbox.css'; 11 | import 'jsoneditor/src/css/contextmenu.css'; 12 | 13 | function isValid(value) { 14 | return value !== '' && value !== undefined && value !== null; 15 | } 16 | 17 | class JsonEditor extends React.Component { 18 | static propTypes = { 19 | rootName: PropTypes.string, 20 | value: PropTypes.any, 21 | mode: PropTypes.oneOf(['tree', 'code', 'view']), 22 | modes: React.PropTypes.array, 23 | schema: PropTypes.object, 24 | height: PropTypes.string, 25 | onChange: PropTypes.func, 26 | }; 27 | 28 | static defaultProps = { 29 | rootName: '', 30 | value: '', 31 | mode: 'tree', 32 | modes: ['tree', 'code'], 33 | schema: null, 34 | onChange: () => {} 35 | }; 36 | 37 | state = { 38 | hasValue: false, 39 | initialValue: this.props.value, 40 | showDiff: true 41 | }; 42 | 43 | constructor(props) { 44 | super(props); 45 | if (typeof JSONEditor === undefined) { 46 | throw new Error('JSONEditor is undefined!'); 47 | } 48 | } 49 | 50 | handleInputChange = () => { 51 | try { 52 | this.setState({hasValue: isValid(this._jsoneditor.get())}); 53 | if (this.props.onChange) { 54 | let schemaCheck = true; 55 | if (this.props.schema) { 56 | schemaCheck = this._jsoneditor.validateSchema(this._jsoneditor.get()); 57 | } 58 | this.props.onChange(this._jsoneditor.get(), this.state.hasValue, schemaCheck); 59 | } 60 | } catch (e) { 61 | this.props.onChange(null, false, false); 62 | } 63 | } 64 | 65 | componentDidMount() { 66 | var container = this.editorEl;//ReactDOM.findDOMNode(this); 67 | var options = { 68 | name: this.props.rootName, 69 | mode: this.props.mode, 70 | modes: this.props.modes, 71 | schema: this.props.schema, 72 | onChange: this.handleInputChange, 73 | }; 74 | 75 | this._jsoneditor = new JSONEditor(container, options, this.props.value); 76 | this.setState({hasValue: true}); 77 | this._jsoneditor.focus(); 78 | } 79 | 80 | componentWillReceiveProps(nextProps) { 81 | if (nextProps.rootName !== this.props.rootName) { 82 | this._jsoneditor.setName(nextProps.rootName); 83 | } 84 | } 85 | 86 | componentWillUnmount() { 87 | this._jsoneditor.destroy(); 88 | } 89 | 90 | renderDiff = () => { 91 | if (_.isEqual(this.state.initialValue, this.props.value)) { 92 | return
No difference detected.
; 93 | } else { 94 | return ( 95 | 100 | ); 101 | } 102 | } 103 | 104 | render() { 105 | return ( 106 |
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 | }; --------------------------------------------------------------------------------