diff --git a/assets/translations/en-us.yaml b/assets/translations/en-us.yaml index 228fd440e6..876d38e760 100644 --- a/assets/translations/en-us.yaml +++ b/assets/translations/en-us.yaml @@ -291,7 +291,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: |- @@ -331,6 +336,10 @@ asyncButton: action: 'Delete' waiting: 'Deleting…' success: 'Deleted' + remove: + action: 'Remove' + waiting: 'Removing…' + success: 'Removed' continue: action: 'Continue' waiting: 'Saving…' @@ -1648,7 +1657,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: @@ -1830,6 +1920,7 @@ serviceTypes: nodeport: Node Port servicesPage: + anyNode: Any Node labelsAnnotations: label: Labels & Annotations affinity: @@ -2931,6 +3022,11 @@ typeLabel: one { RKE2 Cluster } other { RKE2 Clusters } } + group.principal: |- + {count, plural, + one { Group } + other { Groups } + } action: clone: Clone @@ -2948,6 +3044,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..34623c1cc9 --- /dev/null +++ b/components/GlobalRoleBindings.vue @@ -0,0 +1,221 @@ + + + + + + diff --git a/components/PromptRemove.vue b/components/PromptRemove.vue index 30f890054b..abee7cd264 100644 --- a/components/PromptRemove.vue +++ b/components/PromptRemove.vue @@ -106,6 +106,12 @@ export default { return this.t('promptRemove.protip', { alternateLabel }); }, + deleteDisabled() { + const confirmFailed = this.needsConfirm && this.confirmName !== this.names[0]; + + return this.preventDelete || confirmFailed; + }, + ...mapState('action-menu', ['showPromptRemove', 'toRemove']), ...mapGetters({ t: 'i18n/t' }) }, @@ -155,25 +161,20 @@ export default { }, remove(btnCB) { - if (this.needsConfirm && this.confirmName !== this.names[0]) { - this.error = 'Resource names do not match'; - btnCB(false); - // if doneLocation is defined, redirect after deleting + // if doneLocation is defined, redirect after deleting + let goTo; + + if (this.doneLocation) { + // doneLocation will recompute to undefined when delete request completes + goTo = { ...this.doneLocation }; + } + + const serialRemove = this.toRemove.some(resource => resource.removeSerially); + + if (serialRemove) { + this.serialRemove(goTo, btnCB); } else { - let goTo; - - if (this.doneLocation) { - // doneLocation will recompute to undefined when delete request completes - goTo = { ...this.doneLocation }; - } - - const serialRemove = this.toRemove.some(resource => resource.removeSerially); - - if (serialRemove) { - this.serialRemove(goTo, btnCB); - } else { - this.parallelRemove(goTo, btnCB); - } + this.parallelRemove(goTo, btnCB); } }, @@ -269,7 +270,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..ae6fd35256 100644 --- a/components/form/Checkbox.vue +++ b/components/form/Checkbox.vue @@ -1,11 +1,12 @@ + + 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/components/formatter/ServiceTargets.vue b/components/formatter/ServiceTargets.vue index af5eafd0ad..b9d7e990f6 100644 --- a/components/formatter/ServiceTargets.vue +++ b/components/formatter/ServiceTargets.vue @@ -1,8 +1,8 @@ - - diff --git a/config/product/auth.js b/config/product/auth.js index 4e7355a4d9..980f80a9aa 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,64 @@ 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() => { + // Determine if the user can get fetch global roles & global role bindings. If not there's not much point in showing the table + const canFetchGlobalRoles = !!store.getters[`management/schemaFor`](RBAC.GLOBAL_ROLE); + const canFetchGlobalRoleBindings = !!store.getters[`management/schemaFor`](RBAC.GLOBAL_ROLE_BINDING); + + if (!canFetchGlobalRoles || !canFetchGlobalRoleBindings) { + return []; + } + + // Groups are a list of principals filtered via those that have group roles bound to them + 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 +117,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 c9e78c87a5..9c2f702101 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. @@ -606,10 +607,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 = { @@ -713,6 +715,21 @@ export const CONFIGURED_RECEIVER = { 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', + export const ACCESS_KEY = { name: 'name', labelKey: 'tableHeaders.accessKey', diff --git a/config/types.js b/config/types.js index 253ec53dbe..a04e7e8541 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/detail/cis.cattle.io.clusterscan.vue b/detail/cis.cattle.io.clusterscan.vue index 73af2b2254..baf6664334 100644 --- a/detail/cis.cattle.io.clusterscan.vue +++ b/detail/cis.cattle.io.clusterscan.vue @@ -293,7 +293,7 @@ export default {