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', []) perms.setdefault('envs', [])
return perms 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): def has_perms(self, codes):
# return self.is_supper or self.role in codes # return self.is_supper or self.role in codes
return self.is_supper return self.is_supper
@ -68,6 +77,7 @@ class Role(models.Model, ModelMixin):
desc = models.CharField(max_length=255, null=True) desc = models.CharField(max_length=255, null=True)
page_perms = models.TextField(null=True) page_perms = models.TextField(null=True)
deploy_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_at = models.CharField(max_length=20, default=human_datetime)
created_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='+') 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 = super().to_dict(*args, **kwargs)
tmp['page_perms'] = json.loads(self.page_perms) if self.page_perms else None 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['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() tmp['used'] = self.user_set.count()
return tmp return tmp
@ -85,6 +96,12 @@ class Role(models.Model, ModelMixin):
self.deploy_perms = json.dumps(perms) self.deploy_perms = json.dumps(perms)
self.save() 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): def __repr__(self):
return '<Role name=%r>' % self.name return '<Role name=%r>' % self.name

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ from apps.host.models import Host
from apps.app.models import Deploy from apps.app.models import Deploy
from apps.schedule.models import Task from apps.schedule.models import Task
from apps.monitor.models import Detection from apps.monitor.models import Detection
from apps.account.models import Role
from libs.ssh import SSH, AuthenticationException from libs.ssh import SSH, AuthenticationException
from libs import human_datetime, AttrDict from libs import human_datetime, AttrDict
from openpyxl import load_workbook from openpyxl import load_workbook
@ -18,10 +19,13 @@ class HostView(View):
def get(self, request): def get(self, request):
host_id = request.GET.get('id') host_id = request.GET.get('id')
if host_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)) return json_response(Host.objects.get(pk=host_id))
hosts = Host.objects.filter(deleted_by_id__isnull=True) hosts = Host.objects.filter(deleted_by_id__isnull=True)
zones = [x['zone'] for x in hosts.order_by('zone').values('zone').distinct()] 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): def post(self, request):
form, error = JsonParser( form, error = JsonParser(
@ -43,7 +47,9 @@ class HostView(View):
elif Host.objects.filter(name=form.name, deleted_by_id__isnull=True).exists(): elif Host.objects.filter(name=form.name, deleted_by_id__isnull=True).exists():
return json_response(error=f'已存在的主机名称【{form.name}') return json_response(error=f'已存在的主机名称【{form.name}')
else: 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) return json_response(error=error)
def patch(self, request): def patch(self, request):
@ -76,6 +82,9 @@ class HostView(View):
detection = Detection.objects.filter(type__in=('3', '4'), addr=form.id).first() detection = Detection.objects.filter(type__in=('3', '4'), addr=form.id).first()
if detection: if detection:
return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机') 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( Host.objects.filter(pk=form.id).update(
deleted_at=human_datetime(), deleted_at=human_datetime(),
deleted_by=request.user, 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: if valid_ssh(data.hostname, data.port, data.username, data.pop('password') or password) is False:
summary['fail'].append(i) summary['fail'].append(i)
continue 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) summary['success'].append(i)
return json_response(summary) return json_response(summary)

View File

@ -72,7 +72,7 @@ class SSHConsumer(WebsocketConsumer):
def connect(self): def connect(self):
user = User.objects.filter(access_token=self.token).first() 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.accept()
self._init() self._init()
else: else:

View File

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

View File

@ -72,7 +72,7 @@ class ComTable extends React.Component {
}; };
render() { render() {
let data = store.records; let data = store.permRecords;
if (store.f_name) { if (store.f_name) {
data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase())) 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 { class Store {
@observable records = []; @observable records = [];
@observable zones = []; @observable zones = [];
@observable permRecords = [];
@observable record = {}; @observable record = {};
@observable idMap = {}; @observable idMap = {};
@observable isFetching = false; @observable isFetching = false;
@ -22,9 +23,10 @@ class Store {
fetchRecords = () => { fetchRecords = () => {
this.isFetching = true; this.isFetching = true;
return http.get('/api/host/') return http.get('/api/host/')
.then(({hosts, zones}) => { .then(({hosts, zones, perms}) => {
this.records = hosts; this.records = hosts;
this.zones = zones; this.zones = zones;
this.permRecords = hosts.filter(item => perms.includes(item.id));
for (let item of hosts) { for (let item of hosts) {
this.idMap[item.id] = item 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 envStore from 'pages/config/environment/store';
import appStore from 'pages/config/app/store'; import appStore from 'pages/config/app/store';
import requestStore from 'pages/deploy/request/store'; import requestStore from 'pages/deploy/request/store';
import hostStore from 'pages/host/store';
class LoginIndex extends React.Component { class LoginIndex extends React.Component {
constructor(props) { constructor(props) {
@ -26,7 +27,8 @@ class LoginIndex extends React.Component {
envStore.records = []; envStore.records = [];
appStore.records = []; appStore.records = [];
requestStore.records = []; requestStore.records = [];
requestStore.deploys = [] requestStore.deploys = [];
hostStore.records = [];
} }
handleSubmit = () => { handleSubmit = () => {

View File

@ -25,6 +25,7 @@ class WebSSH extends React.Component {
this.state = { this.state = {
visible: false, visible: false,
uploading: false, uploading: false,
managerDisabled: true,
host: {}, host: {},
percent: 0 percent: 0
} }
@ -71,17 +72,17 @@ class WebSSH extends React.Component {
http.get(`/api/host/?id=${this.id}`) http.get(`/api/host/?id=${this.id}`)
.then(res => { .then(res => {
document.title = res.name; document.title = res.name;
this.setState({host: res}) this.setState({host: res, managerDisabled: false})
}) })
}; };
render() { render() {
const {host, visible} = this.state; const {host, visible, managerDisabled} = this.state;
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
<div>{host.name} | {host.username}@{host.hostname}:{host.port}</div> <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>
<div className={styles.terminal}> <div className={styles.terminal}>
<div ref={ref => this.container = ref}/> <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 ellipsis: true
}, { }, {
title: '操作', title: '操作',
width: 300, width: 400,
render: info => ( render: info => (
<span> <span>
<LinkButton onClick={() => store.showForm(info)}>编辑</LinkButton> <LinkButton onClick={() => store.showForm(info)}>编辑</LinkButton>
@ -37,6 +37,8 @@ class ComTable extends React.Component {
<Divider type="vertical"/> <Divider type="vertical"/>
<LinkButton onClick={() => store.showDeployPerm(info)}>发布权限</LinkButton> <LinkButton onClick={() => store.showDeployPerm(info)}>发布权限</LinkButton>
<Divider type="vertical"/> <Divider type="vertical"/>
<LinkButton onClick={() => store.showHostPerm(info)}>主机权限</LinkButton>
<Divider type="vertical"/>
<LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton> <LinkButton onClick={() => this.handleDelete(info)}>删除</LinkButton>
</span> </span>
) )

View File

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

View File

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