Files
req/resx/res_mask_field.go
what 09f4c40d67 新增 resx 包剩余实现:字段/查询字段/脱敏/变更行等类型及测试
res_field.go/res_field_option.go(字段定义)、res_query_field.go/res_query_field_option.go(查询字段)、res_change_row.go(变更行)、res_mask_field.go(字段脱敏标记)及对应测试文件,配合此前已提交的 resource.go/res_interceptor.go 组成完整的 resx 资源实现包。
2026-07-22 09:11:50 +08:00

84 lines
4.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package resx
import (
"fmt"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/req"
)
// MaskField 用 "table.col" 或裸列名 "col" 标记一个列需要按对应 req.ResField 的 Roles 做字段级
// 权限脱敏。dataProcessor.maskSelect 只处理带这个标记的列,未标记的列(包括看起来像同名字段的
// 普通列)一律不处理——标记是显式的,不靠猜。
//
// 不带表名的裸列名(比如 "salary")表示当前资源自己的字段;带表名的(比如 "Department.salary"
// 里的 table 部分通常是查询里用的别名(当前资源自己的默认别名,或者 JOIN 进来的资源用的别名)。
// 这里不会立即去查是哪个资源、哪个字段:MaskField 被调用的时候,SELECT 列表所在的 SelectDataset
// 可能还没拼完(FROM/JOIN 子句可能还没加上去),而且别名也不一定等于资源自己的 code(比如同一个
// 资源自连接查询,两个别名不可能都等于资源自己的 code)——这两种情况都得等查询真正执行前
// dataProcessor.maskSelect,在 exec.Hooks.Before 里跑,那时候才有完整的 FROM/JOIN 子句)才能
// 正确解析出别名对应的真实表名,再去 container 里的 req.GetResource 按 code/table/uuid 查资源、
// 按 col 查字段。
//
// 用法:resx.MaskField("salary").As("salary")、resx.MaskField("Department.salary").As("salary")
func MaskField(ident string) exp.IdentifierExpression {
parsed := exp.ParseIdentifier(ident)
code, isStringCode := parsed.GetCol().(string)
if !isStringCode || code == "" {
panic(fmt.Errorf("resx.MaskField: ident %q 不是合法的列名", ident))
}
return lazyMaskedIdent{IdentifierExpression: parsed, codeResource: parsed.GetTable(), code: code}
}
// lazyMaskResolver 是 lazyMaskedIdent/lazyMaskedAliased 共用的类型断言接口:dataProcessor.maskSelect
// 靠它从 SELECT 列表里挑出被 MaskField 标记过、还没解析成具体 req.ResField 的列。codeResource 为
// 空表示裸列名,对应当前资源自己的字段。
//
// 这里特意不叫 aliasMaskField("table.col") 里 table 这部分,语义上对应的是
// req.ResField.GetCodeResource()(哪个资源),不是 SQL 里"起别名"的 alias(比如 .As(...) 里的
// 输出列名)——这个包里两种"alias"都会出现(resolveJoinTable 是在拿它去 FROM/JOIN 子句里当 SQL
// 别名搜,那里用 alias 这个词没问题;但站在 MaskField 被构造出来这一刻,它就是资源标识,叫
// codeResource 才不会跟 .As(...) 那个"别名"混在一起)。
type lazyMaskResolver interface {
lazyMaskField() (codeResource, code string)
}
// lazyMaskedIdent 包装一个 exp.IdentifierExpression,同时打上"待解析"标记(只有资源标识+字段
// code,还没查出 req.ResField)。除 As() 外的其它方法(Eq()/In()/Table() 等)都是内嵌接口自动
// 透传,不用手写。
type lazyMaskedIdent struct {
exp.IdentifierExpression
codeResource string
code string
}
func (m lazyMaskedIdent) lazyMaskField() (string, string) { return m.codeResource, m.code }
// As 必须单独实现:IdentifierExpression.As() 返回的是 exp.AliasedExpression,是另一个类型,
// 如果不单独处理,直接透传内嵌值的 As() 会丢掉标记(调用方几乎总会紧跟着调用 As() 给列起别名)。
func (m lazyMaskedIdent) As(val interface{}) exp.AliasedExpression {
return lazyMaskedAliased{AliasedExpression: m.IdentifierExpression.As(val), codeResource: m.codeResource, code: m.code}
}
// lazyMaskedAliased 包装一个 exp.AliasedExpressionMaskField(...).As(...) 链式调用之后的结果),
// 同时保留"待解析"标记
type lazyMaskedAliased struct {
exp.AliasedExpression
codeResource string
code string
}
func (m lazyMaskedAliased) lazyMaskField() (string, string) { return m.codeResource, m.code }
// hasFieldAccess 判断 u 是否有权限读写 field 的真实值:field.GetRoles() 为空表示不限制,非空时
// u 的角色只要有一个在列表里就算有权限(复用 req.User.HasUserRoles,同时拿到超级管理员的豁免)
func hasFieldAccess(field req.ResField, u req.User) bool {
roles := field.GetRoles()
if len(roles) == 0 {
return true
}
return u.HasUserRoles(roles...)
}