diff --git a/app/models/podsecuritypolicytemplate.js b/app/models/podsecuritypolicytemplate.js new file mode 100644 index 000000000..a37b27350 --- /dev/null +++ b/app/models/podsecuritypolicytemplate.js @@ -0,0 +1,43 @@ +import { inject as service } from '@ember/service'; +import { getOwner } from '@ember/application'; +import { next } from '@ember/runloop'; + +import Resource from 'ember-api-store/models/resource'; +import PolledResource from 'ui/mixins/cattle-polled-resource'; + +var PodSecurityPolicyTemplate = Resource.extend(PolledResource, { + type: 'podSecurityPolicyTemplate', + router: service(), + + actions: { + edit: function() { + this.get('router').transitionTo('global-admin.policies.edit', this.get('id')); + }, + }, + + availableActions: function () { + return [ + { label: 'action.edit', icon: 'icon icon-edit', action: 'edit', enabled: true }, + { divider: true }, + { label: 'action.remove', icon: 'icon icon-trash', action: 'promptDelete', enabled: true, altAction: 'delete', bulkable: true }, + { divider: true }, + { label: 'action.viewInApi', icon: 'icon icon-external-link', action: 'goToApi', enabled: true }, + ]; + }.property(), + + delete: function (/*arguments*/) { + var promise = this._super.apply(this, arguments); + return promise.then(() => { + this.set('state', 'removed'); + }).catch((err) => { + this.get('growl').fromError('Error deleting', err); + }); + }, +}); + +PodSecurityPolicyTemplate.reopenClass({ + pollTransitioningDelay: 1000, + pollTransitioningInterval: 5000, +}); + +export default PodSecurityPolicyTemplate; diff --git a/lib/global-admin/addon/components/form-basic-policy/component.js b/lib/global-admin/addon/components/form-basic-policy/component.js new file mode 100644 index 000000000..b50360284 --- /dev/null +++ b/lib/global-admin/addon/components/form-basic-policy/component.js @@ -0,0 +1,34 @@ +import Component from '@ember/component'; +import layout from './template'; +import C from 'ui/utils/constants'; + +const policies = C.BASIC_POD_SECURITY_POLICIES; + +export default Component.extend({ + layout, + + classNames: ['accordion-wrapper'], + + model: null, + basicPolicies: null, + + init() { + this._super(...arguments); + const basicPolicies = []; + for (let i = 0; i < policies.length / 3; i++) { + basicPolicies.push(policies.slice(i * 3, i * 3 + 3)); + } + this.set('basicPolicies', basicPolicies); + }, + + didReceiveAttrs() { + if (!this.get('expandFn')) { + this.set('expandFn', function (item) { + item.toggleProperty('expanded'); + }); + } + }, + + statusClass: null, + status: null, +}); diff --git a/lib/global-admin/addon/components/form-basic-policy/template.hbs b/lib/global-admin/addon/components/form-basic-policy/template.hbs new file mode 100644 index 000000000..6616ae7b3 --- /dev/null +++ b/lib/global-admin/addon/components/form-basic-policy/template.hbs @@ -0,0 +1,24 @@ +{{#accordion-list-item + title=(t 'formBasicPodSecurityPolicy.title') + detail=(t 'formBasicPodSecurityPolicy.detail') + status=status + statusClass=statusClass + expandAll=expandAll + expand=(action expandFn) +}} + {{#each basicPolicies as |row index|}} +
+ {{#each row as |policy|}} +
+ +
+ +
+
+ +
+
+ {{/each}} +
+ {{/each}} +{{/accordion-list-item}} diff --git a/lib/global-admin/addon/components/form-capability-policy/component.js b/lib/global-admin/addon/components/form-capability-policy/component.js new file mode 100644 index 000000000..e219135e4 --- /dev/null +++ b/lib/global-admin/addon/components/form-capability-policy/component.js @@ -0,0 +1,91 @@ +import Component from '@ember/component'; +import layout from './template'; +import C from 'ui/utils/constants'; + +// @TODO-2.0 This api doesn't work. this.get('store').getById('schema','container').get('resourceFields.capAdd').options.sort(); +const choices = [ + "AUDIT_CONTROL", + "AUDIT_WRITE", + "BLOCK_SUSPEND", + "CHOWN", + "DAC_OVERRIDE", + "DAC_READ_SEARCH", + "FOWNER", + "FSETID", + "IPC_LOCK", + "IPC_OWNER", + "KILL", + "LEASE", + "LINUX_IMMUTABLE", + "MAC_ADMIN", + "MAC_OVERRIDE", + "MKNOD", + "NET_ADMIN", + "NET_BIND_SERVICE", + "NET_BROADCAST", + "NET_RAW", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYSLOG", + "SYS_ADMIN", + "SYS_BOOT", + "SYS_CHROOT", + "SYS_MODULE", + "SYS_NICE", + "SYS_PACCT", + "SYS_PTRACE", + "SYS_RAWIO", + "SYS_RESOURCE", + "SYS_TIME", + "SYS_TTY_CONFIG", + "WAKE_ALARM" +]; + +export default Component.extend({ + layout, + + classNames: ['accordion-wrapper'], + + model: null, + basicPolicies: null, + + init() { + this._super(...arguments); + this.initCapability(); + }, + + actions: { + modifyCapabilities: function(type, select) { + let options = Array.prototype.slice.call(select.target.options, 0); + let selectedOptions = []; + + options.filterBy('selected', true).forEach((cap) => { + return selectedOptions.push(cap.value); + }); + + this.set(`model.${type}`, selectedOptions); + }, + }, + + capabilityChoices: null, + + initCapability: function() { + this.set('model.allowedCapabilities', this.get('model.allowedCapabilities') || []); + this.set('model.defaultAddCapabilities', this.get('model.defaultAddCapabilities') || []); + this.set('model.requiredDropCapabilities', this.get('model.requiredDropCapabilities') || []); + this.set('capabilityChoices',choices); + }, + + didReceiveAttrs() { + if (!this.get('expandFn')) { + this.set('expandFn', function (item) { + item.toggleProperty('expanded'); + }); + } + }, + + statusClass: null, + status: null, +}); diff --git a/lib/global-admin/addon/components/form-capability-policy/template.hbs b/lib/global-admin/addon/components/form-capability-policy/template.hbs new file mode 100644 index 000000000..3cf74b62d --- /dev/null +++ b/lib/global-admin/addon/components/form-capability-policy/template.hbs @@ -0,0 +1,38 @@ +{{#accordion-list-item + title=(t 'formCapabilityPodSecurityPolicy.title') + detail=(t 'formCapabilityPodSecurityPolicy.detail') + status=status + statusClass=statusClass + expandAll=expandAll + expand=(action expandFn) +}} +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+{{/accordion-list-item}} diff --git a/lib/global-admin/addon/components/form-volume-policy/component.js b/lib/global-admin/addon/components/form-volume-policy/component.js new file mode 100644 index 000000000..b1e736b62 --- /dev/null +++ b/lib/global-admin/addon/components/form-volume-policy/component.js @@ -0,0 +1,50 @@ +import Component from '@ember/component'; +import layout from './template'; +import C from 'ui/utils/constants'; + +const choices = C.VOLUME_POLICIES; + +export default Component.extend({ + layout, + + classNames: ['accordion-wrapper'], + + model: null, + basicPolicies: null, + + init() { + this._super(...arguments); + this.initVolume(); + }, + + actions: { + modifyVolumes: function(type, select) { + let options = Array.prototype.slice.call(select.target.options, 0); + let selectedOptions = []; + + options.filterBy('selected', true).forEach((cap) => { + return selectedOptions.push(cap.value); + }); + + this.set('model.volumes', selectedOptions); + }, + }, + + volumeChoices: null, + + initVolume: function() { + this.set('model.volumes', this.get('model.volumes') || []); + this.set('volumeChoices',choices); + }, + + didReceiveAttrs() { + if (!this.get('expandFn')) { + this.set('expandFn', function (item) { + item.toggleProperty('expanded'); + }); + } + }, + + statusClass: null, + status: null, +}); diff --git a/lib/global-admin/addon/components/form-volume-policy/template.hbs b/lib/global-admin/addon/components/form-volume-policy/template.hbs new file mode 100644 index 000000000..aa732ce9e --- /dev/null +++ b/lib/global-admin/addon/components/form-volume-policy/template.hbs @@ -0,0 +1,19 @@ +{{#accordion-list-item + title=(t 'formVolumePodSecurityPolicy.title') + detail=(t 'formVolumePodSecurityPolicy.detail') + status=status + statusClass=statusClass + expandAll=expandAll + expand=(action expandFn) +}} +
+
+ + +
+
+{{/accordion-list-item}} diff --git a/lib/global-admin/addon/components/new-edit-policy/component.js b/lib/global-admin/addon/components/new-edit-policy/component.js new file mode 100644 index 000000000..9faf4f360 --- /dev/null +++ b/lib/global-admin/addon/components/new-edit-policy/component.js @@ -0,0 +1,69 @@ +import { alias } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import Component from '@ember/component'; +import NewOrEdit from 'ui/mixins/new-or-edit'; + +export default Component.extend(NewOrEdit, { + intl: service(), + router: service(), + model: null, + + primaryResource: alias('model.policy'), + + actions: { + cancel() { + this.goBack(); + }, + }, + + goBack: function() { + this.get('router').transitionTo('global-admin.policies.index'); + }, + + doesNameExist() { + const policy = this.get('primaryResource'); + const currentPolicies = this.get('model.policies'); + + if (currentPolicies.findBy('name', policy.get('name'))) { + return true; + } + + return false; + }, + + validate: function () { + var errors = this.get('errors', errors) || []; + + if ((this.get('model.policy.name') || '').trim().length === 0) { + errors.push(this.get('intl').findTranslationByKey('podSecurityPoliciesPage.new.errors.nameReq')); + } + + if (!this.get('editing') && this.doesNameExist()) { + errors.push(this.get('intl').findTranslationByKey('podSecurityPoliciesPage.new.errors.nameInExists')); + } + + if (errors.length) { + this.set('errors', errors.uniq()); + return false; + } + else { + this.set('errors', null); + } + + return true; + }, + + willSave() { + let ok = this._super(...arguments); + if (ok) { + const policy = this.get('primaryResource'); + const name = (policy.get('name') || '').trim().toLowerCase(); + policy.set('name', name); + } + return ok; + }, + + doneSaving() { + this.goBack(); + }, +}); diff --git a/lib/global-admin/addon/components/new-edit-policy/template.hbs b/lib/global-admin/addon/components/new-edit-policy/template.hbs new file mode 100644 index 000000000..88b76552b --- /dev/null +++ b/lib/global-admin/addon/components/new-edit-policy/template.hbs @@ -0,0 +1,53 @@ +
+
+ {{#if editing}} +

{{t 'podSecurityPoliciesPage.editPodSecurityPolicy'}}

+ {{else}} +

{{t 'podSecurityPoliciesPage.addPodSecurityPolicy'}}

+ {{/if}} +
+
+ +
+ {{form-name-description + name=model.policy.name + nameRequired=true + nameDisabled=editing + namePlaceholder="podSecurityPoliciesPage.new.form.name.placeholder" + colClass="col span-7" + descriptionShown=false + }} +
+ +
+ {{#accordion-list as |al expandFn|}} +
+ {{form-basic-policy + model=model.policy + expandAll=al.expandAll + expandFn=expandFn + }} +
+ +
+ {{form-capability-policy + model=model.policy + expandAll=al.expandAll + expandFn=expandFn + }} +
+ +
+ {{form-volume-policy + model=model.policy + expandAll=al.expandAll + expandFn=expandFn + }} +
+ + {{/accordion-list}} +
+ + +{{top-errors errors=errors}} +{{save-cancel createLabel=(if editing 'podSecurityPoliciesPage.saveEdit' 'podSecurityPoliciesPage.saveNew') save="save" cancel="cancel"}} \ No newline at end of file diff --git a/lib/global-admin/addon/controllers/policies/index.js b/lib/global-admin/addon/controllers/policies/index.js new file mode 100644 index 000000000..3b0127141 --- /dev/null +++ b/lib/global-admin/addon/controllers/policies/index.js @@ -0,0 +1,22 @@ +import Controller from '@ember/controller'; +import FilterState from 'ui/mixins/filter-state'; + +const headers = [ + { + translationKey: 'podSecurityPoliciesPage.index.table.name', + name: 'name', + sort: ['name'], + }, + { + translationKey: 'podSecurityPoliciesPage.index.table.created', + name: 'created', + sort: ['created'], + width: '200', + }, +] + +export default Controller.extend(FilterState, { + sortBy: 'name', + headers: headers, + searchText: '', +}); diff --git a/lib/global-admin/addon/controllers/roles/index.js b/lib/global-admin/addon/controllers/roles/index.js index 9c05fb2fd..12a869b0c 100644 --- a/lib/global-admin/addon/controllers/roles/index.js +++ b/lib/global-admin/addon/controllers/roles/index.js @@ -1,4 +1,5 @@ import Controller from '@ember/controller'; +import FilterState from 'ui/mixins/filter-state'; import { inject as service } from '@ember/service'; const headers = [ @@ -15,7 +16,7 @@ const headers = [ }, ] -export default Controller.extend({ +export default Controller.extend(FilterState, { sortBy: 'name', headers: headers, queryParams: ['type'], diff --git a/lib/global-admin/addon/routes.js b/lib/global-admin/addon/routes.js index a687ff902..36fc279b7 100644 --- a/lib/global-admin/addon/routes.js +++ b/lib/global-admin/addon/routes.js @@ -12,6 +12,12 @@ export default buildRoutes(function() { this.route('new', {path: '/add'}); }); + this.route('policies', {path: '/policies'}, function() { + this.route('index', {path: '/'}); + this.route('edit', {path: '/:policy_id'}); + this.route('new', {path: '/add'}); + }); + this.route('audit-logs'); this.route('catalog'); diff --git a/lib/global-admin/addon/routes/policies/edit.js b/lib/global-admin/addon/routes/policies/edit.js new file mode 100644 index 000000000..3bdb18786 --- /dev/null +++ b/lib/global-admin/addon/routes/policies/edit.js @@ -0,0 +1,18 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default Route.extend({ + authzStore: service('authz-store'), + model: function (params) { + return this.get('authzStore').find(`podSecurityPolicyTemplate`, null, { url: 'podSecurityPolicyTemplates', forceReload: true, removeMissing: true }).then((policies) => { + const policy = policies.findBy('id', params.policy_id); + if (!policy) { + this.replaceWith('policies.index'); + } + return { + policy, + policies, + } + }); + }, +}); diff --git a/lib/global-admin/addon/routes/policies/index.js b/lib/global-admin/addon/routes/policies/index.js new file mode 100644 index 000000000..3bb5463c7 --- /dev/null +++ b/lib/global-admin/addon/routes/policies/index.js @@ -0,0 +1,9 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default Route.extend({ + authzStore: service('authz-store'), + model: function (params) { + return this.get('authzStore').find('podSecurityPolicyTemplate', null, { url: 'podSecurityPolicyTemplates', forceReload: true, removeMissing: true }).then((policies) => policies); + }, +}); diff --git a/lib/global-admin/addon/routes/policies/new.js b/lib/global-admin/addon/routes/policies/new.js new file mode 100644 index 000000000..e2513ce65 --- /dev/null +++ b/lib/global-admin/addon/routes/policies/new.js @@ -0,0 +1,25 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default Route.extend({ + authzStore: service('authz-store'), + model: function () { + var policy = this.get('authzStore').createRecord({ + type: 'podSecurityPolicyTemplate', + name: '', + allowPrivilegeEscalation: false, + defaultAllowPrivilegeEscalation: false, + hostIPC: false, + hostNetwork: false, + hostPID: false, + privileged: false, + readOnlyRootFilesystem: false, + }); + return this.get('authzStore').find('podSecurityPolicyTemplate', null, { url: 'podSecurityPolicyTemplates', forceReload: true, removeMissing: true }).then((policies) => { + return { + policy: policy, + policies: policies, + } + }); + }, +}); diff --git a/lib/global-admin/addon/templates/application.hbs b/lib/global-admin/addon/templates/application.hbs index b7f7739e1..d9ae8ef77 100644 --- a/lib/global-admin/addon/templates/application.hbs +++ b/lib/global-admin/addon/templates/application.hbs @@ -4,6 +4,7 @@
  • {{#link-to "accounts"}}{{t 'nav.admin.accounts'}}{{/link-to}}
  • {{#link-to "roles.index" (query-params type="project")}}{{t 'nav.admin.projectRoles'}}{{/link-to}}
  • {{#link-to "roles.index" (query-params type="cluster")}}{{t 'nav.admin.clusterRoles'}}{{/link-to}}
  • +
  • {{#link-to "policies.index"}}{{t 'nav.admin.podSecurityPolicies'}}{{/link-to}}
  • {{#link-to "audit-logs"}}{{t 'nav.admin.audit'}}{{/link-to}}
  • {{#link-to "settings.auth"}}{{t 'nav.admin.settings.auth'}}{{/link-to}}
  • {{#link-to "settings.machine"}}{{t 'nav.admin.settings.machine'}}{{/link-to}}
  • diff --git a/lib/global-admin/addon/templates/policies/edit.hbs b/lib/global-admin/addon/templates/policies/edit.hbs new file mode 100644 index 000000000..f01cce83e --- /dev/null +++ b/lib/global-admin/addon/templates/policies/edit.hbs @@ -0,0 +1 @@ +{{new-edit-policy model=model editing=true}} \ No newline at end of file diff --git a/lib/global-admin/addon/templates/policies/index.hbs b/lib/global-admin/addon/templates/policies/index.hbs new file mode 100644 index 000000000..aaeaa48ed --- /dev/null +++ b/lib/global-admin/addon/templates/policies/index.hbs @@ -0,0 +1,37 @@ +
    + {{#sortable-table + classNames="grid sortable-table" + fullRows=true + sortBy=sortBy + headers=headers + searchText=searchText + body=filtered + rightActions=true + as |sortable kind row dt| + }} + {{#if (eq kind "row")}} + + +   + + + {{#link-to "policies.edit" row.id}} + {{row.name}} + {{/link-to}} + + + {{date-calendar row.created}} + + + {{action-menu model=row}} + + + {{else if (eq kind "nomatch")}} + {{t 'podSecurityPoliciesPage.index.table.noMatch'}} + {{else if (eq kind "norows")}} + {{t 'podSecurityPoliciesPage.index.table.noData'}} + {{else if (eq kind "right-actions")}} + {{#link-to "policies.new" classNames="btn btn-sm bg-primary right-divider-btn"}} {{t 'podSecurityPoliciesPage.addPodSecurityPolicy'}} {{/link-to}} + {{/if}} + {{/sortable-table}} +
    diff --git a/lib/global-admin/addon/templates/policies/new.hbs b/lib/global-admin/addon/templates/policies/new.hbs new file mode 100644 index 000000000..b6a6f216d --- /dev/null +++ b/lib/global-admin/addon/templates/policies/new.hbs @@ -0,0 +1 @@ +{{new-edit-policy model=model editing=false}} \ No newline at end of file diff --git a/lib/global-admin/addon/templates/roles/index.hbs b/lib/global-admin/addon/templates/roles/index.hbs index 730aa5e2b..94063a86d 100644 --- a/lib/global-admin/addon/templates/roles/index.hbs +++ b/lib/global-admin/addon/templates/roles/index.hbs @@ -5,7 +5,7 @@ sortBy=sortBy headers=headers searchText=searchText - body=model + body=filtered rightActions=true as |sortable kind row dt| }} diff --git a/lib/shared/addon/utils/constants.js b/lib/shared/addon/utils/constants.js index f8b8f1940..58c743c53 100644 --- a/lib/shared/addon/utils/constants.js +++ b/lib/shared/addon/utils/constants.js @@ -250,6 +250,47 @@ var C = { 'watch', ], + BASIC_POD_SECURITY_POLICIES: [ + 'allowPrivilegeEscalation', + 'defaultAllowPrivilegeEscalation', + 'hostIPC', + 'hostNetwork', + 'hostPID', + 'privileged', + 'readOnlyRootFilesystem' + ], + + VOLUME_POLICIES: [ + 'azureFile', + 'azureDisk', + 'flocker', + 'flexVolume', + 'hostPath', + 'emptyDir', + 'gcePersistentDisk', + 'awsElasticBlockStore', + 'gitRepo', + 'secret', + 'nfs', + 'iscsi', + 'glusterfs', + 'persistentVolumeClaim', + 'rbd', + 'cinder', + 'cephFS', + 'downwardAPI', + 'fc', + 'configMap', + 'vsphereVolume', + 'quobyte', + 'photonPersistentDisk', + 'projected', + 'portworxVolume', + 'scaleIO', + 'storageos', + '*', + ], + PROJECT_ROLE_RULES: [ 'CertificateSigningRequests', 'ComponentStatuses', diff --git a/translations/en-us.yaml b/translations/en-us.yaml index bbeb8c046..4e5e99fa8 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -122,6 +122,26 @@ accountsPage: labelText: Description placeholder: "e.g. This account is for John Smith" +podSecurityPoliciesPage: + index: + table: + name: Name + created: Created Time + noData: There are no pod security policies yet + noMatch: No pod security policies match the current search + addPodSecurityPolicy: Add Pod Security Policy + editPodSecurityPolicy: Edit Pod Security Policy + saveEdit: Edit + saveNew: Create + new: + errors: + nameReq: Name is requried. + nameInExists: Name is already exists. Please use a new pod security policy name. + form: + name: + labelText: Name + placeholder: "e.g. policy" + rolesPage: index: clusterRoles: Cluster Roles @@ -2494,6 +2514,50 @@ hookPage: url: label: Trigger URL +formCapabilityPodSecurityPolicy: + title: Capability Policies + detail: Config set of capability Policies + capabilities: + allow: Allowed Capabilities + add: Default Add Capabilities + drop: Required Drop Capabilities + +formVolumePodSecurityPolicy: + title: Volume Policy + detail: Control the usage of volume types + volumes: Volumes + +formBasicPodSecurityPolicy: + title: Basic Policies + detail: Config basic pod security policies + allowPrivilegeEscalation: + label: Allow Privilege Escalation + enable: "Yes: Running of a container that allow privilege escalation from its parent" + disable: No + defaultAllowPrivilegeEscalation: + label: Default Allow Privilege Escalation + enable: "Yes: Control whether a process can gain more privileges than its parent process" + disable: No + hostIPC: + label: Host IPC + enable: "Yes: The use of host’s IPC namespace" + disable: No + hostNetwork: + label: Host Network + enable: "Yes: The use of host networking" + disable: No + hostPID: + label: Host PID + enable: "Yes: The use of host’s PID namespace" + disable: No + privileged: + label: Privileged + enable: "Yes: Running of privileged containers" + disable: No + readOnlyRootFilesystem: + label: Read Only Root Filesystem + enable: "Yes: Requiring the use of a read only root file system" + disable: No hostSettings: header: Host Registration URL @@ -3791,6 +3855,7 @@ nav: tab: Admin Settings audit: Audit Log processes: Processes + podSecurityPolicies: Pod Security Policies accounts: Accounts catalog: Catalogs ha: HA