diff --git a/spug_api/apps/account/models.py b/spug_api/apps/account/models.py index 820f1dc..2a9fb27 100644 --- a/spug_api/apps/account/models.py +++ b/spug_api/apps/account/models.py @@ -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 '' % self.name diff --git a/spug_api/apps/account/views.py b/spug_api/apps/account/views.py index ef5a949..afb9675 100644 --- a/spug_api/apps/account/views.py +++ b/spug_api/apps/account/views.py @@ -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) diff --git a/spug_api/apps/exec/views.py b/spug_api/apps/exec/views.py index c859bbd..a814eb3 100644 --- a/spug_api/apps/exec/views.py +++ b/spug_api/apps/exec/views.py @@ -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( diff --git a/spug_api/apps/file/views.py b/spug_api/apps/file/views.py index efb0cc6..70fef44 100644 --- a/spug_api/apps/file/views.py +++ b/spug_api/apps/file/views.py @@ -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='未找到指定主机') diff --git a/spug_api/apps/host/views.py b/spug_api/apps/host/views.py index ef8e270..6936882 100644 --- a/spug_api/apps/host/views.py +++ b/spug_api/apps/host/views.py @@ -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) diff --git a/spug_api/consumer/consumers.py b/spug_api/consumer/consumers.py index fc07321..fbc839d 100644 --- a/spug_api/consumer/consumers.py +++ b/spug_api/consumer/consumers.py @@ -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: diff --git a/spug_web/src/pages/exec/task/HostSelector.js b/spug_web/src/pages/exec/task/HostSelector.js index 05df23a..628ffb3 100644 --- a/spug_web/src/pages/exec/task/HostSelector.js +++ b/spug_web/src/pages/exec/task/HostSelector.js @@ -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())) } diff --git a/spug_web/src/pages/host/Table.js b/spug_web/src/pages/host/Table.js index b059b9d..ea6b30f 100644 --- a/spug_web/src/pages/host/Table.js +++ b/spug_web/src/pages/host/Table.js @@ -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())) } diff --git a/spug_web/src/pages/host/store.js b/spug_web/src/pages/host/store.js index 88a4273..148aa31 100644 --- a/spug_web/src/pages/host/store.js +++ b/spug_web/src/pages/host/store.js @@ -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 } diff --git a/spug_web/src/pages/login/index.js b/spug_web/src/pages/login/index.js index ac27f15..b0ab671 100644 --- a/spug_web/src/pages/login/index.js +++ b/spug_web/src/pages/login/index.js @@ -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 = () => { diff --git a/spug_web/src/pages/ssh/index.js b/spug_web/src/pages/ssh/index.js index 0f72f06..a15f9e7 100644 --- a/spug_web/src/pages/ssh/index.js +++ b/spug_web/src/pages/ssh/index.js @@ -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 (
{host.name} | {host.username}@{host.hostname}:{host.port}
- +
this.container = ref}/> diff --git a/spug_web/src/pages/system/role/HostPerm.js b/spug_web/src/pages/system/role/HostPerm.js new file mode 100644 index 0000000..9324d92 --- /dev/null +++ b/spug_web/src/pages/system/role/HostPerm.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * 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 ( + store.hostPermVisible = false} + confirmLoading={this.state.loading} + onOk={this.handleSubmit}> + + + store.hostPerms = keys} + render={item => `${item.zone} - ${item.name}`}/> + + + ) + } +} + +export default HostPerm diff --git a/spug_web/src/pages/system/role/Table.js b/spug_web/src/pages/system/role/Table.js index 05d2ec7..3b7ebd1 100644 --- a/spug_web/src/pages/system/role/Table.js +++ b/spug_web/src/pages/system/role/Table.js @@ -28,7 +28,7 @@ class ComTable extends React.Component { ellipsis: true }, { title: '操作', - width: 300, + width: 400, render: info => ( store.showForm(info)}>编辑 @@ -37,6 +37,8 @@ class ComTable extends React.Component { store.showDeployPerm(info)}>发布权限 + store.showHostPerm(info)}>主机权限 + this.handleDelete(info)}>删除 ) diff --git a/spug_web/src/pages/system/role/index.js b/spug_web/src/pages/system/role/index.js index 098e648..4fbfbcb 100644 --- a/spug_web/src/pages/system/role/index.js +++ b/spug_web/src/pages/system/role/index.js @@ -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 && } {store.pagePermVisible && } {store.deployPermVisible && } + {store.hostPermVisible && } ) }) diff --git a/spug_web/src/pages/system/role/store.js b/spug_web/src/pages/system/role/store.js index 0392000..24cf0c8 100644 --- a/spug_web/src/pages/system/role/store.js +++ b/spug_web/src/pages/system/role/store.js @@ -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'] || [] } }