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.'
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
},
|
||||
|
||||
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 {
|
|||
<button class="btn role-secondary" @click="close">
|
||||
Cancel
|
||||
</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>
|
||||
</Card>
|
||||
</modal>
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
};
|
||||
</script>
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LabeledSelect
|
||||
v-model="newValue"
|
||||
:mode="mode"
|
||||
label="Add Member"
|
||||
:label="retainSelection ? `Select Member` : `Add Member`"
|
||||
placeholder="Start typing to search for principals"
|
||||
:options="options"
|
||||
:searchable="true"
|
||||
:filterable="false"
|
||||
class="select-principal"
|
||||
:class="{'retain-selection': retainSelection}"
|
||||
@input="add"
|
||||
@search="onSearch"
|
||||
>
|
||||
|
|
@ -137,5 +153,22 @@ export default {
|
|||
<template #option="option">
|
||||
<Principal :key="option.label" :value="option.label" :use-muted="false" />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-principal {
|
||||
&.retain-selection {
|
||||
min-height: 84px;
|
||||
&.focused {
|
||||
.principal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { _EDIT } from '@/config/query-params';
|
||||
import { _EDIT, _VIEW } from '@/config/query-params';
|
||||
import { addObject, removeObject } from '@/utils/array';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
type: [Boolean, Array],
|
||||
default: false
|
||||
},
|
||||
|
||||
|
|
@ -43,11 +44,19 @@ export default {
|
|||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
valueWhenTrue: {
|
||||
type: null,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
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.metaKey = event.metaKey;
|
||||
|
||||
this.$emit('input', !this.value);
|
||||
$(this.$el).trigger(click);
|
||||
// Flip the value
|
||||
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>
|
||||
<label
|
||||
class="checkbox-container"
|
||||
:class="{disabled}"
|
||||
:class="{ 'disabled': isDisabled}"
|
||||
@keydown.enter.prevent="clicked($event)"
|
||||
@keydown.space.prevent="clicked($event)"
|
||||
@click.stop.prevent="clicked($event)"
|
||||
>
|
||||
<input
|
||||
:checked="value"
|
||||
:v-model="value"
|
||||
v-model="value"
|
||||
:checked="isChecked"
|
||||
:value="valueWhenTrue"
|
||||
type="checkbox"
|
||||
:tabindex="-1"
|
||||
@click.stop.prevent
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { NODE } from '@/config/types';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
|
@ -17,49 +18,54 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
nodes() {
|
||||
return this.$store.getters['cluster/all'](NODE);
|
||||
},
|
||||
// value may be JSON from "field.cattle.io/publicEndpoints" label
|
||||
parsed() {
|
||||
const nodes = this.nodes;
|
||||
const nodeWithExternal = nodes.find(node => !!node.externalIp) || {};
|
||||
const externalIp = nodeWithExternal.externalIp;
|
||||
|
||||
if ( this.value && this.value.length ) {
|
||||
let out ;
|
||||
|
||||
try {
|
||||
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) {
|
||||
return this.value[0];
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
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() {
|
||||
const link = this.bestLink;
|
||||
const { parsed } = this;
|
||||
|
||||
if ( link) {
|
||||
if ( parsed) {
|
||||
if (this.parsed[0].protocol) {
|
||||
return this.parsed[0].protocol;
|
||||
}
|
||||
|
||||
const match = link.match(/^([^:]+):\/\//);
|
||||
const match = parsed.match(/^([^:]+):\/\//);
|
||||
|
||||
if ( match ) {
|
||||
return match[1];
|
||||
|
|
@ -76,11 +82,18 @@ export default {
|
|||
|
||||
<template>
|
||||
<span>
|
||||
<a v-if="bestLink" :href="bestLink" target="_blank" rel="nofollow noopener noreferrer">
|
||||
<span v-if="parsed[0].port">{{ parsed[0].port }}/</span>{{ protocol }}
|
||||
</a>
|
||||
<span v-else class="text-muted">
|
||||
—
|
||||
</span>
|
||||
<template v-for="endpoint in parsed">
|
||||
<span v-if="endpoint.display" :key="endpoint.display" class="block">{{ endpoint.display }}</span>
|
||||
<a
|
||||
v-else
|
||||
:key="endpoint.link"
|
||||
class="block"
|
||||
:href="endpoint.link"
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
>
|
||||
<span v-if="endpoint.port">{{ endpoint.port }}/</span>{{ endpoint.protocol }}
|
||||
</a>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { isV4Format, isV6Format } from 'ip';
|
||||
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
components: { CopyToClipboard },
|
||||
props: {
|
||||
|
|
@ -13,7 +13,8 @@ export default {
|
|||
computed: {
|
||||
showBoth() {
|
||||
return this.row.internalIp !== this.row.externalIp;
|
||||
}
|
||||
},
|
||||
...mapGetters({ t: 'i18n/t' })
|
||||
},
|
||||
methods: {
|
||||
isIp(ip) {
|
||||
|
|
@ -29,7 +30,7 @@ export default {
|
|||
{{ row.externalIp }} <CopyToClipboard label-as="tooltip" :text="row.externalIp" class="icon-btn" action-color="bg-transparent" />
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('internalExternalIp.none') }}
|
||||
{{ t('generic.none') }}
|
||||
</template>
|
||||
|
||||
<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" />
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('internalExternalIp.none') }}
|
||||
{{ t('generic.none') }}
|
||||
</template>
|
||||
</template>
|
||||
</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>
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { CATTLE_PUBLIC_ENDPOINTS } from '@/config/labels-annotations';
|
||||
import has from 'lodash/has';
|
||||
import Endpoints from '@/components/formatter/Endpoints';
|
||||
import has from 'lodash/has';
|
||||
|
||||
export default {
|
||||
components: { Endpoints },
|
||||
|
|
@ -31,6 +31,7 @@ export default {
|
|||
|
||||
return false;
|
||||
},
|
||||
|
||||
parsed() {
|
||||
const { row, hasPublic } = this;
|
||||
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 { 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
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ export default {
|
|||
<template #sub-row="{row, fullColspan}">
|
||||
<tr>
|
||||
<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
|
||||
class="sub-table"
|
||||
:rows="row.nodeRows"
|
||||
|
|
|
|||
|
|
@ -155,12 +155,15 @@ export default {
|
|||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
<template v-if="model.enabled && !isSaving">
|
||||
<template v-if="model.enabled && !isEnabling && !editConfig">
|
||||
<Banner color="success clearfix">
|
||||
<div class="pull-left mt-10">
|
||||
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</Banner>
|
||||
|
|
|
|||
|
|
@ -84,12 +84,15 @@ export default {
|
|||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
<template v-if="model.enabled && !isEnabling">
|
||||
<template v-if="model.enabled && !isEnabling && !editConfig">
|
||||
<Banner color="success clearfix">
|
||||
<div class="pull-left mt-10">
|
||||
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</Banner>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import UnitInput from '@/components/form/UnitInput';
|
|||
import Banner from '@/components/Banner';
|
||||
import FileSelector from '@/components/form/FileSelector';
|
||||
|
||||
const DEFAULT_NON_TLS_PORT = 389;
|
||||
const DEFAULT_TLS_PORT = 636;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RadioGroup,
|
||||
|
|
@ -59,6 +62,17 @@ export default {
|
|||
if (neu) {
|
||||
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"
|
||||
@cancel="done"
|
||||
>
|
||||
<template v-if="model.enabled && !isEnabling">
|
||||
<template v-if="model.enabled && !isEnabling && !editConfig">
|
||||
<Banner color="success clearfix">
|
||||
<div class="pull-left mt-10">
|
||||
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</Banner>
|
||||
|
|
|
|||
|
|
@ -76,12 +76,15 @@ export default {
|
|||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
<template v-if="model.enabled && !isEnabling">
|
||||
<template v-if="model.enabled && !isEnabling && !editConfig">
|
||||
<Banner color="success clearfix">
|
||||
<div class="pull-left mt-10">
|
||||
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</Banner>
|
||||
|
|
@ -138,12 +141,11 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<div v-if="NAME !== 'okta'" class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="model.entityID"
|
||||
:label="t(`authConfig.saml.entityID`)"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
import { TYPES, DISPLAY_TYPES } from '@/models/secret';
|
||||
import { TYPES } from '@/models/secret';
|
||||
import { base64Encode, base64Decode } from '@/utils/crypto';
|
||||
import { NAMESPACE } from '@/config/types';
|
||||
import CreateEditView from '@/mixins/create-edit-view';
|
||||
|
|
@ -112,9 +112,11 @@ export default {
|
|||
const out = [];
|
||||
|
||||
this.types.forEach((type) => {
|
||||
const displayType = this.typeDisplay(type);
|
||||
|
||||
const subtype = {
|
||||
id: type,
|
||||
label: DISPLAY_TYPES[type] || '',
|
||||
label: displayType,
|
||||
bannerAbbrv: this.initialDisplayFor(type)
|
||||
};
|
||||
|
||||
|
|
@ -245,12 +247,18 @@ export default {
|
|||
|
||||
selectType(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?
|
||||
initialDisplayFor(type) {
|
||||
const typeDisplay = DISPLAY_TYPES[type];
|
||||
const typeDisplay = this.typeDisplay(type);
|
||||
|
||||
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>
|
||||
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';
|
||||
|
||||
const schema = {
|
||||
|
|
@ -18,7 +18,13 @@ export default {
|
|||
components: { Loading, ResourceTable },
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ export default {
|
|||
this.model.openLdapConfig = {};
|
||||
this.showLdap = false;
|
||||
}
|
||||
if (this.value.configType === 'saml') {
|
||||
if (!this.model.rancherApiHost || !this.model.rancherApiHost.length) {
|
||||
this.$set(this.model, 'rancherApiHost', this.serverUrl);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
@ -108,7 +113,7 @@ export default {
|
|||
if (!this.model.accessMode) {
|
||||
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
|
||||
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 { TARGET_WORKLOADS, TIMESTAMP, UI_MANAGED } from '@/config/labels-annotations';
|
||||
import { WORKLOAD_TYPES, POD, ENDPOINTS, SERVICE } from '@/config/types';
|
||||
import { get, set } from '@/utils/object';
|
||||
import { WORKLOAD_TYPES, POD, SERVICE } from '@/config/types';
|
||||
import { clone, get, set } from '@/utils/object';
|
||||
import day from 'dayjs';
|
||||
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
|
||||
servicesFromContainerPorts() {
|
||||
return async(mode) => {
|
||||
|
|
@ -391,10 +379,12 @@ export default {
|
|||
case 'NodePort':
|
||||
nodePort.spec.ports.push(portSpec);
|
||||
break;
|
||||
case 'LoadBalancer':
|
||||
portSpec.port = port._lbPort;
|
||||
loadBalancer.spec.ports.push(portSpec);
|
||||
break;
|
||||
case 'LoadBalancer': {
|
||||
const lbPort = clone(portSpec);
|
||||
|
||||
lbPort.port = port._lbPort;
|
||||
loadBalancer.spec.ports.push(lbPort);
|
||||
break; }
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,12 +127,13 @@ export default {
|
|||
}
|
||||
});
|
||||
if ( this.remember ) {
|
||||
this.$cookies.set(USERNAME, this.username, {
|
||||
this.$cookies.set(USERNAME, this.username,{
|
||||
encode: x => x,
|
||||
maxAge: 86400 * 365,
|
||||
secure: true,
|
||||
path: '/',
|
||||
});
|
||||
|
||||
} else {
|
||||
this.$cookies.remove(USERNAME);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ export default {
|
|||
window.close();
|
||||
}
|
||||
} 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');
|
||||
|
||||
if ( samlProviders.includes(configQuery) ) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default {
|
|||
AUTH_CONFIG() {
|
||||
return MANAGEMENT.AUTH_CONFIG;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
// 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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue