Skip to content

Commit

Permalink
Kubernetes Secrets Engine (#17893)
Browse files Browse the repository at this point in the history
* Ember Engine for Kubernetes Secrets Engine (#17881)

* adds in-repo ember engine for kubernetes secrets engine

* updates kubernetes engine class name

* Kubernetes route plumbing (#17895)

* kubernetes route plumbing

* adds kubernetes role index route with redirect to details

* adds kubernetes as mountable and supported secrets engine (#17891)

* adds models, adapters and serializers for kubernetes secrets engine (#18010)

* adds mirage factories and handlers for kubernetes (#17943)

* Kubernetes Secrets Engine Configuration (#18093)

* moves RadioCard component to core addon

* adds kubernetes configuration view

* fixes tests using RadioCard after label for and input id changes

* adds confirm modal when editing kubernetes config

* addresses review comments

* Kubernetes Configuration View (#18147)

* removes configuration edit and index routes

* adds kubernetes configuration view

* Kubernetes Roles List (#18211)

* removes configuration edit and index routes

* adds kubernetes configuration view

* adds kubernetes secrets engine roles list view

* updates role details disabled state to explicitly check for false

* VAULT-9863 Kubernetes Overview Page (#18232)

* Add overview page view

* Add overview page tests

* Address feedback to update tests and minor changes

* Use template built in helper for conditionally showing num roles

* Set up roleOptions in constructor

* Set up models in tests and fix minor bug

* Kubernetes Secrets Engine Create/Edit Views (#18271)

* moves kv-object-editor to core addon

* moves json-editor to core addon

* adds kubernetes secrets engine create/edit views

* updates kubernetes/role adapter test

* addresses feedback

* fixes issue with overview route showing 404 page (#18303)

* Kubernetes Role Details View (#18294)

* moves format-duration helper to core addon

* adds kubernetes secrets engine role details view

* adds tests for role details page component

* adds capabilities checks for toolbar actions

* fixes list link for secrets in an ember engine (#18313)

* Manual Testing: Bug Fixes and Improvements (#18333)

* updates overview, configuration and roles components to pass args for individual model properties

* bug fixes and improvements

* adds top level index route to redirect to overview

* VAULT-9877 Kubernetes Credential Generate/View Pages (#18270)

* Add credentials route with create and view components

* Update mirage response for creds and add ajax post call for creds in adapter

* Move credentials create and view into one component

* Add test classes

* Remove files and update backend property name

* Code cleanup and add tests

* Put test helper in helper function

* Add one more test!

* Add code optimizations

* Fix model in route and add form

* Add onSubmit to form and preventDefault

* Fix tests

* Update mock data for test to be strong rather than record

* adds acceptance tests for kubernetes secrets engine roles (#18360)

* VAULT-11862 Kubernetes acceptance tests (#18431)

* VAULT-12185 overview acceptance tests

* VAULT-12298 credentials acceptance tests

* VAULT-12186 configuration acceptance tests

* VAULT-12127 Refactor breadcrumbs to use breadcrumb component (#18489)

* VAULT-12127 Refactor breadcrumbs to use Page::Breadcrumbs component

* Fix failing tests by adding breadcrumbs properties

* VAULT-12166 add jsdocs to kubernetes secrets engine pages (#18509)

* fixes incorrect merge conflict resolution

* updates kubernetes check env vars endpoint (#18588)

* hides kubernetes ca cert field if not defined in configuration view

* fixes loading substate handling issue (#18592)

* adds changelog entry

Co-authored-by: Kianna <[email protected]>
  • Loading branch information
zofskeez and kiannaquach authored Jan 18, 2023
1 parent afac0f7 commit 44a8e1b
Show file tree
Hide file tree
Showing 89 changed files with 3,676 additions and 7 deletions.
3 changes: 3 additions & 0 deletions changelog/17893.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
ui: Adds Kubernetes secrets engine
```
38 changes: 38 additions & 0 deletions ui/app/adapters/kubernetes/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import ApplicationAdapter from 'vault/adapters/application';
import { encodePath } from 'vault/utils/path-encoding-helpers';

export default class KubernetesConfigAdapter extends ApplicationAdapter {
namespace = 'v1';

getURL(backend, path = 'config') {
return `${this.buildURL()}/${encodePath(backend)}/${path}`;
}
urlForUpdateRecord(name, modelName, snapshot) {
return this.getURL(snapshot.attr('backend'));
}
urlForDeleteRecord(backend) {
return this.getURL(backend);
}

queryRecord(store, type, query) {
const { backend } = query;
return this.ajax(this.getURL(backend), 'GET').then((resp) => {
resp.backend = backend;
return resp;
});
}
createRecord() {
return this._saveRecord(...arguments);
}
updateRecord() {
return this._saveRecord(...arguments);
}
_saveRecord(store, { modelName }, snapshot) {
const data = store.serializerFor(modelName).serialize(snapshot);
const url = this.getURL(snapshot.attr('backend'));
return this.ajax(url, 'POST', { data }).then(() => data);
}
checkConfigVars(backend) {
return this.ajax(`${this.getURL(backend, 'check')}`, 'GET');
}
}
46 changes: 46 additions & 0 deletions ui/app/adapters/kubernetes/role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import NamedPathAdapter from 'vault/adapters/named-path';
import { encodePath } from 'vault/utils/path-encoding-helpers';

export default class KubernetesRoleAdapter extends NamedPathAdapter {
getURL(backend, name) {
const base = `${this.buildURL()}/${encodePath(backend)}/roles`;
return name ? `${base}/${name}` : base;
}
urlForQuery({ backend }) {
return this.getURL(backend);
}
urlForUpdateRecord(name, modelName, snapshot) {
return this.getURL(snapshot.attr('backend'), name);
}
urlForDeleteRecord(name, modelName, snapshot) {
return this.getURL(snapshot.attr('backend'), name);
}

query(store, type, query) {
const { backend } = query;
return this.ajax(this.getURL(backend), 'GET', { data: { list: true } }).then((resp) => {
return resp.data.keys.map((name) => ({ name, backend }));
});
}
queryRecord(store, type, query) {
const { backend, name } = query;
return this.ajax(this.getURL(backend, name), 'GET').then((resp) => {
resp.data.backend = backend;
resp.data.name = name;
return resp.data;
});
}
generateCredentials(backend, data) {
const generateCredentialsUrl = `${this.buildURL()}/${encodePath(backend)}/creds/${data.role}`;

return this.ajax(generateCredentialsUrl, 'POST', { data }).then((response) => {
const { lease_id, lease_duration, data } = response;

return {
lease_id,
lease_duration,
...data,
};
});
}
}
8 changes: 8 additions & 0 deletions ui/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export default class App extends Application {
},
},
},
kubernetes: {
dependencies: {
services: ['router', 'store', 'secret-mount-path', 'flashMessages'],
externalRoutes: {
secrets: 'vault.cluster.secrets.backends',
},
},
},
pki: {
dependencies: {
services: [
Expand Down
8 changes: 8 additions & 0 deletions ui/app/helpers/mountable-secret-engines.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ const MOUNTABLE_SECRET_ENGINES = [
type: 'totp',
category: 'generic',
},
{
displayName: 'Kubernetes',
value: 'kubernetes',
type: 'kubernetes',
engineRoute: 'kubernetes.overview',
category: 'generic',
glyph: 'kubernetes-color',
},
];

export function mountableEngines() {
Expand Down
1 change: 1 addition & 0 deletions ui/app/helpers/supported-secret-backends.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const SUPPORTED_SECRET_BACKENDS = [
'kmip',
'transform',
'keymgmt',
'kubernetes',
];

export function supportedSecretBackends() {
Expand Down
27 changes: 27 additions & 0 deletions ui/app/models/kubernetes/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Model, { attr } from '@ember-data/model';
import { withFormFields } from 'vault/decorators/model-form-fields';

@withFormFields(['kubernetesHost', 'serviceAccountJwt', 'kubernetesCaCert'])
export default class KubernetesConfigModel extends Model {
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
@attr('string', {
label: 'Kubernetes host',
subText:
'Kubernetes API URL to connect to. Defaults to https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT if those environment variables are set.',
})
kubernetesHost;
@attr('string', {
label: 'Service account JWT',
subText:
'The JSON web token of the service account used by the secret engine to manage Kubernetes roles. Defaults to the local pod’s JWT if found.',
})
serviceAccountJwt;
@attr('string', {
label: 'Kubernetes CA Certificate',
subText:
'PEM-encoded CA certificate to use by the secret engine to verify the Kubernetes API server certificate. Defaults to the local pod’s CA if found.',
editType: 'textarea',
})
kubernetesCaCert;
@attr('boolean', { defaultValue: false }) disableLocalCaJwt;
}
153 changes: 153 additions & 0 deletions ui/app/models/kubernetes/role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import Model, { attr } from '@ember-data/model';
import { withModelValidations } from 'vault/decorators/model-validations';
import { withFormFields } from 'vault/decorators/model-form-fields';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { tracked } from '@glimmer/tracking';

const validations = {
name: [{ type: 'presence', message: 'Name is required' }],
};
const formFieldProps = [
'name',
'serviceAccountName',
'kubernetesRoleType',
'kubernetesRoleName',
'allowedKubernetesNamespaces',
'tokenMaxTtl',
'tokenDefaultTtl',
'nameTemplate',
];

@withModelValidations(validations)
@withFormFields(formFieldProps)
export default class KubernetesRoleModel extends Model {
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
@attr('string', {
label: 'Role name',
subText: 'The role’s name in Vault.',
})
name;

@attr('string', {
label: 'Service account name',
subText: 'Vault will use the default template when generating service accounts, roles and role bindings.',
})
serviceAccountName;

@attr('string', {
label: 'Kubernetes role type',
editType: 'radio',
possibleValues: ['Role', 'ClusterRole'],
})
kubernetesRoleType;

@attr('string', {
label: 'Kubernetes role name',
subText: 'Vault will use the default template when generating service accounts, roles and role bindings.',
})
kubernetesRoleName;

@attr('string', {
label: 'Service account name',
subText: 'Vault will use the default template when generating service accounts, roles and role bindings.',
})
serviceAccountName;

@attr('string', {
label: 'Allowed Kubernetes namespaces',
subText:
'A list of the valid Kubernetes namespaces in which this role can be used for creating service accounts. If set to "*" all namespaces are allowed.',
})
allowedKubernetesNamespaces;

@attr({
label: 'Max Lease TTL',
editType: 'ttl',
})
tokenMaxTtl;

@attr({
label: 'Default Lease TTL',
editType: 'ttl',
})
tokenDefaultTtl;

@attr('string', {
label: 'Name template',
editType: 'optionalText',
defaultSubText:
'Vault will use the default template when generating service accounts, roles and role bindings.',
subText: 'Vault will use the default template when generating service accounts, roles and role bindings.',
})
nameTemplate;

@attr extraAnnotations;
@attr extraLabels;

@attr('string') generatedRoleRules;

@tracked _generationPreference;
get generationPreference() {
// when the user interacts with the radio cards the value will be set to the pseudo prop which takes precedence
if (this._generationPreference) {
return this._generationPreference;
}
// for existing roles, default the value based on which model prop has value -- only one can be set
let pref = null;
if (this.serviceAccountName) {
pref = 'basic';
} else if (this.kubernetesRoleName) {
pref = 'expanded';
} else if (this.generatedRoleRules) {
pref = 'full';
}
return pref;
}
set generationPreference(pref) {
// unset model props specific to filteredFormFields when changing preference
// only one of service_account_name, kubernetes_role_name or generated_role_rules can be set
const props = {
basic: ['kubernetesRoleType', 'kubernetesRoleName', 'generatedRoleRules', 'nameTemplate'],
expanded: ['serviceAccountName', 'generatedRoleRules'],
full: ['serviceAccountName', 'kubernetesRoleName'],
}[pref];
props.forEach((prop) => (this[prop] = null));
this._generationPreference = pref;
}

get filteredFormFields() {
// return different form fields based on generationPreference
const hiddenFieldIndices = {
basic: [2, 3, 7], // kubernetesRoleType, kubernetesRoleName and nameTemplate
expanded: [1], // serviceAccountName
full: [1, 3], // serviceAccountName and kubernetesRoleName
}[this.generationPreference];

return hiddenFieldIndices
? this.formFields.filter((field, index) => !hiddenFieldIndices.includes(index))
: null;
}

@lazyCapabilities(apiPath`${'backend'}/roles/${'name'}`, 'backend', 'name') rolePath;
@lazyCapabilities(apiPath`${'backend'}/creds/${'name'}`, 'backend', 'name') credsPath;
@lazyCapabilities(apiPath`${'backend'}/roles`, 'backend') rolesPath;

get canCreate() {
return this.rolePath.get('canCreate');
}
get canDelete() {
return this.rolePath.get('canDelete');
}
get canEdit() {
return this.rolePath.get('canUpdate');
}
get canRead() {
return this.rolePath.get('canRead');
}
get canList() {
return this.rolesPath.get('canList');
}
get canGenerateCreds() {
return this.credsPath.get('canCreate');
}
}
1 change: 1 addition & 0 deletions ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ Router.map(function () {
this.route('backends', { path: '/' });
this.route('backend', { path: '/:backend' }, function () {
this.mount('kmip');
this.mount('kubernetes');
if (config.environment !== 'production') {
this.mount('pki');
}
Expand Down
3 changes: 2 additions & 1 deletion ui/app/routes/vault/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
return true;
},
loading(transition) {
if (transition.queryParamsOnly || Ember.testing) {
const isSameRoute = transition.from?.name === transition.to?.name;
if (isSameRoute || Ember.testing) {
return;
}
// eslint-disable-next-line ember/no-controller-access-in-routes
Expand Down
12 changes: 12 additions & 0 deletions ui/app/serializers/kubernetes/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ApplicationSerializer from '../application';

export default class KubernetesConfigSerializer extends ApplicationSerializer {
primaryKey = 'backend';

serialize() {
const json = super.serialize(...arguments);
// remove backend value from payload
delete json.backend;
return json;
}
}
12 changes: 12 additions & 0 deletions ui/app/serializers/kubernetes/role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ApplicationSerializer from '../application';

export default class KubernetesConfigSerializer extends ApplicationSerializer {
primaryKey = 'name';

serialize() {
const json = super.serialize(...arguments);
// remove backend value from payload
delete json.backend;
return json;
}
}
Loading

0 comments on commit 44a8e1b

Please sign in to comment.