Merge pull request #104 from lanrenwo/main

新增用户策略功能
This commit is contained in:
bjdgyc 2022-06-14 14:19:17 +08:00 committed by GitHub
commit 25cb1fc19a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 693 additions and 2 deletions

View File

@ -0,0 +1,98 @@
package admin
import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"github.com/bjdgyc/anylink/dbdata"
)
func PolicyList(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
pageS := r.FormValue("page")
page, _ := strconv.Atoi(pageS)
if page < 1 {
page = 1
}
var pageSize = dbdata.PageSize
count := dbdata.CountAll(&dbdata.Policy{})
var datas []dbdata.Policy
err := dbdata.Find(&datas, pageSize, page)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
data := map[string]interface{}{
"count": count,
"page_size": pageSize,
"datas": datas,
}
RespSucess(w, data)
}
func PolicyDetail(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
idS := r.FormValue("id")
id, _ := strconv.Atoi(idS)
if id < 1 {
RespError(w, RespParamErr, "Id错误")
return
}
var data dbdata.Policy
err := dbdata.One("Id", id, &data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, data)
}
func PolicySet(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
defer r.Body.Close()
v := &dbdata.Policy{}
err = json.Unmarshal(body, v)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
err = dbdata.SetPolicy(v)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, nil)
}
func PolicyDel(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
idS := r.FormValue("id")
id, _ := strconv.Atoi(idS)
if id < 1 {
RespError(w, RespParamErr, "Id错误")
return
}
data := dbdata.Policy{Id: id}
err := dbdata.Del(&data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, nil)
}

View File

@ -52,6 +52,10 @@ func StartAdmin() {
r.HandleFunc("/user/ip_map/detail", UserIpMapDetail)
r.HandleFunc("/user/ip_map/set", UserIpMapSet)
r.HandleFunc("/user/ip_map/del", UserIpMapDel)
r.HandleFunc("/user/policy/list", PolicyList)
r.HandleFunc("/user/policy/detail", PolicyDetail)
r.HandleFunc("/user/policy/set", PolicySet)
r.HandleFunc("/user/policy/del", PolicyDel)
r.HandleFunc("/group/list", GroupList)
r.HandleFunc("/group/names", GroupNames)

View File

@ -25,7 +25,7 @@ func initDb() {
}
// 初始化数据库
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{})
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{})
if err != nil {
base.Fatal(err)
}

View File

@ -161,7 +161,7 @@ func SetGroup(g *Group) error {
} else {
_, ok := authRegistry[authType]
if !ok {
return errors.New("未知的认证方式: " + fmt.Sprintf("%s", g.Auth["type"]))
return errors.New("未知的认证方式: " + authType)
}
auth := makeInstance(authType).(IUserAuth)
err = auth.checkData(g.Auth)

101
server/dbdata/policy.go Normal file
View File

@ -0,0 +1,101 @@
package dbdata
import (
"errors"
"net"
"strings"
"time"
)
func GetPolicy(Username string) *Policy {
policyData := &Policy{}
err := One("Username", Username, policyData)
if err != nil {
return policyData
}
return policyData
}
func SetPolicy(p *Policy) error {
var err error
if p.Username == "" {
return errors.New("用户名错误")
}
// 包含路由
routeInclude := []ValData{}
for _, v := range p.RouteInclude {
if v.Val != "" {
if v.Val == All {
routeInclude = append(routeInclude, v)
continue
}
ipMask, _, err := parseIpNet(v.Val)
if err != nil {
return errors.New("RouteInclude 错误" + err.Error())
}
v.IpMask = ipMask
routeInclude = append(routeInclude, v)
}
}
p.RouteInclude = routeInclude
// 排除路由
routeExclude := []ValData{}
for _, v := range p.RouteExclude {
if v.Val != "" {
ipMask, _, err := parseIpNet(v.Val)
if err != nil {
return errors.New("RouteExclude 错误" + err.Error())
}
v.IpMask = ipMask
routeExclude = append(routeExclude, v)
}
}
p.RouteExclude = routeExclude
// DNS 判断
clientDns := []ValData{}
for _, v := range p.ClientDns {
if v.Val != "" {
ip := net.ParseIP(v.Val)
if ip.String() != v.Val {
return errors.New("DNS IP 错误")
}
clientDns = append(clientDns, v)
}
}
if len(routeInclude) == 0 || (len(routeInclude) == 1 && routeInclude[0].Val == "all") {
if len(clientDns) == 0 {
return errors.New("默认路由必须设置一个DNS")
}
}
p.ClientDns = clientDns
// 域名拆分隧道,不能同时填写
p.DsIncludeDomains = strings.TrimSpace(p.DsIncludeDomains)
p.DsExcludeDomains = strings.TrimSpace(p.DsExcludeDomains)
if p.DsIncludeDomains != "" && p.DsExcludeDomains != "" {
return errors.New("包含/排除域名不能同时填写")
}
// 校验包含域名的格式
err = CheckDomainNames(p.DsIncludeDomains)
if err != nil {
return errors.New("包含域名有误:" + err.Error())
}
// 校验排除域名的格式
err = CheckDomainNames(p.DsExcludeDomains)
if err != nil {
return errors.New("排除域名有误:" + err.Error())
}
p.UpdatedAt = time.Now()
if p.Id > 0 {
err = Set(p)
} else {
err = Add(p)
}
return err
}

View File

@ -0,0 +1,45 @@
package dbdata
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetPolicy(t *testing.T) {
ast := assert.New(t)
preIpData()
defer closeIpdata()
// 添加 Policy
p1 := Policy{Username: "a1", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsExcludeDomains: "baidu.com,163.com"}
err := SetPolicy(&p1)
ast.Nil(err)
p2 := Policy{Username: "a2", ClientDns: []ValData{{Val: "114.114.114.114"}}, DsExcludeDomains: "com.cn,qq.com"}
err = SetPolicy(&p2)
ast.Nil(err)
route := []ValData{{Val: "192.168.1.1/24"}}
p3 := Policy{Username: "a3", ClientDns: []ValData{{Val: "114.114.114.114"}}, RouteInclude: route, DsExcludeDomains: "com.cn,qq.com"}
err = SetPolicy(&p3)
ast.Nil(err)
// 判断 IpMask
ast.Equal(p3.RouteInclude[0].IpMask, "192.168.1.1/255.255.255.0")
route2 := []ValData{{Val: "192.168.2.1/24"}}
p4 := Policy{Username: "a4", ClientDns: []ValData{{Val: "114.114.114.114"}}, RouteExclude: route2, DsIncludeDomains: "com.cn,qq.com"}
err = SetPolicy(&p4)
ast.Nil(err)
// 判断 IpMask
ast.Equal(p4.RouteExclude[0].IpMask, "192.168.2.1/255.255.255.0")
// 判断所有数据
var userPolicy *Policy
pAll := []string{"a1", "a2", "a3", "a4"}
for _, v := range pAll {
userPolicy = GetPolicy(v)
ast.NotEqual(userPolicy.Id, 0, "user policy id is zero")
}
}

View File

@ -68,3 +68,17 @@ type AccessAudit struct {
DstPort uint16 `json:"dst_port" xorm:"not null"`
CreatedAt time.Time `json:"created_at" xorm:"DateTime"`
}
type Policy struct {
Id int `json:"id" xorm:"pk autoincr not null"`
Username string `json:"username" xorm:"varchar(60) not null unique"`
AllowLan bool `json:"allow_lan" xorm:"Bool"`
ClientDns []ValData `json:"client_dns" xorm:"Text"`
RouteInclude []ValData `json:"route_include" xorm:"Text"`
RouteExclude []ValData `json:"route_exclude" xorm:"Text"`
DsExcludeDomains string `json:"ds_exclude_domains" xorm:"Text"`
DsIncludeDomains string `json:"ds_include_domains" xorm:"Text"`
Status int8 `json:"status" xorm:"Int"` // 1正常 0 禁用
CreatedAt time.Time `json:"created_at" xorm:"DateTime created"`
UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
}

View File

@ -58,4 +58,12 @@ func TestCheckUser(t *testing.T) {
ast.Equal("aaa Radius服务器连接异常, 请检测服务器和端口", err.Error())
}
// 添加用户策略
dns2 := []ValData{{Val: "8.8.8.8"}}
route2 := []ValData{{Val: "192.168.2.1/24"}}
p1 := Policy{Username: "aaa", Status: 1, ClientDns: dns2, RouteInclude: route2}
err = SetPolicy(&p1)
ast.Nil(err)
err = CheckUser("aaa", u.PinCode, group)
ast.Nil(err)
}

View File

@ -100,6 +100,9 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
//HttpSetHeader(w, "X-CSTP-Default-Domain", cSess.LocalIp)
HttpSetHeader(w, "X-CSTP-Base-MTU", cstpBaseMtu)
// 设置用户策略
SetUserPolicy(sess.Username, cSess.Group)
// 允许本地LAN访问vpn网络必须放在路由的第一个
if cSess.Group.AllowLan {
HttpSetHeader(w, "X-CSTP-Split-Exclude", "0.0.0.0/255.255.255.255")
@ -209,3 +212,17 @@ func SetPostAuthXml(g *dbdata.Group, w http.ResponseWriter) error {
HttpSetHeader(w, "X-CSTP-Post-Auth-XML", result.String())
return nil
}
// 设置用户策略, 覆盖Group的属性值
func SetUserPolicy(username string, g *dbdata.Group) {
userPolicy := dbdata.GetPolicy(username)
if userPolicy.Id != 0 && userPolicy.Status == 1 {
base.Debug(username + " use UserPolicy")
g.AllowLan = userPolicy.AllowLan
g.ClientDns = userPolicy.ClientDns
g.RouteInclude = userPolicy.RouteInclude
g.RouteExclude = userPolicy.RouteExclude
g.DsExcludeDomains = userPolicy.DsExcludeDomains
g.DsIncludeDomains = userPolicy.DsIncludeDomains
}
}

View File

@ -42,6 +42,7 @@
</template>
<el-menu-item index="/admin/user/list">用户列表</el-menu-item>
<el-menu-item index="/admin/user/policy">用户策略</el-menu-item>
<el-menu-item index="/admin/user/online">在线用户</el-menu-item>
<el-menu-item index="/admin/user/ip_map">IP映射</el-menu-item>
</el-submenu>

View File

@ -0,0 +1,402 @@
<template>
<div>
<el-card>
<el-form :inline="true">
<el-form-item>
<el-button
size="small"
type="primary"
icon="el-icon-plus"
@click="handleEdit('')">添加
</el-button>
</el-form-item>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
border>
<el-table-column
sortable="true"
prop="id"
label="ID"
width="60">
</el-table-column>
<el-table-column
prop="username"
label="用户名">
</el-table-column>
<el-table-column
prop="allow_lan"
label="本地网络">
<template slot-scope="scope">
<el-switch
v-model="scope.row.allow_lan"
disabled>
</el-switch>
</template>
</el-table-column>
<el-table-column
prop="client_dns"
label="客户端DNS"
width="160">
<template slot-scope="scope">
<el-row v-for="(item,inx) in scope.row.client_dns" :key="inx">{{ item.val }}</el-row>
</template>
</el-table-column>
<el-table-column
prop="route_include"
label="包含路由"
width="200">
<template slot-scope="scope">
<el-row v-for="(item,inx) in scope.row.route_include.slice(0, 5)" :key="inx">{{ item.val }}</el-row>
<el-row v-if="scope.row.route_include.length > 5">...</el-row>
</template>
</el-table-column>
<el-table-column
prop="route_exclude"
label="排除路由"
width="200">
<template slot-scope="scope">
<el-row v-for="(item,inx) in scope.row.route_exclude.slice(0, 5)" :key="inx">{{ item.val }}</el-row>
<el-row v-if="scope.row.route_exclude.length > 5">...</el-row>
</template>
</el-table-column>
<el-table-column
prop="status"
label="状态"
width="70">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 1" type="success">可用</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column
prop="updated_at"
label="更新时间"
:formatter="tableDateFormat">
</el-table-column>
<el-table-column
label="操作"
width="150">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="handleEdit(scope.row)">编辑
</el-button>
<el-popconfirm
style="margin-left: 10px"
@confirm="handleDel(scope.row)"
title="确定要删除用户策略项吗?">
<el-button
slot="reference"
size="mini"
type="danger">删除
</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination
background
layout="prev, pager, next"
:pager-count="11"
@current-change="pageChange"
:current-page="page"
:total="count">
</el-pagination>
</el-card>
<!--新增修改弹出框-->
<el-dialog
:close-on-click-modal="false"
title="用户策略"
:visible.sync="user_edit_dialog"
width="750px"
top="50px"
center>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
<el-tabs v-model="activeTab">
<el-tab-pane label="通用" name="general">
<el-form-item label="ID" prop="id">
<el-input v-model="ruleForm.id" disabled></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username" :disabled="ruleForm.id > 0"></el-input>
</el-form-item>
<el-form-item label="本地网络" prop="allow_lan">
<el-switch
v-model="ruleForm.allow_lan">
</el-switch>
</el-form-item>
<el-form-item label="客户端DNS" prop="client_dns">
<el-row class="msg-info">
<el-col :span="20">输入IP格式如: 192.168.0.10</el-col>
<el-col :span="4">
<el-button size="mini" type="success" icon="el-icon-plus" circle
@click.prevent="addDomain(ruleForm.client_dns)"></el-button>
</el-col>
</el-row>
<el-row v-for="(item,index) in ruleForm.client_dns"
:key="index" style="margin-bottom: 5px" :gutter="10">
<el-col :span="10">
<el-input v-model="item.val"></el-input>
</el-col>
<el-col :span="12">
<el-input v-model="item.note" placeholder="备注"></el-input>
</el-col>
<el-col :span="2">
<el-button size="mini" type="danger" icon="el-icon-minus" circle
@click.prevent="removeDomain(ruleForm.client_dns,index)"></el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="ruleForm.status">
<el-radio :label="1" border>启用</el-radio>
<el-radio :label="0" border>停用</el-radio>
</el-radio-group>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="路由设置" name="route">
<el-form-item label="包含路由" prop="route_include">
<el-row class="msg-info">
<el-col :span="20">输入CIDR格式如: 192.168.1.0/24</el-col>
<el-col :span="4">
<el-button size="mini" type="success" icon="el-icon-plus" circle
@click.prevent="addDomain(ruleForm.route_include)"></el-button>
</el-col>
</el-row>
<el-row v-for="(item,index) in ruleForm.route_include"
:key="index" style="margin-bottom: 5px" :gutter="10">
<el-col :span="10">
<el-input v-model="item.val"></el-input>
</el-col>
<el-col :span="12">
<el-input v-model="item.note" placeholder="备注"></el-input>
</el-col>
<el-col :span="2">
<el-button size="mini" type="danger" icon="el-icon-minus" circle
@click.prevent="removeDomain(ruleForm.route_include,index)"></el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="排除路由" prop="route_exclude">
<el-row class="msg-info">
<el-col :span="20">输入CIDR格式如: 192.168.2.0/24</el-col>
<el-col :span="4">
<el-button size="mini" type="success" icon="el-icon-plus" circle
@click.prevent="addDomain(ruleForm.route_exclude)"></el-button>
</el-col>
</el-row>
<el-row v-for="(item,index) in ruleForm.route_exclude"
:key="index" style="margin-bottom: 5px" :gutter="10">
<el-col :span="10">
<el-input v-model="item.val"></el-input>
</el-col>
<el-col :span="12">
<el-input v-model="item.note" placeholder="备注"></el-input>
</el-col>
<el-col :span="2">
<el-button size="mini" type="danger" icon="el-icon-minus" circle
@click.prevent="removeDomain(ruleForm.route_exclude,index)"></el-button>
</el-col>
</el-row>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="动态拆分隧道" name="ds_domains">
<el-form-item label="包含域名" prop="ds_include_domains">
<el-input type="textarea" :rows="5" v-model="ruleForm.ds_include_domains"></el-input>
</el-form-item>
<el-form-item label="排除域名" prop="ds_exclude_domains">
<el-input type="textarea" :rows="5" v-model="ruleForm.ds_exclude_domains"></el-input>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
<el-button @click="disVisible">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "Policy",
components: {},
mixins: [],
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['用户信息', '用户策略'])
},
mounted() {
this.getData(1)
},
data() {
return {
page: 1,
tableData: [],
count: 10,
activeTab : "general",
ruleForm: {
bandwidth: 0,
status: 1,
allow_lan: true,
client_dns: [{val: '114.114.114.114'}],
route_include: [{val: 'all', note: '默认全局代理'}],
route_exclude: [],
re_upper_limit : 0,
},
rules: {
name: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
],
bandwidth: [
{required: true, message: '请输入带宽限制', trigger: 'blur'},
{type: 'number', message: '带宽必须为数字值'}
],
status: [
{required: true}
],
},
}
},
methods: {
handleDel(row) {
axios.post('/user/policy/del?id=' + row.id).then(resp => {
const rdata = resp.data;
if (rdata.code === 0) {
this.$message.success(rdata.msg);
this.getData(1);
} else {
this.$message.error(rdata.msg);
}
console.log(rdata);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
handleEdit(row) {
!this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
console.log(row)
this.activeTab = "general"
this.user_edit_dialog = true
if (!row) {
return;
}
axios.get('/user/policy/detail', {
params: {
id: row.id,
}
}).then(resp => {
this.ruleForm = resp.data.data
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
pageChange(p) {
this.getData(p)
},
getData(page) {
this.page = page
axios.get('/user/policy/list', {
params: {
page: page,
}
}).then(resp => {
const rdata = resp.data.data;
console.log(rdata);
this.tableData = rdata.datas;
this.count = rdata.count
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
removeDomain(arr, index) {
console.log(index)
if (index >= 0 && index < arr.length) {
arr.splice(index, 1)
}
// let index = arr.indexOf(item);
// if (index !== -1 && arr.length > 1) {
// arr.splice(index, 1)
// }
// arr.pop()
},
addDomain(arr) {
arr.push({val: "", action: "allow", port: 0});
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
console.log('error submit!!');
return false;
}
axios.post('/user/policy/set', this.ruleForm).then(resp => {
const rdata = resp.data;
if (rdata.code === 0) {
this.$message.success(rdata.msg);
this.getData(1);
this.user_edit_dialog = false
} else {
this.$message.error(rdata.msg);
}
console.log(rdata);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
}
</script>
<style scoped>
.msg-info {
background-color: #f4f4f5;
color: #909399;
padding: 0 5px;
margin: 0;
box-sizing: border-box;
border-radius: 4px;
font-size: 12px;
}
.el-select {
width: 80px;
}
</style>

View File

@ -20,6 +20,7 @@ const routes = [
{path: 'set/audit', component: () => import('@/pages/set/Audit')},
{path: 'user/list', component: () => import('@/pages/user/List')},
{path: 'user/policy', component: () => import('@/pages/user/Policy')},
{path: 'user/online', component: () => import('@/pages/user/Online')},
{path: 'user/ip_map', component: () => import('@/pages/user/IpMap')},