feat: add tenant login IP whitelist management

This commit is contained in:
richjohnrambo
2026-09-09 09:39:55 +08:00
parent 99307f1c98
commit 964746c19b
12 changed files with 400 additions and 9 deletions
+2
View File
@@ -20,6 +20,8 @@ const (
ImUserRegisterTypeAdmin = "admin"
ImUserRegisterTypeMobile = "mobile"
ImUserRegisterTypeInviteCode = "invite_code"
ImLoginChannelApp = "app"
ImLoginChannelWeb = "web"
)
var ImUserRegisterTypeOptions = []*model.Option{
@@ -64,3 +64,16 @@ func (c *cImUserPolicyGroup) MemberRemove(ctx context.Context, req *imuserpolicy
err = service.SysImUserPolicyGroup().MemberRemove(ctx, &req.ImUserPolicyGroupMemberRemoveInp)
return
}
func (c *cImUserPolicyGroup) LoginIPWhitelist(ctx context.Context, req *imuserpolicygroup.LoginIPWhitelistReq) (res *imuserpolicygroup.LoginIPWhitelistRes, err error) {
data, err := service.SysImUserPolicyGroup().LoginIPWhitelist(ctx, &req.ImTenantLoginIPWhitelistInp)
if err != nil {
return nil, err
}
return &imuserpolicygroup.LoginIPWhitelistRes{ImTenantLoginIPWhitelistModel: data}, nil
}
func (c *cImUserPolicyGroup) LoginIPWhitelistSave(ctx context.Context, req *imuserpolicygroup.LoginIPWhitelistSaveReq) (res *imuserpolicygroup.LoginIPWhitelistSaveRes, err error) {
err = service.SysImUserPolicyGroup().LoginIPWhitelistSave(ctx, &req.ImTenantLoginIPWhitelistSaveInp)
return
}
@@ -6,6 +6,7 @@ import (
"hotgo/internal/dao"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"hotgo/internal/service"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
@@ -90,6 +91,17 @@ func (c *cConfig) ThirdParty(ctx context.Context, req *api.ThirdPartyReq) (res *
return &api.ThirdPartyRes{Values: values}, nil
}
func (c *cConfig) TenantLoginIPWhitelist(ctx context.Context, req *api.TenantLoginIPWhitelistReq) (res *api.TenantLoginIPWhitelistRes, err error) {
if _, err = getTenant(ctx, dao.ImTenant.Columns().Id, req.Id); err != nil {
return nil, err
}
data, err := service.SysImUserPolicyGroup().LoginIPWhitelistByTenant(ctx, req.Id)
if err != nil {
return nil, err
}
return &api.TenantLoginIPWhitelistRes{TenantId: data.TenantId, AppList: data.AppList, WebList: data.WebList}, nil
}
func getTenant(ctx context.Context, field string, value any) (*entity.ImTenant, error) {
var tenant *entity.ImTenant
if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).Where(field, value).Scan(&tenant); err != nil {
@@ -18,6 +18,8 @@ import (
type sSysImUserPolicyGroup struct{}
const imTenantLoginIPWhitelistTable = "im_tenant_login_ip_whitelists"
func NewSysImUserPolicyGroup() *sSysImUserPolicyGroup { return &sSysImUserPolicyGroup{} }
func init() { service.RegisterSysImUserPolicyGroup(NewSysImUserPolicyGroup()) }
@@ -288,3 +290,85 @@ func (s *sSysImUserPolicyGroup) MemberRemove(ctx context.Context, in *sysin.ImUs
_ = writeImAdminAudit(ctx, tenantID, "im_user_policy_group_member_remove", "im_user_policy_group", groupID, "success", g.Map{"user_id": row[mc.UserId].Int64()})
return publishImPolicyEvent(ctx, tenantID, tenantCode, "user_policy_group.updated", "user_policy_groups", uint(groupID))
}
func (s *sSysImUserPolicyGroup) LoginIPWhitelist(ctx context.Context, in *sysin.ImTenantLoginIPWhitelistInp) (res *sysin.ImTenantLoginIPWhitelistModel, err error) {
if err = in.Filter(ctx); err != nil {
return nil, err
}
tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId)
if err != nil {
return nil, err
}
return s.LoginIPWhitelistByTenant(ctx, tenantID)
}
func (s *sSysImUserPolicyGroup) LoginIPWhitelistByTenant(ctx context.Context, tenantID int64) (res *sysin.ImTenantLoginIPWhitelistModel, err error) {
if tenantID <= 0 {
return nil, gerror.New("租户ID不合法")
}
rows, err := imtenant.RegistryDB().Model(imTenantLoginIPWhitelistTable).Safe().Ctx(ctx).
Fields("channel", "ip_cidr").
Where("tenant_id", tenantID).
Where("status", consts.StatusEnabled).
WhereNull("deleted_at").
OrderAsc("channel").OrderAsc("id").All()
if err != nil {
return nil, gerror.Wrap(err, "获取租户登录IP白名单失败")
}
res = &sysin.ImTenantLoginIPWhitelistModel{
TenantId: tenantID,
AppList: make([]string, 0),
WebList: make([]string, 0),
}
for _, row := range rows {
switch row["channel"].String() {
case consts.ImLoginChannelApp:
res.AppList = append(res.AppList, row["ip_cidr"].String())
case consts.ImLoginChannelWeb:
res.WebList = append(res.WebList, row["ip_cidr"].String())
}
}
return res, nil
}
func (s *sSysImUserPolicyGroup) LoginIPWhitelistSave(ctx context.Context, in *sysin.ImTenantLoginIPWhitelistSaveInp) (err error) {
if err = in.Filter(ctx); err != nil {
return err
}
tenantID, _, err := resolveImPolicyTenant(ctx, in.TenantId)
if err != nil {
return err
}
now := gtime.Now()
operatorID := contexts.GetUserId(ctx)
db := imtenant.RegistryDB()
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if _, txErr := tx.Model(imTenantLoginIPWhitelistTable).Safe().Ctx(ctx).
Where("tenant_id", tenantID).WhereNull("deleted_at").
Data(g.Map{"status": consts.StatusDisable, "deleted_at": now, "deleted_by": operatorID, "updated_at": now, "updated_by": operatorID}).Update(); txErr != nil {
return txErr
}
for channel, values := range map[string][]string{
consts.ImLoginChannelApp: in.AppList,
consts.ImLoginChannelWeb: in.WebList,
} {
for _, value := range values {
if _, txErr := tx.Exec(`INSERT INTO im_tenant_login_ip_whitelists
(tenant_id,channel,ip_cidr,status,created_by,updated_by)
VALUES(?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE status=VALUES(status),deleted_at=NULL,deleted_by=NULL,updated_at=CURRENT_TIMESTAMP(3),updated_by=VALUES(updated_by)`,
tenantID, channel, value, consts.StatusEnabled, operatorID, operatorID); txErr != nil {
return txErr
}
}
}
return nil
})
if err != nil {
return gerror.Wrap(err, "保存租户登录IP白名单失败")
}
_ = writeImAdminAudit(ctx, tenantID, "im_tenant_login_ip_whitelist_save", "im_tenant", tenantID, "success", g.Map{
"app_count": len(in.AppList), "web_count": len(in.WebList),
})
return nil
}
@@ -143,3 +143,21 @@ func TestImUserPolicyGroupInputFilter(t *testing.T) {
t.Fatal("无效用户ID应被拒绝")
}
}
func TestImTenantLoginIPWhitelistSaveFilter(t *testing.T) {
in := &ImTenantLoginIPWhitelistSaveInp{
TenantId: 1,
AppList: []string{" 203.0.113.10 ", "2001:db8::/32", "203.0.113.10"},
WebList: []string{"198.51.100.7/24"},
}
if err := in.Filter(context.Background()); err != nil {
t.Fatalf("合法租户登录白名单不应报错: %v", err)
}
if len(in.AppList) != 2 || in.AppList[0] != "203.0.113.10" || in.WebList[0] != "198.51.100.0/24" {
t.Fatalf("IP白名单未正确规范化: app=%v web=%v", in.AppList, in.WebList)
}
in.AppList = []string{"not-an-ip"}
if err := in.Filter(context.Background()); err == nil {
t.Fatal("非法IP地址应被拒绝")
}
}
@@ -2,6 +2,7 @@ package sysin
import (
"context"
"net/netip"
"regexp"
"strings"
@@ -155,3 +156,66 @@ type ImUserPolicyGroupMemberRemoveInp struct {
}
func (in *ImUserPolicyGroupMemberRemoveInp) Filter(ctx context.Context) error { return nil }
type ImTenantLoginIPWhitelistInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
}
func (in *ImTenantLoginIPWhitelistInp) Filter(ctx context.Context) error {
if in.TenantId < 0 {
return gerror.New("租户ID不合法")
}
return nil
}
type ImTenantLoginIPWhitelistSaveInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
AppList []string `json:"appList" dc:"APP登录IP白名单"`
WebList []string `json:"webList" dc:"Web登录IP白名单"`
}
func (in *ImTenantLoginIPWhitelistSaveInp) Filter(ctx context.Context) (err error) {
if in.TenantId < 0 {
return gerror.New("租户ID不合法")
}
if in.AppList, err = normalizeTenantLoginIPList(in.AppList); err != nil {
return gerror.Wrap(err, "APP登录白名单不合法")
}
if in.WebList, err = normalizeTenantLoginIPList(in.WebList); err != nil {
return gerror.Wrap(err, "Web登录白名单不合法")
}
return nil
}
type ImTenantLoginIPWhitelistModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
AppList []string `json:"appList" dc:"APP登录IP白名单"`
WebList []string `json:"webList" dc:"Web登录IP白名单"`
}
func normalizeTenantLoginIPList(values []string) ([]string, error) {
if len(values) > 500 {
return nil, gerror.New("单个渠道最多配置500条IP或CIDR")
}
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if prefix, err := netip.ParsePrefix(value); err == nil {
value = prefix.Masked().String()
} else if address, addressErr := netip.ParseAddr(value); addressErr == nil {
value = address.String()
} else {
return nil, gerror.Newf("%s 不是有效的IPv4、IPv6或CIDR", value)
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result, nil
}
+3
View File
@@ -377,6 +377,9 @@ type (
MemberList(ctx context.Context, in *sysin.ImUserPolicyGroupMemberListInp) (list []*sysin.ImUserPolicyGroupMemberModel, totalCount int, err error)
MemberAdd(ctx context.Context, in *sysin.ImUserPolicyGroupMemberAddInp) (err error)
MemberRemove(ctx context.Context, in *sysin.ImUserPolicyGroupMemberRemoveInp) (err error)
LoginIPWhitelist(ctx context.Context, in *sysin.ImTenantLoginIPWhitelistInp) (res *sysin.ImTenantLoginIPWhitelistModel, err error)
LoginIPWhitelistByTenant(ctx context.Context, tenantId int64) (res *sysin.ImTenantLoginIPWhitelistModel, err error)
LoginIPWhitelistSave(ctx context.Context, in *sysin.ImTenantLoginIPWhitelistSaveInp) (err error)
}
ISysImSensitiveWord interface {
List(ctx context.Context, in *sysin.ImSensitiveWordListInp) (list []*sysin.ImSensitiveWordListModel, totalCount int, err error)