Merge pull request #1454 from loganhz/pod-security

[WIP] Implement CRUD for pod security policies
This commit is contained in:
Vincent Fiduccia 2017-11-29 14:18:04 -07:00 committed by GitHub
commit 506bd9ebae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 650 additions and 2 deletions

View File

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

View File

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

View File

@ -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|}}
<div class="{{if (eq index 0) 'row box' 'row box mt-20'}}">
{{#each row as |policy|}}
<div class="col span-4">
<label class="acc-label">{{t (concat-str 'formBasicPodSecurityPolicy' policy 'label' character='.')}}</label>
<div class="radio">
<label>{{radio-button selection=(get model policy) value=true}} {{t (concat-str 'formBasicPodSecurityPolicy' policy 'enable' character='.')}}</label>
</div>
<div class="radio">
<label>{{radio-button selection=(get model policy) value=false}} {{t (concat-str 'formBasicPodSecurityPolicy' policy 'disable' character='.')}}</label>
</div>
</div>
{{/each}}
</div>
{{/each}}
{{/accordion-list-item}}

View File

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

View File

@ -0,0 +1,38 @@
{{#accordion-list-item
title=(t 'formCapabilityPodSecurityPolicy.title')
detail=(t 'formCapabilityPodSecurityPolicy.detail')
status=status
statusClass=statusClass
expandAll=expandAll
expand=(action expandFn)
}}
<div class="row">
<div class="col box span-4">
<label class="acc-label">{{t 'formCapabilityPodSecurityPolicy.capabilities.allow'}}</label>
<select class="form-control select-cap-allow" multiple="true" onchange={{action 'modifyCapabilities' 'allowedCapabilities' }}>
{{#each capabilityChoices as |choice|}}
<option value={{choice}} selected={{array-includes model.allowedCapabilities choice}}>{{choice}}</option>
{{/each}}
</select>
</div>
<div class="col box span-4">
<label class="acc-label">{{t 'formCapabilityPodSecurityPolicy.capabilities.add'}}</label>
<select class="form-control select-cap-add" multiple="true" onchange={{action 'modifyCapabilities' 'defaultAddCapabilities' }}>
{{#each capabilityChoices as |choice|}}
<option value={{choice}} selected={{array-includes model.defaultAddCapabilities choice}}>{{choice}}</option>
{{/each}}
</select>
</div>
<div class="col box span-4">
<label class="acc-label">{{t 'formCapabilityPodSecurityPolicy.capabilities.drop'}}</label>
<select class="form-control select-cap-drop" multiple="true" onchange={{action 'modifyCapabilities' 'requiredDropCapabilities'}}>
{{#each capabilityChoices as |choice|}}
<option value={{choice}} selected={{array-includes model.requiredDropCapabilities choice}}>{{choice}}</option>
{{/each}}
</select>
</div>
</div>
{{/accordion-list-item}}

View File

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

View File

@ -0,0 +1,19 @@
{{#accordion-list-item
title=(t 'formVolumePodSecurityPolicy.title')
detail=(t 'formVolumePodSecurityPolicy.detail')
status=status
statusClass=statusClass
expandAll=expandAll
expand=(action expandFn)
}}
<div class="row">
<div class="col box span-12">
<label class="acc-label">{{t 'formVolumePodSecurityPolicy.volumes'}}</label>
<select class="form-control" multiple="true" onchange={{action 'modifyVolumes'}}>
{{#each volumeChoices as |choice|}}
<option value={{choice}} selected={{array-includes model.volumes choice}}>{{choice}}</option>
{{/each}}
</select>
</div>
</div>
{{/accordion-list-item}}

View File

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

View File

@ -0,0 +1,53 @@
<section class="header clearfix">
<div class="pull-left">
{{#if editing}}
<h1>{{t 'podSecurityPoliciesPage.editPodSecurityPolicy'}}</h1>
{{else}}
<h1>{{t 'podSecurityPoliciesPage.addPodSecurityPolicy'}}</h1>
{{/if}}
</div>
</section>
<section class="horizontal-form container-fluid">
{{form-name-description
name=model.policy.name
nameRequired=true
nameDisabled=editing
namePlaceholder="podSecurityPoliciesPage.new.form.name.placeholder"
colClass="col span-7"
descriptionShown=false
}}
</section>
<section>
{{#accordion-list as |al expandFn|}}
<div class="mt-20">
{{form-basic-policy
model=model.policy
expandAll=al.expandAll
expandFn=expandFn
}}
</div>
<div class="mt-20">
{{form-capability-policy
model=model.policy
expandAll=al.expandAll
expandFn=expandFn
}}
</div>
<div class="mt-20">
{{form-volume-policy
model=model.policy
expandAll=al.expandAll
expandFn=expandFn
}}
</div>
{{/accordion-list}}
</section>
{{top-errors errors=errors}}
{{save-cancel createLabel=(if editing 'podSecurityPoliciesPage.saveEdit' 'podSecurityPoliciesPage.saveNew') save="save" cancel="cancel"}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
<li>{{#link-to "accounts"}}{{t 'nav.admin.accounts'}}{{/link-to}}</li>
<li>{{#link-to "roles.index" (query-params type="project")}}{{t 'nav.admin.projectRoles'}}{{/link-to}}</li>
<li>{{#link-to "roles.index" (query-params type="cluster")}}{{t 'nav.admin.clusterRoles'}}{{/link-to}}</li>
<li>{{#link-to "policies.index"}}{{t 'nav.admin.podSecurityPolicies'}}{{/link-to}}</li>
<li>{{#link-to "audit-logs"}}{{t 'nav.admin.audit'}}{{/link-to}}</li>
<li>{{#link-to "settings.auth"}}{{t 'nav.admin.settings.auth'}}{{/link-to}}</li>
<li>{{#link-to "settings.machine"}}{{t 'nav.admin.settings.machine'}}{{/link-to}}</li>

View File

@ -0,0 +1 @@
{{new-edit-policy model=model editing=true}}

View File

@ -0,0 +1,37 @@
<section class="instances">
{{#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")}}
<tr class="main-row">
<td valign="middle" class="row-check" style="padding-top: 2px;">
&nbsp;
</td>
<td data-title="{{t 'podSecurityPoliciesPage.index.table.name'}}:" class="clip">
{{#link-to "policies.edit" row.id}}
{{row.name}}
{{/link-to}}
</td>
<td data-title="{{t 'podSecurityPoliciesPage.index.table.created'}}:" >
{{date-calendar row.created}}
</td>
<td data-title="{{t 'generic.actions'}}:" class="actions">
{{action-menu model=row}}
</td>
</tr>
{{else if (eq kind "nomatch")}}
<td colspan="6" class="text-center text-muted lacsso pt-20 pb-20">{{t 'podSecurityPoliciesPage.index.table.noMatch'}}</td>
{{else if (eq kind "norows")}}
<td colspan="6" class="text-center text-muted lacsso pt-20 pb-20">{{t 'podSecurityPoliciesPage.index.table.noData'}}</td>
{{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}}
</section>

View File

@ -0,0 +1 @@
{{new-edit-policy model=model editing=false}}

View File

@ -5,7 +5,7 @@
sortBy=sortBy
headers=headers
searchText=searchText
body=model
body=filtered
rightActions=true
as |sortable kind row dt|
}}

View File

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

View File

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