From 57b16e42fef86c2beeff68ccc4cd88579f458127 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 26 Jan 2021 20:13:55 +0000 Subject: [PATCH 01/13] =?UTF-8?q?Add=20Users=20&=20Auth=20/=20Groups=20pag?= =?UTF-8?q?e,=20assign=20Global=20Roles=20to=20Groups=20-=20Add=20groups?= =?UTF-8?q?=20page=20with=20table=20to=20the=20auth=20product=20-=20Allow?= =?UTF-8?q?=20user=20to=20assign=20roles=20to=20groups=20previously=20with?= =?UTF-8?q?out=20roles=20or=20edit=20=C2=A0=20groups=20with=20existing=20r?= =?UTF-8?q?oles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comments - I haven't added any special ux for the case where there's no auth   provider and therefore no groups - ./components/GlobalRoleBindings.vue will be updated when the same   component is used for assign global roles to a user principal - ./components/GlobalRoleBindings.vue ln 139 Couldn't create a binding   without the generateName metadata property. Have given this a   `ui-` prefix. Is this correct? - In order to determine which global roles are bound to each principal   (so we can filter by principals that have them).. we go out and fetch   ALL role bindings. Is this too costly? - On the groups page the 'refresh' button is quite big, we should   consider reducing this --- assets/translations/en-us.yaml | 101 +++++++- components/GlobalRoleBindings.vue | 220 ++++++++++++++++++ components/PromptRemove.vue | 2 +- components/auth/SelectPrincipal.vue | 43 +++- components/form/Checkbox.vue | 38 ++- components/formatter/Principal.vue | 19 ++ .../formatter/PrincipalGroupBindings.vue | 53 +++++ config/product/auth.js | 67 +++++- config/table-headers.js | 16 ++ config/types.js | 4 +- edit/group.principal.vue | 63 +++++ list/group.principal.vue | 102 ++++++++ models/group.principal.js | 61 +++++ .../management.cattle.io.globalrolebinding.js | 21 ++ .../auth/group.principal/assign-edit.vue | 115 +++++++++ plugins/steve/actions.js | 3 +- 16 files changed, 904 insertions(+), 24 deletions(-) create mode 100644 components/GlobalRoleBindings.vue create mode 100644 components/formatter/Principal.vue create mode 100644 components/formatter/PrincipalGroupBindings.vue create mode 100644 edit/group.principal.vue create mode 100644 list/group.principal.vue create mode 100644 models/group.principal.js create mode 100644 models/management.cattle.io.globalrolebinding.js create mode 100644 pages/c/_cluster/auth/group.principal/assign-edit.vue diff --git a/assets/translations/en-us.yaml b/assets/translations/en-us.yaml index 85e7571762..34c308eb0d 100644 --- a/assets/translations/en-us.yaml +++ b/assets/translations/en-us.yaml @@ -106,6 +106,7 @@ suffix: ############################## # Components & Pages ############################## + authConfig: accessMode: label: 'Configure who should be able to login and use {vendor}' @@ -250,7 +251,12 @@ authConfig: enabled: '{provider} is currently enabled.' testAndEnable: Test and Enable Authentication - +authGroups: + actions: + refresh: Refresh Group Memberships + assignRoles: Assign Global Roles + assignEdit: + assignTitle: Assign Global Roles To Group assignTo: title: |- @@ -290,6 +296,10 @@ asyncButton: action: 'Delete' waiting: 'Deleting…' success: 'Deleted' + remove: + action: 'Remove' + waiting: 'Removing…' + success: 'Removed' continue: action: 'Continue' waiting: 'Saving…' @@ -1614,7 +1624,88 @@ rbac: defaultLabel: Project Creator Default noContext: label: No Context - + globalRoles: + types: + global: + label: Global Permissions + detail: |- + Controls what access the {isUser, select, + true {user} + false {group}} has to administer the overall {appName} installation. + custom: + label: Custom + detail: Roles not created by Rancher. + builtin: + label: Built-in + detail: Additional roles to define more fine-grain permissions model. + unknownRole: + detail: No description provided + role: + admin: + label: Administrator + detail: Administrators have full control over the entire installation and all resources in all clusters. + restricted-admin: + label: Restricted Administrator + detail: Restricted Admins have full control over all resources in all downstream clusters but no access to the local cluster. + user: + label: Standard User + detail: Standard Users can create new clusters and manage clusters and projects they have been granted access to. + user-base: + label: User-Base + detail: User-Base users have login-access only. + clusters-create: + label: Create new Clusters + detail: Allows the user to create new clusters and become the owner of them. Standard Users have this permission by default. + clustertemplates-create: + label: Create new RKE Cluster Templates + detail: Allows the user to create new RKE cluster templates and become the owner of them. + authn-manage: + label: Configure Authentication + detail: Allows the user to enable, configure, and disable all Authentication provider settings. + catalogs-manage: + label: Configure Catalogs + detail: Allows the user to add, edit, and remove Catalogs. + clusters-manage: + label: Manage all Clusters + detail: Allows the user to manage all clusters, including ones they are not a member of. + clusterscans-manage: + label: Manage CIS Cluster Scans + detail: Allows the user to launch new and manage CIS cluster scans. + kontainerdrivers-manage: + label: Create new Cluster Drivers + detail: Allows the user to create new cluster drivers and become the owner of them. + features-manage: + label: Configure Feature Flags + detail: Allows the user to enable and disable custom features via feature flag settings. + nodedrivers-manage: + label: Configure Node Drivers + detail: Allows the user to enable, configure, and remove all Node Driver settings. + nodetemplates-manage: + label: Manage Node Templates + detail: Allows the user to define, edit, and remove Node Templates. + podsecuritypolicytemplates-manage: + label: Manage Pod Security Policies (PSPs) + detail: Allows the user to define, edit, and remove PSPs. + roles-manage: + label: Manage Roles + detail: Allows the user to define, edit, and remove Role definitions. + settings-manage: + label: Manage Settings + detail: Allows the user to manage Rancher Settings. + users-manage: + label: Manage Users + detail: Allows the user to create, remove, and set passwords for all Users. + catalogs-use: + label: Use Catalogs + detail: Allows the user to see and deploy Templates from the Catalog. Standard Users have this permission by default. + nodetemplates-use: + label: Use Node Templates + detail: Allows the user to deploy new Nodes using any existing Node Templates. + view-rancher-metrics: + label: View Rancher Metrics + detail: Allows the user to view Metrics through the API. + base: + label: Login Access resourceDetail: detailTop: @@ -2888,6 +2979,11 @@ typeLabel: one { RKE2 Cluster } other { RKE2 Clusters } } + group.principal: |- + {count, plural, + one { Group } + other { Groups } + } action: clone: Clone @@ -2905,6 +3001,7 @@ action: show: Show hide: Hide copy: Copy + unassign: 'Unassign' unit: sec: secs diff --git a/components/GlobalRoleBindings.vue b/components/GlobalRoleBindings.vue new file mode 100644 index 0000000000..209ac4a639 --- /dev/null +++ b/components/GlobalRoleBindings.vue @@ -0,0 +1,220 @@ + + + + + + diff --git a/components/PromptRemove.vue b/components/PromptRemove.vue index 30f890054b..91414d3278 100644 --- a/components/PromptRemove.vue +++ b/components/PromptRemove.vue @@ -269,7 +269,7 @@ export default { - + diff --git a/components/auth/SelectPrincipal.vue b/components/auth/SelectPrincipal.vue index b43fab4b4a..bcb7683726 100644 --- a/components/auth/SelectPrincipal.vue +++ b/components/auth/SelectPrincipal.vue @@ -22,6 +22,17 @@ export default { default() { return ['group']; }, + }, + + // either 'user' or 'group' + searchGroupTypes: { + type: String, + default: null, + }, + + retainSelection: { + type: Boolean, + default: false } }, @@ -68,7 +79,9 @@ export default { methods: { add(id) { this.$emit('add', id); - this.newValue = ''; + if (!this.retainSelection) { + this.newValue = ''; + } }, onSearch(str, loading, vm) { @@ -97,7 +110,10 @@ export default { type: NORMAN.PRINCIPAL, actionName: 'search', opt: { url: '/v3/principals?action=search' }, - body: { name: str } + body: { + name: str, + principalType: this.searchGroupTypes + } }); if ( this.searchStr === str ) { @@ -113,18 +129,18 @@ export default { } }; -} - + + diff --git a/components/form/Checkbox.vue b/components/form/Checkbox.vue index 8e8cf4181b..0e04d7b5bc 100644 --- a/components/form/Checkbox.vue +++ b/components/form/Checkbox.vue @@ -1,11 +1,11 @@ + + diff --git a/components/formatter/PrincipalGroupBindings.vue b/components/formatter/PrincipalGroupBindings.vue new file mode 100644 index 0000000000..a468ebd725 --- /dev/null +++ b/components/formatter/PrincipalGroupBindings.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/config/product/auth.js b/config/product/auth.js index 4e7355a4d9..bd30b644d8 100644 --- a/config/product/auth.js +++ b/config/product/auth.js @@ -1,6 +1,7 @@ import { DSL } from '@/store/type-map'; // import { STATE, NAME as NAME_COL, AGE } from '@/config/table-headers'; -import { MANAGEMENT } from '@/config/types'; +import { MANAGEMENT, NORMAN, RBAC } from '@/config/types'; +import { GROUP_NAME, GROUP_ROLE_NAME } from '@/config/table-headers'; export const NAME = 'auth'; @@ -8,11 +9,12 @@ export function init(store) { const { product, basicType, - // weightType, + weightType, configureType, componentForType, - // headers, - // mapType, + headers, + mapType, + spoofedType, virtualType, } = DSL(store, NAME); @@ -21,7 +23,7 @@ export function init(store) { inStore: 'management', icon: 'user', removable: false, - weight: -1, + weight: 50, showClusterSwitcher: false, }); @@ -34,6 +36,54 @@ export function init(store) { route: { name: 'c-cluster-auth-config' }, }); + spoofedType({ + label: store.getters['type-map/labelFor']({ id: NORMAN.SPOOFED.GROUP_PRINCIPAL }, 2), + type: NORMAN.SPOOFED.GROUP_PRINCIPAL, + collectionMethods: [], + schemas: [ + { + id: NORMAN.SPOOFED.GROUP_PRINCIPAL, + type: 'schema', + collectionMethods: [], + resourceFields: {}, + } + ], + getInstances: async() => { + const principals = await store.dispatch('rancher/findAll', { + type: NORMAN.PRINCIPAL, + opt: { url: '/v3/principals' } + }); + const globalRoleBindings = await store.dispatch('management/findAll', { + type: RBAC.GLOBAL_ROLE_BINDING, + opt: { force: true } + }); + + // Up front fetch all global roles, instead of individually when needed (results in many duplicated requests) + await store.dispatch('management/findAll', { type: RBAC.GLOBAL_ROLE }); + + return principals + .filter(principal => principal.principalType === 'group' && + !!globalRoleBindings.find(globalRoleBinding => globalRoleBinding.groupPrincipalName === principal.id) + ) + .map(principal => ({ + ...principal, + type: NORMAN.SPOOFED.GROUP_PRINCIPAL + })); + } + }); + configureType(NORMAN.SPOOFED.GROUP_PRINCIPAL, { + isCreatable: false, + showAge: false, + showState: false, + isRemovable: false, + showListMasthead: false, + }); + + // Use labelFor... so lookup succeeds with .'s in path.... and end result is 'trimmed' as per other entries + mapType(NORMAN.SPOOFED.GROUP_PRINCIPAL, store.getters['type-map/labelFor']({ id: NORMAN.SPOOFED.GROUP_PRINCIPAL }, 2)); + + weightType(NORMAN.SPOOFED.GROUP_PRINCIPAL, -1, true); + weightType(MANAGEMENT.USER, 100); configureType(MANAGEMENT.USER, { showListMasthead: false }); configureType(MANAGEMENT.AUTH_CONFIG, { @@ -57,6 +107,11 @@ export function init(store) { basicType([ 'config', MANAGEMENT.USER, - // MANAGEMENT.GROUP, + NORMAN.SPOOFED.GROUP_PRINCIPAL + ]); + + headers(NORMAN.SPOOFED.GROUP_PRINCIPAL, [ + GROUP_NAME, + GROUP_ROLE_NAME ]); } diff --git a/config/table-headers.js b/config/table-headers.js index e51c9f8b10..144b5f4563 100644 --- a/config/table-headers.js +++ b/config/table-headers.js @@ -704,3 +704,19 @@ export const CONFIGURED_RECEIVER = { formatter: 'Link', formatterOpts: { options: { internal: true } }, }; + +export const GROUP_NAME = { + name: 'group-name', + label: 'Group Name', + value: 'id', + sort: ['name'], + search: ['name'], + formatter: 'Principal', + width: 350 +}; +export const GROUP_ROLE_NAME = { + name: 'group-role-names', + label: 'Group Role Names', + value: 'id', + formatter: 'PrincipalGroupBindings', +}; diff --git a/config/types.js b/config/types.js index 718778fd01..356f6071dd 100644 --- a/config/types.js +++ b/config/types.js @@ -15,7 +15,9 @@ export const NORMAN = { AUTH_CONFIG: 'authconfig', PRINCIPAL: 'principal', USER: 'user', - TOKEN: 'token', + TOKEN: 'token', + GROUP: 'group', + SPOOFED: { GROUP_PRINCIPAL: 'group.principal' } }; // Public (via Norman) diff --git a/edit/group.principal.vue b/edit/group.principal.vue new file mode 100644 index 0000000000..b5c720977f --- /dev/null +++ b/edit/group.principal.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/list/group.principal.vue b/list/group.principal.vue new file mode 100644 index 0000000000..af8123cf00 --- /dev/null +++ b/list/group.principal.vue @@ -0,0 +1,102 @@ + + + diff --git a/models/group.principal.js b/models/group.principal.js new file mode 100644 index 0000000000..d4acae8427 --- /dev/null +++ b/models/group.principal.js @@ -0,0 +1,61 @@ +import { NORMAN, RBAC } from '@/config/types'; +import { clone } from '@/utils/object'; +import principal from './principal'; + +export default { + + ...principal, + + canViewInApi() { + return false; + }, + + nameDisplay() { + return this.principalNameDisplay; + }, + + principalNameDisplay() { + const principal = this.$rootGetters['rancher/byId'](NORMAN.PRINCIPAL, this.id); + + return `${ principal.name } (${ principal.displayType })`; + }, + + detailLocation() { + const detailLocation = clone(this._detailLocation); + + detailLocation.params.id = this.id; // Base fn removes part of the id (`github_team://3375666` --> `3375666`) + + return detailLocation; + }, + + availableActions() { + return [ + { + action: 'goToEdit', + label: this.t('action.edit'), + icon: 'icon icon-edit', + enabled: true, + }, + { + action: 'unassignGroupRoles', + label: this.t('action.unassign'), + icon: 'icon icon-trash', + bulkable: true, + enabled: true, + bulkAction: 'unassignGroupRoles', + }, + ]; + }, + + unassignGroupRoles() { + return (resources = this) => { + const principals = Array.isArray(resources) ? resources : [resources]; + + const globalRoleBindings = this.$rootGetters['management/all'](RBAC.GLOBAL_ROLE_BINDING) + .filter(globalRoleBinding => principals.find(principal => principal.id === globalRoleBinding.groupPrincipalName)); + + this.$dispatch('promptRemove', globalRoleBindings); + }; + }, + +}; diff --git a/models/management.cattle.io.globalrolebinding.js b/models/management.cattle.io.globalrolebinding.js new file mode 100644 index 0000000000..541922fa25 --- /dev/null +++ b/models/management.cattle.io.globalrolebinding.js @@ -0,0 +1,21 @@ +import { NORMAN, RBAC } from '@/config/types'; + +export default { + nameDisplay() { + const roleName = this.$getters['byId'](RBAC.GLOBAL_ROLE, this.globalRoleName); + + const ownersName = this.groupPrincipalName ? this._displayPrincipal : this._displayUser; + + return `${ roleName.displayName } (${ ownersName })` ; + }, + + _displayPrincipal() { + const principal = this.$rootGetters['rancher/byId'](NORMAN.PRINCIPAL, this.groupPrincipalName); + + return `${ principal.name } - ${ principal.displayType }`; + }, + + _displayUser() { + return this.user; + } +}; diff --git a/pages/c/_cluster/auth/group.principal/assign-edit.vue b/pages/c/_cluster/auth/group.principal/assign-edit.vue new file mode 100644 index 0000000000..baeb1d7db3 --- /dev/null +++ b/pages/c/_cluster/auth/group.principal/assign-edit.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/plugins/steve/actions.js b/plugins/steve/actions.js index 6dc1978977..3151f38c31 100644 --- a/plugins/steve/actions.js +++ b/plugins/steve/actions.js @@ -13,7 +13,8 @@ export default { // Spoofing is handled here to ensure it's done for both yaml and form editing. // It became apparent that this was the only place that both intersected if (opt.url.includes(SPOOFED_PREFIX) || opt.url.includes(SPOOFED_API_PREFIX)) { - const [empty, scheme, type, id] = opt.url.split('/'); // eslint-disable-line no-unused-vars + const [empty, scheme, type, ...rest] = opt.url.split('/'); // eslint-disable-line no-unused-vars + const id = rest.join('/'); // Cover case where id contains '/' const isApi = scheme === SPOOFED_API_PREFIX; const typemapGetter = id ? 'getSpoofedInstance' : 'getSpoofedInstances'; const schemas = await rootGetters['cluster/all'](SCHEMA); From b17797a5b1f5c024dc0f22d1928287ec905fac5b Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 28 Jan 2021 13:59:11 +0000 Subject: [PATCH 02/13] Fixes & Changes following review --- components/GlobalRoleBindings.vue | 1 + components/form/Checkbox.vue | 5 +++-- list/group.principal.vue | 13 ++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/components/GlobalRoleBindings.vue b/components/GlobalRoleBindings.vue index 209ac4a639..abc38464ed 100644 --- a/components/GlobalRoleBindings.vue +++ b/components/GlobalRoleBindings.vue @@ -152,6 +152,7 @@ export default { await Promise.all(existingBindings.map(existingBinding => existingBinding.remove())); }, async save() { + // Ensure roles are added before removed (in case by removing one user is unable to add another) await this.saveAddedRoles(); await this.saveRemovedRoles(); } diff --git a/components/form/Checkbox.vue b/components/form/Checkbox.vue index 0e04d7b5bc..ae6fd35256 100644 --- a/components/form/Checkbox.vue +++ b/components/form/Checkbox.vue @@ -1,6 +1,7 @@ - - diff --git a/config/table-headers.js b/config/table-headers.js index e51c9f8b10..7c58abef0c 100644 --- a/config/table-headers.js +++ b/config/table-headers.js @@ -1,3 +1,4 @@ +import { CATTLE_PUBLIC_ENDPOINTS } from '@/config/labels-annotations'; import { NODE as NODE_TYPE } from '@/config/types'; // Note: 'id' is always the last sort, so you don't have to specify it here. @@ -598,10 +599,11 @@ export const WORKSPACE = { export const WORKLOAD_IMAGES = { ...POD_IMAGES, value: '' }; export const WORKLOAD_ENDPOINTS = { - name: 'workloadEndpoints', - labelKey: 'tableHeaders.endpoints', - value: 'endpoints', - formatter: 'WorkloadEndpoints' + name: 'workloadEndpoints', + labelKey: 'tableHeaders.endpoints', + value: `$['metadata']['annotations']['${ CATTLE_PUBLIC_ENDPOINTS }']`, + formatter: 'Endpoints', + dashIfEmpty: true, }; export const FLEET_SUMMARY = { diff --git a/list/service.vue b/list/service.vue new file mode 100644 index 0000000000..abd85952a5 --- /dev/null +++ b/list/service.vue @@ -0,0 +1,43 @@ + + + diff --git a/list/workload.vue b/list/workload.vue index baf1b1618b..4ecb194a79 100644 --- a/list/workload.vue +++ b/list/workload.vue @@ -1,6 +1,6 @@ diff --git a/pages/c/_cluster/auth/config/index.vue b/pages/c/_cluster/auth/config/index.vue index 7411d44798..5c04fc7c42 100644 --- a/pages/c/_cluster/auth/config/index.vue +++ b/pages/c/_cluster/auth/config/index.vue @@ -15,7 +15,8 @@ export default { if ( enabled.length === 1 ) { redirect({ name: 'c-cluster-auth-config-id', - params: { id: enabled[0].id } + params: { id: enabled[0].id }, + query: { mode: _EDIT } }); return { nonLocal };