Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kubernetes Secrets Engine #17893

Merged
merged 29 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
da75948
Ember Engine for Kubernetes Secrets Engine (#17881)
zofskeez Nov 10, 2022
b879092
Kubernetes route plumbing (#17895)
zofskeez Nov 14, 2022
16f8477
adds kubernetes as mountable and supported secrets engine (#17891)
zofskeez Nov 14, 2022
39e67ff
adds models, adapters and serializers for kubernetes secrets engine (…
zofskeez Nov 17, 2022
0d48342
adds mirage factories and handlers for kubernetes (#17943)
zofskeez Nov 17, 2022
3a8ebf0
Kubernetes Secrets Engine Configuration (#18093)
zofskeez Nov 23, 2022
58e10b6
Merge branch 'main' into ui/kubernetes-secrets-engine
zofskeez Nov 23, 2022
8f5c46f
Merge branch 'main' into ui/kubernetes-secrets-engine
zofskeez Nov 28, 2022
2692eb5
Kubernetes Configuration View (#18147)
zofskeez Nov 30, 2022
489e2e5
Kubernetes Roles List (#18211)
zofskeez Dec 5, 2022
5bd9bfe
VAULT-9863 Kubernetes Overview Page (#18232)
kiannaquach Dec 6, 2022
c560b12
Kubernetes Secrets Engine Create/Edit Views (#18271)
zofskeez Dec 9, 2022
4133803
fixes issue with overview route showing 404 page (#18303)
zofskeez Dec 9, 2022
6e7384e
Kubernetes Role Details View (#18294)
zofskeez Dec 9, 2022
0de8307
fixes list link for secrets in an ember engine (#18313)
zofskeez Dec 12, 2022
4e46c2e
Manual Testing: Bug Fixes and Improvements (#18333)
zofskeez Dec 13, 2022
aff339e
VAULT-9877 Kubernetes Credential Generate/View Pages (#18270)
kiannaquach Dec 13, 2022
2236321
Merge branch 'main' into ui/kubernetes-secrets-engine
zofskeez Dec 13, 2022
eae3361
adds acceptance tests for kubernetes secrets engine roles (#18360)
zofskeez Dec 14, 2022
3bff9f9
VAULT-11862 Kubernetes acceptance tests (#18431)
kiannaquach Dec 16, 2022
1534eb4
VAULT-12127 Refactor breadcrumbs to use breadcrumb component (#18489)
kiannaquach Dec 20, 2022
7c98a71
VAULT-12166 add jsdocs to kubernetes secrets engine pages (#18509)
kiannaquach Dec 21, 2022
cc2fd48
Merge branch 'main' into ui/kubernetes-secrets-engine
zofskeez Jan 3, 2023
423132d
fixes incorrect merge conflict resolution
zofskeez Jan 3, 2023
68d0bfe
updates kubernetes check env vars endpoint (#18588)
zofskeez Jan 3, 2023
c8edd33
Merge branch 'main' into ui/kubernetes-secrets-engine
zofskeez Jan 3, 2023
12b3505
hides kubernetes ca cert field if not defined in configuration view
zofskeez Jan 5, 2023
128fc57
fixes loading substate handling issue (#18592)
zofskeez Jan 5, 2023
74f2493
adds changelog entry
zofskeez Jan 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zofskeez - Could you please open a PR to change this to the formatting for the "features" section of the changelog? Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mladlow oops I missed that in the readme! Sorry about that. #19062 updates the formatting.

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