mirror of https://github.com/openspug/spug
A 角色管理新增主机权限控制功能
parent
4fdafabbed
commit
0c1336aa88
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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='未找到指定主机')
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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}/>
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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'] || []
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue