feat: 系统授权 ip 支持 ip 段 (#2352)

This commit is contained in:
ssongliu 2023-09-20 12:02:20 +08:00 committed by GitHub
parent bbf2c50b25
commit a520bdbe56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 44 deletions

View file

@ -2,11 +2,13 @@ package middleware
import ( import (
"errors" "errors"
"net"
"strings" "strings"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/repo" "github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -25,7 +27,10 @@ func WhiteAllow() gin.HandlerFunc {
} }
clientIP := c.ClientIP() clientIP := c.ClientIP()
for _, ip := range strings.Split(status.Value, ",") { for _, ip := range strings.Split(status.Value, ",") {
if len(ip) != 0 && ip == clientIP { if len(ip) == 0 {
continue
}
if ip == clientIP || (strings.Contains(ip, "/") && checkIpInCidr(ip, clientIP)) {
c.Next() c.Next()
return return
} }
@ -33,3 +38,26 @@ func WhiteAllow() gin.HandlerFunc {
helper.ErrorWithDetail(c, constant.CodeErrIP, constant.ErrTypeInternalServer, errors.New("IP address not allowed")) helper.ErrorWithDetail(c, constant.CodeErrIP, constant.ErrTypeInternalServer, errors.New("IP address not allowed"))
} }
} }
func checkIpInCidr(cidr, checkIP string) bool {
ip, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
global.LOG.Errorf("parse CIDR %s failed, err: %v", cidr, err)
return false
}
for ip := ip.Mask(ipNet.Mask); ipNet.Contains(ip); incIP(ip) {
if ip.String() == checkIP {
return true
}
}
return false
}
func incIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}

View file

@ -1091,7 +1091,7 @@ const message = {
'After setting the authorized IP address, only the IP address in the setting can access the 1Panel service. Do you want to continue?', 'After setting the authorized IP address, only the IP address in the setting can access the 1Panel service. Do you want to continue?',
allowIPsHelper1: 'If the authorized IP address is empty, the authorized IP address is canceled', allowIPsHelper1: 'If the authorized IP address is empty, the authorized IP address is canceled',
allowIPEgs: allowIPEgs:
'If multiple ip authorizations exist, newlines need to be displayed. For example, \n172.16.10.111 \n172.16.10.112', 'If multiple ip authorizations exist, newlines need to be displayed. For example, \n172.16.10.111 \n172.16.10.0/24',
mfa: 'MFA', mfa: 'MFA',
secret: 'Secret', secret: 'Secret',
mfaInterval: 'Refresh interval (s)', mfaInterval: 'Refresh interval (s)',

View file

@ -1079,7 +1079,7 @@ const message = {
allowIPsHelper: '設置授權 IP 僅有設置中的 IP 可以訪問 1Panel 服務', allowIPsHelper: '設置授權 IP 僅有設置中的 IP 可以訪問 1Panel 服務',
allowIPsWarning: '設置授權 IP 僅有設置中的 IP 可以訪問 1Panel 服務是否繼續', allowIPsWarning: '設置授權 IP 僅有設置中的 IP 可以訪問 1Panel 服務是否繼續',
allowIPsHelper1: '授權 IP 為空時則取消授權 IP', allowIPsHelper1: '授權 IP 為空時則取消授權 IP',
allowIPEgs: '當存在多個授權 IP 需要換行顯示 \n172.16.10.111 \n172.16.10.112', allowIPEgs: '當存在多個授權 IP 需要換行顯示 \n172.16.10.111 \n172.16.10.0/24',
mfa: '兩步驗證', mfa: '兩步驗證',
secret: '密鑰', secret: '密鑰',
mfaAlert: '兩步驗證密碼是基於當前時間生成請確保服務器時間已同步', mfaAlert: '兩步驗證密碼是基於當前時間生成請確保服務器時間已同步',

View file

@ -1079,7 +1079,7 @@ const message = {
allowIPsHelper: '设置授权 IP 仅有设置中的 IP 可以访问 1Panel 服务', allowIPsHelper: '设置授权 IP 仅有设置中的 IP 可以访问 1Panel 服务',
allowIPsWarning: '设置授权 IP 仅有设置中的 IP 可以访问 1Panel 服务是否继续', allowIPsWarning: '设置授权 IP 仅有设置中的 IP 可以访问 1Panel 服务是否继续',
allowIPsHelper1: '授权 IP 为空时则取消授权 IP', allowIPsHelper1: '授权 IP 为空时则取消授权 IP',
allowIPEgs: '当存在多个授权 IP 需要换行显示 \n172.16.10.111 \n172.16.10.112', allowIPEgs: '当存在多个授权 IP 需要换行显示 \n172.16.10.111 \n172.16.10.0/24',
mfa: '两步验证', mfa: '两步验证',
secret: '密钥', secret: '密钥',
mfaAlert: '两步验证密码是基于当前时间生成请确保服务器时间已同步', mfaAlert: '两步验证密码是基于当前时间生成请确保服务器时间已同步',

View file

@ -4,15 +4,22 @@
<template #header> <template #header>
<DrawerHeader :header="$t('setting.allowIPs')" :back="handleClose" /> <DrawerHeader :header="$t('setting.allowIPs')" :back="handleClose" />
</template> </template>
<el-form label-position="top" @submit.prevent v-loading="loading"> <el-form
ref="formRef"
label-position="top"
@submit.prevent
:model="form"
:rules="rules"
v-loading="loading"
>
<el-row type="flex" justify="center"> <el-row type="flex" justify="center">
<el-col :span="22"> <el-col :span="22">
<el-form-item :label="$t('setting.allowIPs')"> <el-form-item :label="$t('setting.allowIPs')" prop="allowIPs">
<el-input <el-input
type="textarea" type="textarea"
:placeholder="$t('setting.allowIPEgs')" :placeholder="$t('setting.allowIPEgs')"
:autosize="{ minRows: 8, maxRows: 10 }" :autosize="{ minRows: 8, maxRows: 10 }"
v-model="allowIPs" v-model="form.allowIPs"
/> />
<span class="input-help">{{ $t('setting.allowIPsHelper1') }}</span> <span class="input-help">{{ $t('setting.allowIPsHelper1') }}</span>
</el-form-item> </el-form-item>
@ -22,7 +29,7 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button> <el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSave()"> <el-button :disabled="loading" type="primary" @click="onSave(formRef)">
{{ $t('commons.button.confirm') }} {{ $t('commons.button.confirm') }}
</el-button> </el-button>
</span> </span>
@ -31,59 +38,82 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { reactive, ref } from 'vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { updateSetting } from '@/api/modules/setting'; import { updateSetting } from '@/api/modules/setting';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox, FormInstance } from 'element-plus';
import { checkIpV4V6 } from '@/utils/util'; import { checkCidr, checkIpV4V6 } from '@/utils/util';
import DrawerHeader from '@/components/drawer-header/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue';
const emit = defineEmits<{ (e: 'search'): void }>(); const emit = defineEmits<{ (e: 'search'): void }>();
const allowIPs = ref(); const form = reactive({
allowIPs: '',
});
const rules = reactive({
allowIPs: [{ validator: checkAddress, trigger: 'blur' }],
});
function checkAddress(rule: any, value: any, callback: any) {
if (form.allowIPs !== '') {
let addrs = form.allowIPs.split('\n');
for (const item of addrs) {
if (item === '0.0.0.0') {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
if (item.indexOf('/') !== -1) {
if (checkCidr(item)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
} else {
if (checkIpV4V6(item)) {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
}
}
callback();
}
}
const formRef = ref<FormInstance>();
interface DialogProps { interface DialogProps {
allowIPs: string; allowIPs: string;
} }
const drawerVisiable = ref(); const drawerVisiable = ref();
const loading = ref(); const loading = ref();
const acceptParams = (params: DialogProps): void => { const acceptParams = (params: DialogProps): void => {
allowIPs.value = params.allowIPs; form.allowIPs = params.allowIPs;
drawerVisiable.value = true; drawerVisiable.value = true;
}; };
const onSave = async () => { const onSave = async (formEl: FormInstance | undefined) => {
if (allowIPs.value) { if (!formEl) return;
let ips = allowIPs.value.split('\n'); formEl.validate(async (valid) => {
for (const ip of ips) { if (!valid) return;
if (ip) { let title = form.allowIPs ? i18n.global.t('setting.allowIPs') : i18n.global.t('setting.unAllowIPs');
if (checkIpV4V6(ip) || ip === '0.0.0.0') { let allow = form.allowIPs
MsgError(i18n.global.t('firewall.addressFormatError')); ? i18n.global.t('setting.allowIPsWarning')
return false; : i18n.global.t('setting.unAllowIPsWarning');
} ElMessageBox.confirm(allow, title, {
} confirmButtonText: i18n.global.t('commons.button.confirm'),
} cancelButtonText: i18n.global.t('commons.button.cancel'),
} type: 'info',
let title = allowIPs.value ? i18n.global.t('setting.allowIPs') : i18n.global.t('setting.unAllowIPs'); }).then(async () => {
let allow = allowIPs.value ? i18n.global.t('setting.allowIPsWarning') : i18n.global.t('setting.unAllowIPsWarning'); loading.value = true;
ElMessageBox.confirm(allow, title, {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await updateSetting({ key: 'AllowIPs', value: allowIPs.value.replaceAll('\n', ',') }) await updateSetting({ key: 'AllowIPs', value: form.allowIPs.replaceAll('\n', ',') })
.then(() => { .then(() => {
loading.value = false; loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search'); emit('search');
handleClose(); handleClose();
}) })
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
});
}); });
}; };