mirror of https://github.com/rancher/dashboard.git
Merge remote-tracking branch 'upstream/master' into api-keys
This commit is contained in:
commit
5a7963e5b0
|
|
@ -291,7 +291,12 @@ authConfig:
|
||||||
enabled: '{provider} is currently enabled.'
|
enabled: '{provider} is currently enabled.'
|
||||||
testAndEnable: Test and Enable Authentication
|
testAndEnable: Test and Enable Authentication
|
||||||
|
|
||||||
|
authGroups:
|
||||||
|
actions:
|
||||||
|
refresh: Refresh Group Memberships
|
||||||
|
assignRoles: Assign Global Roles
|
||||||
|
assignEdit:
|
||||||
|
assignTitle: Assign Global Roles To Group
|
||||||
|
|
||||||
assignTo:
|
assignTo:
|
||||||
title: |-
|
title: |-
|
||||||
|
|
@ -331,6 +336,10 @@ asyncButton:
|
||||||
action: 'Delete'
|
action: 'Delete'
|
||||||
waiting: 'Deleting…'
|
waiting: 'Deleting…'
|
||||||
success: 'Deleted'
|
success: 'Deleted'
|
||||||
|
remove:
|
||||||
|
action: 'Remove'
|
||||||
|
waiting: 'Removing…'
|
||||||
|
success: 'Removed'
|
||||||
continue:
|
continue:
|
||||||
action: 'Continue'
|
action: 'Continue'
|
||||||
waiting: 'Saving…'
|
waiting: 'Saving…'
|
||||||
|
|
@ -1648,7 +1657,88 @@ rbac:
|
||||||
defaultLabel: Project Creator Default
|
defaultLabel: Project Creator Default
|
||||||
noContext:
|
noContext:
|
||||||
label: No Context
|
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:
|
resourceDetail:
|
||||||
detailTop:
|
detailTop:
|
||||||
|
|
@ -1830,6 +1920,7 @@ serviceTypes:
|
||||||
nodeport: Node Port
|
nodeport: Node Port
|
||||||
|
|
||||||
servicesPage:
|
servicesPage:
|
||||||
|
anyNode: Any Node
|
||||||
labelsAnnotations:
|
labelsAnnotations:
|
||||||
label: Labels & Annotations
|
label: Labels & Annotations
|
||||||
affinity:
|
affinity:
|
||||||
|
|
@ -2931,6 +3022,11 @@ typeLabel:
|
||||||
one { RKE2 Cluster }
|
one { RKE2 Cluster }
|
||||||
other { RKE2 Clusters }
|
other { RKE2 Clusters }
|
||||||
}
|
}
|
||||||
|
group.principal: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Group }
|
||||||
|
other { Groups }
|
||||||
|
}
|
||||||
|
|
||||||
action:
|
action:
|
||||||
clone: Clone
|
clone: Clone
|
||||||
|
|
@ -2948,6 +3044,7 @@ action:
|
||||||
show: Show
|
show: Show
|
||||||
hide: Hide
|
hide: Hide
|
||||||
copy: Copy
|
copy: Copy
|
||||||
|
unassign: 'Unassign'
|
||||||
|
|
||||||
unit:
|
unit:
|
||||||
sec: secs
|
sec: secs
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { RBAC } from '@/config/types';
|
||||||
|
import Checkbox from '@/components/form/Checkbox';
|
||||||
|
import { _VIEW } from '@/config/query-params';
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
Loading,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _VIEW,
|
||||||
|
},
|
||||||
|
principalId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async fetch() {
|
||||||
|
try {
|
||||||
|
this.allRoles = await this.$store.dispatch('management/findAll', { type: RBAC.GLOBAL_ROLE });
|
||||||
|
|
||||||
|
if (!this.sortedRoles) {
|
||||||
|
this.sortedRoles = {
|
||||||
|
global: {},
|
||||||
|
builtin: {},
|
||||||
|
custom: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.allRoles.forEach((role) => {
|
||||||
|
const type = this.getRoleType(role);
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
this.sortedRoles[type][role.id] = {
|
||||||
|
label: this.t(`rbac.globalRoles.role.${ role.id }.label`) || role.displayName,
|
||||||
|
description: this.t(`rbac.globalRoles.role.${ role.id }.detail`) || role.description || this.t(`rbac.globalRoles.unknownRole.detail`),
|
||||||
|
id: role.id,
|
||||||
|
role,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Moving this out into the watch has issues....
|
||||||
|
this.globalRoleBindings = await this.$store.dispatch('management/findAll', { type: RBAC.GLOBAL_ROLE_BINDING });
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
globalPermissions: [
|
||||||
|
'admin',
|
||||||
|
'restricted-admin',
|
||||||
|
'user',
|
||||||
|
'user-base',
|
||||||
|
],
|
||||||
|
user: null, // TODO: Populate in edit user mode
|
||||||
|
globalRoleBindings: null,
|
||||||
|
sortedRoles: null,
|
||||||
|
selectedRoles: [],
|
||||||
|
roleChanges: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||||
|
watch: {
|
||||||
|
principalId(principalId, oldPrincipalId) {
|
||||||
|
if (principalId === oldPrincipalId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getRoleType(role) {
|
||||||
|
if (this.globalPermissions.find(p => p === role.id)) {
|
||||||
|
return 'global';
|
||||||
|
} else if (role.hidden) {
|
||||||
|
return null;
|
||||||
|
} else if (role.builtin) {
|
||||||
|
return 'builtin';
|
||||||
|
} else {
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getUnique(...ids) {
|
||||||
|
return `${ this.principalId }-${ ids.join('-') }`;
|
||||||
|
},
|
||||||
|
update() {
|
||||||
|
if (!this.principalId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedRoles = [];
|
||||||
|
this.startingSelectedRoles = [];
|
||||||
|
|
||||||
|
const boundRoles = this.globalRoleBindings.filter(globalRoleBinding => globalRoleBinding.groupPrincipalName === this.principalId);
|
||||||
|
|
||||||
|
Object.entries(this.sortedRoles).forEach(([type, types]) => {
|
||||||
|
Object.entries(types).forEach(([roleId, mappedRole]) => {
|
||||||
|
const boundRole = boundRoles.find(boundRole => boundRole.globalRoleName === roleId);
|
||||||
|
|
||||||
|
if (!!boundRole) {
|
||||||
|
this.selectedRoles.push(roleId);
|
||||||
|
this.startingSelectedRoles.push({
|
||||||
|
roleId,
|
||||||
|
bindingId: boundRole.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: If in create user mode... apply default selection using role.newUserDefault
|
||||||
|
// TODO: If in create/edit user mode... apply validation as per rancher/ui lib/global-admin/addon/components/form-global-roles/component.js
|
||||||
|
// Should validation come in via validationErrors property on model?
|
||||||
|
},
|
||||||
|
checkboxChanged() {
|
||||||
|
const addRoles = this.selectedRoles
|
||||||
|
.filter(selected => !this.startingSelectedRoles.find(startingRole => startingRole.roleId === selected));
|
||||||
|
const removeBindings = this.startingSelectedRoles
|
||||||
|
.filter(startingRole => !this.selectedRoles.find(selected => selected === startingRole.roleId))
|
||||||
|
.map(startingRole => startingRole.bindingId);
|
||||||
|
|
||||||
|
this.roleChanges = {
|
||||||
|
initialRoles: this.startingSelectedRoles,
|
||||||
|
addRoles,
|
||||||
|
removeBindings
|
||||||
|
};
|
||||||
|
this.$emit('changed', this.roleChanges);
|
||||||
|
},
|
||||||
|
async saveAddedRoles() {
|
||||||
|
const newBindings = await Promise.all(this.roleChanges.addRoles.map(role => this.$store.dispatch(`management/create`, {
|
||||||
|
type: RBAC.GLOBAL_ROLE_BINDING,
|
||||||
|
metadata: { generateName: `grb-` },
|
||||||
|
globalRoleName: role,
|
||||||
|
groupPrincipalName: this.principalId,
|
||||||
|
})));
|
||||||
|
|
||||||
|
await Promise.all(newBindings.map(newBinding => newBinding.save()));
|
||||||
|
},
|
||||||
|
async saveRemovedRoles() {
|
||||||
|
const existingBindings = await Promise.all(this.roleChanges.removeBindings.map(bindingId => this.$store.dispatch('management/find', {
|
||||||
|
type: RBAC.GLOBAL_ROLE_BINDING,
|
||||||
|
id: bindingId
|
||||||
|
})));
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<form v-if="selectedRoles">
|
||||||
|
<div v-for="(sortedRole, type) in sortedRoles" :key="getUnique(type)" class="role-group mb-10">
|
||||||
|
<template v-if="Object.keys(sortedRole).length">
|
||||||
|
<h2>{{ t(`rbac.globalRoles.types.${type}.label`) }}</h2>
|
||||||
|
<div class="type-description mb-10">
|
||||||
|
{{ t(`rbac.globalRoles.types.${type}.detail`, { type: 'Application', isUser: !!user }) }}
|
||||||
|
</div>
|
||||||
|
<div class="checkbox-section" :class="'checkbox-section--' + type">
|
||||||
|
<div v-for="(role, roleId) in sortedRoles[type]" :key="getUnique(type, roleId)" class="checkbox mb-10 mr-10">
|
||||||
|
<Checkbox
|
||||||
|
:key="getUnique(type, roleId, 'checkbox')"
|
||||||
|
v-model="selectedRoles"
|
||||||
|
:value-when-true="roleId"
|
||||||
|
:label="role.label"
|
||||||
|
:mode="mode"
|
||||||
|
@input="checkboxChanged"
|
||||||
|
/>
|
||||||
|
<div class="description">
|
||||||
|
{{ role.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
$detailSize: 11px;
|
||||||
|
.role-group {
|
||||||
|
.type-description {
|
||||||
|
font-size: $detailSize;
|
||||||
|
}
|
||||||
|
.checkbox-section {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
|
||||||
|
&--global {
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: $detailSize;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -106,6 +106,12 @@ export default {
|
||||||
return this.t('promptRemove.protip', { alternateLabel });
|
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']),
|
...mapState('action-menu', ['showPromptRemove', 'toRemove']),
|
||||||
...mapGetters({ t: 'i18n/t' })
|
...mapGetters({ t: 'i18n/t' })
|
||||||
},
|
},
|
||||||
|
|
@ -155,25 +161,20 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(btnCB) {
|
remove(btnCB) {
|
||||||
if (this.needsConfirm && this.confirmName !== this.names[0]) {
|
// if doneLocation is defined, redirect after deleting
|
||||||
this.error = 'Resource names do not match';
|
let goTo;
|
||||||
btnCB(false);
|
|
||||||
// if doneLocation is defined, redirect after deleting
|
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 {
|
} else {
|
||||||
let goTo;
|
this.parallelRemove(goTo, btnCB);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -269,7 +270,7 @@ export default {
|
||||||
<button class="btn role-secondary" @click="close">
|
<button class="btn role-secondary" @click="close">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<AsyncButton mode="delete" class="btn bg-error ml-10" :disabled="preventDelete" @click="remove" />
|
<AsyncButton mode="delete" class="btn bg-error ml-10" :disabled="deleteDisabled" @click="remove" />
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</modal>
|
</modal>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,17 @@ export default {
|
||||||
default() {
|
default() {
|
||||||
return ['group'];
|
return ['group'];
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// either 'user' or 'group'
|
||||||
|
searchGroupTypes: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
retainSelection: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -68,7 +79,9 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
add(id) {
|
add(id) {
|
||||||
this.$emit('add', id);
|
this.$emit('add', id);
|
||||||
this.newValue = '';
|
if (!this.retainSelection) {
|
||||||
|
this.newValue = '';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearch(str, loading, vm) {
|
onSearch(str, loading, vm) {
|
||||||
|
|
@ -97,7 +110,10 @@ export default {
|
||||||
type: NORMAN.PRINCIPAL,
|
type: NORMAN.PRINCIPAL,
|
||||||
actionName: 'search',
|
actionName: 'search',
|
||||||
opt: { url: '/v3/principals?action=search' },
|
opt: { url: '/v3/principals?action=search' },
|
||||||
body: { name: str }
|
body: {
|
||||||
|
name: str,
|
||||||
|
principalType: this.searchGroupTypes
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if ( this.searchStr === str ) {
|
if ( this.searchStr === str ) {
|
||||||
|
|
@ -113,18 +129,18 @@ export default {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model="newValue"
|
v-model="newValue"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
label="Add Member"
|
:label="retainSelection ? `Select Member` : `Add Member`"
|
||||||
placeholder="Start typing to search for principals"
|
placeholder="Start typing to search for principals"
|
||||||
:options="options"
|
:options="options"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:filterable="false"
|
:filterable="false"
|
||||||
|
class="select-principal"
|
||||||
|
:class="{'retain-selection': retainSelection}"
|
||||||
@input="add"
|
@input="add"
|
||||||
@search="onSearch"
|
@search="onSearch"
|
||||||
>
|
>
|
||||||
|
|
@ -137,5 +153,22 @@ export default {
|
||||||
<template #option="option">
|
<template #option="option">
|
||||||
<Principal :key="option.label" :value="option.label" :use-muted="false" />
|
<Principal :key="option.label" :value="option.label" :use-muted="false" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="retainSelection" #selected-option="option">
|
||||||
|
<Principal :key="option.label" :value="option.label" :use-muted="false" class="mt-10 mb-10" />
|
||||||
|
</template>
|
||||||
</LabeledSelect>
|
</LabeledSelect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.select-principal {
|
||||||
|
&.retain-selection {
|
||||||
|
min-height: 84px;
|
||||||
|
&.focused {
|
||||||
|
.principal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { _EDIT } from '@/config/query-params';
|
import { _EDIT, _VIEW } from '@/config/query-params';
|
||||||
|
import { addObject, removeObject } from '@/utils/array';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: Boolean,
|
type: [Boolean, Array],
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -43,11 +44,19 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
valueWhenTrue: {
|
||||||
|
type: null,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isDisabled() {
|
isDisabled() {
|
||||||
return (this.disabled || this.mode === 'view' );
|
return (this.disabled || this.mode === _VIEW );
|
||||||
|
},
|
||||||
|
isChecked() {
|
||||||
|
return this.isMulti() ? this.value.find(v => v === this.valueWhenTrue) : this.value === this.valueWhenTrue;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -61,9 +70,22 @@ export default {
|
||||||
click.ctrlKey = event.ctrlKey;
|
click.ctrlKey = event.ctrlKey;
|
||||||
click.metaKey = event.metaKey;
|
click.metaKey = event.metaKey;
|
||||||
|
|
||||||
this.$emit('input', !this.value);
|
// Flip the value
|
||||||
$(this.$el).trigger(click);
|
if (this.isMulti()) {
|
||||||
|
if (this.isChecked) {
|
||||||
|
removeObject(this.value, this.valueWhenTrue);
|
||||||
|
} else {
|
||||||
|
addObject(this.value, this.valueWhenTrue);
|
||||||
|
}
|
||||||
|
this.$emit('input', this.value);
|
||||||
|
} else {
|
||||||
|
this.$emit('input', !this.value);
|
||||||
|
$(this.$el).trigger(click);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
isMulti() {
|
||||||
|
return Array.isArray(this.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -72,14 +94,15 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
class="checkbox-container"
|
class="checkbox-container"
|
||||||
:class="{disabled}"
|
:class="{ 'disabled': isDisabled}"
|
||||||
@keydown.enter.prevent="clicked($event)"
|
@keydown.enter.prevent="clicked($event)"
|
||||||
@keydown.space.prevent="clicked($event)"
|
@keydown.space.prevent="clicked($event)"
|
||||||
@click.stop.prevent="clicked($event)"
|
@click.stop.prevent="clicked($event)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:checked="value"
|
v-model="value"
|
||||||
:v-model="value"
|
:checked="isChecked"
|
||||||
|
:value="valueWhenTrue"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:tabindex="-1"
|
:tabindex="-1"
|
||||||
@click.stop.prevent
|
@click.stop.prevent
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { NODE } from '@/config/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -17,49 +18,54 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
nodes() {
|
||||||
|
return this.$store.getters['cluster/all'](NODE);
|
||||||
|
},
|
||||||
// value may be JSON from "field.cattle.io/publicEndpoints" label
|
// value may be JSON from "field.cattle.io/publicEndpoints" label
|
||||||
parsed() {
|
parsed() {
|
||||||
|
const nodes = this.nodes;
|
||||||
|
const nodeWithExternal = nodes.find(node => !!node.externalIp) || {};
|
||||||
|
const externalIp = nodeWithExternal.externalIp;
|
||||||
|
|
||||||
if ( this.value && this.value.length ) {
|
if ( this.value && this.value.length ) {
|
||||||
let out ;
|
let out ;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
out = JSON.parse(this.value);
|
out = JSON.parse(this.value);
|
||||||
|
out.forEach((endpoint) => {
|
||||||
|
let protocol = 'http';
|
||||||
|
|
||||||
|
if (endpoint.port === 443) {
|
||||||
|
protocol = 'https';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpoint.addresses) {
|
||||||
|
endpoint.link = `${ protocol }://${ endpoint.addresses[0] }:${ endpoint.port }`;
|
||||||
|
} else if (externalIp) {
|
||||||
|
endpoint.link = `${ protocol }://${ externalIp }:${ endpoint.port }`;
|
||||||
|
} else {
|
||||||
|
endpoint.display = `[${ this.t('servicesPage.anyNode') }]:${ endpoint.port }`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return this.value[0];
|
return this.value[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
bestLink() {
|
|
||||||
if (this.parsed && this.parsed.length ) {
|
|
||||||
if (this.parsed[0].addresses) {
|
|
||||||
let protocol = 'http';
|
|
||||||
|
|
||||||
if (this.parsed[0].port === 443) {
|
|
||||||
protocol = 'https';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${ protocol }://${ this.parsed[0].addresses[0] }:${ this.parsed[0].port }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.parsed;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
protocol() {
|
protocol() {
|
||||||
const link = this.bestLink;
|
const { parsed } = this;
|
||||||
|
|
||||||
if ( link) {
|
if ( parsed) {
|
||||||
if (this.parsed[0].protocol) {
|
if (this.parsed[0].protocol) {
|
||||||
return this.parsed[0].protocol;
|
return this.parsed[0].protocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = link.match(/^([^:]+):\/\//);
|
const match = parsed.match(/^([^:]+):\/\//);
|
||||||
|
|
||||||
if ( match ) {
|
if ( match ) {
|
||||||
return match[1];
|
return match[1];
|
||||||
|
|
@ -76,11 +82,18 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
<a v-if="bestLink" :href="bestLink" target="_blank" rel="nofollow noopener noreferrer">
|
<template v-for="endpoint in parsed">
|
||||||
<span v-if="parsed[0].port">{{ parsed[0].port }}/</span>{{ protocol }}
|
<span v-if="endpoint.display" :key="endpoint.display" class="block">{{ endpoint.display }}</span>
|
||||||
</a>
|
<a
|
||||||
<span v-else class="text-muted">
|
v-else
|
||||||
—
|
:key="endpoint.link"
|
||||||
</span>
|
class="block"
|
||||||
|
:href="endpoint.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="nofollow noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span v-if="endpoint.port">{{ endpoint.port }}/</span>{{ endpoint.protocol }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { isV4Format, isV6Format } from 'ip';
|
import { isV4Format, isV6Format } from 'ip';
|
||||||
import CopyToClipboard from '@/components/CopyToClipboard';
|
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
export default {
|
export default {
|
||||||
components: { CopyToClipboard },
|
components: { CopyToClipboard },
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -13,7 +13,8 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
showBoth() {
|
showBoth() {
|
||||||
return this.row.internalIp !== this.row.externalIp;
|
return this.row.internalIp !== this.row.externalIp;
|
||||||
}
|
},
|
||||||
|
...mapGetters({ t: 'i18n/t' })
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isIp(ip) {
|
isIp(ip) {
|
||||||
|
|
@ -29,7 +30,7 @@ export default {
|
||||||
{{ row.externalIp }} <CopyToClipboard label-as="tooltip" :text="row.externalIp" class="icon-btn" action-color="bg-transparent" />
|
{{ row.externalIp }} <CopyToClipboard label-as="tooltip" :text="row.externalIp" class="icon-btn" action-color="bg-transparent" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ t('internalExternalIp.none') }}
|
{{ t('generic.none') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="showBoth">
|
<template v-if="showBoth">
|
||||||
|
|
@ -37,7 +38,7 @@ export default {
|
||||||
/ {{ row.internalIp }} <CopyToClipboard label-as="tooltip" :text="row.internalIp" class="icon-btn" action-color="bg-transparent" />
|
/ {{ row.internalIp }} <CopyToClipboard label-as="tooltip" :text="row.internalIp" class="icon-btn" action-color="bg-transparent" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ t('internalExternalIp.none') }}
|
{{ t('generic.none') }}
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script>
|
||||||
|
import PrincipalComponent from '@/components/auth/Principal';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { PrincipalComponent },
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PrincipalComponent :key="value" :value="value" :use-muted="false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script>
|
||||||
|
import { NORMAN, RBAC } from '@/config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
boundRoles() {
|
||||||
|
const principal = this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.value);
|
||||||
|
const globalRoleBindings = this.$store.getters['management/all'](RBAC.GLOBAL_ROLE_BINDING);
|
||||||
|
|
||||||
|
return globalRoleBindings
|
||||||
|
// Bindings for this group
|
||||||
|
.filter(globalRoleBinding => globalRoleBinding.groupPrincipalName === principal.id)
|
||||||
|
// Display name of role associated with binding
|
||||||
|
.map((binding) => {
|
||||||
|
const role = this.$store.getters['management/byId'](RBAC.GLOBAL_ROLE, binding.globalRoleName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
detailLocation: role.detailLocation,
|
||||||
|
label: role ? role.displayName : role.id, // nameDisplay contains principal name, not required here
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pgb">
|
||||||
|
<template v-for="(role, i) in boundRoles">
|
||||||
|
<nuxt-link :key="role.id" :to="role.detailLocation">
|
||||||
|
{{ role.label }}
|
||||||
|
</nuxt-link>
|
||||||
|
<template v-if="i + 1 < boundRoles.length">
|
||||||
|
,
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.pgb{
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import { CATTLE_PUBLIC_ENDPOINTS } from '@/config/labels-annotations';
|
import { CATTLE_PUBLIC_ENDPOINTS } from '@/config/labels-annotations';
|
||||||
import has from 'lodash/has';
|
|
||||||
import Endpoints from '@/components/formatter/Endpoints';
|
import Endpoints from '@/components/formatter/Endpoints';
|
||||||
|
import has from 'lodash/has';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Endpoints },
|
components: { Endpoints },
|
||||||
|
|
@ -31,6 +31,7 @@ export default {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
parsed() {
|
parsed() {
|
||||||
const { row, hasPublic } = this;
|
const { row, hasPublic } = this;
|
||||||
const {
|
const {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const endpoints = this.value.split(',');
|
|
||||||
|
|
||||||
return { endpoints };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span>
|
|
||||||
<template v-for="endpoint in endpoints">
|
|
||||||
<span v-if="endpoint==='<none>'" :key="endpoint">{{ endpoint }}</span>
|
|
||||||
<a v-else :key="endpoint" rel="nofollow noopener noreferrer" target="_blank" :href="`//${endpoint}`">{{ endpoint }}</a>
|
|
||||||
<br :key="endpoint+'br'" />
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { DSL } from '@/store/type-map';
|
import { DSL } from '@/store/type-map';
|
||||||
// import { STATE, NAME as NAME_COL, AGE } from '@/config/table-headers';
|
// 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';
|
export const NAME = 'auth';
|
||||||
|
|
||||||
|
|
@ -8,11 +9,12 @@ export function init(store) {
|
||||||
const {
|
const {
|
||||||
product,
|
product,
|
||||||
basicType,
|
basicType,
|
||||||
// weightType,
|
weightType,
|
||||||
configureType,
|
configureType,
|
||||||
componentForType,
|
componentForType,
|
||||||
// headers,
|
headers,
|
||||||
// mapType,
|
mapType,
|
||||||
|
spoofedType,
|
||||||
virtualType,
|
virtualType,
|
||||||
} = DSL(store, NAME);
|
} = DSL(store, NAME);
|
||||||
|
|
||||||
|
|
@ -21,7 +23,7 @@ export function init(store) {
|
||||||
inStore: 'management',
|
inStore: 'management',
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
removable: false,
|
removable: false,
|
||||||
weight: -1,
|
weight: 50,
|
||||||
showClusterSwitcher: false,
|
showClusterSwitcher: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -34,6 +36,64 @@ export function init(store) {
|
||||||
route: { name: 'c-cluster-auth-config' },
|
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.USER, { showListMasthead: false });
|
||||||
|
|
||||||
configureType(MANAGEMENT.AUTH_CONFIG, {
|
configureType(MANAGEMENT.AUTH_CONFIG, {
|
||||||
|
|
@ -57,6 +117,11 @@ export function init(store) {
|
||||||
basicType([
|
basicType([
|
||||||
'config',
|
'config',
|
||||||
MANAGEMENT.USER,
|
MANAGEMENT.USER,
|
||||||
// MANAGEMENT.GROUP,
|
NORMAN.SPOOFED.GROUP_PRINCIPAL
|
||||||
|
]);
|
||||||
|
|
||||||
|
headers(NORMAN.SPOOFED.GROUP_PRINCIPAL, [
|
||||||
|
GROUP_NAME,
|
||||||
|
GROUP_ROLE_NAME
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { CATTLE_PUBLIC_ENDPOINTS } from '@/config/labels-annotations';
|
||||||
import { NODE as NODE_TYPE } from '@/config/types';
|
import { NODE as NODE_TYPE } from '@/config/types';
|
||||||
|
|
||||||
// Note: 'id' is always the last sort, so you don't have to specify it here.
|
// 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_IMAGES = { ...POD_IMAGES, value: '' };
|
||||||
|
|
||||||
export const WORKLOAD_ENDPOINTS = {
|
export const WORKLOAD_ENDPOINTS = {
|
||||||
name: 'workloadEndpoints',
|
name: 'workloadEndpoints',
|
||||||
labelKey: 'tableHeaders.endpoints',
|
labelKey: 'tableHeaders.endpoints',
|
||||||
value: 'endpoints',
|
value: `$['metadata']['annotations']['${ CATTLE_PUBLIC_ENDPOINTS }']`,
|
||||||
formatter: 'WorkloadEndpoints'
|
formatter: 'Endpoints',
|
||||||
|
dashIfEmpty: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FLEET_SUMMARY = {
|
export const FLEET_SUMMARY = {
|
||||||
|
|
@ -713,6 +715,21 @@ export const CONFIGURED_RECEIVER = {
|
||||||
formatterOpts: { options: { internal: true } },
|
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 = {
|
export const ACCESS_KEY = {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
labelKey: 'tableHeaders.accessKey',
|
labelKey: 'tableHeaders.accessKey',
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ export const NORMAN = {
|
||||||
AUTH_CONFIG: 'authconfig',
|
AUTH_CONFIG: 'authconfig',
|
||||||
PRINCIPAL: 'principal',
|
PRINCIPAL: 'principal',
|
||||||
USER: 'user',
|
USER: 'user',
|
||||||
TOKEN: 'token',
|
TOKEN: 'token',
|
||||||
|
GROUP: 'group',
|
||||||
|
SPOOFED: { GROUP_PRINCIPAL: 'group.principal' }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Public (via Norman)
|
// Public (via Norman)
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ export default {
|
||||||
<template #sub-row="{row, fullColspan}">
|
<template #sub-row="{row, fullColspan}">
|
||||||
<tr>
|
<tr>
|
||||||
<td :colspan="fullColspan">
|
<td :colspan="fullColspan">
|
||||||
<Banner v-if="row.remediation" class="sub-banner" :label="remediationDisplay(row)" color="warning" />
|
<Banner v-if="(row.state==='fail' || row.state==='warn')&& row.remediation" class="sub-banner" :label="remediationDisplay(row)" color="warning" />
|
||||||
<SortableTable
|
<SortableTable
|
||||||
class="sub-table"
|
class="sub-table"
|
||||||
:rows="row.nodeRows"
|
:rows="row.nodeRows"
|
||||||
|
|
|
||||||
|
|
@ -155,12 +155,15 @@ export default {
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
<template v-if="model.enabled && !isSaving">
|
<template v-if="model.enabled && !isEnabling && !editConfig">
|
||||||
<Banner color="success clearfix">
|
<Banner color="success clearfix">
|
||||||
<div class="pull-left mt-10">
|
<div class="pull-left mt-10">
|
||||||
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
|
<button type="button" class="btn-sm role-primary" @click="goToEdit">
|
||||||
|
{{ t('action.edit') }}
|
||||||
|
</button>
|
||||||
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
||||||
</div>
|
</div>
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,15 @@ export default {
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
<template v-if="model.enabled && !isEnabling">
|
<template v-if="model.enabled && !isEnabling && !editConfig">
|
||||||
<Banner color="success clearfix">
|
<Banner color="success clearfix">
|
||||||
<div class="pull-left mt-10">
|
<div class="pull-left mt-10">
|
||||||
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
|
<button type="button" class="btn-sm role-primary" @click="goToEdit">
|
||||||
|
{{ t('action.edit') }}
|
||||||
|
</button>
|
||||||
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
||||||
</div>
|
</div>
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ import UnitInput from '@/components/form/UnitInput';
|
||||||
import Banner from '@/components/Banner';
|
import Banner from '@/components/Banner';
|
||||||
import FileSelector from '@/components/form/FileSelector';
|
import FileSelector from '@/components/form/FileSelector';
|
||||||
|
|
||||||
|
const DEFAULT_NON_TLS_PORT = 389;
|
||||||
|
const DEFAULT_TLS_PORT = 636;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
|
@ -59,6 +62,17 @@ export default {
|
||||||
if (neu) {
|
if (neu) {
|
||||||
this.model.starttls = false;
|
this.model.starttls = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expectedCurrentDefault = neu ? DEFAULT_NON_TLS_PORT : DEFAULT_TLS_PORT;
|
||||||
|
const newDefault = neu ? DEFAULT_TLS_PORT : DEFAULT_NON_TLS_PORT;
|
||||||
|
|
||||||
|
// Note: The defualt port value is a number
|
||||||
|
// If the user edits this value, the type will be a string
|
||||||
|
// Thus, we will only change the value when the user toggles the TLS flag if they have
|
||||||
|
// NOT edited the port value in any way
|
||||||
|
if (this.model.port === expectedCurrentDefault) {
|
||||||
|
this.value.port = newDefault;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,12 +86,15 @@ export default {
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
<template v-if="model.enabled && !isEnabling">
|
<template v-if="model.enabled && !isEnabling && !editConfig">
|
||||||
<Banner color="success clearfix">
|
<Banner color="success clearfix">
|
||||||
<div class="pull-left mt-10">
|
<div class="pull-left mt-10">
|
||||||
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
|
<button type="button" class="btn-sm role-primary" @click="goToEdit">
|
||||||
|
{{ t('action.edit') }}
|
||||||
|
</button>
|
||||||
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
||||||
</div>
|
</div>
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|
|
||||||
|
|
@ -76,12 +76,15 @@ export default {
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
<template v-if="model.enabled && !isEnabling">
|
<template v-if="model.enabled && !isEnabling && !editConfig">
|
||||||
<Banner color="success clearfix">
|
<Banner color="success clearfix">
|
||||||
<div class="pull-left mt-10">
|
<div class="pull-left mt-10">
|
||||||
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
|
<button type="button" class="btn-sm role-primary" @click="goToEdit">
|
||||||
|
{{ t('action.edit') }}
|
||||||
|
</button>
|
||||||
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
||||||
</div>
|
</div>
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|
@ -138,12 +141,11 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div v-if="NAME !== 'okta'" class="col span-6">
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model="model.entityID"
|
v-model="model.entityID"
|
||||||
:label="t(`authConfig.saml.entityID`)"
|
:label="t(`authConfig.saml.entityID`)"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script>
|
||||||
|
import CreateEditView from '@/mixins/create-edit-view';
|
||||||
|
import GlobalRoleBindings from '@/components/GlobalRoleBindings.vue';
|
||||||
|
import CruResource from '@/components/CruResource';
|
||||||
|
import { exceptionToErrorsArray } from '@/utils/error';
|
||||||
|
import { NORMAN } from '@/config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlobalRoleBindings,
|
||||||
|
CruResource
|
||||||
|
},
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
errors: [],
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async save(buttonDone) {
|
||||||
|
this.errors = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$refs.grb.save();
|
||||||
|
|
||||||
|
await this.$store.dispatch('cluster/findAll', {
|
||||||
|
type: NORMAN.SPOOFED.GROUP_PRINCIPAL,
|
||||||
|
opt: { force: true }
|
||||||
|
}, { root: true }); // See PromptRemove.vue
|
||||||
|
|
||||||
|
this.$router.replace({ name: this.doneRoute });
|
||||||
|
buttonDone(true);
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changed(changes) {
|
||||||
|
this.valid = !!changes.addRoles.length || !!changes.removeBindings.length;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="value"
|
||||||
|
:validation-passed="valid"
|
||||||
|
:errors="errors"
|
||||||
|
:can-yaml="false"
|
||||||
|
@finish="save"
|
||||||
|
>
|
||||||
|
<GlobalRoleBindings ref="grb" :principal-id="value.id" :mode="mode" @changed="changed" />
|
||||||
|
</CruResource>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { TYPES, DISPLAY_TYPES } from '@/models/secret';
|
import { TYPES } from '@/models/secret';
|
||||||
import { base64Encode, base64Decode } from '@/utils/crypto';
|
import { base64Encode, base64Decode } from '@/utils/crypto';
|
||||||
import { NAMESPACE } from '@/config/types';
|
import { NAMESPACE } from '@/config/types';
|
||||||
import CreateEditView from '@/mixins/create-edit-view';
|
import CreateEditView from '@/mixins/create-edit-view';
|
||||||
|
|
@ -112,9 +112,11 @@ export default {
|
||||||
const out = [];
|
const out = [];
|
||||||
|
|
||||||
this.types.forEach((type) => {
|
this.types.forEach((type) => {
|
||||||
|
const displayType = this.typeDisplay(type);
|
||||||
|
|
||||||
const subtype = {
|
const subtype = {
|
||||||
id: type,
|
id: type,
|
||||||
label: DISPLAY_TYPES[type] || '',
|
label: displayType,
|
||||||
bannerAbbrv: this.initialDisplayFor(type)
|
bannerAbbrv: this.initialDisplayFor(type)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -245,12 +247,18 @@ export default {
|
||||||
|
|
||||||
selectType(type) {
|
selectType(type) {
|
||||||
this.$set(this.value, '_type', type);
|
this.$set(this.value, '_type', type);
|
||||||
this.$emit('set-subtype', DISPLAY_TYPES[type]);
|
this.$emit('set-subtype', this.typeDisplay(type));
|
||||||
|
},
|
||||||
|
|
||||||
|
typeDisplay(type) {
|
||||||
|
const fallback = type.replace(/^kubernetes.io\//, '');
|
||||||
|
|
||||||
|
return this.$store.getters['i18n/withFallback'](`secret.types."${ type }"`, null, fallback);
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO icons for secret types?
|
// TODO icons for secret types?
|
||||||
initialDisplayFor(type) {
|
initialDisplayFor(type) {
|
||||||
const typeDisplay = DISPLAY_TYPES[type];
|
const typeDisplay = this.typeDisplay(type);
|
||||||
|
|
||||||
return typeDisplay.split('').filter(letter => letter.match(/[A-Z]/)).join('');
|
return typeDisplay.split('').filter(letter => letter.match(/[A-Z]/)).join('');
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
<script>
|
||||||
|
import ResourceTable from '@/components/ResourceTable';
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
import Masthead from '@/components/ResourceList/Masthead';
|
||||||
|
import { NORMAN } from '@/config/types';
|
||||||
|
import AsyncButton from '@/components/AsyncButton';
|
||||||
|
import { applyProducts } from '@/store/type-map';
|
||||||
|
import { NAME } from '@/config/product/auth';
|
||||||
|
import { MODE, _EDIT } from '@/config/query-params';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AsyncButton, ResourceTable, Masthead, Loading
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
resource: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async fetch() {
|
||||||
|
this.rows = await this.$store.dispatch('cluster/findAll', { type: NORMAN.SPOOFED.GROUP_PRINCIPAL }, { root: true }); // See PromptRemove.vue
|
||||||
|
|
||||||
|
const principals = await this.$store.dispatch('rancher/findAll', { type: NORMAN.PRINCIPAL, opt: { url: '/v3/principals' } });
|
||||||
|
|
||||||
|
this.hasGroups = principals.filter(principal => principal.principalType === 'group')?.length;
|
||||||
|
|
||||||
|
this.canRefresh = await this.$store.dispatch('rancher/request', { url: '/v3/users?limit=0' })
|
||||||
|
.then(res => !!res?.actions?.refreshauthprovideraccess);
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: null,
|
||||||
|
hasGroups: false,
|
||||||
|
canRefresh: false,
|
||||||
|
assignLocation: {
|
||||||
|
path: `/c/local/${ NAME }/${ NORMAN.SPOOFED.GROUP_PRINCIPAL }/assign-edit`,
|
||||||
|
query: { [MODE]: _EDIT }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async refreshGroupMemberships(buttonDone) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('rancher/request', {
|
||||||
|
url: '/v3/users?action=refreshauthprovideraccess',
|
||||||
|
method: 'post',
|
||||||
|
data: { },
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is needed in SSR, but not SPA. If this is not here... when cluster/findAll is dispatched... we fail to find the spoofed
|
||||||
|
// type's `getInstance` fn as it hasn't been registered (`instanceMethods` in type-map file is empty)
|
||||||
|
await applyProducts(this.$store);
|
||||||
|
|
||||||
|
this.rows = await this.$store.dispatch('cluster/findAll', {
|
||||||
|
type: NORMAN.SPOOFED.GROUP_PRINCIPAL,
|
||||||
|
opt: { force: true }
|
||||||
|
}, { root: true });
|
||||||
|
|
||||||
|
buttonDone(true);
|
||||||
|
} catch (err) {
|
||||||
|
this.$store.dispatch('growl/fromError', { title: 'Error refreshing group memberships', err }, { root: true });
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else>
|
||||||
|
<Masthead
|
||||||
|
:schema="schema"
|
||||||
|
:resource="resource"
|
||||||
|
>
|
||||||
|
<template slot="extraActions">
|
||||||
|
<AsyncButton
|
||||||
|
v-if="canRefresh"
|
||||||
|
mode="refresh"
|
||||||
|
:action-label="t('authGroups.actions.refresh')"
|
||||||
|
:waiting-label="t('authGroups.actions.refresh')"
|
||||||
|
:success-label="t('authGroups.actions.refresh')"
|
||||||
|
:error-label="t('authGroups.actions.refresh')"
|
||||||
|
@click="refreshGroupMemberships"
|
||||||
|
/>
|
||||||
|
<n-link
|
||||||
|
v-if="hasGroups"
|
||||||
|
:to="assignLocation"
|
||||||
|
class="btn role-primary"
|
||||||
|
>
|
||||||
|
{{ t("authGroups.actions.assignRoles") }}
|
||||||
|
</n-link>
|
||||||
|
</template>
|
||||||
|
</Masthead>
|
||||||
|
|
||||||
|
<ResourceTable :schema="schema" :rows="rows" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import ResourceTable from '@/components/ResourceTable';
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
import { NODE } from '@/config/types';
|
||||||
|
import { allHash } from '@/utils/promise';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ListService',
|
||||||
|
components: { Loading, ResourceTable },
|
||||||
|
// fetch nodes before loading this page, as they may be referenced in the Target table column
|
||||||
|
async fetch() {
|
||||||
|
const store = this.$store;
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
let hasNodes = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const schema = store.getters[`${ inStore }/schemaFor`](NODE);
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
|
hasNodes = true;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const hash = { rows: store.dispatch(`${ inStore }/findAll`, { type: this.$attrs.resource }) };
|
||||||
|
|
||||||
|
if (hasNodes) {
|
||||||
|
hash.nodes = store.dispatch(`${ inStore }/findAll`, { type: NODE });
|
||||||
|
}
|
||||||
|
const res = await allHash(hash);
|
||||||
|
|
||||||
|
this.rows = res.rows;
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { rows: [] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<ResourceTable v-else :schema="$attrs.schema" :rows="rows" :headers="$attrs.headers" :group-by="$attrs.groupBy" />
|
||||||
|
</template>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import ResourceTable from '@/components/ResourceTable';
|
import ResourceTable from '@/components/ResourceTable';
|
||||||
import { WORKLOAD_TYPES, SCHEMA, ENDPOINTS } from '@/config/types';
|
import { WORKLOAD_TYPES, SCHEMA, NODE } from '@/config/types';
|
||||||
import Loading from '@/components/Loading';
|
import Loading from '@/components/Loading';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
|
|
@ -18,7 +18,13 @@ export default {
|
||||||
components: { Loading, ResourceTable },
|
components: { Loading, ResourceTable },
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
this.$store.dispatch('cluster/findAll', { type: ENDPOINTS });
|
try {
|
||||||
|
const schema = this.$store.getters[`cluster/schemaFor`](NODE);
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
|
this.$store.dispatch('cluster/findAll', { type: NODE });
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
let resources;
|
let resources;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,11 @@ export default {
|
||||||
this.model.openLdapConfig = {};
|
this.model.openLdapConfig = {};
|
||||||
this.showLdap = false;
|
this.showLdap = false;
|
||||||
}
|
}
|
||||||
|
if (this.value.configType === 'saml') {
|
||||||
|
if (!this.model.rancherApiHost || !this.model.rancherApiHost.length) {
|
||||||
|
this.$set(this.model, 'rancherApiHost', this.serverUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -108,7 +113,7 @@ export default {
|
||||||
if (!this.model.accessMode) {
|
if (!this.model.accessMode) {
|
||||||
this.model.accessMode = 'unrestricted';
|
this.model.accessMode = 'unrestricted';
|
||||||
}
|
}
|
||||||
await this.model.doAction('testAndApply', obj);
|
await this.model.doAction('testAndApply', obj, { redirectUnauthorized: false });
|
||||||
}
|
}
|
||||||
// Reload principals to get the new ones from the provider
|
// Reload principals to get the new ones from the provider
|
||||||
this.principals = await this.$store.dispatch('rancher/findAll', {
|
this.principals = await this.$store.dispatch('rancher/findAll', {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { insertAt } from '@/utils/array';
|
import { insertAt } from '@/utils/array';
|
||||||
import { TARGET_WORKLOADS, TIMESTAMP, UI_MANAGED } from '@/config/labels-annotations';
|
import { TARGET_WORKLOADS, TIMESTAMP, UI_MANAGED } from '@/config/labels-annotations';
|
||||||
import { WORKLOAD_TYPES, POD, ENDPOINTS, SERVICE } from '@/config/types';
|
import { WORKLOAD_TYPES, POD, SERVICE } from '@/config/types';
|
||||||
import { get, set } from '@/utils/object';
|
import { clone, get, set } from '@/utils/object';
|
||||||
import day from 'dayjs';
|
import day from 'dayjs';
|
||||||
import { _CREATE } from '@/config/query-params';
|
import { _CREATE } from '@/config/query-params';
|
||||||
|
|
||||||
|
|
@ -286,18 +286,6 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
endpoints() {
|
|
||||||
const endpoints = this.$rootGetters['cluster/byId'](ENDPOINTS, this.id);
|
|
||||||
|
|
||||||
if (endpoints) {
|
|
||||||
const out = endpoints.metadata.fields[1];
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 30422
|
|
||||||
|
|
||||||
// create clusterip, nodeport, loadbalancer services from container port spec
|
// create clusterip, nodeport, loadbalancer services from container port spec
|
||||||
servicesFromContainerPorts() {
|
servicesFromContainerPorts() {
|
||||||
return async(mode) => {
|
return async(mode) => {
|
||||||
|
|
@ -391,10 +379,12 @@ export default {
|
||||||
case 'NodePort':
|
case 'NodePort':
|
||||||
nodePort.spec.ports.push(portSpec);
|
nodePort.spec.ports.push(portSpec);
|
||||||
break;
|
break;
|
||||||
case 'LoadBalancer':
|
case 'LoadBalancer': {
|
||||||
portSpec.port = port._lbPort;
|
const lbPort = clone(portSpec);
|
||||||
loadBalancer.spec.ports.push(portSpec);
|
|
||||||
break;
|
lbPort.port = port._lbPort;
|
||||||
|
loadBalancer.spec.ports.push(lbPort);
|
||||||
|
break; }
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,12 +127,13 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if ( this.remember ) {
|
if ( this.remember ) {
|
||||||
this.$cookies.set(USERNAME, this.username, {
|
this.$cookies.set(USERNAME, this.username,{
|
||||||
encode: x => x,
|
encode: x => x,
|
||||||
maxAge: 86400 * 365,
|
maxAge: 86400 * 365,
|
||||||
secure: true,
|
secure: true,
|
||||||
path: '/',
|
path: '/',
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.$cookies.remove(USERNAME);
|
this.$cookies.remove(USERNAME);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,9 @@ export default {
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { params, query } = this.$route;
|
const { query } = this.$route;
|
||||||
|
|
||||||
if ( window.opener && !get(params, 'login') && !get(params, 'errorCode') ) {
|
if ( window.opener ) {
|
||||||
const configQuery = get(query, 'config');
|
const configQuery = get(query, 'config');
|
||||||
|
|
||||||
if ( samlProviders.includes(configQuery) ) {
|
if ( samlProviders.includes(configQuery) ) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export default {
|
||||||
AUTH_CONFIG() {
|
AUTH_CONFIG() {
|
||||||
return MANAGEMENT.AUTH_CONFIG;
|
return MANAGEMENT.AUTH_CONFIG;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ export default {
|
||||||
if ( enabled.length === 1 ) {
|
if ( enabled.length === 1 ) {
|
||||||
redirect({
|
redirect({
|
||||||
name: 'c-cluster-auth-config-id',
|
name: 'c-cluster-auth-config-id',
|
||||||
params: { id: enabled[0].id }
|
params: { id: enabled[0].id },
|
||||||
|
query: { mode: _EDIT }
|
||||||
});
|
});
|
||||||
|
|
||||||
return { nonLocal };
|
return { nonLocal };
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
<script>
|
||||||
|
import FooterComponent from '@/components/form/Footer';
|
||||||
|
import SelectPrincipal from '@/components/auth/SelectPrincipal.vue';
|
||||||
|
import GlobalRoleBindings from '@/components/GlobalRoleBindings.vue';
|
||||||
|
import { NORMAN } from '@/config/types';
|
||||||
|
import { _VIEW } from '@/config/query-params';
|
||||||
|
import { exceptionToErrorsArray } from '@/utils/error';
|
||||||
|
import { NAME } from '@/config/product/auth';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SelectPrincipal,
|
||||||
|
FooterComponent,
|
||||||
|
GlobalRoleBindings,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
errors: [],
|
||||||
|
principalId: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
mode() {
|
||||||
|
return !this.principalId ? _VIEW : this.$route.query.mode || _VIEW;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setPrincipal(id) {
|
||||||
|
this.principalId = id;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
await this.return();
|
||||||
|
},
|
||||||
|
async save(buttonDone) {
|
||||||
|
this.errors = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$refs.grb.save();
|
||||||
|
|
||||||
|
await this.$store.dispatch('cluster/findAll', {
|
||||||
|
type: NORMAN.SPOOFED.GROUP_PRINCIPAL,
|
||||||
|
opt: { force: true }
|
||||||
|
}, { root: true }); // See PromptRemove.vue
|
||||||
|
|
||||||
|
this.$router.replace({
|
||||||
|
name: `c-cluster-product-resource`,
|
||||||
|
params: {
|
||||||
|
cluster: 'local',
|
||||||
|
product: NAME,
|
||||||
|
resource: NORMAN.SPOOFED.GROUP_PRINCIPAL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonDone(true);
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
buttonDone(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div class="masthead">
|
||||||
|
<header>
|
||||||
|
<div class="title">
|
||||||
|
<h1 class="m-0">
|
||||||
|
{{ t('authGroups.assignEdit.assignTitle') }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<SelectPrincipal :retain-selection="true" class="mb-20" :show-my-group-types="['group']" :search-group-types="'group'" @add="setPrincipal" />
|
||||||
|
|
||||||
|
<GlobalRoleBindings ref="grb" :principal-id="principalId" :mode="mode" />
|
||||||
|
|
||||||
|
<FooterComponent
|
||||||
|
:mode="mode"
|
||||||
|
:errors="errors"
|
||||||
|
@save="save"
|
||||||
|
@done="cancel"
|
||||||
|
>
|
||||||
|
</footercomponent>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.masthead {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
HEADER {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items:center;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -13,7 +13,8 @@ export default {
|
||||||
// Spoofing is handled here to ensure it's done for both yaml and form editing.
|
// 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
|
// It became apparent that this was the only place that both intersected
|
||||||
if (opt.url.includes(SPOOFED_PREFIX) || opt.url.includes(SPOOFED_API_PREFIX)) {
|
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 isApi = scheme === SPOOFED_API_PREFIX;
|
||||||
const typemapGetter = id ? 'getSpoofedInstance' : 'getSpoofedInstances';
|
const typemapGetter = id ? 'getSpoofedInstance' : 'getSpoofedInstances';
|
||||||
const schemas = await rootGetters['cluster/all'](SCHEMA);
|
const schemas = await rootGetters['cluster/all'](SCHEMA);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue