新增 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 资源实现包。
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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 为
|
||||
// 空表示裸列名,对应当前资源自己的字段。
|
||||
//
|
||||
// 这里特意不叫 alias:MaskField("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.AliasedExpression(MaskField(...).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...)
|
||||
}
|
||||
Reference in New Issue
Block a user