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...) }