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

API rewrite + 0.6.0 compatibility #46

Merged
merged 20 commits into from
Feb 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"rules": {
"no-const-assign": "warn",
"no-this-before-super": "warn",
"no-undef": "warn",
"no-unreachable": "warn",
"no-unused-vars": "warn",
"constructor-super": "warn",
"valid-typeof": "warn",
"no-console": 0
},
"plugins": [
"react"
],
"extends": ["eslint:recommended", "plugin:react/recommended"]
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ jspm_packages

# build folder
dist/*

# Visual studio code
.vscode/
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
A beautiful way to manage your secrets in Vault
![Landing Page](https://github.com/djenriquez/vault-ui/raw/master/images/Landing.jpg)

![Secrets Management](https://github.com/djenriquez/vault-ui/raw/master/images/Secrets.png)
![New Secrets](https://github.com/djenriquez/vault-ui/raw/master/images/NewSecret.png)
![Secrets Management](https://github.com/djenriquez/vault-ui/raw/master/images/Home.png)

## Configuration
Configuration is accessed by clicking on the configuration cog on the login page.
![Configuration](https://github.com/djenriquez/vault-ui/raw/master/images/Config.png)

### Vault Endpoint
Users can enter in the full endpoint to Vault, including scheme. When running the docker image, it is possible to
set the environment variables `VAULT_URL_DEFAULT` and `VAULT_AUTH_DEFAULT`.
Expand All @@ -25,20 +25,36 @@ Currently supported authentication backends:
- [LDAP](https://www.vaultproject.io/docs/auth/ldap.html)
- [Tokens](https://www.vaultproject.io/docs/auth/token.html)

### Token authentication by header (SSO)
In some cases, users might want to use middleware to authenticate into Vault-UI for purposes like SSO. In this case, the `VAULT_SUPPLIED_TOKEN_HEADER` may be populated with the name of the header that contains a token to be used for authentication.

## Secrets
![Secrets Management](https://github.com/djenriquez/vault-ui/raw/master/images/Secrets.png)

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.
![New Secrets](https://github.com/djenriquez/vault-ui/raw/master/images/NewSecret.png)

Secrets also are accessible directly by key from a browser by navigating to the URI `/secrets/<mount>/<namespace>/key`. For example, if you have a generic secret key of /hello/world/vault using the generic mount `secret`, one can navigate to this directly through http://vault-ui.myorg.com/secrets/secret/hello/world/vault.

### Root key bias
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" }`.
<img src="https://github.com/djenriquez/vault-ui/raw/master/images/RootKey.png" height="240">

## Policies
Policies can be entered in as JSON or as HCL. If entered in as HCL, it will be converted to JSON as required for the PUT command in Vault's API. However, existing policies that are in HCL will continue to be displayed in HCL.
Policies are managed also using the [josdejong/jsoneditor](https://github.com/josdejong/jsoneditor) JSON editor. Currently, GitHub and raw Tokens are the only supported authentication backends for associated policies.

## Token Management
![Token Management](https://github.com/djenriquez/vault-ui/raw/master/images/TokenManagement.png)

Users now have the ability to create and revoke tokens.
![Token Management](https://github.com/djenriquez/vault-ui/raw/master/images/NewToken.png)

## Response Wrapping
Vault-UI supports response-wrapping raw values. It currently does not support wrapping of existing secrets.
![Response Wrapping](https://github.com/djenriquez/vault-ui/raw/master/images/ResponseWrapping.png)

## Run
Vault-UI Docker images are automatically built using an [automated build
on Docker Hub](https://hub.docker.com/r/djenriquez/vault-ui/builds/).
Vault-UI Docker images are automatically built using an [automated build on Docker Hub](https://hub.docker.com/r/djenriquez/vault-ui/builds/). We encourage that versioned images are used for production.
To run Vault-UI using the latest Docker image:
```bash
docker run -d \
Expand Down Expand Up @@ -67,6 +83,8 @@ into with username "test" and password "test":
./run-docker-compose-dev
```

If major changes are made, be sure to run `docker-compose build` to rebuild dependencies.

### Without Docker
The following will spin up a Vault UI server only. It will not set up
Vault for you:
Expand Down
27 changes: 15 additions & 12 deletions app/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Health from './components/Health/Health.jsx';
import Policies from './components/Policies/Home.jsx';
import Settings from './components/Settings/Settings.jsx';
import ResponseWrapper from './components/ResponseWrapper/ResponseWrapper.jsx';
import TokenManage from './components/Tokens/Manage.jsx'

injectTapEventPlugin();

Expand Down Expand Up @@ -44,16 +45,18 @@ const muiTheme = getMuiTheme({

ReactDOM.render((
<MuiThemeProvider muiTheme={muiTheme}>
<Router history={browserHistory}>
<Route path="/login" component={Login}/>
<Route path="/" component={App} onEnter={checkAccessToken}>
<Route path="/secrets" component={Secrets}/>
<Route path="/health" component={Health}/>
<Route path="/secrets" component={Secrets}/>
<Route path="/settings" component={Settings}/>
<Route path="/responsewrapper" component={ResponseWrapper}/>
<Route path="/policies/:policy" component={Policies}/>
</Route>
</Router>
</MuiThemeProvider>
<Router history={browserHistory}>
<Route path="/login" component={Login}/>
<Route path="/" component={App} onEnter={checkAccessToken}>
<Route path="/secrets" component={Secrets}>
<Route path="**" component={Secrets}/>
</Route>
<Route path="/health" component={Health}/>
<Route path="/settings" component={Settings}/>
<Route path="/responsewrapper" component={ResponseWrapper}/>
<Route path="/policies/:policy" component={Policies}/>
<Route path="/tokens" component={TokenManage}/>
</Route>
</Router>
</MuiThemeProvider>
), document.getElementById('app'))
3 changes: 3 additions & 0 deletions app/components/App/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export default class App extends React.Component {
if (!window.localStorage.getItem('showDeleteModal')) {
window.localStorage.setItem('showDeleteModal', 'true');
}
if (!window.localStorage.getItem('enableCapabilitiesCache')) {
window.localStorage.setItem('enableCapabilitiesCache', 'true');
}
document.addEventListener("snackbar", (e) => {
this.setState({
snackbarMessage: e.detail.message,
Expand Down
172 changes: 100 additions & 72 deletions app/components/Login/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import Dialog from 'material-ui/Dialog';
import SelectField from 'material-ui/SelectField';
import MenuItem from 'material-ui/MenuItem';
import FlatButton from 'material-ui/FlatButton';
import Snackbar from 'material-ui/Snackbar';
import { browserHistory } from 'react-router';
import axios from 'axios';
import _ from 'lodash';
import { callVaultApi } from '../shared/VaultUtils.jsx'

export default class Login extends React.Component {
constructor(props) {
Expand All @@ -26,7 +28,7 @@ export default class Login extends React.Component {
password: "",
loginMethodType: this.getVaultAuthMethod(),
tmpLoginMethodType: this.getVaultAuthMethod(),
settingsChanged: false
settingsChanged: false,
};

_.bindAll(
Expand All @@ -37,14 +39,15 @@ export default class Login extends React.Component {
'renderSettingsDialog',
'renderSelectedLoginOption',
'validateUsernamePassword',
'checkSettings'
'checkSettings',
'login'
)

// If a token was supplied in the window.suppliedAuthToken variable, then simulate a login
if ( window.suppliedAuthToken && this.state.vaultUrl ) {
if (window.suppliedAuthToken && this.state.vaultUrl) {
this.state.loginMethodType = 'TOKEN';
this.state.authToken = window.suppliedAuthToken;
this.validateToken({keyCode: 13});
this.validateToken({ keyCode: 13 });
}

}
Expand All @@ -59,17 +62,76 @@ export default class Login extends React.Component {
}

getVaultUrl() {
if (window.localStorage.getItem("vaultUrl"))
return window.localStorage.getItem("vaultUrl");
else
return window.defaultUrl;
if (window.localStorage.getItem("vaultUrl"))
return window.localStorage.getItem("vaultUrl");
else
return window.defaultUrl;
}

getVaultAuthMethod() {
if (window.localStorage.getItem("loginMethodType"))
return window.localStorage.getItem("loginMethodType");
else
return window.defaultAuth;
if (window.localStorage.getItem("loginMethodType"))
return window.localStorage.getItem("loginMethodType");
else
return window.defaultAuth;
}

login() {
let method = '';
let uri = '';
let query = null;
let data = null;
let headers = null;

switch (this.state.loginMethodType) {
case "TOKEN":
method = 'get';
uri = 'auth/token/lookup-self';
headers = { "X-Vault-Token": this.state.authToken };
break;
case "GITHUB":
method = 'post';
uri = `auth/github/login`;
data = { token: this.state.authToken };
break;
case "LDAP":
method = 'post';
uri = `auth/ldap/login/${this.state.username}`;
data = { password: this.state.password };
break;
case "USERNAMEPASSWORD":
method = 'post';
uri = `auth/userpass/login/${this.state.username}`;
data = { password: this.state.password };
break;
default:
throw new Error(`Login method type: '${this.state.loginMethodType}' is not supported`);
}

let instance = axios.create({
baseURL: '/v1/'
});

instance.request({
url: uri,
method: method,
data: data,
params: { "vaultaddr": this.state.vaultUrl },
headers: headers
})
.then((resp) => {
//console.log(resp);
if (this.state.loginMethodType == "TOKEN") {
this.setAccessToken({
client_token: resp.data.data.id,
lease_duration: resp.data.lease_duration
});
} else {
this.setAccessToken(resp.data.auth);
}
})
.catch((error) => {
this.setState({ errorMessage: `Error: ${error}` });
});
}

validateUsernamePassword(e) {
Expand All @@ -87,21 +149,8 @@ export default class Login extends React.Component {
this.setState({ errorMessage: "No password provided." });
return;
}
axios.post('/login', {
"VaultUrl": this.getVaultUrl(),
"Creds": {
"Type": this.getVaultAuthMethod(),
"Username": this.state.username,
"Password": this.state.password
}
})
.then((resp) => {
this.setAccessToken(resp);
})
.catch((err) => {
console.error(err);
this.setState({ errorMessage: err.response.data })
});

this.login();
}
}

Expand All @@ -115,17 +164,7 @@ export default class Login extends React.Component {
this.setState({ errorMessage: "No auth token provided." });
return;
}
axios.post('/login', { "VaultUrl": this.getVaultUrl(), "Creds": { "Type": this.state.loginMethodType, "Token": this.state.authToken } })
.then((resp) => {
this.setAccessToken(resp);
})
.catch((err) => {
console.error(err.stack);
this.setState({
errorMessage: `${window.suppliedAuthToken ? 'Login was attempted using a server supplied token. Please contact your network administrator. -- ' : ''}${err.response.data}`,
loginMethodType: this.getVaultAuthMethod()
});
});
this.login();
}
}

Expand All @@ -139,28 +178,17 @@ export default class Login extends React.Component {
this.setState({ errorMessage: "No auth token provided." });
return;
}
axios.post('/login', { "VaultUrl": this.getVaultUrl(), "Creds": { "Type": this.state.loginMethodType, "Token": this.state.authToken } })
.then((resp) => {
this.setAccessToken(resp);
})
.catch((err) => {
console.error(err.stack);
this.setState({ errorMessage: err.response.data })
});

this.login();
}
}

setAccessToken(resp) {
// { client_token: '145a495d-dc52-4539-1de8-94e819ba1317',
// accessor: '1275f43d-1287-7df2-d17a-6956181a5238',
// policies: [ 'default', 'insp-power-user' ],
// metadata: { org: 'Openmail', username: 'djenriquez' },
// lease_duration: 3600,
// renewable: true }
let accessToken = _.get(resp, 'data.client_token');
let accessToken = _.get(resp, 'client_token');
if (accessToken) {
window.localStorage.setItem('capability_cache', JSON.stringify({}));
window.localStorage.setItem("vaultAccessToken", accessToken);
let leaseDuration = _.get(resp, 'data.lease_duration') === 0 ? -1 : _.get(resp, 'data.lease_duration') * 1000
let leaseDuration = _.get(resp, 'lease_duration') === 0 ? -1 : _.get(resp, 'lease_duration') * 1000
window.localStorage.setItem('vaultAccessTokenExpiration', leaseDuration)
window.localStorage.setItem('vaultUrl', this.getVaultUrl());
window.localStorage.setItem('loginMethodType', this.getVaultAuthMethod());
Expand Down Expand Up @@ -266,24 +294,24 @@ export default class Login extends React.Component {
);
case "LDAP":
return (
<div>
<TextField
fullWidth={true}
className="col-xs-12"
hintText="Enter LDAP username"
onKeyDown={this.validateUsernamePassword}
onChange={(e, v) => this.setState({ username: v })}
/>
<TextField
fullWidth={true}
className="col-xs-12"
type="password"
hintText="Enter LDAP password"
onKeyDown={this.validateUsernamePassword}
onChange={(e, v) => this.setState({ password: v })}
/>
<div className={styles.error}>{this.state.errorMessage}</div>
</div>
<div>
<TextField
fullWidth={true}
className="col-xs-12"
hintText="Enter LDAP username"
onKeyDown={this.validateUsernamePassword}
onChange={(e, v) => this.setState({ username: v })}
/>
<TextField
fullWidth={true}
className="col-xs-12"
type="password"
hintText="Enter LDAP password"
onKeyDown={this.validateUsernamePassword}
onChange={(e, v) => this.setState({ password: v })}
/>
<div className={styles.error}>{this.state.errorMessage}</div>
</div>
);
case "USERNAMEPASSWORD":
return (
Expand Down
Loading