A 角色管理新增主机权限控制功能

pull/103/head
vapao 2020-05-29 10:58:42 +08:00
parent 4fdafabbed
commit 0c1336aa88
15 changed files with 153 additions and 13 deletions

View File

@ -51,6 +51,15 @@ class User(models.Model, ModelMixin):
perms.setdefault('envs', [])
return perms
@property
def host_perms(self):
return json.loads(self.role.host_perms) if self.role.host_perms else []
def has_host_perm(self, host_id):
if isinstance(host_id, (list, set, tuple)):
return self.is_supper or set(host_id).issubset(set(self.host_perms))
return self.is_supper or int(host_id) in self.host_perms
def has_perms(self, codes):
# return self.is_supper or self.role in codes
return self.is_supper
@ -68,6 +77,7 @@ class Role(models.Model, ModelMixin):
desc = models.CharField(max_length=255, null=True)
page_perms = models.TextField(null=True)
deploy_perms = models.TextField(null=True)
host_perms = models.TextField(null=True)
created_at = models.CharField(max_length=20, default=human_datetime)
created_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='+')
@ -76,6 +86,7 @@ class Role(models.Model, ModelMixin):
tmp = super().to_dict(*args, **kwargs)
tmp['page_perms'] = json.loads(self.page_perms) if self.page_perms else None
tmp['deploy_perms'] = json.loads(self.deploy_perms) if self.deploy_perms else None
tmp['host_perms'] = json.loads(self.host_perms) if self.host_perms else None
tmp['used'] = self.user_set.count()
return tmp
@ -85,6 +96,12 @@ class Role(models.Model, ModelMixin):
self.deploy_perms = json.dumps(perms)
self.save()
def add_host_perm(self, value):
perms = json.loads(self.host_perms) if self.host_perms else []
perms.append(value)
self.host_perms = json.dumps(perms)
self.save()
def __repr__(self):
return '<Role name=%r>' % self.name

View File

@ -91,7 +91,8 @@ class RoleView(View):
form, error = JsonParser(
Argument('id', type=int, help='参数错误'),
Argument('page_perms', type=dict, required=False),
Argument('deploy_perms', type=dict, required=False)
Argument('deploy_perms', type=dict, required=False),
Argument('host_perms', type=list, required=False)
).parse(request.body)
if error is None:
role = Role.objects.filter(pk=form.pop('id')).first()
@ -101,6 +102,8 @@ class RoleView(View):
role.page_perms = json.dumps(form.page_perms)
if form.deploy_perms is not None:
role.deploy_perms = json.dumps(form.deploy_perms)
if form.host_perms is not None:
role.host_perms = json.dumps(form.host_perms)
role.user_set.update(token_expired=0)
role.save()
return json_response(error=error)

View File

@ -47,6 +47,8 @@ def do_task(request):
Argument('command', help='请输入执行命令内容')
).parse(request.body)
if error is None:
if not request.user.has_host_perm(form.host_ids):
return json_response(error='无权访问主机,请联系管理员')
token = Channel.get_token()
for host in Host.objects.filter(id__in=form.host_ids):
Channel.send_ssh_executor(

View File

@ -17,6 +17,8 @@ class FileView(View):
Argument('path', help='参数错误')
).parse(request.GET)
if error is None:
if not request.user.has_host_perm(form.id):
return json_response(error='无权访问主机,请联系管理员')
host = Host.objects.get(pk=form.id)
if not host:
return json_response(error='未找到指定主机')
@ -33,6 +35,8 @@ class ObjectView(View):
Argument('file', help='请输入文件路径')
).parse(request.GET)
if error is None:
if not request.user.has_host_perm(form.id):
return json_response(error='无权访问主机,请联系管理员')
host = Host.objects.filter(pk=form.id).first()
if not host:
return json_response(error='未找到指定主机')
@ -50,6 +54,8 @@ class ObjectView(View):
Argument('path', help='参数错误'),
).parse(request.POST)
if error is None:
if not request.user.has_host_perm(form.id):
return json_response(error='无权访问主机,请联系管理员')
file = request.FILES.get('file')
if not file:
return json_response(error='请选择要上传的文件')
@ -68,6 +74,8 @@ class ObjectView(View):
Argument('file', help='请输入文件路径')
).parse(request.GET)
if error is None:
if not request.user.has_host_perm(form.id):
return json_response(error='无权访问主机,请联系管理员')
host = Host.objects.get(pk=form.id)
if not host:
return json_response(error='未找到指定主机')

View File

@ -9,6 +9,7 @@ from apps.host.models import Host
from apps.app.models import Deploy
from apps.schedule.models import Task
from apps.monitor.models import Detection
from apps.account.models import Role
from libs.ssh import SSH, AuthenticationException
from libs import human_datetime, AttrDict
from openpyxl import load_workbook
@ -18,10 +19,13 @@ class HostView(View):
def get(self, request):
host_id = request.GET.get('id')
if host_id:
if int(host_id) not in request.user.host_perms:
return json_response(error='无权访问该主机,请联系管理员')
return json_response(Host.objects.get(pk=host_id))
hosts = Host.objects.filter(deleted_by_id__isnull=True)
zones = [x['zone'] for x in hosts.order_by('zone').values('zone').distinct()]
return json_response({'zones': zones, 'hosts': [x.to_dict() for x in hosts]})
perms = [x.id for x in hosts] if request.user.is_supper else request.user.host_perms
return json_response({'zones': zones, 'hosts': [x.to_dict() for x in hosts], 'perms': perms})
def post(self, request):
form, error = JsonParser(
@ -43,7 +47,9 @@ class HostView(View):
elif Host.objects.filter(name=form.name, deleted_by_id__isnull=True).exists():
return json_response(error=f'已存在的主机名称【{form.name}')
else:
Host.objects.create(created_by=request.user, **form)
host = Host.objects.create(created_by=request.user, **form)
if request.user.role:
request.user.role.add_host_perm(host.id)
return json_response(error=error)
def patch(self, request):
@ -76,6 +82,9 @@ class HostView(View):
detection = Detection.objects.filter(type__in=('3', '4'), addr=form.id).first()
if detection:
return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机')
role = Role.objects.filter(host_perms__regex=fr'\D{form.id}\D').first()
if role:
return json_response(error=f'角色【{role.name}】的主机权限关联了该主机,请解除关联后再尝试删除该主机')
Host.objects.filter(pk=form.id).update(
deleted_at=human_datetime(),
deleted_by=request.user,
@ -110,7 +119,9 @@ def post_import(request):
if valid_ssh(data.hostname, data.port, data.username, data.pop('password') or password) is False:
summary['fail'].append(i)
continue
Host.objects.create(created_by=request.user, **data)
host = Host.objects.create(created_by=request.user, **data)
if request.user.role:
request.user.role.add_host_perm(host.id)
summary['success'].append(i)
return json_response(summary)

View File

@ -72,7 +72,7 @@ class SSHConsumer(WebsocketConsumer):
def connect(self):
user = User.objects.filter(access_token=self.token).first()
if user and user.token_expired >= time.time() and user.is_active:
if user and user.token_expired >= time.time() and user.is_active and user.has_host_perm(self.id):
self.accept()
self._init()
else:

View File

@ -76,7 +76,7 @@ class HostSelector extends React.Component {
render() {
const {selectedRows} = this.state;
let data = store.records;
let data = store.permRecords;
if (store.f_name) {
data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase()))
}

View File

@ -72,7 +72,7 @@ class ComTable extends React.Component {
};
render() {
let data = store.records;
let data = store.permRecords;
if (store.f_name) {
data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase()))
}

View File

@ -9,6 +9,7 @@ import http from 'libs/http';
class Store {
@observable records = [];
@observable zones = [];
@observable permRecords = [];
@observable record = {};
@observable idMap = {};
@observable isFetching = false;
@ -22,9 +23,10 @@ class Store {
fetchRecords = () => {
this.isFetching = true;
return http.get('/api/host/')
.then(({hosts, zones}) => {
.then(({hosts, zones, perms}) => {
this.records = hosts;
this.zones = zones;
this.permRecords = hosts.filter(item => perms.includes(item.id));
for (let item of hosts) {
this.idMap[item.id] = item
}

View File

@ -12,6 +12,7 @@ import logo from 'layout/logo-spug-txt.png';
import envStore from 'pages/config/environment/store';
import appStore from 'pages/config/app/store';
import requestStore from 'pages/deploy/request/store';
import hostStore from 'pages/host/store';
class LoginIndex extends React.Component {
constructor(props) {
@ -26,7 +27,8 @@ class LoginIndex extends React.Component {
envStore.records = [];
appStore.records = [];
requestStore.records = [];
requestStore.deploys = []
requestStore.deploys = [];
hostStore.records = [];
}
handleSubmit = () => {

View File

@ -25,6 +25,7 @@ class WebSSH extends React.Component {
this.state = {
visible: false,
uploading: false,
managerDisabled: true,
host: {},
percent: 0
}
@ -71,17 +72,17 @@ class WebSSH extends React.Component {
http.get(`/api/host/?id=${this.id}`)
.then(res => {
document.title = res.name;
this.setState({host: res})
this.setState({host: res, managerDisabled: false})
})
};
render() {
const {host, visible} = this.state;
const {host, visible, managerDisabled} = this.state;
return (
<div className={styles.container}>
<div className={styles.header}>
<div>{host.name} | {host.username}@{host.hostname}:{host.port}</div>
<Button type="primary" icon="folder-open" onClick={this.handleShow}>文件管理器</Button>
<Button disabled={managerDisabled} type="primary" icon="folder-open" onClick={this.handleShow}>文件管理器</Button>
</div>
<div className={styles.terminal}>
<div ref={ref => this.container = ref}/>

View File

@ -0,0 +1,82 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the MIT License.
*/
import React from 'react';
import { observer } from 'mobx-react';
import { Modal, Form, Transfer, message, Alert } from 'antd';
import http from 'libs/http';
import hostStore from 'pages/host/store';
import store from './store';
@observer
class HostPerm extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
hosts: [],
apps: []
}
}
componentDidMount() {
if (hostStore.records.length === 0) {
hostStore.fetchRecords().then(
() => this._updateRecords(hostStore.records)
)
} else {
this._updateRecords(hostStore.records)
}
}
_updateRecords = (records) => {
const hosts = records.map(x => {
return {...x, key: x.id}
});
this.setState({hosts})
};
handleSubmit = () => {
this.setState({loading: true});
http.patch('/api/account/role/', {id: store.record.id, host_perms: store.hostPerms})
.then(res => {
message.success('操作成功');
store.hostPermVisible = false;
store.fetchRecords()
}, () => this.setState({loading: false}))
};
render() {
return (
<Modal
visible
width={800}
maskClosable={false}
title="主机权限设置"
onCancel={() => store.hostPermVisible = false}
confirmLoading={this.state.loading}
onOk={this.handleSubmit}>
<Alert
closable
showIcon
type="info"
message="小提示"
style={{width: 600, margin: '0 auto 20px', color: '#31708f !important'}}
description="主机权限将全局影响属于该角色的用户能够看到的主机。"/>
<Form.Item label="设置可访问的主机" style={{padding: '0 20px'}}>
<Transfer
listStyle={{width: 325, minHeight: 300}}
titles={['所有主机', '已选主机']}
dataSource={this.state.hosts}
targetKeys={store.hostPerms}
onChange={keys => store.hostPerms = keys}
render={item => `${item.zone} - ${item.name}`}/>
</Form.Item>
</Modal>
)
}
}
export default HostPerm

View File

@ -28,7 +28,7 @@ class ComTable extends React.Component {
ellipsis: true
}, {
title: '操作',
width: 300,
width: 400,
render: info => (
<span>
<LinkButton onClick={() => store.showForm(info)}>编辑</LinkButton>
@ -37,6 +37,8 @@ class ComTable extends React.Component {
<Divider type="vertical"/>
<LinkButton onClick={() => store.showDeployPerm(info)}>发布权限</LinkButton>
<Divider type="vertical"/>
<LinkButton onClick={() => store.showHostPerm(info)}>主机权限</LinkButton>
<Divider type="vertical"/>
<LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton>
</span>
)

View File

@ -11,6 +11,7 @@ import ComTable from './Table';
import ComForm from './Form';
import PagePerm from './PagePerm';
import DeployPerm from './DeployPerm';
import HostPerm from './HostPerm';
import store from './store';
export default observer(function () {
@ -31,6 +32,7 @@ export default observer(function () {
{store.formVisible && <ComForm/>}
{store.pagePermVisible && <PagePerm/>}
{store.deployPermVisible && <DeployPerm/>}
{store.hostPermVisible && <HostPerm/>}
</AuthCard>
)
})

View File

@ -15,10 +15,12 @@ class Store {
@observable record = {};
@observable permissions = lds.cloneDeep(codes);
@observable deployRel = {};
@observable hostPerms = [];
@observable isFetching = false;
@observable formVisible = false;
@observable pagePermVisible = false;
@observable deployPermVisible = false;
@observable hostPermVisible = false;
@observable f_name;
@ -58,6 +60,12 @@ class Store {
this.record = info;
this.deployPermVisible = true;
this.deployRel = info.deploy_perms || {}
};
showHostPerm = (info) => {
this.record = info;
this.hostPermVisible = true;
this.hostPerms = info['host_perms'] || []
}
}