Merge pull request #4248 from hashicorp/oss-ui

Moving the UI to OSS
This commit is contained in:
Matthew Irish 2018-04-03 09:37:31 -05:00 committed by GitHub
commit 3d52c8c7c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
852 changed files with 41982 additions and 2 deletions

1
.gitignore vendored
View File

@ -65,6 +65,7 @@ tags
ui/dist
ui/tmp
ui/root
http/bindata_assetfs.go
# dependencies
ui/node_modules

View File

@ -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 {

View File

@ -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.

16
http/stub_assets.go Normal file
View File

@ -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
}

4
ui/.bowerrc Normal file
View File

@ -0,0 +1,4 @@
{
"directory": "bower_components",
"analytics": false
}

20
ui/.editorconfig Normal file
View File

@ -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

10
ui/.ember-cli Normal file
View File

@ -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"
}

24
ui/.eslintrc.js Normal file
View File

@ -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
}
};

23
ui/.gitignore vendored Normal file
View File

@ -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

25
ui/.travis.yml Normal file
View File

@ -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

3
ui/.watchmanconfig Normal file
View File

@ -0,0 +1,3 @@
{
"ignore_dirs": ["tmp", "dist"]
}

45
ui/README.md Normal file
View File

@ -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/)

View File

@ -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;
},
});

View File

@ -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 };
});
},
});

View File

@ -0,0 +1,2 @@
import AuthConfig from '../_base';
export default AuthConfig.extend();

View File

@ -0,0 +1,2 @@
import AuthConfig from '../_base';
export default AuthConfig.extend();

View File

@ -0,0 +1,2 @@
import AuthConfig from '../_base';
export default AuthConfig.extend();

View File

@ -0,0 +1,2 @@
import AuthConfig from './_base';
export default AuthConfig.extend();

View File

@ -0,0 +1,2 @@
import AuthConfig from './_base';
export default AuthConfig.extend();

View File

@ -0,0 +1,2 @@
import AuthConfig from './_base';
export default AuthConfig.extend();

View File

@ -0,0 +1,2 @@
import AuthConfig from './_base';
export default AuthConfig.extend();

View File

@ -0,0 +1,2 @@
import AuthConfig from './_base';
export default AuthConfig.extend();

View File

@ -0,0 +1,2 @@
import AuthConfig from './_base';
export default AuthConfig.extend();

View File

@ -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);
},
});

View File

@ -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;
});
},
});

187
ui/app/adapters/cluster.js Normal file
View File

@ -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 });
},
});

View File

@ -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);
});
},
});

View File

@ -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);
},
});

View File

@ -0,0 +1,3 @@
import IdentityAdapter from './base';
export default IdentityAdapter.extend();

View File

@ -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') };
});
},
});

View File

@ -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;
});
},
});

View File

@ -0,0 +1,3 @@
import IdentityAdapter from './base';
export default IdentityAdapter.extend();

View File

@ -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;
});
},
});

57
ui/app/adapters/lease.js Normal file
View File

@ -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;
});
},
});

View File

@ -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');
},
});

3
ui/app/adapters/node.js Normal file
View File

@ -0,0 +1,3 @@
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend();

View File

@ -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`;
},
});

View File

@ -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');
},
});

View File

@ -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}`;
},
});

View File

@ -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);
});
},
});

View File

@ -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;
});
},
});

24
ui/app/adapters/pki.js Normal file
View File

@ -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);
});
},
});

35
ui/app/adapters/policy.js Normal file
View File

@ -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 } });
},
});

View File

@ -0,0 +1,3 @@
import PolicyAdapter from '../policy';
export default PolicyAdapter.extend();

View File

@ -0,0 +1,3 @@
import PolicyAdapter from '../policy';
export default PolicyAdapter.extend();

View File

@ -0,0 +1,3 @@
import PolicyAdapter from '../policy';
export default PolicyAdapter.extend();

View File

@ -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);
},
});

View File

@ -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);
},
});

View File

@ -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);
},
});

View File

@ -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');
},
});

View File

@ -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 });
},
});

86
ui/app/adapters/secret.js Normal file
View File

@ -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;
});
},
});

View File

@ -0,0 +1,7 @@
import SSHAdapter from './ssh';
export default SSHAdapter.extend({
url(role) {
return `/v1/${role.backend}/creds/${role.name}`;
},
});

View File

@ -0,0 +1,7 @@
import SSHAdapter from './ssh';
export default SSHAdapter.extend({
url(role) {
return `/v1/${role.backend}/sign/${role.name}`;
},
});

24
ui/app/adapters/ssh.js Normal file
View File

@ -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);
});
},
});

23
ui/app/adapters/tools.js Normal file
View File

@ -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);
},
});

View File

@ -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,
});
},
});

16
ui/app/app.js Normal file
View File

@ -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;

View File

View File

@ -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;

View File

@ -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.');
}),
});

View File

@ -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)
);
},
},
});

View File

@ -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');
});
},
},
});

View File

@ -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);
},
});

View File

@ -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);
}
},
},
});

View File

@ -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')();
},
},
});

View File

@ -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');
}
},
},
});

View File

@ -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')}`;
}),
});

View File

@ -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,
});

View File

@ -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'],
});

View File

@ -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}`;
},
}),
});

View File

@ -0,0 +1,7 @@
import Ember from 'ember';
export default Ember.Component.extend({
classNames: 'column',
header: null,
content: null,
});

View File

@ -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: () => {},
});

View File

@ -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));
}
},
},
});

View File

@ -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();
},
},
});

View File

@ -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(' ');
}),
});

View File

@ -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)}`;
}),
});

View File

@ -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();
}
},
});

View File

@ -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}".`);
}
}),
});

View File

@ -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';
}),
});

View File

@ -0,0 +1,7 @@
import Ember from 'ember';
export default Ember.Component.extend({
'data-test-component': 'info-tooltip',
tagName: 'span',
classNames: ['is-inline-block'],
});

View File

@ -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);
}
},
});

View File

@ -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;
}),
});

View File

@ -0,0 +1,5 @@
import Ember from 'ember';
export default Ember.Component.extend({
tagName: '',
});

View File

@ -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());
},
},
});

View File

@ -0,0 +1,7 @@
import Ember from 'ember';
Ember.LinkComponent.reopen({
activeClass: 'is-active',
});
export default Ember.LinkComponent;

View File

@ -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;

View File

@ -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]);
}),
});

View File

@ -0,0 +1,8 @@
import Ember from 'ember';
const { inject } = Ember;
export default Ember.Component.extend({
tagName: '',
version: inject.service(),
});

View File

@ -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);
},
},
});

View File

@ -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');
}
}
),
});

View File

@ -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',
});

View File

@ -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);
},
},
});

View File

@ -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);
}
},
},
});

View File

@ -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);
},
},
});

View File

@ -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);
}
},
},
});

View File

@ -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'),
});

View File

@ -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: '' });
},
},
});

View File

@ -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));
},
},
});

View File

@ -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