commit
3d52c8c7c1
|
@ -65,6 +65,7 @@ tags
|
|||
ui/dist
|
||||
ui/tmp
|
||||
ui/root
|
||||
http/bindata_assetfs.go
|
||||
|
||||
# dependencies
|
||||
ui/node_modules
|
||||
|
|
|
@ -452,6 +452,7 @@ func (c *ServerCommand) Run(args []string) int {
|
|||
ClusterName: config.ClusterName,
|
||||
CacheSize: config.CacheSize,
|
||||
PluginDirectory: config.PluginDirectory,
|
||||
EnableUI: config.EnableUI,
|
||||
EnableRaw: config.EnableRawEndpoint,
|
||||
}
|
||||
if c.flagDev {
|
||||
|
@ -607,6 +608,16 @@ CLUSTER_SYNTHESIS_COMPLETE:
|
|||
coreConfig.ClusterAddr = u.String()
|
||||
}
|
||||
|
||||
// Override the UI enabling config by the environment variable
|
||||
if enableUI := os.Getenv("VAULT_UI"); enableUI != "" {
|
||||
var err error
|
||||
coreConfig.EnableUI, err = strconv.ParseBool(enableUI)
|
||||
if err != nil {
|
||||
c.UI.Output("Error parsing the environment variable VAULT_UI")
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the core
|
||||
core, newCoreError := vault.NewCore(coreConfig)
|
||||
if newCoreError != nil {
|
||||
|
|
|
@ -6,9 +6,11 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/hashicorp/errwrap"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/vault/helper/consts"
|
||||
|
@ -54,6 +56,9 @@ const (
|
|||
|
||||
var (
|
||||
ReplicationStaleReadTimeout = 2 * time.Second
|
||||
|
||||
// Set to false by stub_asset if the ui build tag isn't enabled
|
||||
uiBuiltIn = true
|
||||
)
|
||||
|
||||
// Handler returns an http.Handler for the API. This can be used on
|
||||
|
@ -82,6 +87,14 @@ func Handler(core *vault.Core) http.Handler {
|
|||
}
|
||||
mux.Handle("/v1/sys/", handleRequestForwarding(core, handleLogical(core, false, nil)))
|
||||
mux.Handle("/v1/", handleRequestForwarding(core, handleLogical(core, false, nil)))
|
||||
if core.UIEnabled() == true {
|
||||
if uiBuiltIn {
|
||||
mux.Handle("/ui/", http.StripPrefix("/ui/", handleUIHeaders(core, handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()})))))
|
||||
} else {
|
||||
mux.Handle("/ui/", handleUIHeaders(core, handleUIStub()))
|
||||
}
|
||||
mux.Handle("/", handleRootRedirect())
|
||||
}
|
||||
|
||||
// Wrap the handler in another handler to trigger all help paths.
|
||||
helpWrappedHandler := wrapHelpHandler(mux, core)
|
||||
|
@ -145,6 +158,72 @@ func stripPrefix(prefix, path string) (string, bool) {
|
|||
return path, true
|
||||
}
|
||||
|
||||
func handleUIHeaders(core *vault.Core, h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
header := w.Header()
|
||||
|
||||
userHeaders, err := core.UIHeaders()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if userHeaders != nil {
|
||||
for k := range userHeaders {
|
||||
v := userHeaders.Get(k)
|
||||
header.Set(k, v)
|
||||
}
|
||||
}
|
||||
h.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
func handleUI(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
h.ServeHTTP(w, req)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func handleUIStub() http.Handler {
|
||||
stubHTML := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<p>Vault UI is not available in this binary. To get Vault UI do one of the following:</p>
|
||||
<ul>
|
||||
<li><a href="https://www.vaultproject.io/downloads.html">Download an official release</a></li>
|
||||
<li>Run <code>make release</code> to create your own release binaries.
|
||||
<li>Run <code>make dev-ui</code> to create a development binary with the UI.
|
||||
</ul>
|
||||
</html>
|
||||
`
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Write([]byte(stubHTML))
|
||||
})
|
||||
}
|
||||
|
||||
func handleRootRedirect() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, "/ui/", 307)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
type UIAssetWrapper struct {
|
||||
FileSystem *assetfs.AssetFS
|
||||
}
|
||||
|
||||
func (fs *UIAssetWrapper) Open(name string) (http.File, error) {
|
||||
file, err := fs.FileSystem.Open(name)
|
||||
if err == nil {
|
||||
return file, nil
|
||||
}
|
||||
// serve index.html instead of 404ing
|
||||
if err == os.ErrNotExist {
|
||||
return fs.FileSystem.Open("index.html")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func parseRequest(r *http.Request, w http.ResponseWriter, out interface{}) error {
|
||||
// Limit the maximum number of bytes to MaxRequestSize to protect
|
||||
// against an indefinite amount of data being read.
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// +build !ui
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||
)
|
||||
|
||||
func init() {
|
||||
uiBuiltIn = false
|
||||
}
|
||||
|
||||
// assetFS is a stub for building Vault without a UI.
|
||||
func assetFS() *assetfs.AssetFS {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"directory": "bower_components",
|
||||
"analytics": false
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.hbs]
|
||||
insert_final_newline = false
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
/**
|
||||
Ember CLI sends analytics information by default. The data is completely
|
||||
anonymous, but there are times when you might want to disable this behavior.
|
||||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": true,
|
||||
"output-path": "../pkg/web_ui"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2017,
|
||||
sourceType: 'module',
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
},
|
||||
rules: {
|
||||
"no-unused-vars": ["error", { "ignoreRestSiblings": true }]
|
||||
},
|
||||
globals: {
|
||||
base64js: true,
|
||||
TextEncoderLite: true,
|
||||
TextDecoderLite: true,
|
||||
Duration: true
|
||||
}
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/bower_components
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage/*
|
||||
/libpeerconnection.log
|
||||
npm-debug.log*
|
||||
yarn-error.log
|
||||
testem.log
|
||||
|
||||
# ember-try
|
||||
.node_modules.ember-try/
|
||||
bower.json.ember-try
|
||||
package.json.ember-try
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
language: node_js
|
||||
node_js:
|
||||
- "4"
|
||||
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.npm
|
||||
- $HOME/.cache # includes bowers cache
|
||||
|
||||
before_install:
|
||||
- npm config set spin false
|
||||
- npm install -g bower
|
||||
- bower --version
|
||||
- npm install phantomjs-prebuilt
|
||||
- node_modules/phantomjs-prebuilt/bin/phantomjs --version
|
||||
|
||||
install:
|
||||
- npm install
|
||||
- bower install
|
||||
|
||||
script:
|
||||
- npm test
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignore_dirs": ["tmp", "dist"]
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
# vault
|
||||
|
||||
This README outlines the details of collaborating on this Ember application.
|
||||
A short introduction of this app could easily go here.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You will need the following things properly installed on your computer.
|
||||
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Node.js](https://nodejs.org/) (with NPM)
|
||||
* [Bower](https://bower.io/)
|
||||
* [Ember CLI](https://ember-cli.com/)
|
||||
|
||||
|
||||
## Running / Development
|
||||
|
||||
* `ember serve`
|
||||
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
||||
|
||||
### Code Generators
|
||||
|
||||
Make use of the many generators for code, try `ember help generate` for more details
|
||||
|
||||
### Running Tests
|
||||
|
||||
* `ember test`
|
||||
* `ember test --server`
|
||||
|
||||
### Building
|
||||
|
||||
* `ember build` (development)
|
||||
* `ember build --environment production` (production)
|
||||
|
||||
### Deploying
|
||||
|
||||
Specify what it takes to deploy your app.
|
||||
|
||||
## Further Reading / Useful Links
|
||||
|
||||
* [ember.js](http://emberjs.com/)
|
||||
* [ember-cli](https://ember-cli.com/)
|
||||
* Development Browser Extensions
|
||||
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
|
||||
* [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)
|
|
@ -0,0 +1,86 @@
|
|||
import Ember from 'ember';
|
||||
import DS from 'ember-data';
|
||||
import fetch from 'fetch';
|
||||
|
||||
const POLLING_URL_PATTERNS = ['sys/seal-status', 'sys/health', 'sys/replication/status'];
|
||||
|
||||
export default DS.RESTAdapter.extend({
|
||||
auth: Ember.inject.service(),
|
||||
|
||||
flashMessages: Ember.inject.service(),
|
||||
|
||||
namespace: 'v1/sys',
|
||||
|
||||
shouldReloadAll() {
|
||||
return true;
|
||||
},
|
||||
|
||||
shouldReloadRecord() {
|
||||
return true;
|
||||
},
|
||||
|
||||
shouldBackgroundReloadRecord() {
|
||||
return false;
|
||||
},
|
||||
|
||||
_preRequest(url, options) {
|
||||
const token = this.get('auth.currentToken');
|
||||
if (token && !options.unauthenticated) {
|
||||
options.headers = Ember.assign(options.headers || {}, {
|
||||
'X-Vault-Token': token,
|
||||
});
|
||||
if (options.wrapTTL) {
|
||||
Ember.assign(options.headers, { 'X-Vault-Wrap-TTL': options.wrapTTL });
|
||||
}
|
||||
}
|
||||
const isPolling = POLLING_URL_PATTERNS.some(str => url.includes(str));
|
||||
if (!isPolling) {
|
||||
this.get('auth').setLastFetch(Date.now());
|
||||
}
|
||||
if (this.get('auth.shouldRenew')) {
|
||||
this.get('auth').renew();
|
||||
}
|
||||
options.timeout = 60000;
|
||||
return options;
|
||||
},
|
||||
|
||||
ajax(url, type, options = {}) {
|
||||
let opts = this._preRequest(url, options);
|
||||
|
||||
return this._super(url, type, opts).then((...args) => {
|
||||
const [resp] = args;
|
||||
if (resp && resp.warnings) {
|
||||
const flash = this.get('flashMessages');
|
||||
resp.warnings.forEach(message => {
|
||||
flash.info(message);
|
||||
});
|
||||
}
|
||||
return Ember.RSVP.resolve(...args);
|
||||
});
|
||||
},
|
||||
|
||||
// for use on endpoints that don't return JSON responses
|
||||
rawRequest(url, type, options = {}) {
|
||||
let opts = this._preRequest(url, options);
|
||||
return fetch(url, {
|
||||
method: type | 'GET',
|
||||
headers: opts.headers | {},
|
||||
}).then(response => {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return Ember.RSVP.resolve(response);
|
||||
} else {
|
||||
return Ember.RSVP.reject();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleResponse(status, headers, payload, requestData) {
|
||||
const returnVal = this._super(...arguments);
|
||||
// ember data errors don't have the status code, so we add it here
|
||||
if (returnVal instanceof DS.AdapterError) {
|
||||
Ember.set(returnVal, 'httpStatus', status);
|
||||
Ember.set(returnVal, 'path', requestData.url);
|
||||
}
|
||||
return returnVal;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import ApplicationAdapter from '../application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: '/v1/auth',
|
||||
|
||||
pathForType(modelType) {
|
||||
// we want the last part of the path
|
||||
const type = modelType.split('/').pop();
|
||||
if (type === 'identity-whitelist' || type === 'roletag-blacklist') {
|
||||
return `tidy/${type}`;
|
||||
}
|
||||
return type;
|
||||
},
|
||||
|
||||
buildURL(modelName, id, snapshot) {
|
||||
const backendId = id ? id : snapshot.belongsTo('backend').id;
|
||||
let url = `${this.get('namespace')}/${backendId}/config`;
|
||||
// aws has a lot more config endpoints
|
||||
if (modelName.includes('aws')) {
|
||||
url = `${url}/${this.pathForType(modelName)}`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
const id = snapshot.belongsTo('backend').id;
|
||||
return this._super(...arguments).then(() => {
|
||||
return { id };
|
||||
});
|
||||
},
|
||||
|
||||
updateRecord(store, type, snapshot) {
|
||||
const id = snapshot.belongsTo('backend').id;
|
||||
return this._super(...arguments).then(() => {
|
||||
return { id };
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
import AuthConfig from '../_base';
|
||||
export default AuthConfig.extend();
|
|
@ -0,0 +1,2 @@
|
|||
import AuthConfig from '../_base';
|
||||
export default AuthConfig.extend();
|
|
@ -0,0 +1,2 @@
|
|||
import AuthConfig from '../_base';
|
||||
export default AuthConfig.extend();
|
|
@ -0,0 +1,2 @@
|
|||
import AuthConfig from './_base';
|
||||
export default AuthConfig.extend();
|
|
@ -0,0 +1,2 @@
|
|||
import AuthConfig from './_base';
|
||||
export default AuthConfig.extend();
|
|
@ -0,0 +1,2 @@
|
|||
import AuthConfig from './_base';
|
||||
export default AuthConfig.extend();
|
|
@ -0,0 +1,2 @@
|
|||
import AuthConfig from './_base';
|
||||
export default AuthConfig.extend();
|
|
@ -0,0 +1,2 @@
|
|||
import AuthConfig from './_base';
|
||||
export default AuthConfig.extend();
|
|
@ -0,0 +1,2 @@
|
|||
import AuthConfig from './_base';
|
||||
export default AuthConfig.extend();
|
|
@ -0,0 +1,41 @@
|
|||
import Ember from 'ember';
|
||||
import ApplicationAdapter from './application';
|
||||
import DS from 'ember-data';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
url(path) {
|
||||
const url = `${this.buildURL()}/auth`;
|
||||
return path ? url + '/' + path : url;
|
||||
},
|
||||
|
||||
// used in updateRecord on the model#tune action
|
||||
pathForType() {
|
||||
return 'mounts/auth';
|
||||
},
|
||||
|
||||
findAll() {
|
||||
return this.ajax(this.url(), 'GET').catch(e => {
|
||||
if (e instanceof DS.AdapterError) {
|
||||
Ember.set(e, 'policyPath', 'sys/auth');
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot);
|
||||
const path = snapshot.attr('path');
|
||||
|
||||
return this.ajax(this.url(path), 'POST', { data }).then(() => {
|
||||
// ember data doesn't like 204s if it's not a DELETE
|
||||
return {
|
||||
data: Ember.assign({}, data, { path: path + '/', id: path }),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
urlForDeleteRecord(id, modelName, snapshot) {
|
||||
return this.url(snapshot.id);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import ApplicationAdapter from './application';
|
||||
import DS from 'ember-data';
|
||||
import Ember from 'ember';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
pathForType() {
|
||||
return 'capabilities-self';
|
||||
},
|
||||
|
||||
findRecord(store, type, id) {
|
||||
return this.ajax(this.buildURL(type), 'POST', { data: { path: id } }).catch(e => {
|
||||
if (e instanceof DS.AdapterError) {
|
||||
Ember.set(e, 'policyPath', 'sys/capabilities-self');
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
const { id } = query;
|
||||
return this.findRecord(store, type, id).then(resp => {
|
||||
resp.path = id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,187 @@
|
|||
import Ember from 'ember';
|
||||
import ApplicationAdapter from './application';
|
||||
import DS from 'ember-data';
|
||||
|
||||
const { AdapterError } = DS;
|
||||
const { assert, inject } = Ember;
|
||||
|
||||
const ENDPOINTS = ['health', 'seal-status', 'tokens', 'token', 'seal', 'unseal', 'init', 'capabilities-self'];
|
||||
|
||||
const REPLICATION_ENDPOINTS = {
|
||||
reindex: 'reindex',
|
||||
recover: 'recover',
|
||||
status: 'status',
|
||||
|
||||
primary: ['enable', 'disable', 'demote', 'secondary-token', 'revoke-secondary'],
|
||||
|
||||
secondary: ['enable', 'disable', 'promote', 'update-primary'],
|
||||
};
|
||||
|
||||
const REPLICATION_MODES = ['dr', 'performance'];
|
||||
export default ApplicationAdapter.extend({
|
||||
version: inject.service(),
|
||||
shouldBackgroundReloadRecord() {
|
||||
return true;
|
||||
},
|
||||
findRecord(store, type, id, snapshot) {
|
||||
let fetches = {
|
||||
health: this.health(),
|
||||
sealStatus: this.sealStatus().catch(e => e),
|
||||
};
|
||||
if (this.get('version.isEnterprise')) {
|
||||
fetches.replicationStatus = this.replicationStatus().catch(e => e);
|
||||
}
|
||||
return Ember.RSVP.hash(fetches).then(({ health, sealStatus, replicationStatus }) => {
|
||||
let ret = {
|
||||
id,
|
||||
name: snapshot.attr('name'),
|
||||
};
|
||||
ret = Ember.assign(ret, health);
|
||||
if (sealStatus instanceof AdapterError === false) {
|
||||
ret = Ember.assign(ret, { nodes: [sealStatus] });
|
||||
}
|
||||
if (replicationStatus && replicationStatus instanceof AdapterError === false) {
|
||||
ret = Ember.assign(ret, replicationStatus.data);
|
||||
}
|
||||
return Ember.RSVP.resolve(ret);
|
||||
});
|
||||
},
|
||||
|
||||
pathForType(type) {
|
||||
return type === 'cluster' ? 'clusters' : Ember.String.pluralize(type);
|
||||
},
|
||||
|
||||
health() {
|
||||
return this.ajax(this.urlFor('health'), 'GET', {
|
||||
data: { standbycode: 200, sealedcode: 200, uninitcode: 200, drsecondarycode: 200 },
|
||||
unauthenticated: true,
|
||||
});
|
||||
},
|
||||
|
||||
features() {
|
||||
return this.ajax(`${this.buildURL()}/license/features`, 'GET', {
|
||||
unauthenticated: true,
|
||||
});
|
||||
},
|
||||
|
||||
sealStatus() {
|
||||
return this.ajax(this.urlFor('seal-status'), 'GET', { unauthenticated: true });
|
||||
},
|
||||
|
||||
seal() {
|
||||
return this.ajax(this.urlFor('seal'), 'PUT');
|
||||
},
|
||||
|
||||
unseal(data) {
|
||||
return this.ajax(this.urlFor('unseal'), 'PUT', {
|
||||
data,
|
||||
unauthenticated: true,
|
||||
});
|
||||
},
|
||||
|
||||
initCluster(data) {
|
||||
return this.ajax(this.urlFor('init'), 'PUT', {
|
||||
data,
|
||||
unauthenticated: true,
|
||||
});
|
||||
},
|
||||
|
||||
authenticate({ backend, data }) {
|
||||
const { token, password, username, path } = data;
|
||||
const url = this.urlForAuth(backend, username, path);
|
||||
const verb = backend === 'token' ? 'GET' : 'POST';
|
||||
let options = {
|
||||
unauthenticated: true,
|
||||
};
|
||||
if (backend === 'token') {
|
||||
options.headers = {
|
||||
'X-Vault-Token': token,
|
||||
};
|
||||
} else {
|
||||
options.data = token ? { token, password } : { password };
|
||||
}
|
||||
|
||||
return this.ajax(url, verb, options);
|
||||
},
|
||||
|
||||
urlFor(endpoint) {
|
||||
if (!ENDPOINTS.includes(endpoint)) {
|
||||
throw new Error(
|
||||
`Calls to a ${endpoint} endpoint are not currently allowed in the vault cluster adapater`
|
||||
);
|
||||
}
|
||||
return `${this.buildURL()}/${endpoint}`;
|
||||
},
|
||||
|
||||
urlForAuth(type, username, path) {
|
||||
const authBackend = type.toLowerCase();
|
||||
const authURLs = {
|
||||
github: 'login',
|
||||
userpass: `login/${encodeURIComponent(username)}`,
|
||||
ldap: `login/${encodeURIComponent(username)}`,
|
||||
okta: `login/${encodeURIComponent(username)}`,
|
||||
token: 'lookup-self',
|
||||
};
|
||||
const urlSuffix = authURLs[authBackend];
|
||||
const urlPrefix = path && authBackend !== 'token' ? path : authBackend;
|
||||
if (!urlSuffix) {
|
||||
throw new Error(`There is no auth url for ${type}.`);
|
||||
}
|
||||
return `/v1/auth/${urlPrefix}/${urlSuffix}`;
|
||||
},
|
||||
|
||||
urlForReplication(replicationMode, clusterMode, endpoint) {
|
||||
let suffix;
|
||||
const errString = `Calls to replication ${endpoint} endpoint are not currently allowed in the vault cluster adapater`;
|
||||
if (clusterMode) {
|
||||
assert(errString, REPLICATION_ENDPOINTS[clusterMode].includes(endpoint));
|
||||
suffix = `${replicationMode}/${clusterMode}/${endpoint}`;
|
||||
} else {
|
||||
assert(errString, REPLICATION_ENDPOINTS[endpoint]);
|
||||
suffix = `${endpoint}`;
|
||||
}
|
||||
return `${this.buildURL()}/replication/${suffix}`;
|
||||
},
|
||||
|
||||
replicationStatus() {
|
||||
return this.ajax(`${this.buildURL()}/replication/status`, 'GET', { unauthenticated: true });
|
||||
},
|
||||
|
||||
replicationDrPromote(data, options) {
|
||||
const verb = options && options.checkStatus ? 'GET' : 'PUT';
|
||||
return this.ajax(`${this.buildURL()}/replication/dr/secondary/promote`, verb, {
|
||||
data,
|
||||
unauthenticated: true,
|
||||
});
|
||||
},
|
||||
|
||||
generateDrOperationToken(data, options) {
|
||||
const verb = options && options.checkStatus ? 'GET' : 'PUT';
|
||||
let url = `${this.buildURL()}/replication/dr/secondary/generate-operation-token/`;
|
||||
if (!data || data.pgp_key || data.otp) {
|
||||
// start the generation
|
||||
url = url + 'attempt';
|
||||
} else {
|
||||
// progress the operation
|
||||
url = url + 'update';
|
||||
}
|
||||
return this.ajax(url, verb, {
|
||||
data,
|
||||
unauthenticated: true,
|
||||
});
|
||||
},
|
||||
|
||||
replicationAction(action, replicationMode, clusterMode, data) {
|
||||
assert(
|
||||
`${replicationMode} is an unsupported replication mode.`,
|
||||
replicationMode && REPLICATION_MODES.includes(replicationMode)
|
||||
);
|
||||
|
||||
const url =
|
||||
action === 'recover' || action === 'reindex'
|
||||
? this.urlForReplication(replicationMode, null, action)
|
||||
: this.urlForReplication(replicationMode, clusterMode, action);
|
||||
|
||||
return this.ajax(url, 'POST', { data });
|
||||
},
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
url(role, isSTS) {
|
||||
if (isSTS) {
|
||||
return `/v1/${role.backend}/sts/${role.name}`;
|
||||
}
|
||||
return `/v1/${role.backend}/creds/${role.name}`;
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
const isSTS = snapshot.attr('withSTS');
|
||||
const options = isSTS ? { data: { ttl: snapshot.attr('ttl') } } : {};
|
||||
const method = isSTS ? 'POST' : 'GET';
|
||||
const role = snapshot.attr('role');
|
||||
const url = this.url(role, isSTS);
|
||||
|
||||
return this.ajax(url, method, options).then(response => {
|
||||
response.id = snapshot.id;
|
||||
response.modelName = type.modelName;
|
||||
store.pushPayload(type.modelName, response);
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import ApplicationAdapater from '../application';
|
||||
|
||||
export default ApplicationAdapater.extend({
|
||||
namespace: 'v1',
|
||||
pathForType(type) {
|
||||
return type;
|
||||
},
|
||||
|
||||
urlForQuery() {
|
||||
return this._super(...arguments) + '?list=true';
|
||||
},
|
||||
|
||||
query(store, type) {
|
||||
return this.ajax(this.buildURL(type.modelName, null, null, 'query'), 'GET');
|
||||
},
|
||||
|
||||
buildURL(modelName, id, snapshot, requestType, query) {
|
||||
if (requestType === 'createRecord') {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
return this._super(`${modelName}/id`, id, snapshot, requestType, query);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import IdentityAdapter from './base';
|
||||
|
||||
export default IdentityAdapter.extend();
|
|
@ -0,0 +1,17 @@
|
|||
import IdentityAdapter from './base';
|
||||
|
||||
export default IdentityAdapter.extend({
|
||||
buildURL() {
|
||||
// first arg is modelName which we're hardcoding in the call to _super.
|
||||
let [, ...args] = arguments;
|
||||
return this._super('identity/entity/merge', ...args);
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
return this._super(...arguments).then(() => {
|
||||
// return the `to` id here so we can redirect to it on success
|
||||
// (and because ember _loves_ 204s for createRecord)
|
||||
return { id: snapshot.attr('toEntityId') };
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import IdentityAdapter from './base';
|
||||
|
||||
export default IdentityAdapter.extend({
|
||||
lookup(store, data) {
|
||||
let url = `/${this.urlPrefix()}/identity/lookup/entity`;
|
||||
return this.ajax(url, 'POST', { data }).then(response => {
|
||||
// unsuccessful lookup is a 204
|
||||
if (!response) return;
|
||||
let modelName = 'identity/entity';
|
||||
store.push(
|
||||
store
|
||||
.serializerFor(modelName)
|
||||
.normalizeResponse(store, store.modelFor(modelName), response, response.data.id, 'findRecord')
|
||||
);
|
||||
return response;
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import IdentityAdapter from './base';
|
||||
|
||||
export default IdentityAdapter.extend();
|
|
@ -0,0 +1,18 @@
|
|||
import IdentityAdapter from './base';
|
||||
|
||||
export default IdentityAdapter.extend({
|
||||
lookup(store, data) {
|
||||
let url = `/${this.urlPrefix()}/identity/lookup/group`;
|
||||
return this.ajax(url, 'POST', { data }).then(response => {
|
||||
// unsuccessful lookup is a 204
|
||||
if (!response) return;
|
||||
let modelName = 'identity/group';
|
||||
store.push(
|
||||
store
|
||||
.serializerFor(modelName)
|
||||
.normalizeResponse(store, store.modelFor(modelName), response, response.data.id, 'findRecord')
|
||||
);
|
||||
return response;
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
revokePrefix(prefix) {
|
||||
let url = this.buildURL() + '/leases/revoke-prefix/' + prefix;
|
||||
url = url.replace(/\/$/, '');
|
||||
return this.ajax(url, 'PUT');
|
||||
},
|
||||
|
||||
forceRevokePrefix(prefix) {
|
||||
let url = this.buildURL() + '/leases/revoke-prefix/' + prefix;
|
||||
url = url.replace(/\/$/, '');
|
||||
return this.ajax(url, 'PUT');
|
||||
},
|
||||
|
||||
renew(lease_id, interval) {
|
||||
let url = this.buildURL() + '/leases/renew';
|
||||
return this.ajax(url, 'PUT', {
|
||||
data: {
|
||||
lease_id,
|
||||
interval,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const lease_id = snapshot.id;
|
||||
return this.ajax(this.buildURL() + '/leases/revoke', 'PUT', {
|
||||
data: {
|
||||
lease_id,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
const { lease_id } = query;
|
||||
return this.ajax(this.buildURL() + '/leases/lookup', 'PUT', {
|
||||
data: {
|
||||
lease_id,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
const prefix = query.prefix || '';
|
||||
return this.ajax(this.buildURL() + '/leases/lookup/' + prefix, 'GET', {
|
||||
data: {
|
||||
list: true,
|
||||
},
|
||||
}).then(resp => {
|
||||
if (prefix) {
|
||||
resp.prefix = prefix;
|
||||
}
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
url(id) {
|
||||
return `${this.buildURL()}/replication/performance/primary/mount-filter/${id}`;
|
||||
},
|
||||
|
||||
findRecord(store, type, id) {
|
||||
return this.ajax(this.url(id), 'GET').then(resp => {
|
||||
resp.id = id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
return this.ajax(this.url(snapshot.id), 'PUT', {
|
||||
data: this.serialize(snapshot),
|
||||
});
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createRecord(...arguments);
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
return this.ajax(this.url(snapshot.id), 'DELETE');
|
||||
},
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend();
|
|
@ -0,0 +1,8 @@
|
|||
import Adapter from './pki';
|
||||
|
||||
export default Adapter.extend({
|
||||
url(_, snapshot) {
|
||||
const backend = snapshot.attr('backend');
|
||||
return `/v1/${backend}/root/sign-intermediate`;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
defaultSerializer: 'ssh',
|
||||
|
||||
url(snapshot, action) {
|
||||
const { backend, caType, type } = snapshot.attributes();
|
||||
if (action === 'sign-intermediate') {
|
||||
return `/v1/${backend}/root/sign-intermediate`;
|
||||
}
|
||||
if (action === 'set-signed-intermediate') {
|
||||
return `/v1/${backend}/intermediate/set-signed`;
|
||||
}
|
||||
if (action === 'upload') {
|
||||
return `/v1/${backend}/config/ca`;
|
||||
}
|
||||
return `/v1/${backend}/${caType}/generate/${type}`;
|
||||
},
|
||||
|
||||
createRecordOrUpdate(store, type, snapshot, requestType) {
|
||||
const serializer = store.serializerFor(this.get('defaultSerializer'));
|
||||
const isUpload = snapshot.attr('uploadPemBundle');
|
||||
const isSetSignedIntermediate = snapshot.adapterOptions.method === 'setSignedIntermediate';
|
||||
let action = snapshot.adapterOptions.method === 'signIntermediate' ? 'sign-intermediate' : null;
|
||||
let data;
|
||||
if (isUpload) {
|
||||
action = 'upload';
|
||||
data = { pem_bundle: snapshot.attr('pemBundle') };
|
||||
} else if (isSetSignedIntermediate) {
|
||||
action = 'set-signed-intermediate';
|
||||
data = { certificate: snapshot.attr('certificate') };
|
||||
} else {
|
||||
data = serializer.serialize(snapshot, requestType);
|
||||
}
|
||||
|
||||
return this.ajax(this.url(snapshot, action), 'POST', { data }).then(response => {
|
||||
// uploading CA, setting signed intermediate cert, and attempting to generate
|
||||
// a new CA if one exists, all return a 204
|
||||
if (!response) {
|
||||
response = {};
|
||||
}
|
||||
response.id = snapshot.id;
|
||||
response.modelName = type.modelName;
|
||||
store.pushPayload(type.modelName, response);
|
||||
});
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createRecordOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createRecordOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const backend = snapshot.attr('backend');
|
||||
return this.ajax(`/v1/${backend}/root`, 'DELETE');
|
||||
},
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import Adapter from './pki';
|
||||
|
||||
export default Adapter.extend({
|
||||
url(role, snapshot) {
|
||||
if (snapshot.attr('signVerbatim') === true) {
|
||||
return `/v1/${role.backend}/sign-verbatim/${role.name}`;
|
||||
}
|
||||
return `/v1/${role.backend}/sign/${role.name}`;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
import Ember from 'ember';
|
||||
import Adapter from './pki';
|
||||
|
||||
export default Adapter.extend({
|
||||
url(role) {
|
||||
return `/v1/${role.backend}/issue/${role.name}`;
|
||||
},
|
||||
|
||||
urlFor(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/certs`;
|
||||
if (id) {
|
||||
url = `${this.buildURL()}/${backend}/cert/${id}`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
optionsForQuery(id) {
|
||||
let data = {};
|
||||
if (!id) {
|
||||
data['list'] = true;
|
||||
}
|
||||
return { data };
|
||||
},
|
||||
|
||||
fetchByQuery(store, query) {
|
||||
const { backend, id } = query;
|
||||
return this.ajax(this.urlFor(backend, id), 'GET', this.optionsForQuery(id)).then(resp => {
|
||||
const data = {
|
||||
backend,
|
||||
};
|
||||
if (id) {
|
||||
data.serial_number = id;
|
||||
data.id = id;
|
||||
data.id_for_nav = `cert/${id}`;
|
||||
}
|
||||
return Ember.assign({}, resp, data);
|
||||
});
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
|
||||
updateRecord(store, type, snapshot) {
|
||||
if (snapshot.adapterOptions.method !== 'revoke') {
|
||||
return;
|
||||
}
|
||||
const id = snapshot.id;
|
||||
const backend = snapshot.record.get('backend');
|
||||
const data = {
|
||||
serial_number: id,
|
||||
};
|
||||
return this.ajax(`${this.buildURL()}/${backend}/revoke`, 'POST', { data }).then(resp => {
|
||||
const data = {
|
||||
id,
|
||||
serial_number: id,
|
||||
backend,
|
||||
};
|
||||
return Ember.assign({}, resp, data);
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,122 @@
|
|||
import ApplicationAdapter from './application';
|
||||
import DS from 'ember-data';
|
||||
import Ember from 'ember';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
defaultSerializer: 'config',
|
||||
|
||||
urlFor(backend, section) {
|
||||
const urls = {
|
||||
tidy: `/v1/${backend}/tidy`,
|
||||
urls: `/v1/${backend}/config/urls`,
|
||||
crl: `/v1/${backend}/config/crl`,
|
||||
};
|
||||
return urls[section];
|
||||
},
|
||||
|
||||
createOrUpdate(store, type, snapshot) {
|
||||
const url = this.urlFor(snapshot.record.get('backend'), snapshot.adapterOptions.method);
|
||||
const serializer = store.serializerFor(this.get('defaultSerializer'));
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
const data = snapshot.adapterOptions.fields.reduce((data, field) => {
|
||||
let attr = snapshot.attr(field);
|
||||
if (attr) {
|
||||
serializer.serializeAttribute(snapshot, data, field, attr);
|
||||
} else {
|
||||
data[serializer.keyForAttribute(field)] = attr;
|
||||
}
|
||||
return data;
|
||||
}, {});
|
||||
return this.ajax(url, 'POST', { data });
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments, 'update');
|
||||
},
|
||||
|
||||
fetchSection(backendPath, section) {
|
||||
const sections = ['cert', 'urls', 'crl', 'tidy'];
|
||||
if (!section || !sections.includes(section)) {
|
||||
const error = new DS.AdapterError();
|
||||
Ember.set(error, 'httpStatus', 404);
|
||||
throw error;
|
||||
}
|
||||
return this[`fetch${Ember.String.capitalize(section)}`](backendPath);
|
||||
},
|
||||
|
||||
id(backendPath) {
|
||||
return backendPath + '-config-ca';
|
||||
},
|
||||
|
||||
fetchCert(backendPath) {
|
||||
// these are all un-authed so using `fetch` directly works
|
||||
const derURL = `/v1/${backendPath}/ca`;
|
||||
const pemURL = `${derURL}/pem`;
|
||||
const chainURL = `${derURL}_chain`;
|
||||
|
||||
return Ember.RSVP.hash({
|
||||
backend: backendPath,
|
||||
id: this.id(backendPath),
|
||||
der: this.rawRequest(derURL, { unauthenticate: true }).then(response => response.blob()),
|
||||
pem: this.rawRequest(pemURL, { unauthenticate: true }).then(response => response.text()),
|
||||
ca_chain: this.rawRequest(chainURL, { unauthenticate: true }).then(response => response.text()),
|
||||
});
|
||||
},
|
||||
|
||||
fetchUrls(backendPath) {
|
||||
const url = `/v1/${backendPath}/config/urls`;
|
||||
const id = this.id(backendPath);
|
||||
return this.ajax(url, 'GET')
|
||||
.then(resp => {
|
||||
resp.id = id;
|
||||
resp.backend = backendPath;
|
||||
return resp;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.httpStatus === 404) {
|
||||
return Ember.RSVP.resolve({ id });
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchCrl(backendPath) {
|
||||
const url = `/v1/${backendPath}/config/crl`;
|
||||
const id = this.id(backendPath);
|
||||
return this.ajax(url, 'GET')
|
||||
.then(resp => {
|
||||
resp.id = id;
|
||||
resp.backend = backendPath;
|
||||
return resp;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.httpStatus === 404) {
|
||||
return { id };
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchTidy(backendPath) {
|
||||
const id = this.id(backendPath);
|
||||
return Ember.RSVP.resolve({ id, backend: backendPath });
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
const { backend, section } = query;
|
||||
return this.fetchSection(backend, section).then(resp => {
|
||||
resp.backend = backend;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import ApplicationAdapter from './application';
|
||||
import Ember from 'ember';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
defaultSerializer: 'ssh',
|
||||
|
||||
url(/*role*/) {
|
||||
Ember.assert('Override the `url` method to extend the SSH adapter', false);
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot, requestType) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot, requestType);
|
||||
const role = snapshot.attr('role');
|
||||
|
||||
return this.ajax(this.url(role, snapshot), 'POST', { data }).then(response => {
|
||||
response.id = snapshot.id;
|
||||
response.modelName = type.modelName;
|
||||
store.pushPayload(type.modelName, response);
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import Ember from 'ember';
|
||||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1/sys',
|
||||
pathForType(type) {
|
||||
let path = type.replace('policy', 'policies');
|
||||
return path;
|
||||
},
|
||||
|
||||
createOrUpdate(store, type, snapshot) {
|
||||
const serializer = store.serializerFor('policy');
|
||||
const data = serializer.serialize(snapshot);
|
||||
const name = snapshot.attr('name');
|
||||
|
||||
return this.ajax(this.buildURL(type.modelName, name), 'PUT', { data }).then(() => {
|
||||
// doing this to make it like a Vault response - ember data doesn't like 204s if it's not a DELETE
|
||||
return {
|
||||
data: Ember.assign({}, snapshot.record.toJSON(), { id: name }),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
query(store, type) {
|
||||
return this.ajax(this.buildURL(type.modelName), 'GET', { data: { list: true } });
|
||||
},
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import PolicyAdapter from '../policy';
|
||||
|
||||
export default PolicyAdapter.extend();
|
|
@ -0,0 +1,3 @@
|
|||
import PolicyAdapter from '../policy';
|
||||
|
||||
export default PolicyAdapter.extend();
|
|
@ -0,0 +1,3 @@
|
|||
import PolicyAdapter from '../policy';
|
||||
|
||||
export default PolicyAdapter.extend();
|
|
@ -0,0 +1,69 @@
|
|||
import ApplicationAdapter from './application';
|
||||
import Ember from 'ember';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
createOrUpdate(store, type, snapshot, requestType) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot, requestType);
|
||||
const { id } = snapshot;
|
||||
let url = this.urlForRole(snapshot.record.get('backend'), id);
|
||||
|
||||
return this.ajax(url, 'POST', { data });
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments, 'update');
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const { id } = snapshot;
|
||||
return this.ajax(this.urlForRole(snapshot.record.get('backend'), id), 'DELETE');
|
||||
},
|
||||
|
||||
pathForType() {
|
||||
return 'roles';
|
||||
},
|
||||
|
||||
urlForRole(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/roles`;
|
||||
if (id) {
|
||||
url = url + '/' + id;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
optionsForQuery(id) {
|
||||
let data = {};
|
||||
if (!id) {
|
||||
data['list'] = true;
|
||||
}
|
||||
return { data };
|
||||
},
|
||||
|
||||
fetchByQuery(store, query) {
|
||||
const { id, backend } = query;
|
||||
return this.ajax(this.urlForRole(backend, id), 'GET', this.optionsForQuery(id)).then(resp => {
|
||||
const data = {
|
||||
id,
|
||||
name: id,
|
||||
backend,
|
||||
};
|
||||
|
||||
return Ember.assign({}, resp, data);
|
||||
});
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
import ApplicationAdapter from './application';
|
||||
import Ember from 'ember';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
createOrUpdate(store, type, snapshot, requestType) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot, requestType);
|
||||
const { id } = snapshot;
|
||||
let url = this.urlForRole(snapshot.record.get('backend'), id);
|
||||
|
||||
return this.ajax(url, 'POST', { data });
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments, 'update');
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const { id } = snapshot;
|
||||
return this.ajax(this.urlForRole(snapshot.record.get('backend'), id), 'DELETE');
|
||||
},
|
||||
|
||||
pathForType() {
|
||||
return 'roles';
|
||||
},
|
||||
|
||||
urlForRole(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/roles`;
|
||||
if (id) {
|
||||
url = url + '/' + id;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
optionsForQuery(id) {
|
||||
let data = {};
|
||||
if (!id) {
|
||||
data['list'] = true;
|
||||
}
|
||||
return { data };
|
||||
},
|
||||
|
||||
fetchByQuery(store, query) {
|
||||
const { id, backend } = query;
|
||||
return this.ajax(this.urlForRole(backend, id), 'GET', this.optionsForQuery(id)).then(resp => {
|
||||
const data = {
|
||||
id,
|
||||
name: id,
|
||||
backend,
|
||||
};
|
||||
|
||||
return Ember.assign({}, resp, data);
|
||||
});
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
import ApplicationAdapter from './application';
|
||||
import Ember from 'ember';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
defaultSerializer: 'role',
|
||||
|
||||
createOrUpdate(store, type, snapshot, requestType) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot, requestType);
|
||||
const { id } = snapshot;
|
||||
let url = this.urlForRole(snapshot.record.get('backend'), id);
|
||||
|
||||
return this.ajax(url, 'POST', { data });
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments, 'update');
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const { id } = snapshot;
|
||||
return this.ajax(this.urlForRole(snapshot.record.get('backend'), id), 'DELETE');
|
||||
},
|
||||
|
||||
pathForType() {
|
||||
return 'roles';
|
||||
},
|
||||
|
||||
urlForRole(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/roles`;
|
||||
if (id) {
|
||||
url = url + '/' + id;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
optionsForQuery(id) {
|
||||
let data = {};
|
||||
if (!id) {
|
||||
data['list'] = true;
|
||||
}
|
||||
return { data };
|
||||
},
|
||||
|
||||
fetchByQuery(store, query) {
|
||||
const { id, backend } = query;
|
||||
let zeroAddressAjax = Ember.RSVP.resolve();
|
||||
const queryAjax = this.ajax(this.urlForRole(backend, id), 'GET', this.optionsForQuery(id));
|
||||
if (!id) {
|
||||
zeroAddressAjax = this.findAllZeroAddress(store, query);
|
||||
}
|
||||
|
||||
return Ember.RSVP.allSettled([queryAjax, zeroAddressAjax]).then(results => {
|
||||
// query result 404d, so throw the adapterError
|
||||
if (!results[0].value) {
|
||||
throw results[0].reason;
|
||||
}
|
||||
let resp = {
|
||||
id,
|
||||
name: id,
|
||||
backend,
|
||||
};
|
||||
|
||||
results.forEach(result => {
|
||||
if (result.value) {
|
||||
if (result.value.data.roles) {
|
||||
resp = Ember.assign({}, resp, { zero_address_roles: result.value.data.roles });
|
||||
} else {
|
||||
resp = Ember.assign({}, resp, result.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
findAllZeroAddress(store, query) {
|
||||
const { backend } = query;
|
||||
const url = `/v1/${backend}/config/zeroaddress`;
|
||||
return this.ajax(url, 'GET');
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
import Ember from 'ember';
|
||||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
createOrUpdate(store, type, snapshot) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot);
|
||||
const { id } = snapshot;
|
||||
|
||||
return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'POST', { data });
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const { id } = snapshot;
|
||||
return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'DELETE');
|
||||
},
|
||||
|
||||
urlForSecret(backend, id) {
|
||||
let url = this.buildURL() + '/' + backend + '/';
|
||||
if (!Ember.isEmpty(id)) {
|
||||
url = url + id;
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
|
||||
optionsForQuery(id, action) {
|
||||
let data = {};
|
||||
if (action === 'query') {
|
||||
data['list'] = true;
|
||||
}
|
||||
|
||||
return { data };
|
||||
},
|
||||
|
||||
fetchByQuery(query, action) {
|
||||
const { id, backend } = query;
|
||||
return this.ajax(this.urlForSecret(backend, id), 'GET', this.optionsForQuery(id, action)).then(resp => {
|
||||
resp.id = id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.fetchByQuery(query, 'query');
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this.fetchByQuery(query, 'queryRecord');
|
||||
},
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
import Ember from 'ember';
|
||||
import ApplicationAdapter from './application';
|
||||
import DS from 'ember-data';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
url(path) {
|
||||
const url = `${this.buildURL()}/mounts`;
|
||||
return path ? url + '/' + path : url;
|
||||
},
|
||||
|
||||
pathForType(type) {
|
||||
let path;
|
||||
switch (type) {
|
||||
case 'cluster':
|
||||
path = 'clusters';
|
||||
break;
|
||||
case 'secret-engine':
|
||||
path = 'mounts';
|
||||
break;
|
||||
default:
|
||||
path = Ember.String.pluralize(type);
|
||||
break;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
|
||||
query() {
|
||||
return this.ajax(this.url(), 'GET').catch(e => {
|
||||
if (e instanceof DS.AdapterError) {
|
||||
Ember.set(e, 'policyPath', 'sys/mounts');
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot);
|
||||
const path = snapshot.attr('path');
|
||||
|
||||
return this.ajax(this.url(path), 'POST', { data }).then(() => {
|
||||
// ember data doesn't like 204s if it's not a DELETE
|
||||
return {
|
||||
data: Ember.assign({}, data, { path: path + '/', id: path }),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
findRecord(store, type, path, snapshot) {
|
||||
if (snapshot.attr('type') === 'ssh') {
|
||||
return this.ajax(`/v1/${path}/config/ca`, 'GET');
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
if (query.type === 'aws') {
|
||||
return this.ajax(`/v1/${query.backend}/config/lease`, 'GET').then(resp => {
|
||||
resp.path = query.backend + '/';
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
updateRecord(store, type, snapshot) {
|
||||
const { apiPath, options, adapterMethod } = snapshot.adapterOptions;
|
||||
if (adapterMethod) {
|
||||
return this[adapterMethod](...arguments);
|
||||
}
|
||||
if (apiPath) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot);
|
||||
const path = snapshot.id;
|
||||
return this.ajax(`/v1/${path}/${apiPath}`, options.isDelete ? 'DELETE' : 'POST', { data });
|
||||
}
|
||||
},
|
||||
|
||||
saveAWSRoot(store, type, snapshot) {
|
||||
let { data } = snapshot.adapterOptions;
|
||||
const path = snapshot.id;
|
||||
return this.ajax(`/v1/${path}/config/root`, 'POST', { data });
|
||||
},
|
||||
|
||||
saveAWSLease(store, type, snapshot) {
|
||||
let { data } = snapshot.adapterOptions;
|
||||
const path = snapshot.id;
|
||||
return this.ajax(`/v1/${path}/config/lease`, 'POST', { data });
|
||||
},
|
||||
|
||||
saveZeroAddressConfig(store, type, snapshot) {
|
||||
const path = snapshot.id;
|
||||
const roles = store.peekAll('role-ssh').filterBy('zeroAddress').mapBy('id').join(',');
|
||||
const url = `/v1/${path}/config/zeroaddress`;
|
||||
const data = { roles };
|
||||
if (roles === '') {
|
||||
return this.ajax(url, 'DELETE');
|
||||
}
|
||||
return this.ajax(url, 'POST', { data });
|
||||
},
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
import Ember from 'ember';
|
||||
import ApplicationAdapter from './application';
|
||||
const { computed } = Ember;
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
headers: computed(function() {
|
||||
return {
|
||||
'X-Vault-Kv-Client': 'v1',
|
||||
};
|
||||
}),
|
||||
|
||||
createOrUpdate(store, type, snapshot) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot);
|
||||
const { id } = snapshot;
|
||||
|
||||
return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'POST', {
|
||||
data: { data },
|
||||
});
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const { id } = snapshot;
|
||||
return this.ajax(this.urlForSecret(snapshot.attr('backend'), id), 'DELETE');
|
||||
},
|
||||
|
||||
urlForSecret(backend, id, infix = 'data') {
|
||||
let url = `${this.buildURL()}/${backend}/${infix}/`;
|
||||
if (!Ember.isEmpty(id)) {
|
||||
url = url + id;
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
|
||||
optionsForQuery(id, action) {
|
||||
let data = {};
|
||||
if (action === 'query') {
|
||||
data['list'] = true;
|
||||
}
|
||||
|
||||
return { data };
|
||||
},
|
||||
|
||||
urlForQuery(query) {
|
||||
let { id, backend } = query;
|
||||
return this.urlForSecret(backend, id, 'metadata');
|
||||
},
|
||||
|
||||
urlForQueryRecord(query) {
|
||||
let { id, backend } = query;
|
||||
return this.urlForSecret(backend, id);
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.ajax(
|
||||
this.urlForQuery(query, type.modelName),
|
||||
'GET',
|
||||
this.optionsForQuery(query.id, 'query')
|
||||
).then(resp => {
|
||||
resp.id = query.id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this.ajax(
|
||||
this.urlForQueryRecord(query, type.modelName),
|
||||
'GET',
|
||||
this.optionsForQuery(query.id, 'queryRecord')
|
||||
).then(resp => {
|
||||
resp.id = query.id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import SSHAdapter from './ssh';
|
||||
|
||||
export default SSHAdapter.extend({
|
||||
url(role) {
|
||||
return `/v1/${role.backend}/creds/${role.name}`;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import SSHAdapter from './ssh';
|
||||
|
||||
export default SSHAdapter.extend({
|
||||
url(role) {
|
||||
return `/v1/${role.backend}/sign/${role.name}`;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import ApplicationAdapter from './application';
|
||||
import Ember from 'ember';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
defaultSerializer: 'ssh',
|
||||
|
||||
url(/*role*/) {
|
||||
Ember.assert('Override the `url` method to extend the SSH adapter', false);
|
||||
},
|
||||
|
||||
createRecord(store, type, snapshot, requestType) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot, requestType);
|
||||
const role = snapshot.attr('role');
|
||||
|
||||
return this.ajax(this.url(role), 'POST', { data }).then(response => {
|
||||
response.id = snapshot.id;
|
||||
response.modelName = type.modelName;
|
||||
store.pushPayload(type.modelName, response);
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
const WRAPPING_ENDPOINTS = ['lookup', 'wrap', 'unwrap', 'rewrap'];
|
||||
const TOOLS_ENDPOINTS = ['random', 'hash'];
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
toolUrlFor(action) {
|
||||
const isWrapping = WRAPPING_ENDPOINTS.includes(action);
|
||||
const isTool = TOOLS_ENDPOINTS.includes(action);
|
||||
const prefix = isWrapping ? 'wrapping' : 'tools';
|
||||
if (!isWrapping && !isTool) {
|
||||
throw new Error(`Calls to a ${action} endpoint are not currently allowed in the tool adapter`);
|
||||
}
|
||||
return `${this.buildURL()}/${prefix}/${action}`;
|
||||
},
|
||||
|
||||
toolAction(action, data, options = {}) {
|
||||
const { wrapTTL } = options;
|
||||
const url = this.toolUrlFor(action);
|
||||
const ajaxOptions = wrapTTL ? { data, wrapTTL } : { data };
|
||||
return this.ajax(url, 'POST', ajaxOptions);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
import Ember from 'ember';
|
||||
import ApplicationAdapter from './application';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
createOrUpdate(store, type, snapshot, requestType) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot, requestType);
|
||||
const { id } = snapshot;
|
||||
let url = this.urlForSecret(snapshot.record.get('backend'), id);
|
||||
|
||||
if (requestType === 'update') {
|
||||
url = url + '/config';
|
||||
}
|
||||
|
||||
return this.ajax(url, 'POST', { data });
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments, 'update');
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const { id } = snapshot;
|
||||
return this.ajax(this.urlForSecret(snapshot.record.get('backend'), id), 'DELETE');
|
||||
},
|
||||
|
||||
pathForType(type) {
|
||||
let path;
|
||||
switch (type) {
|
||||
case 'cluster':
|
||||
path = 'clusters';
|
||||
break;
|
||||
case 'secret-engine':
|
||||
path = 'secrets';
|
||||
break;
|
||||
default:
|
||||
path = Ember.String.pluralize(type);
|
||||
break;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
|
||||
urlForSecret(backend, id) {
|
||||
let url = `${this.buildURL()}/${backend}/keys/`;
|
||||
if (id) {
|
||||
url += id;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
urlForAction(action, backend, id, param) {
|
||||
let urlBase = `${this.buildURL()}/${backend}/${action}`;
|
||||
// these aren't key-specific
|
||||
if (action === 'hash' || action === 'random') {
|
||||
return urlBase;
|
||||
}
|
||||
if (action === 'datakey' && param) {
|
||||
// datakey action has `wrapped` or `plaintext` as part of the url
|
||||
return `${urlBase}/${param}/${id}`;
|
||||
}
|
||||
if (action === 'export' && param) {
|
||||
let [type, version] = param;
|
||||
const exportBase = `${urlBase}/${type}-key/${id}`;
|
||||
return version ? `${exportBase}/${version}` : exportBase;
|
||||
}
|
||||
return `${urlBase}/${id}`;
|
||||
},
|
||||
|
||||
optionsForQuery(id) {
|
||||
let data = {};
|
||||
if (!id) {
|
||||
data['list'] = true;
|
||||
}
|
||||
return { data };
|
||||
},
|
||||
|
||||
fetchByQuery(query) {
|
||||
const { id, backend } = query;
|
||||
return this.ajax(this.urlForSecret(backend, id), 'GET', this.optionsForQuery(id)).then(resp => {
|
||||
resp.id = id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.fetchByQuery(query);
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this.fetchByQuery(query);
|
||||
},
|
||||
|
||||
// rotate, encrypt, decrypt, sign, verify, hmac, rewrap, datakey
|
||||
keyAction(action, { backend, id, payload }, options = {}) {
|
||||
const verb = action === 'export' ? 'GET' : 'POST';
|
||||
const { wrapTTL } = options;
|
||||
if (action === 'rotate') {
|
||||
return this.ajax(this.urlForSecret(backend, id) + '/rotate', verb);
|
||||
}
|
||||
const { param } = payload;
|
||||
|
||||
delete payload.param;
|
||||
return this.ajax(this.urlForAction(action, backend, id, param), verb, {
|
||||
data: payload,
|
||||
wrapTTL,
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import Ember from 'ember';
|
||||
import Resolver from './resolver';
|
||||
import loadInitializers from 'ember-load-initializers';
|
||||
import config from './config/environment';
|
||||
|
||||
let App;
|
||||
|
||||
App = Ember.Application.extend({
|
||||
modulePrefix: config.modulePrefix,
|
||||
podModulePrefix: config.podModulePrefix,
|
||||
Resolver,
|
||||
});
|
||||
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,32 @@
|
|||
import Ember from 'ember';
|
||||
import { task } from 'ember-concurrency';
|
||||
import DS from 'ember-data';
|
||||
|
||||
const { inject } = Ember;
|
||||
|
||||
const AuthConfigBase = Ember.Component.extend({
|
||||
tagName: '',
|
||||
model: null,
|
||||
|
||||
flashMessages: inject.service(),
|
||||
|
||||
saveModel: task(function*() {
|
||||
try {
|
||||
yield this.get('model').save();
|
||||
} catch (err) {
|
||||
// AdapterErrors are handled by the error-message component
|
||||
// in the form
|
||||
if (err instanceof DS.AdapterError === false) {
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.get('flashMessages').success('The configuration was saved successfully.');
|
||||
}),
|
||||
});
|
||||
|
||||
AuthConfigBase.reopenClass({
|
||||
positionalParams: ['model'],
|
||||
});
|
||||
|
||||
export default AuthConfigBase;
|
|
@ -0,0 +1,22 @@
|
|||
import AuthConfigComponent from './config';
|
||||
import { task } from 'ember-concurrency';
|
||||
import DS from 'ember-data';
|
||||
|
||||
export default AuthConfigComponent.extend({
|
||||
saveModel: task(function*() {
|
||||
const model = this.get('model');
|
||||
let data = model.get('config').toJSON();
|
||||
data.description = model.get('description');
|
||||
try {
|
||||
yield model.tune(data);
|
||||
} catch (err) {
|
||||
// AdapterErrors are handled by the error-message component
|
||||
// in the form
|
||||
if (err instanceof DS.AdapterError === false) {
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.get('flashMessages').success('The configuration options were saved successfully.');
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
import Ember from 'ember';
|
||||
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
|
||||
const BACKENDS = supportedAuthBackends();
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['auth-form'],
|
||||
routing: Ember.inject.service('-routing'),
|
||||
auth: Ember.inject.service(),
|
||||
flashMessages: Ember.inject.service(),
|
||||
didRender() {
|
||||
// on very narrow viewports the active tab may be overflowed, so we scroll it into view here
|
||||
this.$('li.is-active').get(0).scrollIntoView();
|
||||
},
|
||||
|
||||
cluster: null,
|
||||
redirectTo: null,
|
||||
|
||||
selectedAuthType: 'token',
|
||||
selectedAuthBackend: Ember.computed('selectedAuthType', function() {
|
||||
return BACKENDS.findBy('type', this.get('selectedAuthType'));
|
||||
}),
|
||||
|
||||
providerComponentName: Ember.computed('selectedAuthBackend.type', function() {
|
||||
const type = Ember.String.dasherize(this.get('selectedAuthBackend.type'));
|
||||
return `auth-form/${type}`;
|
||||
}),
|
||||
|
||||
handleError(e) {
|
||||
this.set('loading', false);
|
||||
this.set('error', `Authentication failed: ${e.errors.join('.')}`);
|
||||
},
|
||||
|
||||
actions: {
|
||||
doSubmit(data) {
|
||||
this.setProperties({
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
const targetRoute = this.get('redirectTo') || 'vault.cluster';
|
||||
//const {password, token, username} = data;
|
||||
const backend = this.get('selectedAuthBackend.type');
|
||||
const path = this.get('customPath');
|
||||
if (this.get('useCustomPath') && path) {
|
||||
data.path = path;
|
||||
}
|
||||
const clusterId = this.get('cluster.id');
|
||||
this.get('auth').authenticate({ clusterId, backend, data }).then(
|
||||
({ isRoot }) => {
|
||||
this.set('loading', false);
|
||||
const transition = this.get('routing.router').transitionTo(targetRoute);
|
||||
if (isRoot) {
|
||||
transition.followRedirects().then(() => {
|
||||
this.get('flashMessages').warning(
|
||||
'You have logged in with a root token. As a security precaution, this root token will not be stored by your browser and you will need to re-authenticate after the window is closed or refreshed.'
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
(...errArgs) => this.handleError(...errArgs)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
auth: Ember.inject.service(),
|
||||
|
||||
routing: Ember.inject.service('-routing'),
|
||||
|
||||
transitionToRoute: function() {
|
||||
var router = this.get('routing.router');
|
||||
router.transitionTo.apply(router, arguments);
|
||||
},
|
||||
|
||||
classNames: 'user-menu auth-info',
|
||||
|
||||
isRenewing: Ember.computed.or('fakeRenew', 'auth.isRenewing'),
|
||||
|
||||
actions: {
|
||||
renewToken() {
|
||||
this.set('fakeRenew', true);
|
||||
Ember.run.later(() => {
|
||||
this.set('fakeRenew', false);
|
||||
this.get('auth').renew();
|
||||
}, 200);
|
||||
},
|
||||
|
||||
revokeToken() {
|
||||
this.get('auth').revokeCurrentToken().then(() => {
|
||||
this.transitionToRoute('vault.cluster.logout');
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,128 @@
|
|||
import Ember from 'ember';
|
||||
import { encodeString, decodeString } from 'vault/utils/b64';
|
||||
|
||||
const { computed, get, set } = Ember;
|
||||
const B64 = 'base64';
|
||||
const UTF8 = 'utf-8';
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'button',
|
||||
attributeBindings: ['type'],
|
||||
type: 'button',
|
||||
classNames: ['button', 'b64-toggle'],
|
||||
classNameBindings: ['isInput:is-input:is-textarea'],
|
||||
|
||||
/*
|
||||
* Whether or not the toggle is associated with an input.
|
||||
* Also bound to `is-input` and `is-textarea` classes
|
||||
* Defaults to true
|
||||
*
|
||||
* @public
|
||||
* @type boolean
|
||||
*/
|
||||
|
||||
isInput: true,
|
||||
|
||||
/*
|
||||
* The value that will be mutated when the encoding is toggled
|
||||
*
|
||||
* @public
|
||||
* @type string
|
||||
*/
|
||||
value: null,
|
||||
|
||||
/*
|
||||
* The encoding of `value` when the component is initialized.
|
||||
* Defaults to 'utf-8'.
|
||||
* Possible values: 'utf-8' and 'base64'
|
||||
*
|
||||
* @public
|
||||
* @type string
|
||||
*/
|
||||
initialEncoding: UTF8,
|
||||
|
||||
/*
|
||||
* A cached version of value - used to determine if the input has changed since encoding.
|
||||
*
|
||||
* @private
|
||||
* @type string
|
||||
*/
|
||||
_value: '',
|
||||
|
||||
/*
|
||||
* The current encoding of `value`.
|
||||
* Possible values: 'utf-8' and 'base64'
|
||||
*
|
||||
* @private
|
||||
* @type string
|
||||
*/
|
||||
currentEncoding: '',
|
||||
|
||||
/*
|
||||
* The encoding when we last mutated `value`.
|
||||
* Possible values: 'utf-8' and 'base64'
|
||||
*
|
||||
* @private
|
||||
* @type string
|
||||
*/
|
||||
lastEncoding: '',
|
||||
|
||||
/*
|
||||
* Is the value known to be base64-encoded.
|
||||
*
|
||||
* @private
|
||||
* @type boolean
|
||||
*/
|
||||
isBase64: computed.equal('currentEncoding', B64),
|
||||
|
||||
/*
|
||||
* Does the current value match the cached _value, i.e. has the input been mutated since we encoded.
|
||||
*
|
||||
* @private
|
||||
* @type boolean
|
||||
*/
|
||||
valuesMatch: computed('value', '_value', function() {
|
||||
const { value, _value } = this.getProperties('value', '_value');
|
||||
const anyBlank = Ember.isBlank(value) || Ember.isBlank(_value);
|
||||
return !anyBlank && value === _value;
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
const initial = get(this, 'initialEncoding');
|
||||
set(this, 'currentEncoding', initial);
|
||||
if (initial === B64) {
|
||||
set(this, '_value', get(this, 'value'));
|
||||
set(this, 'lastEncoding', B64);
|
||||
}
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
// if there's no value, reset encoding
|
||||
if (get(this, 'value') === '') {
|
||||
set(this, 'currentEncoding', UTF8);
|
||||
return;
|
||||
}
|
||||
// the value has changed after we transformed it so we reset currentEncoding
|
||||
if (get(this, 'isBase64') && !get(this, 'valuesMatch')) {
|
||||
set(this, 'currentEncoding', UTF8);
|
||||
}
|
||||
// the value changed back to one we previously had transformed
|
||||
if (get(this, 'lastEncoding') === B64 && get(this, 'valuesMatch')) {
|
||||
set(this, 'currentEncoding', B64);
|
||||
}
|
||||
},
|
||||
|
||||
click() {
|
||||
let val = get(this, 'value');
|
||||
const isUTF8 = get(this, 'currentEncoding') === UTF8;
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
let newVal = isUTF8 ? encodeString(val) : decodeString(val);
|
||||
const encoding = isUTF8 ? B64 : UTF8;
|
||||
set(this, 'value', newVal);
|
||||
set(this, '_value', newVal);
|
||||
set(this, 'lastEncoding', encoding);
|
||||
set(this, 'currentEncoding', encoding);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,203 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { computed, inject } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: 'config-pki-ca',
|
||||
store: inject.service('store'),
|
||||
flashMessages: inject.service(),
|
||||
|
||||
/*
|
||||
* @param boolean
|
||||
* @private
|
||||
* bool that gets flipped if you have a CA cert and click the Replace Cert button
|
||||
*/
|
||||
replaceCA: false,
|
||||
|
||||
/*
|
||||
* @param boolean
|
||||
* @private
|
||||
* bool that gets flipped if you push the click the "Set signed intermediate" button
|
||||
*/
|
||||
setSignedIntermediate: false,
|
||||
|
||||
/*
|
||||
* @param boolean
|
||||
* @private
|
||||
* bool that gets flipped if you push the click the "Set signed intermediate" button
|
||||
*/
|
||||
signIntermediate: false,
|
||||
|
||||
/*
|
||||
* @param boolean
|
||||
* @private
|
||||
*
|
||||
* true when there's no CA cert currently configured
|
||||
*/
|
||||
needsConfig: computed.not('config.pem'),
|
||||
|
||||
/*
|
||||
* @param DS.Model
|
||||
* @private
|
||||
*
|
||||
* a `pki-ca-certificate` model used to back the form when uploading or creating a CA cert
|
||||
* created and set on `init`, and unloaded on willDestroy
|
||||
*
|
||||
*/
|
||||
model: null,
|
||||
|
||||
/*
|
||||
* @param DS.Model
|
||||
* @public
|
||||
*
|
||||
* a `pki-config` model - passed in in the component useage
|
||||
*
|
||||
*/
|
||||
config: null,
|
||||
|
||||
/*
|
||||
* @param Function
|
||||
* @public
|
||||
*
|
||||
* function that gets called to refresh the config model
|
||||
*
|
||||
*/
|
||||
onRefresh: () => {},
|
||||
|
||||
loading: false,
|
||||
|
||||
willDestroy() {
|
||||
const ca = this.get('model');
|
||||
if (ca) {
|
||||
ca.unloadRecord();
|
||||
}
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
createOrReplaceModel(modelType) {
|
||||
const ca = this.get('model');
|
||||
const config = this.get('config');
|
||||
const store = this.get('store');
|
||||
const backend = config.get('backend');
|
||||
if (ca) {
|
||||
ca.unloadRecord();
|
||||
}
|
||||
const caCert = store.createRecord(modelType || 'pki-ca-certificate', {
|
||||
id: `${backend}-ca-cert`,
|
||||
backend,
|
||||
});
|
||||
this.set('model', caCert);
|
||||
},
|
||||
|
||||
/*
|
||||
* @private
|
||||
* @returns array
|
||||
*
|
||||
* When a CA is configured, we let them download
|
||||
* the CA in der, pem, and the CA Chain in pem (if one exists)
|
||||
*
|
||||
* This array provides the text and download hrefs for those links.
|
||||
*
|
||||
*/
|
||||
downloadHrefs: computed('config', 'config.{backend,pem,caChain,der}', function() {
|
||||
const config = this.get('config');
|
||||
const { backend, pem, caChain, der } = config.getProperties('backend', 'pem', 'caChain', 'der');
|
||||
|
||||
if (!pem) {
|
||||
return [];
|
||||
}
|
||||
const pemFile = new File([pem], { type: 'text/plain' });
|
||||
const links = [
|
||||
{
|
||||
display: 'Download CA Certificate in PEM format',
|
||||
name: `${backend}_ca.pem`,
|
||||
url: URL.createObjectURL(pemFile),
|
||||
},
|
||||
{
|
||||
display: 'Download CA Certificate in DER format',
|
||||
name: `${backend}_ca.der`,
|
||||
url: URL.createObjectURL(der),
|
||||
},
|
||||
];
|
||||
if (caChain) {
|
||||
const caChainFile = new File([caChain], { type: 'text/plain' });
|
||||
links.push({
|
||||
display: 'Download CA Certificate Chain',
|
||||
name: `${backend}_ca_chain.pem`,
|
||||
url: URL.createObjectURL(caChainFile),
|
||||
});
|
||||
}
|
||||
return links;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
saveCA(method) {
|
||||
this.set('loading', true);
|
||||
const model = this.get('model');
|
||||
const isUpload = this.get('model.uploadPemBundle');
|
||||
model
|
||||
.save({ adapterOptions: { method } })
|
||||
.then(m => {
|
||||
if (method === 'setSignedIntermediate' || isUpload) {
|
||||
this.send('refresh');
|
||||
this.get('flashMessages').success('The certificate for this backend has been updated.');
|
||||
} else if (!m.get('certificate') && !m.get('csr')) {
|
||||
// if there's no certificate, it wasn't generated and the generation was a noop
|
||||
this.get('flashMessages').warning(
|
||||
'You tried to generate a new root CA, but one currently exists. To replace the existing one, delete it first and then generate again.'
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
},
|
||||
deleteCA() {
|
||||
this.set('loading', true);
|
||||
const model = this.get('model');
|
||||
const backend = model.get('backend');
|
||||
//TODO Is there better way to do this? This forces the saved state so Ember Data will make a server call.
|
||||
model.send('pushedData');
|
||||
model
|
||||
.destroyRecord()
|
||||
.then(() => {
|
||||
this.get('flashMessages').success(
|
||||
`The CA key for ${backend} has been deleted. The old CA certificate will still be accessible for reading until a new certificate/key are generated or uploaded.`
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set('loading', false);
|
||||
this.send('refresh');
|
||||
this.createOrReplaceModel();
|
||||
});
|
||||
},
|
||||
refresh() {
|
||||
this.setProperties({
|
||||
setSignedIntermediate: false,
|
||||
signIntermediate: false,
|
||||
replaceCA: false,
|
||||
});
|
||||
this.get('onRefresh')();
|
||||
},
|
||||
toggleReplaceCA() {
|
||||
if (!this.get('replaceCA')) {
|
||||
this.createOrReplaceModel();
|
||||
}
|
||||
this.toggleProperty('replaceCA');
|
||||
},
|
||||
toggleVal(name, val) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const model = name === 'signIntermediate' ? 'pki-ca-certificate-sign' : null;
|
||||
if (!this.get(name)) {
|
||||
this.createOrReplaceModel(model);
|
||||
}
|
||||
if (val !== undefined) {
|
||||
this.set(name, val);
|
||||
} else {
|
||||
this.toggleProperty(name);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { get, inject } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: 'config-pki',
|
||||
flashMessages: inject.service(),
|
||||
|
||||
/*
|
||||
*
|
||||
* @param String
|
||||
* @public
|
||||
* String corresponding to the route parameter for the current section
|
||||
*
|
||||
*/
|
||||
|
||||
section: null,
|
||||
|
||||
/*
|
||||
* @param DS.Model
|
||||
* @public
|
||||
*
|
||||
* a `pki-config` model - passed in in the component useage
|
||||
*
|
||||
*/
|
||||
config: null,
|
||||
|
||||
/*
|
||||
* @param Function
|
||||
* @public
|
||||
*
|
||||
* function that gets called to refresh the config model
|
||||
*
|
||||
*/
|
||||
onRefresh: () => {},
|
||||
|
||||
loading: false,
|
||||
|
||||
actions: {
|
||||
save(section) {
|
||||
this.set('loading', true);
|
||||
const config = this.get('config');
|
||||
config
|
||||
.save({
|
||||
adapterOptions: {
|
||||
method: section,
|
||||
fields: get(config, `${section}Attrs`).map(attr => attr.name),
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.get('flashMessages').success(`The ${section} config for this backend has been updated.`);
|
||||
// attrs aren't persistent for Tidy
|
||||
if (section === 'tidy') {
|
||||
config.rollbackAttributes();
|
||||
}
|
||||
this.send('refresh');
|
||||
})
|
||||
.finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
},
|
||||
refresh() {
|
||||
this.get('onRefresh')();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import Ember from 'ember';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'span',
|
||||
classNames: ['confirm-action'],
|
||||
layout: hbs`
|
||||
{{#if showConfirm ~}}
|
||||
<span class={{containerClasses}}>
|
||||
<span class={{concat 'confirm-action-text ' messageClasses}}>{{if disabled disabledMessage confirmMessage}}</span>
|
||||
<button {{action 'onConfirm'}} disabled={{disabled}} class={{confirmButtonClasses}} type="button" data-test-confirm-button=true>{{confirmButtonText}}</button>
|
||||
<button {{action 'toggleConfirm'}} type="button" class={{cancelButtonClasses}} data-test-confirm-cancel-button=true>{{cancelButtonText}}</button>
|
||||
</span>
|
||||
{{else}}
|
||||
<button
|
||||
class={{buttonClasses}}
|
||||
type="button"
|
||||
disabled={{disabled}}
|
||||
{{action 'toggleConfirm'}}
|
||||
>
|
||||
{{yield}}
|
||||
</button>
|
||||
{{~/if}}
|
||||
`,
|
||||
|
||||
disabled: false,
|
||||
disabledMessage: 'Complete the form to complete this action',
|
||||
showConfirm: false,
|
||||
messageClasses: 'is-size-8 has-text-grey',
|
||||
confirmButtonClasses: 'is-danger is-outlined button',
|
||||
containerClasses: '',
|
||||
buttonClasses: 'button',
|
||||
buttonText: 'Delete',
|
||||
confirmMessage: 'Are you sure you want to do this?',
|
||||
confirmButtonText: 'Delete',
|
||||
cancelButtonClasses: 'button',
|
||||
cancelButtonText: 'Cancel',
|
||||
// the action to take when we confirm
|
||||
onConfirmAction: null,
|
||||
|
||||
actions: {
|
||||
toggleConfirm() {
|
||||
this.toggleProperty('showConfirm');
|
||||
},
|
||||
|
||||
onConfirm() {
|
||||
const confirmAction = this.get('onConfirmAction');
|
||||
|
||||
if (typeof confirmAction !== 'function') {
|
||||
throw new Error('confirm-action components expects `onConfirmAction` attr to be a function');
|
||||
} else {
|
||||
confirmAction();
|
||||
this.toggleProperty('showConfirm');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import Ember from 'ember';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
const { Component, computed } = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'a',
|
||||
attributeBindings: ['target', 'rel', 'href'],
|
||||
|
||||
layout: hbs`{{yield}}`,
|
||||
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
|
||||
path: '/',
|
||||
href: computed('path', function() {
|
||||
return `https://www.vaultproject.io/docs${this.get('path')}`;
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import Ember from 'ember';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
const { computed } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
layout: hbs`{{actionText}}`,
|
||||
tagName: 'a',
|
||||
role: 'button',
|
||||
attributeBindings: ['role', 'download', 'href'],
|
||||
download: computed('filename', 'extension', function() {
|
||||
return `${this.get('filename')}-${new Date().toISOString()}.${this.get('extension')}`;
|
||||
}),
|
||||
|
||||
href: computed('data', 'mime', 'stringify', function() {
|
||||
let data = this.get('data');
|
||||
const mime = this.get('mime');
|
||||
if (this.get('stringify')) {
|
||||
data = JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
const file = new File([data], { type: mime });
|
||||
return window.URL.createObjectURL(file);
|
||||
}),
|
||||
|
||||
actionText: 'Download',
|
||||
data: null,
|
||||
filename: null,
|
||||
mime: 'text/plain',
|
||||
extension: 'txt',
|
||||
stringify: false,
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'span',
|
||||
classNames: 'badge edition-badge',
|
||||
abbreviation: Ember.computed('edition', function() {
|
||||
const edition = this.get('edition');
|
||||
if (edition == 'Enterprise') {
|
||||
return 'Ent';
|
||||
} else {
|
||||
return edition;
|
||||
}
|
||||
}),
|
||||
attributeBindings: ['edition:aria-label'],
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import Ember from 'ember';
|
||||
import FlashMessage from 'ember-cli-flash/components/flash-message';
|
||||
|
||||
const { computed, getWithDefault } = Ember;
|
||||
|
||||
export default FlashMessage.extend({
|
||||
// override alertType to get Bulma specific prefix
|
||||
//https://github.com/poteto/ember-cli-flash/blob/master/addon/components/flash-message.js#L35
|
||||
alertType: computed('flash.type', {
|
||||
get() {
|
||||
const flashType = getWithDefault(this, 'flash.type', '');
|
||||
let prefix = 'notification has-border is-';
|
||||
|
||||
return `${prefix}${flashType}`;
|
||||
},
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: 'column',
|
||||
header: null,
|
||||
content: null,
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { computed } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
|
||||
/*
|
||||
* @public String
|
||||
* A whitelist of groups to include in the render
|
||||
*/
|
||||
renderGroup: computed(function() {
|
||||
return null;
|
||||
}),
|
||||
|
||||
/*
|
||||
* @public DS.Model
|
||||
* model to be passed down to form-field component
|
||||
* if `fieldGroups` is present on the model then it will be iterated over and
|
||||
* groups of `form-field` components will be rendered
|
||||
*
|
||||
*/
|
||||
model: null,
|
||||
|
||||
/*
|
||||
* @public Function
|
||||
* onChange handler that will get set on the form-field component
|
||||
*
|
||||
*/
|
||||
onChange: () => {},
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
import Ember from 'ember';
|
||||
import { capitalize } from 'vault/helpers/capitalize';
|
||||
import { humanize } from 'vault/helpers/humanize';
|
||||
import { dasherize } from 'vault/helpers/dasherize';
|
||||
const { computed } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
'data-test-field': true,
|
||||
classNames: ['field'],
|
||||
|
||||
/*
|
||||
* @public Function
|
||||
* called whenever a value on the model changes via the component
|
||||
*
|
||||
*/
|
||||
onChange() {},
|
||||
|
||||
/*
|
||||
* @public
|
||||
* @param Object
|
||||
* in the form of
|
||||
* {
|
||||
* name: "foo",
|
||||
* options: {
|
||||
* label: "Foo",
|
||||
* defaultValue: "",
|
||||
* editType: "ttl",
|
||||
* helpText: "This will be in a tooltip"
|
||||
* },
|
||||
* type: "boolean"
|
||||
* }
|
||||
*
|
||||
* this is usually derived from ember model `attributes` lookup,
|
||||
* and all members of `attr.options` are optional
|
||||
*
|
||||
*/
|
||||
attr: null,
|
||||
|
||||
/*
|
||||
* @private
|
||||
* @param string
|
||||
* Computed property used in the label element next to the form element
|
||||
*
|
||||
*/
|
||||
labelString: computed('attr.name', 'attr.options.label', function() {
|
||||
const label = this.get('attr.options.label');
|
||||
const name = this.get('attr.name');
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
if (name) {
|
||||
return capitalize([humanize([dasherize([name])])]);
|
||||
}
|
||||
}),
|
||||
|
||||
// both the path to mutate on the model, and the path to read the value from
|
||||
/*
|
||||
* @private
|
||||
* @param string
|
||||
*
|
||||
* Computed property used to set values on the passed model
|
||||
*
|
||||
*/
|
||||
valuePath: computed('attr.name', 'attr.options.fieldValue', function() {
|
||||
return this.get('attr.options.fieldValue') || this.get('attr.name');
|
||||
}),
|
||||
|
||||
/*
|
||||
*
|
||||
* @public
|
||||
* @param DS.Model
|
||||
*
|
||||
* the Ember Data model that `attr` is defined on
|
||||
*/
|
||||
model: null,
|
||||
|
||||
/*
|
||||
* @private
|
||||
* @param object
|
||||
*
|
||||
* Used by the pgp-file component when an attr is editType of 'file'
|
||||
*/
|
||||
file: { value: '' },
|
||||
emptyData: '{\n}',
|
||||
|
||||
actions: {
|
||||
setFile(_, keyFile) {
|
||||
const path = this.get('valuePath');
|
||||
const { value } = keyFile;
|
||||
this.get('model').set(path, value);
|
||||
this.get('onChange')(path, value);
|
||||
this.set('file', keyFile);
|
||||
},
|
||||
|
||||
setAndBroadcast(path, value) {
|
||||
this.get('model').set(path, value);
|
||||
this.get('onChange')(path, value);
|
||||
},
|
||||
|
||||
codemirrorUpdated(path, value, codemirror) {
|
||||
codemirror.performLint();
|
||||
const hasErrors = codemirror.state.lint.marked.length > 0;
|
||||
|
||||
if (!hasErrors) {
|
||||
this.get('model').set(path, JSON.parse(value));
|
||||
this.get('onChange')(path, JSON.parse(value));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,133 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { get, computed } = Ember;
|
||||
|
||||
const MODEL_TYPES = {
|
||||
'ssh-sign': {
|
||||
model: 'ssh-sign',
|
||||
},
|
||||
'ssh-creds': {
|
||||
model: 'ssh-otp-credential',
|
||||
title: 'Generate SSH Credentials',
|
||||
generatedAttr: 'key',
|
||||
},
|
||||
'aws-creds': {
|
||||
model: 'iam-credential',
|
||||
title: 'Generate IAM Credentials',
|
||||
generateWithoutInput: true,
|
||||
backIsListLink: true,
|
||||
},
|
||||
'aws-sts': {
|
||||
model: 'iam-credential',
|
||||
title: 'Generate IAM Credentials with STS',
|
||||
generatedAttr: 'accessKey',
|
||||
},
|
||||
'pki-issue': {
|
||||
model: 'pki-certificate',
|
||||
title: 'Issue Certificate',
|
||||
generatedAttr: 'certificate',
|
||||
},
|
||||
'pki-sign': {
|
||||
model: 'pki-certificate-sign',
|
||||
title: 'Sign Certificate',
|
||||
generatedAttr: 'certificate',
|
||||
},
|
||||
};
|
||||
|
||||
export default Ember.Component.extend({
|
||||
store: Ember.inject.service(),
|
||||
routing: Ember.inject.service('-routing'),
|
||||
// set on the component
|
||||
backend: null,
|
||||
action: null,
|
||||
role: null,
|
||||
|
||||
model: null,
|
||||
loading: false,
|
||||
emptyData: '{\n}',
|
||||
|
||||
modelForType() {
|
||||
const type = this.get('options');
|
||||
if (type) {
|
||||
return type.model;
|
||||
}
|
||||
// if we don't have a mode for that type then redirect them back to the backend list
|
||||
const router = this.get('routing.router');
|
||||
router.transitionTo.call(router, 'vault.cluster.secrets.backend.list-root', this.get('model.backend'));
|
||||
},
|
||||
|
||||
options: computed('action', 'backend.type', function() {
|
||||
const action = this.get('action') || 'creds';
|
||||
return MODEL_TYPES[`${this.get('backend.type')}-${action}`];
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.createOrReplaceModel();
|
||||
this.maybeGenerate();
|
||||
},
|
||||
|
||||
willDestroy() {
|
||||
this.get('model').unloadRecord();
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
createOrReplaceModel() {
|
||||
const modelType = this.modelForType();
|
||||
const model = this.get('model');
|
||||
const roleModel = this.get('role');
|
||||
if (!modelType) {
|
||||
return;
|
||||
}
|
||||
if (model) {
|
||||
model.unloadRecord();
|
||||
}
|
||||
const attrs = {
|
||||
role: roleModel,
|
||||
id: `${get(roleModel, 'backend')}-${get(roleModel, 'name')}`,
|
||||
};
|
||||
if (this.get('action') === 'sts') {
|
||||
attrs.withSTS = true;
|
||||
}
|
||||
const newModel = this.get('store').createRecord(modelType, attrs);
|
||||
this.set('model', newModel);
|
||||
},
|
||||
|
||||
/*
|
||||
*
|
||||
* @function maybeGenerate
|
||||
*
|
||||
* This method is called on `init`. If there is no input requried (as is the case for AWS IAM creds)
|
||||
* then the `create` action is triggered right away.
|
||||
*
|
||||
*/
|
||||
maybeGenerate() {
|
||||
if (this.get('backend.type') !== 'aws' || this.get('action') === 'sts') {
|
||||
return;
|
||||
}
|
||||
// for normal IAM creds - there's no input, so just generate right away
|
||||
this.send('create');
|
||||
},
|
||||
|
||||
actions: {
|
||||
create() {
|
||||
this.set('loading', true);
|
||||
this.model.save().finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
},
|
||||
|
||||
codemirrorUpdated(attr, val, codemirror) {
|
||||
codemirror.performLint();
|
||||
const hasErrors = codemirror.state.lint.marked.length > 0;
|
||||
|
||||
if (!hasErrors) {
|
||||
Ember.set(this.get('model'), attr, JSON.parse(val));
|
||||
}
|
||||
},
|
||||
|
||||
newModel() {
|
||||
this.createOrReplaceModel();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import Ember from 'ember';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
const { computed } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
layout: hbs`<a href="{{href-to 'vault.cluster' 'vault'}}" class={{class}}>
|
||||
{{#if hasBlock}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
{{text}}
|
||||
{{/if}}
|
||||
</a>
|
||||
`,
|
||||
|
||||
tagName: '',
|
||||
|
||||
text: computed(function() {
|
||||
return 'home';
|
||||
}),
|
||||
|
||||
computedClasses: computed('classNames', function() {
|
||||
return this.get('classNames').join(' ');
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import Ember from 'ember';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
const { computed } = Ember;
|
||||
const GLYPHS_WITH_SVG_TAG = [
|
||||
'folder',
|
||||
'file',
|
||||
'perf-replication',
|
||||
'role',
|
||||
'information-reversed',
|
||||
'true',
|
||||
'false',
|
||||
];
|
||||
|
||||
export default Ember.Component.extend({
|
||||
layout: hbs`
|
||||
{{#if excludeSVG}}
|
||||
{{partial partialName}}
|
||||
{{else}}
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="{{size}}" height="{{size}}" viewBox="0 0 512 512">
|
||||
{{partial partialName}}
|
||||
</svg>
|
||||
{{/if}}
|
||||
`,
|
||||
|
||||
tagName: 'span',
|
||||
excludeIconClass: false,
|
||||
classNameBindings: ['excludeIconClass::icon'],
|
||||
classNames: ['has-current-color-fill'],
|
||||
|
||||
attributeBindings: ['aria-label', 'aria-hidden'],
|
||||
|
||||
glyph: null,
|
||||
|
||||
excludeSVG: computed('glyph', function() {
|
||||
return GLYPHS_WITH_SVG_TAG.includes(this.get('glyph'));
|
||||
}),
|
||||
|
||||
size: computed(function() {
|
||||
return 12;
|
||||
}),
|
||||
|
||||
partialName: computed('glyph', function() {
|
||||
const glyph = this.get('glyph');
|
||||
return `svg/icons/${Ember.String.camelize(glyph)}`;
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import Ember from 'ember';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { humanize } from 'vault/helpers/humanize';
|
||||
|
||||
const { computed } = Ember;
|
||||
export default Ember.Component.extend({
|
||||
model: null,
|
||||
mode: 'create',
|
||||
/*
|
||||
* @param Function
|
||||
* @public
|
||||
*
|
||||
* Optional param to call a function upon successfully mounting a backend
|
||||
*
|
||||
*/
|
||||
onSave: () => {},
|
||||
|
||||
cancelLink: computed('mode', 'model', function() {
|
||||
let { model, mode } = this.getProperties('model', 'mode');
|
||||
let key = `${mode}-${model.get('identityType')}`;
|
||||
let routes = {
|
||||
'create-entity': 'vault.cluster.access.identity',
|
||||
'edit-entity': 'vault.cluster.access.identity.show',
|
||||
'merge-entity-merge': 'vault.cluster.access.identity',
|
||||
'create-entity-alias': 'vault.cluster.access.identity.aliases',
|
||||
'edit-entity-alias': 'vault.cluster.access.identity.aliases.show',
|
||||
'create-group': 'vault.cluster.access.identity',
|
||||
'edit-group': 'vault.cluster.access.identity.show',
|
||||
'create-group-alias': 'vault.cluster.access.identity.aliases',
|
||||
'edit-group-alias': 'vault.cluster.access.identity.aliases.show',
|
||||
};
|
||||
|
||||
return routes[key];
|
||||
}),
|
||||
|
||||
getMessage(model) {
|
||||
let mode = this.get('mode');
|
||||
let typeDisplay = humanize([model.get('identityType')]);
|
||||
if (mode === 'merge') {
|
||||
return 'Successfully merged entities';
|
||||
}
|
||||
if (model.get('id')) {
|
||||
return `Successfully saved ${typeDisplay} ${model.id}.`;
|
||||
}
|
||||
return `Successfully saved ${typeDisplay}.`;
|
||||
},
|
||||
|
||||
save: task(function*() {
|
||||
let model = this.get('model');
|
||||
let message = this.getMessage(model);
|
||||
|
||||
try {
|
||||
yield model.save();
|
||||
} catch (err) {
|
||||
// err will display via model state
|
||||
return;
|
||||
}
|
||||
this.get('flashMessages').success(message);
|
||||
yield this.get('onSave')(model);
|
||||
}).drop(),
|
||||
|
||||
willDestroy() {
|
||||
let model = this.get('model');
|
||||
if (!model.isDestroyed || !model.isDestroying) {
|
||||
model.rollbackAttributes();
|
||||
}
|
||||
},
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
import Ember from 'ember';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { underscore } from 'vault/helpers/underscore';
|
||||
|
||||
const { inject } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
store: inject.service(),
|
||||
flashMessages: inject.service(),
|
||||
routing: inject.service('-routing'),
|
||||
|
||||
// Public API - either 'entity' or 'group'
|
||||
// this will determine which adapter is used to make the lookup call
|
||||
type: 'entity',
|
||||
|
||||
param: 'alias name',
|
||||
paramValue: null,
|
||||
aliasMountAccessor: null,
|
||||
|
||||
authMethods: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.get('store').findAll('auth-method').then(methods => {
|
||||
this.set('authMethods', methods);
|
||||
this.set('aliasMountAccessor', methods.get('firstObject.accessor'));
|
||||
});
|
||||
},
|
||||
|
||||
adapter() {
|
||||
let type = this.get('type');
|
||||
let store = this.get('store');
|
||||
return store.adapterFor(`identity/${type}`);
|
||||
},
|
||||
|
||||
data() {
|
||||
let { param, paramValue, aliasMountAccessor } = this.getProperties(
|
||||
'param',
|
||||
'paramValue',
|
||||
'aliasMountAccessor'
|
||||
);
|
||||
let data = {};
|
||||
|
||||
data[underscore([param])] = paramValue;
|
||||
if (param === 'alias name') {
|
||||
data.alias_mount_accessor = aliasMountAccessor;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
lookup: task(function*() {
|
||||
let flash = this.get('flashMessages');
|
||||
let type = this.get('type');
|
||||
let store = this.get('store');
|
||||
let { param, paramValue } = this.getProperties('param', 'paramValue');
|
||||
let response;
|
||||
try {
|
||||
response = yield this.adapter().lookup(store, this.data());
|
||||
} catch (err) {
|
||||
flash.danger(
|
||||
`We encountered an error attempting the ${type} lookup: ${err.message || err.errors.join('')}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (response) {
|
||||
return this.get('routing.router').transitionTo(
|
||||
'vault.cluster.access.identity.show',
|
||||
response.id,
|
||||
'details'
|
||||
);
|
||||
} else {
|
||||
flash.danger(`We were unable to find an identity ${type} with a "${param}" of "${paramValue}".`);
|
||||
}
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['info-table-row'],
|
||||
isVisible: Ember.computed.or('alwaysRender', 'value'),
|
||||
|
||||
/*
|
||||
* @param boolean
|
||||
* indicates if the component content should be always be rendered.
|
||||
* when false, the value of `value` will be used to determine if the component should render
|
||||
*/
|
||||
alwaysRender: false,
|
||||
|
||||
/*
|
||||
* @param string
|
||||
* the display name for the value
|
||||
*
|
||||
*/
|
||||
label: null,
|
||||
|
||||
/*
|
||||
*
|
||||
* the value of the data passed in - by default the content of the component will only show if there is a value
|
||||
*/
|
||||
value: null,
|
||||
|
||||
valueIsBoolean: Ember.computed('value', function() {
|
||||
return Ember.typeOf(this.get('value')) === 'boolean';
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
'data-test-component': 'info-tooltip',
|
||||
tagName: 'span',
|
||||
classNames: ['is-inline-block'],
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import IvyCodemirrorComponent from './ivy-codemirror';
|
||||
import Ember from 'ember';
|
||||
|
||||
const { assign } = Ember;
|
||||
const JSON_EDITOR_DEFAULTS = {
|
||||
// IMPORTANT: `gutters` must come before `lint` since the presence of
|
||||
// `gutters` is cached internally when `lint` is toggled
|
||||
gutters: ['CodeMirror-lint-markers'],
|
||||
tabSize: 2,
|
||||
mode: 'application/json',
|
||||
lineNumbers: true,
|
||||
lint: { lintOnChange: false },
|
||||
theme: 'hashi',
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
export default IvyCodemirrorComponent.extend({
|
||||
'data-test-component': 'json-editor',
|
||||
updateCodeMirrorOptions() {
|
||||
const options = assign({}, JSON_EDITOR_DEFAULTS, this.get('options'));
|
||||
|
||||
if (options) {
|
||||
Object.keys(options).forEach(function(option) {
|
||||
this.updateCodeMirrorOption(option, options[option]);
|
||||
}, this);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
import Ember from 'ember';
|
||||
import utils from 'vault/lib/key-utils';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'nav',
|
||||
classNames: 'key-value-header breadcrumb',
|
||||
ariaLabel: 'breadcrumbs',
|
||||
attributeBindings: ['ariaLabel:aria-label', 'aria-hidden'],
|
||||
|
||||
baseKey: null,
|
||||
path: null,
|
||||
showCurrent: true,
|
||||
linkToPaths: true,
|
||||
|
||||
stripTrailingSlash(str) {
|
||||
return str[str.length - 1] === '/' ? str.slice(0, -1) : str;
|
||||
},
|
||||
|
||||
currentPath: Ember.computed('mode', 'path', 'showCurrent', function() {
|
||||
const mode = this.get('mode');
|
||||
const path = this.get('path');
|
||||
const showCurrent = this.get('showCurrent');
|
||||
if (!mode || showCurrent === false) {
|
||||
return path;
|
||||
}
|
||||
return `vault.cluster.secrets.backend.${mode}`;
|
||||
}),
|
||||
|
||||
secretPath: Ember.computed('baseKey', 'baseKey.display', 'baseKey.id', 'root', 'showCurrent', function() {
|
||||
let crumbs = [];
|
||||
const root = this.get('root');
|
||||
const baseKey = this.get('baseKey.display') || this.get('baseKey.id');
|
||||
const baseKeyModel = this.get('baseKey.id');
|
||||
|
||||
if (root) {
|
||||
crumbs.push(root);
|
||||
}
|
||||
|
||||
if (!baseKey) {
|
||||
return crumbs;
|
||||
}
|
||||
|
||||
const path = this.get('path');
|
||||
const currentPath = this.get('currentPath');
|
||||
const showCurrent = this.get('showCurrent');
|
||||
const ancestors = utils.ancestorKeysForKey(baseKey);
|
||||
const parts = utils.keyPartsForKey(baseKey);
|
||||
if (!ancestors) {
|
||||
crumbs.push({
|
||||
label: baseKey,
|
||||
text: this.stripTrailingSlash(baseKey),
|
||||
path: currentPath,
|
||||
model: baseKeyModel,
|
||||
});
|
||||
|
||||
if (!showCurrent) {
|
||||
crumbs.pop();
|
||||
}
|
||||
|
||||
return crumbs;
|
||||
}
|
||||
|
||||
ancestors.forEach((ancestor, index) => {
|
||||
crumbs.push({
|
||||
label: parts[index],
|
||||
text: this.stripTrailingSlash(parts[index]),
|
||||
path: path,
|
||||
model: ancestor,
|
||||
});
|
||||
});
|
||||
|
||||
crumbs.push({
|
||||
label: utils.keyWithoutParentKey(baseKey),
|
||||
text: this.stripTrailingSlash(utils.keyWithoutParentKey(baseKey)),
|
||||
path: currentPath,
|
||||
model: baseKeyModel,
|
||||
});
|
||||
|
||||
if (!showCurrent) {
|
||||
crumbs.pop();
|
||||
}
|
||||
|
||||
return crumbs;
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
import Ember from 'ember';
|
||||
import KVObject from 'vault/lib/kv-object';
|
||||
|
||||
const { assert, Component, computed, guidFor } = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
'data-test-component': 'kv-object-editor',
|
||||
classNames: ['field', 'form-section'],
|
||||
// public API
|
||||
// Ember Object to mutate
|
||||
value: null,
|
||||
label: null,
|
||||
helpText: null,
|
||||
// onChange will be called with the changed Value
|
||||
onChange() {},
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
const data = KVObject.create({ content: [] }).fromJSON(this.get('value'));
|
||||
this.set('kvData', data);
|
||||
this.addRow();
|
||||
},
|
||||
|
||||
kvData: null,
|
||||
|
||||
kvDataAsJSON: computed('kvData', 'kvData.[]', function() {
|
||||
return this.get('kvData').toJSON();
|
||||
}),
|
||||
|
||||
kvDataIsAdvanced: computed('kvData', 'kvData.[]', function() {
|
||||
return this.get('kvData').isAdvanced();
|
||||
}),
|
||||
|
||||
kvHasDuplicateKeys: computed('kvData', 'kvData.@each.name', function() {
|
||||
let data = this.get('kvData');
|
||||
return data.uniqBy('name').length !== data.get('length');
|
||||
}),
|
||||
|
||||
addRow() {
|
||||
let data = this.get('kvData');
|
||||
let newObj = { name: '', value: '' };
|
||||
if (!Ember.isNone(data.findBy('name', ''))) {
|
||||
return;
|
||||
}
|
||||
guidFor(newObj);
|
||||
data.addObject(newObj);
|
||||
},
|
||||
actions: {
|
||||
addRow() {
|
||||
this.addRow();
|
||||
},
|
||||
|
||||
updateRow() {
|
||||
let data = this.get('kvData');
|
||||
this.get('onChange')(data.toJSON());
|
||||
},
|
||||
|
||||
deleteRow(object, index) {
|
||||
let data = this.get('kvData');
|
||||
let oldObj = data.objectAt(index);
|
||||
|
||||
assert('object guids match', guidFor(oldObj) === guidFor(object));
|
||||
data.removeAt(index);
|
||||
this.get('onChange')(data.toJSON());
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
Ember.LinkComponent.reopen({
|
||||
activeClass: 'is-active',
|
||||
});
|
||||
|
||||
export default Ember.LinkComponent;
|
|
@ -0,0 +1,35 @@
|
|||
import Ember from 'ember';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
let LinkedBlockComponent = Ember.Component.extend({
|
||||
layout: hbs`{{yield}}`,
|
||||
|
||||
classNames: 'linked-block',
|
||||
|
||||
routing: Ember.inject.service('-routing'),
|
||||
queryParams: null,
|
||||
|
||||
click(event) {
|
||||
const $target = this.$(event.target);
|
||||
const isAnchorOrButton =
|
||||
$target.is('a') ||
|
||||
$target.is('button') ||
|
||||
$target.closest('button', event.currentTarget).length > 0 ||
|
||||
$target.closest('a', event.currentTarget).length > 0;
|
||||
if (!isAnchorOrButton) {
|
||||
const router = this.get('routing.router');
|
||||
const params = this.get('params');
|
||||
const queryParams = this.get('queryParams');
|
||||
if (queryParams) {
|
||||
params.push({ queryParams });
|
||||
}
|
||||
router.transitionTo.apply(router, params);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
LinkedBlockComponent.reopenClass({
|
||||
positionalParams: 'params',
|
||||
});
|
||||
|
||||
export default LinkedBlockComponent;
|
|
@ -0,0 +1,37 @@
|
|||
import Ember from 'ember';
|
||||
import { range } from 'ember-composable-helpers/helpers/range';
|
||||
|
||||
const { computed } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['box', 'is-shadowless', 'list-pagination'],
|
||||
page: null,
|
||||
lastPage: null,
|
||||
link: null,
|
||||
model: null,
|
||||
// number of links to show on each side of page
|
||||
spread: 2,
|
||||
hasNext: computed('page', 'lastPage', function() {
|
||||
return this.get('page') < this.get('lastPage');
|
||||
}),
|
||||
hasPrevious: computed('page', 'lastPage', function() {
|
||||
return this.get('page') > 1;
|
||||
}),
|
||||
|
||||
segmentLinks: computed.gt('lastPage', 10),
|
||||
|
||||
pageRange: computed('page', 'lastPage', function() {
|
||||
const { spread, page, lastPage } = this.getProperties('spread', 'page', 'lastPage');
|
||||
|
||||
let lower = Math.max(2, page - spread);
|
||||
let upper = Math.min(lastPage - 1, lower + spread * 2);
|
||||
// we're closer to lastPage than the spread
|
||||
if (upper - lower < 5) {
|
||||
lower = upper - 4;
|
||||
}
|
||||
if (lastPage <= 10) {
|
||||
return range([1, lastPage, true]);
|
||||
}
|
||||
return range([lower, upper, true]);
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { inject } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
version: inject.service(),
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['column', 'is-sidebar'],
|
||||
classNameBindings: ['isActive:is-active'],
|
||||
isActive: false,
|
||||
actions: {
|
||||
openMenu() {
|
||||
this.set('isActive', true);
|
||||
},
|
||||
closeMenu() {
|
||||
this.set('isActive', false);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
model: null,
|
||||
errors: [],
|
||||
errorMessage: null,
|
||||
|
||||
displayErrors: Ember.computed(
|
||||
'errorMessage',
|
||||
'model.isError',
|
||||
'model.adapterError.errors.@each',
|
||||
'errors',
|
||||
'errors.@each',
|
||||
function() {
|
||||
const errorMessage = this.get('errorMessage');
|
||||
const errors = this.get('errors');
|
||||
const modelIsError = this.get('model.isError');
|
||||
if (errorMessage) {
|
||||
return [errorMessage];
|
||||
}
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (modelIsError) {
|
||||
return this.get('model.adapterError.errors');
|
||||
}
|
||||
}
|
||||
),
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
import { messageTypes } from 'vault/helpers/message-types';
|
||||
const { computed } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
type: null,
|
||||
|
||||
classNameBindings: ['containerClass'],
|
||||
|
||||
containerClass: computed('type', function() {
|
||||
return 'message ' + messageTypes([this.get('type')]).class;
|
||||
}),
|
||||
|
||||
alertType: computed('type', function() {
|
||||
return messageTypes([this.get('type')]);
|
||||
}),
|
||||
|
||||
messageClass: 'message-body',
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import Ember from 'ember';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
const { inject } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
store: inject.service(),
|
||||
|
||||
// Public API
|
||||
//value for the external mount selector
|
||||
value: null,
|
||||
onChange: () => {},
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.get('authMethods').perform();
|
||||
},
|
||||
|
||||
authMethods: task(function*() {
|
||||
let methods = yield this.get('store').findAll('auth-method');
|
||||
if (!this.get('value')) {
|
||||
this.set('value', methods.get('firstObject.accessor'));
|
||||
}
|
||||
return methods;
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
change(value) {
|
||||
this.get('onChange')(value);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,137 @@
|
|||
import Ember from 'ember';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { methods } from 'vault/helpers/mountable-auth-methods';
|
||||
|
||||
const { inject } = Ember;
|
||||
const METHODS = methods();
|
||||
|
||||
export default Ember.Component.extend({
|
||||
store: inject.service(),
|
||||
flashMessages: inject.service(),
|
||||
routing: inject.service('-routing'),
|
||||
|
||||
/*
|
||||
* @param Function
|
||||
* @public
|
||||
*
|
||||
* Optional param to call a function upon successfully mounting a backend
|
||||
*
|
||||
*/
|
||||
onMountSuccess: () => {},
|
||||
onConfigError: () => {},
|
||||
/*
|
||||
* @param String
|
||||
* @public
|
||||
* the type of backend we want to mount
|
||||
* defaults to `auth`
|
||||
*
|
||||
*/
|
||||
mountType: 'auth',
|
||||
|
||||
/*
|
||||
*
|
||||
* @param DS.Model
|
||||
* @private
|
||||
* Ember Data model corresponding to the `mountType`.
|
||||
* Created and set during `init`
|
||||
*
|
||||
*/
|
||||
mountModel: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
const type = this.get('mountType');
|
||||
const modelType = type === 'secret' ? 'secret-engine' : 'auth-method';
|
||||
const model = this.get('store').createRecord(modelType);
|
||||
this.set('mountModel', model);
|
||||
this.changeConfigModel(model.get('type'));
|
||||
},
|
||||
|
||||
willDestroy() {
|
||||
// if unsaved, we want to unload so it doesn't show up in the auth mount list
|
||||
this.get('mountModel').rollbackAttributes();
|
||||
},
|
||||
|
||||
getConfigModelType(methodType) {
|
||||
let noConfig = ['approle'];
|
||||
if (noConfig.includes(methodType)) {
|
||||
return;
|
||||
}
|
||||
if (methodType === 'aws') {
|
||||
return 'auth-config/aws/client';
|
||||
}
|
||||
return `auth-config/${methodType}`;
|
||||
},
|
||||
|
||||
changeConfigModel(methodType) {
|
||||
const mount = this.get('mountModel');
|
||||
const configRef = mount.hasMany('authConfigs').value();
|
||||
const currentConfig = configRef.get('firstObject');
|
||||
if (currentConfig) {
|
||||
// rollbackAttributes here will remove the the config model from the store
|
||||
// because `isNew` will be true
|
||||
currentConfig.rollbackAttributes();
|
||||
}
|
||||
const configType = this.getConfigModelType(methodType);
|
||||
if (!configType) return;
|
||||
const config = this.get('store').createRecord(configType);
|
||||
config.set('backend', mount);
|
||||
},
|
||||
|
||||
checkPathChange(type) {
|
||||
const mount = this.get('mountModel');
|
||||
const currentPath = mount.get('path');
|
||||
// if the current path matches a type (meaning the user hasn't altered it),
|
||||
// change it here to match the new type
|
||||
const isUnchanged = METHODS.findBy('type', currentPath);
|
||||
if (isUnchanged) {
|
||||
mount.set('path', type);
|
||||
}
|
||||
},
|
||||
|
||||
mountBackend: task(function*() {
|
||||
const mountModel = this.get('mountModel');
|
||||
const { type, path } = mountModel.getProperties('type', 'path');
|
||||
try {
|
||||
yield mountModel.save();
|
||||
} catch (err) {
|
||||
// err will display via model state
|
||||
return;
|
||||
}
|
||||
this.get('flashMessages').success(
|
||||
`Successfully mounted ${type} ${this.get('mountType')} method at ${path}.`
|
||||
);
|
||||
yield this.get('saveConfig').perform(mountModel);
|
||||
}).drop(),
|
||||
|
||||
saveConfig: task(function*(mountModel) {
|
||||
const configRef = mountModel.hasMany('authConfigs').value();
|
||||
const config = configRef.get('firstObject');
|
||||
const { type, path } = mountModel.getProperties('type', 'path');
|
||||
try {
|
||||
if (config && Object.keys(config.changedAttributes()).length) {
|
||||
yield config.save();
|
||||
this.get('flashMessages').success(
|
||||
`The config for ${type} ${this.get('mountType')} method at ${path} was saved successfully.`
|
||||
);
|
||||
}
|
||||
yield this.get('onMountSuccess')();
|
||||
} catch (err) {
|
||||
this.get('flashMessages').danger(
|
||||
`There was an error saving the configuration for ${type} ${this.get(
|
||||
'mountType'
|
||||
)} method at ${path}. ${err.errors.join(' ')}`
|
||||
);
|
||||
yield this.get('onConfigError')(mountModel.id);
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
actions: {
|
||||
onTypeChange(path, value) {
|
||||
if (path === 'type') {
|
||||
this.changeConfigModel(value);
|
||||
this.checkPathChange(value);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { get, set } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
config: null,
|
||||
mounts: null,
|
||||
|
||||
// singleton mounts are not eligible for per-mount-filtering
|
||||
singletonMountTypes: ['cubbyhole', 'system', 'token', 'identity'],
|
||||
|
||||
actions: {
|
||||
addOrRemovePath(path, e) {
|
||||
let config = get(this, 'config') || [];
|
||||
let paths = get(config, 'paths').slice();
|
||||
|
||||
if (e.target.checked) {
|
||||
paths.addObject(path);
|
||||
} else {
|
||||
paths.removeObject(path);
|
||||
}
|
||||
|
||||
set(config, 'paths', paths);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,190 @@
|
|||
import Ember from 'ember';
|
||||
import utils from 'vault/lib/key-utils';
|
||||
import keys from 'vault/lib/keycodes';
|
||||
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
|
||||
|
||||
const routeFor = function(type, mode) {
|
||||
const MODES = {
|
||||
secrets: 'vault.cluster.secrets.backend',
|
||||
'secrets-cert': 'vault.cluster.secrets.backend',
|
||||
'policy-show': 'vault.cluster.policy',
|
||||
'policy-list': 'vault.cluster.policies',
|
||||
leases: 'vault.cluster.access.leases',
|
||||
};
|
||||
let useSuffix = true;
|
||||
const typeVal = mode === 'secrets' || mode === 'leases' ? type : type.replace('-root', '');
|
||||
const modeKey = mode + '-' + typeVal;
|
||||
const modeVal = MODES[modeKey] || MODES[mode];
|
||||
if (modeKey === 'policy-list') {
|
||||
useSuffix = false;
|
||||
}
|
||||
|
||||
return useSuffix ? modeVal + '.' + typeVal : modeVal;
|
||||
};
|
||||
|
||||
export default Ember.Component.extend(FocusOnInsertMixin, {
|
||||
classNames: ['navigate-filter'],
|
||||
|
||||
// these get passed in from the outside
|
||||
// actions that get passed in
|
||||
filterFocusDidChange: null,
|
||||
filterDidChange: null,
|
||||
mode: 'secrets',
|
||||
shouldNavigateTree: false,
|
||||
extraNavParams: null,
|
||||
|
||||
baseKey: null,
|
||||
filter: null,
|
||||
filterMatchesKey: null,
|
||||
firstPartialMatch: null,
|
||||
|
||||
routing: Ember.inject.service('-routing'),
|
||||
|
||||
transitionToRoute: function() {
|
||||
var router = this.get('routing.router');
|
||||
router.transitionTo.apply(router, arguments);
|
||||
},
|
||||
|
||||
shouldFocus: false,
|
||||
|
||||
focusFilter: Ember.observer('filter', function() {
|
||||
if (!this.get('filter')) return;
|
||||
Ember.run.schedule('afterRender', this, 'forceFocus');
|
||||
}).on('didInsertElement'),
|
||||
|
||||
keyForNav(key) {
|
||||
if (this.get('mode') !== 'secrets-cert') {
|
||||
return key;
|
||||
}
|
||||
return `cert/${key}`;
|
||||
},
|
||||
onEnter: function(val) {
|
||||
let baseKey = this.get('baseKey');
|
||||
let mode = this.get('mode');
|
||||
let extraParams = this.get('extraNavParams');
|
||||
if (mode.startsWith('secrets') && (!val || val === baseKey)) {
|
||||
return;
|
||||
}
|
||||
if (this.get('filterMatchesKey') && !utils.keyIsFolder(val)) {
|
||||
let params = [routeFor('show', mode), extraParams, this.keyForNav(val)].compact();
|
||||
this.transitionToRoute(...params);
|
||||
} else {
|
||||
if (mode === 'policies') {
|
||||
return;
|
||||
}
|
||||
let route = routeFor('create', mode);
|
||||
if (baseKey) {
|
||||
this.transitionToRoute(route, this.keyForNav(baseKey), {
|
||||
queryParams: {
|
||||
initialKey: val.replace(this.keyForNav(baseKey), ''),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.transitionToRoute(route + '-root', {
|
||||
queryParams: {
|
||||
initialKey: this.keyForNav(val),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// pop to the nearest parentKey or to the root
|
||||
onEscape: function(val) {
|
||||
var key = utils.parentKeyForKey(val) || '';
|
||||
this.get('filterDidChange')(key);
|
||||
this.filterUpdated(key);
|
||||
},
|
||||
|
||||
onTab: function(event) {
|
||||
var firstPartialMatch = this.get('firstPartialMatch.id');
|
||||
if (!firstPartialMatch) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.get('filterDidChange')(firstPartialMatch);
|
||||
this.filterUpdated(firstPartialMatch);
|
||||
},
|
||||
|
||||
// as you type, navigates through the k/v tree
|
||||
filterUpdated: function(val) {
|
||||
var mode = this.get('mode');
|
||||
if (mode === 'policies' || !this.get('shouldNavigateTree')) {
|
||||
this.filterUpdatedNoNav(val, mode);
|
||||
return;
|
||||
}
|
||||
// select the key to nav to, assumed to be a folder
|
||||
var key = val ? val.trim() : '';
|
||||
var isFolder = utils.keyIsFolder(key);
|
||||
|
||||
if (!isFolder) {
|
||||
// nav to the closest parentKey (or the root)
|
||||
key = utils.parentKeyForKey(val) || '';
|
||||
}
|
||||
|
||||
const pageFilter = val.replace(key, '');
|
||||
this.navigate(this.keyForNav(key), mode, pageFilter);
|
||||
},
|
||||
|
||||
navigate(key, mode, pageFilter) {
|
||||
const route = routeFor(key ? 'list' : 'list-root', mode);
|
||||
let args = [route];
|
||||
if (key) {
|
||||
args.push(key);
|
||||
}
|
||||
if (pageFilter && !utils.keyIsFolder(pageFilter)) {
|
||||
args.push({
|
||||
queryParams: {
|
||||
page: 1,
|
||||
pageFilter,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
args.push({
|
||||
queryParams: {
|
||||
page: 1,
|
||||
pageFilter: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.transitionToRoute(...args);
|
||||
},
|
||||
|
||||
filterUpdatedNoNav: function(val, mode) {
|
||||
var key = val ? val.trim() : null;
|
||||
this.transitionToRoute(routeFor('list-root', mode), {
|
||||
queryParams: {
|
||||
pageFilter: key,
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
handleInput: function(event) {
|
||||
var filter = event.target.value;
|
||||
this.get('filterDidChange')(filter);
|
||||
Ember.run.debounce(this, 'filterUpdated', filter, 200);
|
||||
},
|
||||
|
||||
setFilterFocused: function(isFocused) {
|
||||
this.get('filterFocusDidChange')(isFocused);
|
||||
},
|
||||
|
||||
handleKeyPress: function(val, event) {
|
||||
if (event.keyCode === keys.TAB) {
|
||||
this.onTab(event);
|
||||
}
|
||||
},
|
||||
|
||||
handleKeyUp: function(val, event) {
|
||||
var keyCode = event.keyCode;
|
||||
if (keyCode === keys.ENTER) {
|
||||
this.onEnter(val);
|
||||
}
|
||||
if (keyCode === keys.ESC) {
|
||||
this.onEscape(val);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { computed, inject } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
// public
|
||||
model: null,
|
||||
|
||||
tagName: '',
|
||||
routing: inject.service('-routing'),
|
||||
path: computed.alias('routing.router.currentURL'),
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
const { set } = Ember;
|
||||
const BASE_64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gi;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['box', 'is-fullwidth', 'is-marginless', 'is-shadowless'],
|
||||
key: null,
|
||||
index: null,
|
||||
onChange: () => {},
|
||||
|
||||
/*
|
||||
* @public
|
||||
* @param String
|
||||
* Text to use as the label for the file input
|
||||
* If null, a default will be rendered
|
||||
*/
|
||||
label: null,
|
||||
|
||||
/*
|
||||
* @public
|
||||
* @param String
|
||||
* Text to use as help under the file input
|
||||
* If null, a default will be rendered
|
||||
*/
|
||||
fileHelpText: null,
|
||||
|
||||
/*
|
||||
* @public
|
||||
* @param String
|
||||
* Text to use as help under the textarea in text-input mode
|
||||
* If null, a default will be rendered
|
||||
*/
|
||||
textareaHelpText: null,
|
||||
|
||||
readFile(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => this.setPGPKey(reader.result, file.name);
|
||||
// this gives us a base64-encoded string which is important in the onload
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
|
||||
setPGPKey(dataURL, filename) {
|
||||
const b64File = dataURL.split(',')[1].trim();
|
||||
const decoded = atob(b64File).trim();
|
||||
|
||||
// If a b64-encoded file was uploaded, then after decoding, it
|
||||
// will still be b64.
|
||||
// If after decoding it's not b64, we want
|
||||
// the original as it was only encoded when we used `readAsDataURL`.
|
||||
const fileData = decoded.match(BASE_64_REGEX) ? decoded : b64File;
|
||||
this.get('onChange')(this.get('index'), { value: fileData, fileName: filename });
|
||||
},
|
||||
|
||||
actions: {
|
||||
pickedFile(e) {
|
||||
const { files } = e.target;
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0, len = files.length; i < len; i++) {
|
||||
this.readFile(files[i]);
|
||||
}
|
||||
},
|
||||
updateData(e) {
|
||||
const key = this.get('key');
|
||||
set(key, 'value', e.target.value);
|
||||
this.get('onChange')(this.get('index'), this.get('key'));
|
||||
},
|
||||
clearKey() {
|
||||
this.get('onChange')(this.get('index'), { value: '' });
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
onDataUpdate: () => {},
|
||||
listData: Ember.computed('listLength', function() {
|
||||
let num = this.get('listLength');
|
||||
if (num) {
|
||||
num = parseInt(num, 10);
|
||||
}
|
||||
return Array(num || 0).fill(null).map(() => ({ value: '' }));
|
||||
}),
|
||||
listLength: 0,
|
||||
actions: {
|
||||
setKey(index, key) {
|
||||
let listData = this.get('listData');
|
||||
listData.replace(index, 1, key);
|
||||
this.get('onDataUpdate')(listData.compact().map(k => k.value));
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
/*
|
||||
* @public
|
||||
* @param DS.Model
|
||||
*
|
||||
* the pki-certificate model
|
||||
*/
|
||||
item: null,
|
||||
|
||||
actions: {
|
||||
delete(item) {
|
||||
item.save({ adapterOptions: { method: 'revoke' } });
|
||||
},
|
||||
},
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue