Merge pull request #826 from jumpserver/feat_account_manager

feat: 添加账号管理模块
This commit is contained in:
Jiangjie.Bai
2021-06-04 11:21:01 +08:00
committed by GitHub
28 changed files with 693 additions and 551 deletions

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div> <div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" /> <ListTable ref="ListTable" :table-config="iTableConfig" :header-actions="headerActions" />
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true"> <Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<div v-if="MFAConfirmed"> <div v-if="MFAConfirmed">
<el-form label-position="right" label-width="80px" :model="MFAInfo"> <el-form label-position="right" label-width="80px" :model="MFAInfo">
@@ -93,6 +93,10 @@ export default {
hasClone: { hasClone: {
type: Boolean, type: Boolean,
default: true default: true
},
tableConfig: {
type: Object,
default: () => ({})
} }
}, },
data() { data() {
@@ -114,36 +118,31 @@ export default {
password: '', password: '',
private_key: '' private_key: ''
}, },
tableConfig: { defaultTableConfig: {
url: this.url, url: this.url,
columns: [ columns: ['hostname', 'ip', 'username', 'version', 'date_created', 'actions'],
{ columnsMeta: {
prop: 'hostname', 'hostname': {
label: this.$t('assets.Hostname'), label: this.$t('assets.Hostname'),
showOverflowTooltip: true showOverflowTooltip: true
}, },
{ 'ip': {
prop: 'ip',
label: this.$t('assets.ip'), label: this.$t('assets.ip'),
width: '120px' width: '120px'
}, },
{ 'username': {
prop: 'username',
label: this.$t('assets.Username'), label: this.$t('assets.Username'),
showOverflowTooltip: true showOverflowTooltip: true
}, },
{ 'version': {
prop: 'version',
label: this.$t('assets.Version'), label: this.$t('assets.Version'),
width: '70px' width: '70px'
}, },
{ 'date_created': {
prop: 'date_created',
label: this.$t('assets.date_joined'), label: this.$t('assets.date_joined'),
formatter: DateFormatter formatter: DateFormatter
}, },
{ 'actions': {
prop: 'id',
label: this.$t('common.Action'), label: this.$t('common.Action'),
align: 'center', align: 'center',
width: 150, width: 150,
@@ -211,7 +210,7 @@ export default {
] ]
} }
} }
], },
extraQuery: { extraQuery: {
latest: 1 latest: 1
} }
@@ -256,16 +255,19 @@ export default {
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
const now = new Date() const now = new Date()
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000) return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
},
iTableConfig() {
return Object.assign(this.defaultTableConfig, this.tableConfig)
} }
}, },
watch: { watch: {
url(iNew) { url(iNew) {
this.$set(this.tableConfig, 'url', iNew) this.$set(this.iTableConfig, 'url', iNew)
} }
}, },
mounted() { mounted() {
if (this.otherActions) { if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1] const actionColumn = this.iTableConfig.columns[this.iTableConfig.columns.length - 1]
for (const item of this.otherActions) { for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item) actionColumn.formatterArgs.extraActions.push(item)
} }

View File

@@ -522,6 +522,9 @@
}, },
"route": { "route": {
"": "", "": "",
"Accounts": "账号管理",
"AssetAccount": "资产账号",
"ApplicationAccount": "应用账号",
"Ticket":"工单", "Ticket":"工单",
"CommandConfirm": "命令复核", "CommandConfirm": "命令复核",
"AdminUserCreate": "创建管理用户", "AdminUserCreate": "创建管理用户",

View File

@@ -520,6 +520,9 @@
}, },
"route": { "route": {
"": "", "": "",
"Accounts": "Accounts",
"AssetAccount": "Asset account",
"ApplicationAccount": "Application account",
"Ticket": "Tickets", "Ticket": "Tickets",
"CommandConfirm": "Command confirm", "CommandConfirm": "Command confirm",
"AdminUserCreate": "Admin user create", "AdminUserCreate": "Admin user create",

127
src/router/accounts.js Normal file
View File

@@ -0,0 +1,127 @@
import i18n from '@/i18n/i18n'
import empty from '@/layout/empty'
export default [
{
path: 'asset-accounts',
component: empty,
meta: { title: i18n.t('route.AssetAccount') },
children: [
{
path: '',
name: 'AssetAccountList',
component: () => import('@/views/accounts/AssetAccount/AssetAccountList'),
meta: { title: i18n.t('route.AssetAccount') }
},
{
path: 'create',
component: () => import('@/views/accounts/AssetAccount/AssetAccountCreate'),
name: 'AssetAccountCreate',
meta: { title: i18n.t('common.Create'), activeMenu: '/accounts/asset-accounts/' },
hidden: true
}
]
},
{
path: 'application-accounts',
component: empty,
meta: { title: i18n.t('route.AssetAccount') },
children: [
{
path: '',
name: 'ApplicationAccountList',
component: () => import('@/views/accounts/ApplicationAccount/ApplicationAccountList'),
meta: { title: i18n.t('route.ApplicationAccount') }
}
]
},
{
path: 'gathered-user',
component: empty,
redirect: '',
meta: { title: i18n.t('xpack.GatherUser.GatherUserList') },
children: [
{
path: '',
component: () => import('@/views/accounts/GatheredUser/index'),
name: 'GatherUserListIndex',
meta: { title: i18n.t('xpack.GatherUser.GatherUser'), activeMenu: '/accounts/gathered-user' }
},
{
path: '',
component: () => import('@/views/accounts/GatheredUser/GatheredUserList'),
name: 'GatherUserList',
hidden: true,
meta: { title: i18n.t('xpack.GatherUser.GatherUserList'), activeMenu: '/accounts/gathered-user' }
},
{
path: 'tasks',
component: () => import('@/views/accounts/GatheredUser/TaskList'),
name: 'GatherUserTaskList',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskList'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/:id',
component: () => import('@/views/accounts/GatheredUser/TaskDetail/index'),
name: 'GatherUserTaskDetail',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskDetail'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/create',
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskCreate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskCreate'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/:id/update',
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskUpdate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskUpdate'), action: 'update', activeMenu: '/accounts/gathered-user' },
hidden: true
}
]
},
{
path: 'change-auth-plan',
component: empty,
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
children: [
{
path: 'plan',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanList.vue'),
name: 'ChangeAuthPlanList',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' }
},
{
path: 'plan/create',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanCreate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanCreate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'create' },
hidden: true
},
{
path: 'plan/:id/update',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanUpdate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanUpdate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'update' },
hidden: true
},
{
path: 'plan/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/index.vue'),
name: 'ChangeAuthPlanDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
hidden: true
},
{
path: 'plan-execution/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/ChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
name: 'ChangeAuthPlanExecutionDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/accounts/change-auth-plan/plan' },
hidden: true
}
]
}
]

View File

@@ -37,6 +37,7 @@ import TicketsRoutes from './tickets'
import AuditsRoutes from './audits' import AuditsRoutes from './audits'
import commonRoutes from './common' import commonRoutes from './common'
import aclRoutes from './acl' import aclRoutes from './acl'
import AccountRoutes from './accounts'
/** /**
* constantRoutes * constantRoutes
@@ -110,6 +111,18 @@ export const allRoleRoutes = [
meta: { title: i18n.t('route.Applications'), icon: 'th' }, meta: { title: i18n.t('route.Applications'), icon: 'th' },
children: ApplicationsRoute children: ApplicationsRoute
}, },
{
path: '/accounts',
component: Layout,
redirect: '/accounts/asset-accounts/',
name: 'Accounts',
meta: {
licenseRequired: true,
title: i18n.t('route.Accounts'),
icon: 'address-book'
},
children: AccountRoutes
},
{ {
path: '/perms/', path: '/perms/',
component: Layout, component: Layout,

View File

@@ -88,11 +88,14 @@ export function toSafeLocalDateStr(d) {
} }
export function getApiPath(that) { export function getApiPath(that) {
const pagePath = that.$route.path let pagePath = that.$route.path
const isOrgPath = pagePath.split('/').indexOf('orgs') !== -1 const pagePathArray = pagePath.split('/')
if (isOrgPath) { if (pagePathArray.indexOf('orgs') !== -1) {
return `/api/v1/orgs/orgs/${pagePath.split('/').pop()}/` pagePathArray[pagePathArray.indexOf('xpack')] = 'orgs'
} else if (pagePathArray.indexOf('gathered-user') !== -1 || pagePathArray.indexOf('change-auth-plan') !== -1) {
pagePathArray[pagePathArray.indexOf('accounts')] = 'xpack'
} }
pagePath = pagePathArray.join('/')
return `/api/v1${pagePath}/` return `/api/v1${pagePath}/`
} }

View File

@@ -0,0 +1,144 @@
<template>
<Page>
<el-row>
<el-col :span="14">
<GenericListTable
ref="LeftTable"
class="application-table"
:header-actions="leftTable.headerActions"
:table-config="leftTable.tableConfig"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="10">
<GenericListTable
ref="RightTable"
class="application-user-table"
:header-actions="rightTable.headerActions"
:table-config="rightTable.tableConfig"
/>
</el-col>
</el-row>
</Page>
</template>
<script>
import Page from '@/layout/components/Page'
import GenericListTable from '@/layout/components/GenericListTable'
import { DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'AssetAccountList',
components: {
GenericListTable, Page
},
data() {
const vm = this
return {
clickedRow: {},
leftTable: {
tableConfig: {
url: '/api/v1/applications/applications/',
columns: [
'name', 'category_display', 'type_display', 'created_by', 'date_created', 'date_updated',
'comment', 'org_name'
],
columnsShow: {
min: ['name'],
default: ['name', 'category_display', 'type_display']
},
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
getRoute({ row, col, cellValue }) {
return {
'db': 'DatabaseAppDetail', 'remote_app': 'RemoteAppDetail', 'cloud': 'KubernetesAppDetail'
}[row.category]
}
},
showOverflowTooltip: true
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.tableConfig.url = `/api/v1/applications/application-users/?application_id=${row.id}`
vm.clickedRow = row
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
},
rightTable: {
tableConfig: {
url: `/api/v1/applications/application-users/?application_id=`,
columns: [
'name', 'username', 'username_same_with_user', 'protocol', 'login_mode',
'assets_amount', 'priority',
'created_by', 'date_created', 'date_updated', 'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['name', 'username', 'actions'],
default: ['name', 'username', 'date_created', 'actions']
},
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
route: 'SystemUserDetail'
},
showOverflowTooltip: true
},
actions: {
formatterArgs: {
hasUpdate: true, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: false,
onUpdate({ row, col }) {
vm.$router.push({ name: 'SystemUserUpdate', params: { id: row.id }, query: { protocol: row.protocol }})
}
}
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
},
headerActions: {
hasLeftActions: false,
hasImport: false
}
}
}
}
}
</script>
<style lang="scss" scoped>
.application-table ::v-deep .row-clicked, .application-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<Page>
<el-row>
<el-col v-show="iShowTree" :span="iShowTree?4:0">
<AutoDataZTree
ref="AUtoDataZTree"
:setting="treeSetting"
/>
</el-col>
<el-col :span="iShowTree?12:14">
<div class="mini">
<div style="display:block" class="mini-button" @click="iShowTree=!iShowTree">
<i v-show="iShowTree" class="fa fa-angle-left fa-x" /><i v-show="!iShowTree" class="fa fa-angle-right fa-x" />
</div>
</div>
<GenericListTable
ref="LeftTable"
class="asset-table"
:header-actions="leftTable.headerActions"
:table-config="leftTable.tableConfig"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="iShowTree?8:10">
<AssetUserTable
ref="RightTable"
class="asset-user-table"
:url="rightTable.url"
:has-left-actions="true"
:table-config="rightTable.tableConfig"
:has-clone="false"
:has-import="false"
/>
</el-col>
</el-row>
</Page>
</template>
<script>
import Page from '@/layout/components/Page'
import GenericListTable from '@/layout/components/GenericListTable'
import AutoDataZTree from '@/components/AutoDataZTree/index'
import { AssetUserTable } from '@/components'
import { ChoicesFormatter, DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'AssetAccountList',
components: {
AutoDataZTree, GenericListTable, Page, AssetUserTable
},
data() {
const vm = this
return {
clickedRow: {},
iShowTree: true,
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: false,
url: '',
treeUrl: '/api/v1/assets/nodes/children/tree/',
callback: {
onSelected: function(event, treeNode) {
vm.leftTable.tableConfig.url = `/api/v1/assets/assets/?node_id=${treeNode.meta.node.id}`
}
}
},
leftTable: {
tableConfig: {
url: '/api/v1/assets/assets/',
columns: [
'hostname', 'ip', 'public_ip', 'admin_user_display',
'protocols', 'platform', 'connectivity',
'created_by', 'date_created', 'comment', 'org_name'
],
columnsShow: {
min: ['hostname', 'ip', 'platform'],
default: ['hostname', 'ip', 'connectivity', 'platform']
},
columnsMeta: {
hostname: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
routeQuery: {
activeTab: 'Detail'
}
},
showOverflowTooltip: true
},
connectivity: {
label: this.$t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
0: 'fa-times text-danger',
1: 'fa-check text-primary',
2: 'fa-circle text-warning'
},
typeChange: function(val) {
if (!val) {
return 2
}
return val.status
},
hasTips: true
},
width: '90px',
align: 'center'
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.url = `/api/v1/assets/asset-users/?asset_id=${row.id}&latest=1`
vm.clickedRow = row
}
},
headerActions: {
hasLeftActions: true,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
},
rightTable: {
url: `/api/v1/assets/asset-users/?hostname=ShowFirstAssetRelated&latest=1`,
tableConfig: {
columns: ['name', 'username', 'version', 'backend', 'backend_display', 'date_created', 'actions'],
columnsShow: {
min: ['username', 'actions'],
default: ['name', 'username', 'version', 'backend_display', 'date_created', 'actions']
},
columnsMeta: {
name: {
formatter: null,
showOverflowTooltip: true
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
}
}
}
}
}
</script>
<style lang="scss" scoped>
.asset-table ::v-deep .row-clicked, .asset-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.mini-button{
width: 12px;
float: left;
text-align: center;
padding: 5px 0;
background-color: #1ab394;
border-color: #1ab394;
color: #FFFFFF;
border-radius: 3px;
line-height: 1.428;
cursor:pointer;
}
</style>

View File

@@ -0,0 +1,192 @@
<template>
<div>
<el-row>
<el-col v-show="iShowTree" :span="iShowTree?4:0">
<AutoDataZTree
ref="AUtoDataZTree"
:setting="treeSetting"
/>
</el-col>
<el-col :span="iShowTree?12:14">
<div class="mini">
<div style="display:block" class="mini-button" @click="iShowTree=!iShowTree">
<i v-show="iShowTree" class="fa fa-angle-left fa-x" /><i v-show="!iShowTree" class="fa fa-angle-right fa-x" />
</div>
</div>
<GenericListTable
ref="LeftTable"
class="asset-table"
:header-actions="leftTable.headerActions"
:table-config="leftTable.tableConfig"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="iShowTree?8:10">
<AssetUserTable
ref="RightTable"
class="asset-user-table"
:url="rightTable.url"
:has-left-actions="true"
:table-config="rightTable.tableConfig"
:has-clone="false"
:has-import="false"
/>
</el-col>
</el-row>
</div>
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import AutoDataZTree from '@/components/AutoDataZTree/index'
import { AssetUserTable } from '@/components'
import { ChoicesFormatter, DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'AssetAccountList',
components: {
AutoDataZTree, GenericListTable, AssetUserTable
},
data() {
const vm = this
return {
clickedRow: {},
iShowTree: true,
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: false,
url: '',
treeUrl: '/api/v1/assets/nodes/children/tree/',
callback: {
onSelected: function(event, treeNode) {
vm.leftTable.tableConfig.url = `/api/v1/assets/assets/?node_id=${treeNode.meta.node.id}`
}
}
},
leftTable: {
tableConfig: {
url: '/api/v1/assets/assets/',
columns: [
'hostname', 'ip', 'public_ip', 'admin_user_display',
'protocols', 'platform', 'connectivity',
'created_by', 'date_created', 'comment', 'org_name'
],
columnsShow: {
min: ['hostname', 'ip', 'platform'],
default: ['hostname', 'ip', 'connectivity', 'platform']
},
columnsMeta: {
hostname: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
routeQuery: {
activeTab: 'Detail'
}
},
showOverflowTooltip: true
},
connectivity: {
label: this.$t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
0: 'fa-times text-danger',
1: 'fa-check text-primary',
2: 'fa-circle text-warning'
},
typeChange: function(val) {
if (!val) {
return 2
}
return val.status
},
hasTips: true
},
width: '90px',
align: 'center'
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.url = `/api/v1/assets/gathered-users/?asset_id=${row.id}`
vm.clickedRow = row
}
},
headerActions: {
hasLeftActions: true,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
},
rightTable: {
url: `/api/v1/assets/gathered-users/?asset__hostname=ShowFirstAssetRelated`,
tableConfig: {
columns: [
'username', 'date_last_login', 'present', 'ip_last_login', 'date_updated'
],
columnsShow: {
min: ['username'],
default: [
'username', 'date_last_login', 'present', 'ip_last_login', 'date_updated'
]
},
columnsMeta: {
username: {
showOverflowTooltip: true
},
present: {
width: 80
},
ip_last_login: {
width: 120
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
}
}
}
}
}
</script>
<style lang="scss" scoped>
.asset-table ::v-deep .row-clicked, .asset-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.mini-button{
width: 12px;
float: left;
text-align: center;
padding: 5px 0;
background-color: #1ab394;
border-color: #1ab394;
color: #FFFFFF;
border-radius: 3px;
line-height: 1.428;
cursor:pointer;
}
</style>

View File

@@ -1,78 +0,0 @@
<template>
<TreeTable :table-config="tableConfig" :tree-setting="treeSetting" :header-actions="headerActions" />
</template>
<script>
import TreeTable from '@/components/TreeTable'
export default {
components: {
TreeTable
},
data() {
return {
treeSetting: {
showMenu: false,
showRefresh: true,
showAssets: true,
url: '/api/v1/assets/gathered-users/',
nodeUrl: '/api/v1/assets/nodes/',
// ?assets=0不显示资产. =1显示资产
treeUrl: '/api/v1/assets/nodes/children/tree/?assets=1'
},
tableConfig: {
url: '/api/v1/assets/gathered-users/',
hasTree: true,
columns: [
'hostname', 'ip', 'username', 'date_last_login', 'present',
'ip_last_login', 'date_updated'
],
columnsMeta: {
hostname: {
showOverflowTooltip: true
},
ip: {
width: 120
},
username: {
showOverflowTooltip: true
},
present: {
width: 80
},
ip_last_login: {
width: 120
}
}
},
headerActions: {
hasCreate: false,
hasLeftActions: false,
hasImport: false,
searchConfig: {
exclude: ['asset'],
options: [
{
label: this.$t('assets.Hostname'),
value: 'asset__hostname'
},
{
label: 'IP',
value: 'asset__ip'
}
]
}
}
}
},
methods: {
onGatherUserTasks() {
this.$router.push({ name: 'GatherUserTaskList' })
}
}
}
</script>
<style>
</style>

View File

@@ -1,340 +0,0 @@
<template>
<div>
<GenericTreeListPage ref="TreeTablePage" :tree-setting="treeSetting">
<template #table>
<AssetUserTable ref="table" v-bind="assetUserConfig" />
</template>
</GenericTreeListPage>
<Dialog width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<el-row :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAInput" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
<Dialog :title="$t('common.Export')" :visible.sync="showExportDialog" @confirm="handleExportConfirm()" @cancel="handleExportCancel()">
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
<el-radio v-model="exportOption" class="export-item" label="1">{{ this.$t('common.imExport.ExportAll') }}</el-radio>
<br>
<el-radio v-model="exportOption" :disabled="selectedRows.length===0" class="export-item" label="2">{{ this.$t('common.imExport.ExportOnlySelectedItems') }}</el-radio>
<br>
<!-- 去掉导出搜索项-->
<!-- <el-radio v-model="exportOption" disabled class="export-item" label="3">{{ this.$t('common.imExport.ExportOnlyFiltered') }}</el-radio>-->
</el-form-item>
</el-form>
</Dialog>
<Dialog :title="$t('common.Import')" :visible.sync="showImportDialog" @confirm="handleImportConfirm()" @cancel="handleImportCancel()">
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.Import' )" :label-width="'100px'">
<el-radio v-model="importOption" class="export-item" label="1">{{ this.$t('common.Create') }}</el-radio>
<el-radio v-model="importOption" disabled class="export-item" label="2">{{ this.$t('common.Update') }}</el-radio>
<div style="line-height: 1.5">
<span v-if="importOption==='1'" class="el-upload__tip">
{{ this.$t('common.imExport.downloadImportTemplateMsg') }}
<el-link type="success" :underline="false" :href="downloadImportTempUrl">{{ this.$t('common.Download') }}</el-link>
</span>
<span v-else class="el-upload__tip">`
{{ this.$t('common.imExport.downloadUpdateTemplateMsg') }}
<el-link type="success" :underline="false" @click="downloadUpdateTempUrl">{{ this.$t('common.Download') }}</el-link>
</span>
</div>
</el-form-item>
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'">
<el-upload
ref="upload"
action="string"
:http-request="handleImport"
list-type="text/csv"
:limit="1"
:auto-upload="false"
:before-upload="beforeUpload"
>
<el-button size="mini" type="default">{{ this.$t('common.SelectFile') }}</el-button>
<div slot="tip" :class="uploadHelpTextClass" style="line-height: 1.5">{{ this.$t('common.imExport.onlyCSVFilesTips') }}</div>
</el-upload>
</el-form-item>
</el-form>
<div v-if="errorMsg" class="error-msg error-results">
<ul v-if="typeof errorMsg === 'object'">
<li v-for="(item, index) in errorMsg" :key="item + '-' + index"> {{ item }}</li>
</ul>
<span v-else>{{ errorMsg }}</span>
</div>
</Dialog>
</div>
</template>
<script>
import GenericTreeListPage from '@/layout/components/GenericTreeListPage'
import { AssetUserTable } from '@/components'
import Dialog from '@/components/Dialog'
import { setUrlParam } from '@/utils/common'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/DataTable/compenents/el-data-table/utils/query'
import { mapGetters } from 'vuex'
export default {
name: 'VaultList',
components: {
GenericTreeListPage,
AssetUserTable,
Dialog
},
data() {
const vm = this
return {
showImportDialog: false,
importOption: '1',
isCsv: true,
errorMsg: '',
showExportDialog: false,
exportOption: '1',
meta: {},
MfaExpired: 0,
showMFADialog: false,
MFAInput: '',
selectedRows: '',
assetUserConfig: {
hasLeftActions: true,
hasCreate: true,
hasClone: false,
url: '/api/v1/assets/asset-users/',
handleImport: function({ selectedRows }) {
this.selectedRows = selectedRows
this.dialogStatus = 'import'
if (!this.needMFAVerify) {
this.showMFADialog = false
this.showImportDialog = true
} else {
this.showMFADialog = true
}
}.bind(this),
handleExport: function({ selectedRows }) {
this.selectedRows = selectedRows
this.dialogStatus = 'export'
if (!this.needMFAVerify) {
this.showMFADialog = false
this.showExportDialog = true
} else {
this.showMFADialog = true
}
}.bind(this)
},
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: true,
url: '/api/v1/assets/asset-users/',
treeUrl: '/api/v1/assets/nodes/children/tree/?assets=1',
callback: {
onSelected: function(event, treeNode) {
let url = vm.assetUserConfig.url
if (treeNode.meta.type === 'node') {
const nodeId = treeNode.meta.node.id
url = setUrlParam(url, 'asset_id', '')
url = setUrlParam(url, 'node_id', nodeId)
} else if (treeNode.meta.type === 'asset') {
const assetId = treeNode.meta.asset.id
url = setUrlParam(url, 'node_id', '')
url = setUrlParam(url, 'asset_id', assetId)
}
setTimeout(() => {
vm.assetUserConfig.url = url
}, 100)
}
}
}
}
},
computed: {
...mapGetters([
'MFA_TTl',
'MFAVerifyAt',
'publicSettings'
]),
needMFAVerify() {
if (!this.publicSettings.SECURITY_VIEW_AUTH_NEED_MFA) {
return false
}
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
const now = new Date()
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
},
hasSelected() {
return this.selectedRows.length > 0
},
upLoadUrl() {
return this.url
},
downloadImportTempUrl() {
const baseUrl = `/api/v1/assets/asset-user-auth-infos/`
return baseUrl + '?format=csv&template=import&limit=1'
},
uploadHelpTextClass() {
const cls = ['el-upload__tip']
if (!this.isCsv) {
cls.push('error-msg')
}
return cls
},
...mapGetters([
'MFAVerifyAt',
'MFA_TTl'
])
},
methods: {
performUpdate(item) {
this.$axios.put(
`/api/v1/assets/asset-users/`,
item.file,
{ headers: { 'Content-Type': 'text/csv' }, disableFlashErrorMsg: true }
).then((data) => {
const msg = this.$t('common.imExport.updateSuccessMsg', { count: data.length })
this.onSuccess(msg)
}).catch(error => {
this.catchError(error)
})
},
performCreate(item) {
this.$axios.post(
`/api/v1/assets/asset-users/`,
item.file,
{ headers: { 'Content-Type': 'text/csv' }, disableFlashErrorMsg: true }
).then((data) => {
const msg = this.$t('common.imExport.createSuccessMsg', { count: data.length })
this.onSuccess(msg)
}).catch(error => {
this.$message.error(this.$t('common.updateErrorMsg') + ' ' + error)
this.catchError(error)
})
},
catchError(error) {
this.$refs.upload.clearFiles()
if (error.response && error.response.status === 400) {
const errorData = error.response.data
const totalErrorMsg = []
errorData.forEach((value, index) => {
if (typeof value === 'string') {
totalErrorMsg.push(`line ${index}. ${value}`)
} else {
const errorMsg = [`line ${index}. `]
for (const [k, v] of Object.entries(value)) {
if (v) {
errorMsg.push(`${k}: ${v}`)
}
}
if (errorMsg.length > 1) {
totalErrorMsg.push(errorMsg.join(' '))
}
}
})
this.errorMsg = totalErrorMsg
}
},
onSuccess(msg) {
this.errorMsg = ''
this.$message.success(msg)
},
handleImport(item) {
if (this.importOption === '1') {
this.performCreate(item)
} else {
this.performUpdate(item)
}
},
async downloadUpdateTempUrl() {
var resources = []
const data = this.selectedRows
for (let index = 0; index < data.length; index++) {
resources.push(data[index].id)
}
const spm = await createSourceIdCache(resources)
const baseUrl = `/api/v1/assets/asset-user-auth-infos/`
const url = `${baseUrl}?format=csv&template=update&spm=` + spm.spm
return this.downloadCsv(url)
},
async handleImportConfirm() {
this.$refs.upload.submit()
this.showImportDialog = false
},
handleImportCancel() {
this.showImportDialog = false
},
beforeUpload(file) {
this.isCsv = _.endsWith(file.name, 'csv')
return this.isCsv
},
downloadCsv(url) {
const a = document.createElement('a')
a.href = url
a.click()
window.URL.revokeObjectURL(url)
},
async handleExport() {
const url = `/api/v1/assets/asset-user-auth-infos/`
let query = {}
if (this.exportOption === '2') {
const resources = []
const data = this.selectedRows
for (let index = 0; index < data.length; index++) {
resources.push(data[index].id)
}
const spm = await createSourceIdCache(resources)
query['spm'] = spm.spm
} else if (this.exportOption === '3') {
const listTableRef = this.$parent.$parent.$parent.$parent
// console.log(listTableRef)
// console.log(listTableRef.dataTable)
query = listTableRef.dataTable.getQuery()
delete query['limit']
delete query['offset']
}
query['format'] = 'csv'
const queryStr =
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')
return this.downloadCsv(url + queryStr)
},
async handleExportConfirm() {
await this.handleExport()
this.showExportDialog = false
},
handleExportCancel() {
this.showExportDialog = false
},
MFAConfirm() {
if (this.MFAInput.length !== 6) {
return this.$message.error(this.$t('common.MFAErrorMsg'))
}
this.$axios.post(
`/api/v1/authentication/otp/verify/`, {
code: this.MFAInput
}
).then(
res => {
this.$store.dispatch('users/setMFAVerify')
if (this.dialogStatus === 'import') {
this.showMFADialog = false
this.showImportDialog = true
} else {
this.showMFADialog = false
this.showExportDialog = true
}
}
)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -10,47 +10,6 @@ export default {
name: 'Xpack', name: 'Xpack',
meta: { title: 'X-Pack', icon: 'sitemap', licenseRequired: true }, meta: { title: 'X-Pack', icon: 'sitemap', licenseRequired: true },
children: [ children: [
{
path: 'change-auth-plan',
component: empty,
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/xpack/change-auth-plan/plan' },
children: [
{
path: 'plan',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanList.vue'),
name: 'ChangeAuthPlanList',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/xpack/change-auth-plan/plan' }
},
{
path: 'plan/create',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanCreate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanCreate'), activeMenu: '/xpack/change-auth-plan/plan', action: 'create' },
hidden: true
},
{
path: 'plan/:id/update',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanUpdate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanUpdate'), activeMenu: '/xpack/change-auth-plan/plan', action: 'update' },
hidden: true
},
{
path: 'plan/:id',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanDetail/index.vue'),
name: 'ChangeAuthPlanDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/xpack/change-auth-plan/plan' },
hidden: true
},
{
path: 'plan-execution/:id',
component: () => import('@/views/xpack/ChangeAuthPlan/ChangeAuthPlanDetail/ChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
name: 'ChangeAuthPlanExecutionDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/xpack/change-auth-plan/plan' },
hidden: true
}
]
},
{ {
path: 'cloud', path: 'cloud',
component: empty, component: empty,
@@ -127,55 +86,6 @@ export default {
name: 'InterfaceSetting', name: 'InterfaceSetting',
meta: { title: i18n.t('xpack.InterfaceSettings'), permissions: [rolec.PERM_SUPER] } meta: { title: i18n.t('xpack.InterfaceSettings'), permissions: [rolec.PERM_SUPER] }
}, },
{
path: 'gathered-user',
component: empty,
redirect: '',
meta: { title: i18n.t('xpack.GatherUser.GatherUserList') },
children: [
{
path: '',
component: () => import('@/views/xpack/GatheredUser/index'),
name: 'GatherUserListIndex',
meta: { title: i18n.t('xpack.GatherUser.GatherUser'), activeMenu: '/xpack/gathered-user' }
},
{
path: '',
component: () => import('@/views/xpack/GatheredUser/GatheredUserList'),
name: 'GatherUserList',
hidden: true,
meta: { title: i18n.t('xpack.GatherUser.GatherUserList'), activeMenu: '/xpack/gathered-user' }
},
{
path: 'tasks',
component: () => import('@/views/xpack/GatheredUser/TaskList'),
name: 'GatherUserTaskList',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskList'), activeMenu: '/xpack/gathered-user' },
hidden: true
},
{
path: 'tasks/:id',
component: () => import('@/views/xpack/GatheredUser/TaskDetail/index'),
name: 'GatherUserTaskDetail',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskDetail'), activeMenu: '/xpack/gathered-user' },
hidden: true
},
{
path: 'tasks/create',
component: () => import('@/views/xpack/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskCreate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskCreate'), activeMenu: '/xpack/gathered-user' },
hidden: true
},
{
path: 'tasks/:id/update',
component: () => import('@/views/xpack/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskUpdate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskUpdate'), action: 'update', activeMenu: '/xpack/gathered-user' },
hidden: true
}
]
},
{ {
path: 'orgs', path: 'orgs',
component: empty, component: empty,
@@ -211,27 +121,6 @@ export default {
} }
] ]
}, },
{
path: 'vault',
component: empty,
redirect: '',
meta: { },
children: [
{
path: '',
component: () => import('@/views/xpack/Vault/VaultList.vue'),
name: 'VaultList',
meta: { title: i18n.t('xpack.Vault.Vault'), activeMenu: '/xpack/vault' }
},
{
path: 'create',
component: () => import('@/views/xpack/Vault/VaultCreate'),
name: 'VaultCreate',
meta: { title: i18n.t('xpack.Vault.Create'), activeMenu: '/xpack/vault' },
hidden: true
}
]
},
{ {
path: 'system-monitor', path: 'system-monitor',
component: () => import('@/views/xpack/SystemMonitor/index.vue'), component: () => import('@/views/xpack/SystemMonitor/index.vue'),