feat: 新增 WithSilent/WithRolesScope 写操作选项
WithSilent 让写操作跳过变更通知(ResChangeEventFunc/ResWatcher),权限过滤/字段脱敏不受影响,用于类似"记录登录 IP"这种高频写但不需要广播"资源变了"的场景,避免不必要的缓存刷新。 WithRolesScope 显式指定权限过滤范围(ResScopeOn/All 才按角色过滤,Off 或不传都跳过),替代原来一直没接线生效的 Access 选项。两个选项相互独立,可以单独用也可以一起用。
This commit is contained in:
@@ -0,0 +1,928 @@
|
||||
package resx
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/db/exec"
|
||||
"git.fsdpf.net/go/db/exp"
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
// changeRow 保存一行数据的主键、UPDATE/DELETE 前的快照,以及 INSERT/UPDATE 时写入的字段值
|
||||
type changeRow struct {
|
||||
id int64
|
||||
snapshot map[string]any
|
||||
changes map[string]any
|
||||
}
|
||||
|
||||
// dataProcessor 是挂在 db.SelectDataset/InsertDataset/UpdateDataset/DeleteDataset 上的读写钩子。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 权限过滤:根据 DataInterceptor 返回的角色条件改写 SELECT/UPDATE/DELETE 的 WHERE。
|
||||
// 2. 数据规范化:INSERT/UPDATE 前统一清理系统保留字段、按字段类型转换保存值,并为 INSERT 填充默认值。
|
||||
// 3. 变更留痕:写操作完成后无条件调用 ResChangeEventFunc;当 DataInterceptor 判断需要时,写操作前
|
||||
// 抓取快照/写入值,写操作后额外调用 ResChangeRowFunc(由上层实现变更日志写入、ResWatcher
|
||||
// 事件发布等)。
|
||||
// 4. 事务托底:当 onChange 会被调用、且调用方没有显式传入事务时,自动开一个事务包住这次写操作和
|
||||
// onChange 本身;onChange 失败就整体回滚,成功才提交。调用方显式传入的事务不受影响,提交/
|
||||
// 回滚仍由调用方自己负责。
|
||||
type dataProcessor struct {
|
||||
res *resource
|
||||
u req.User
|
||||
tx *db.TxDatabase
|
||||
ownsTx bool // tx 是否由本次写操作自动开启(而非调用方传入),决定 After 要不要 Commit/Rollback
|
||||
onChange ResChangeRowFunc // 非 nil 表示本次写操作需要抓取快照/收集变更内容,由 Before 阶段的 DataInterceptor 决定
|
||||
rows []changeRow
|
||||
silent bool // ResOptions.Silent:跳过变更通知,见 req.WithSilent 注释
|
||||
skipPermission bool // ResOptions.RolesScope 不是 ResScopeOn/ResScopeAll:跳过权限过滤/字段脱敏,见 req.WithRolesScope 注释
|
||||
}
|
||||
|
||||
// UseTx 在 Before 之前调用:如果本次写操作会触发 onChange、且调用方没有显式传入事务,自动开一个
|
||||
// 事务包住写操作和 onChange。调用方已显式传入的事务(dp.tx != nil)不受影响。
|
||||
func (dp *dataProcessor) UseTx(dataset interface{}) (exec.QueryFactory, error) {
|
||||
if dp.res.IsVirtual() || dp.tx != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var category ResEventType
|
||||
switch dataset.(type) {
|
||||
case *db.InsertDataset:
|
||||
category = ResEventInsert
|
||||
case *db.UpdateDataset:
|
||||
category = ResEventUpdate
|
||||
case *db.DeleteDataset:
|
||||
category = ResEventDelete
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
dp.applyIntercept(category)
|
||||
|
||||
if dp.onChange == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tx, err := dp.res.DB().Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dp.tx = tx
|
||||
dp.ownsTx = true
|
||||
|
||||
return exec.NewQueryFactory(tx), nil
|
||||
}
|
||||
|
||||
// applyIntercept 调用容器里注册的 DataInterceptor,返回角色权限过滤条件,并记录本次写完后要不要调用、调用谁。
|
||||
// skipPermission 时丢弃行级权限过滤条件;silent 时不设置 onChange(写完后不会触发变更通知)。两者
|
||||
// 相互独立,可以只生效一个。
|
||||
func (dp *dataProcessor) applyIntercept(category ResEventType) (sub *db.SelectDataset, cond db.Expression) {
|
||||
sub, cond, onChange := dp.res.intercept(dp.u, category)
|
||||
if dp.skipPermission {
|
||||
sub, cond = nil, nil
|
||||
}
|
||||
if !dp.silent {
|
||||
dp.onChange = onChange
|
||||
}
|
||||
return sub, cond
|
||||
}
|
||||
|
||||
// captureSnapshot 在写操作执行前,查出受影响的行存入 rows
|
||||
func (dp *dataProcessor) captureSnapshot(where exp.ExpressionList) {
|
||||
sd := dp.res.GetDBTable(dp.u)
|
||||
if where != nil && len(where.Expressions()) > 0 {
|
||||
sd = sd.Where(where.Expressions()...)
|
||||
}
|
||||
snaps, err := sd.Executor().GetRecords()
|
||||
if err != nil {
|
||||
log.Printf("captureSnapshot err: %v", err)
|
||||
log.Println(sd.Executor().ToSQL())
|
||||
return
|
||||
}
|
||||
dp.rows = make([]changeRow, len(snaps))
|
||||
for i, snap := range snaps {
|
||||
dp.rows[i].snapshot = snap
|
||||
if id, ok := snap["id"]; ok {
|
||||
dp.rows[i].id = cast.ToInt64(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insertRowIds 计算本次 INSERT 各行的自增 id(MySQL: lastId 是第一条,SQLite: lastId 是最后一条)
|
||||
func (dp *dataProcessor) insertRowIds(result sql.Result) []int64 {
|
||||
lastId, _ := result.LastInsertId()
|
||||
count, _ := result.RowsAffected()
|
||||
|
||||
ids := make([]int64, count)
|
||||
for i := int64(0); i < count; i++ {
|
||||
rowId := lastId + i
|
||||
if isLocalDB(dp.res.DB().Dialect()) {
|
||||
rowId = lastId - count + 1 + i
|
||||
}
|
||||
ids[i] = rowId
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// finalizeInsertedRows 把计算好的自增 id 写回每一行;INSERT ... SELECT 场景 Before 阶段拿不到具体数据,
|
||||
// 这里改为按 id 反查补全(见 fetchInsertedRows)
|
||||
func (dp *dataProcessor) finalizeInsertedRows(result sql.Result) {
|
||||
ids := dp.insertRowIds(result)
|
||||
|
||||
if len(dp.rows) == 0 {
|
||||
dp.fetchInsertedRows(ids)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range dp.rows {
|
||||
if i < len(ids) {
|
||||
dp.rows[i].id = ids[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetchInsertedRows 用于 INSERT ... SELECT 场景:写入前拿不到具体行数据,写入后按 id 反查补全 changes
|
||||
func (dp *dataProcessor) fetchInsertedRows(ids []int64) {
|
||||
recs, err := dp.res.GetDBTable(dp.u).Where(db.C("id").In(ids)).Executor().GetRecords()
|
||||
if err != nil {
|
||||
log.Printf("fetchInsertedRows err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
byId := make(map[int64]map[string]any, len(recs))
|
||||
for _, rec := range recs {
|
||||
if id, ok := rec["id"]; ok {
|
||||
byId[cast.ToInt64(id)] = rec
|
||||
}
|
||||
}
|
||||
|
||||
dp.rows = make([]changeRow, len(ids))
|
||||
for i, id := range ids {
|
||||
dp.rows[i].id = id
|
||||
if rec, ok := byId[id]; ok {
|
||||
dp.rows[i].changes = dp.changesFromNewRow(rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildChanges 将本次 UPDATE 实际写入的字段值记入每一行的 changes
|
||||
func (dp *dataProcessor) buildChanges(newValues db.Record) {
|
||||
if len(dp.rows) == 0 || len(newValues) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
resolved := make(map[string]any, len(newValues))
|
||||
for k, v := range newValues {
|
||||
resolved[k] = dp.resolveLiteralValue(v)
|
||||
}
|
||||
|
||||
for i := range dp.rows {
|
||||
dp.rows[i].changes = resolved
|
||||
}
|
||||
}
|
||||
|
||||
// resolveLiteralValue 从 db.V/db.L 构造的字面量表达式中还原真实值,用于变更对比与序列化
|
||||
func (dp *dataProcessor) resolveLiteralValue(v any) any {
|
||||
lit, ok := v.(exp.LiteralExpression)
|
||||
if !ok {
|
||||
return v
|
||||
}
|
||||
if lit.Literal() == "?" {
|
||||
if args := lit.Args(); len(args) == 1 {
|
||||
return args[0]
|
||||
}
|
||||
}
|
||||
return lit.Literal()
|
||||
}
|
||||
|
||||
// changesFromNewRow 还原 INSERT 行的最终保存值,作为 changes 直接记录
|
||||
func (dp *dataProcessor) changesFromNewRow(row map[string]any) map[string]any {
|
||||
data := make(map[string]any, len(row))
|
||||
for k, v := range row {
|
||||
data[k] = dp.resolveLiteralValue(v)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) Before(dataset interface{}) *db.HookError {
|
||||
if err := dp.before(dataset); err != nil {
|
||||
return &db.HookError{Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) before(dataset interface{}) error {
|
||||
switch d := dataset.(type) {
|
||||
case *db.SelectDataset:
|
||||
return dp.beforeSelectDataset(d)
|
||||
case *db.InsertDataset:
|
||||
return dp.beforeInsertDataset(d)
|
||||
case *db.UpdateDataset:
|
||||
return dp.beforeUpdateDataset(d)
|
||||
case *db.DeleteDataset:
|
||||
return dp.beforeDeleteDataset(d)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) After(dataset interface{}, result interface{}) *db.HookError {
|
||||
if err := dp.after(dataset, result); err != nil {
|
||||
return &db.HookError{Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) after(dataset interface{}, result interface{}) error {
|
||||
if dp.res.IsVirtual() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var category ResEventType
|
||||
switch dataset.(type) {
|
||||
case *db.SelectDataset:
|
||||
return nil
|
||||
case *db.InsertDataset:
|
||||
category = ResEventInsert
|
||||
case *db.UpdateDataset:
|
||||
category = ResEventUpdate
|
||||
case *db.DeleteDataset:
|
||||
category = ResEventDelete
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
r := result.(sql.Result)
|
||||
|
||||
if !dp.silent {
|
||||
if err := dp.res.changeEvent(NewResChangeEvent(category, dp.res, dp.u, r)); err != nil {
|
||||
log.Printf("ResChangeEventFunc err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ResChangeRowFunc 由 Before 阶段 DataInterceptor 返回的 onChange 直接提供,非 nil 才调用
|
||||
var hookErr error
|
||||
if dp.onChange != nil {
|
||||
if category == ResEventInsert {
|
||||
dp.finalizeInsertedRows(r)
|
||||
}
|
||||
rows := make([]ResChangeRow, len(dp.rows))
|
||||
for i, row := range dp.rows {
|
||||
rows[i] = NewResChangeRow(category, dp.res, dp.u, row.id, WithOld(row.snapshot), WithNew(row.changes), WithTx(dp.tx))
|
||||
}
|
||||
hookErr = dp.onChange(rows)
|
||||
}
|
||||
|
||||
if dp.ownsTx {
|
||||
if hookErr != nil {
|
||||
if rbErr := dp.tx.Rollback(); rbErr != nil {
|
||||
log.Printf("dataProcessor tx rollback err: %v", rbErr)
|
||||
}
|
||||
} else if cmErr := dp.tx.Commit(); cmErr != nil {
|
||||
hookErr = cmErr
|
||||
}
|
||||
}
|
||||
|
||||
return hookErr
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeInsertDataset(id *db.InsertDataset) error {
|
||||
// INSERT 不涉及权限过滤(新建的行无需按角色可见性过滤),只关心是否需要抓取变更内容
|
||||
dp.applyIntercept(ResEventInsert)
|
||||
|
||||
switch true {
|
||||
case id.GetClauses().HasRows():
|
||||
return dp.beforeInsertRows(id)
|
||||
case id.GetClauses().HasVals():
|
||||
return dp.beforeInsertColsVals(id)
|
||||
case id.GetClauses().HasFrom():
|
||||
return dp.beforeInsertFromQuery(id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeInsertRows(id *db.InsertDataset) error {
|
||||
rows := id.GetClauses().Rows()
|
||||
|
||||
for i := range rows {
|
||||
rowValue := reflect.ValueOf(rows[i])
|
||||
if rowValue.Kind() == reflect.Ptr {
|
||||
rowValue = rowValue.Elem()
|
||||
}
|
||||
if rowValue.Kind() == reflect.Struct {
|
||||
if row, err := exp.NewRecordFromStruct(rowValue.Interface(), true, false); err != nil {
|
||||
return err
|
||||
} else {
|
||||
rows[i] = row
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range rows {
|
||||
row, ok := rows[i].(map[string]any)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("row is %T, want map[string]any", row)
|
||||
}
|
||||
|
||||
if err := dp.normalizeSaveValue(row); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dp.applyDefaultValue(row, true, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dp.onChange != nil {
|
||||
dp.rows = append(dp.rows, changeRow{changes: dp.changesFromNewRow(row)})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeInsertColsVals(id *db.InsertDataset) error {
|
||||
cols := id.GetClauses().Cols()
|
||||
vals := id.GetClauses().Vals()
|
||||
|
||||
colsName := []string{}
|
||||
for _, col := range cols.Columns() {
|
||||
colsName = append(colsName, col.(exp.IdentifierExpression).GetCol().(string))
|
||||
}
|
||||
|
||||
extraColsName := []string{}
|
||||
colsVal := []any{}
|
||||
for k, v := range lo.OmitByKeys(dp.getFieldsDefaultValue(true, false), colsName) {
|
||||
cols = cols.Append(db.C(k))
|
||||
extraColsName = append(extraColsName, k)
|
||||
colsVal = append(colsVal, v)
|
||||
}
|
||||
allColsName := append(append([]string{}, colsName...), extraColsName...)
|
||||
|
||||
for i := 0; i < len(vals); i++ {
|
||||
for j := 0; j < len(vals[i]); j++ {
|
||||
if filed, ok := dp.res.GetField(colsName[j]); ok {
|
||||
vals[i][j] = filed.ToValue(vals[i][j])
|
||||
}
|
||||
}
|
||||
vals[i] = append(vals[i], colsVal...)
|
||||
|
||||
if dp.onChange != nil {
|
||||
row := make(map[string]any, len(allColsName))
|
||||
for j, name := range allColsName {
|
||||
if j < len(vals[i]) {
|
||||
row[name] = vals[i][j]
|
||||
}
|
||||
}
|
||||
dp.rows = append(dp.rows, changeRow{changes: dp.changesFromNewRow(row)})
|
||||
}
|
||||
}
|
||||
|
||||
*id = *id.Cols(cols)
|
||||
return nil
|
||||
}
|
||||
|
||||
// beforeInsertFromQuery INSERT ... SELECT:写入内容来自子查询,写入前无法拿到具体行数据,
|
||||
// changes 由 After 在写入完成后按 id 反查补全(见 fetchInsertedRows)
|
||||
func (dp *dataProcessor) beforeInsertFromQuery(id *db.InsertDataset) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeSaveValue 规范化保存数据:
|
||||
// 1. 移除系统缺省字段, 如果是 insert 需要调用 applyDefaultValue
|
||||
// 2. 移除系统中没有的字段
|
||||
// 3. 移除当前用户没有权限写的字段(ResField.GetRoles,语义同查询脱敏,见 hasFieldAccess)——
|
||||
// 静默丢弃而不是报错,跟"系统中没有的字段"一致:数据库里对应列保持原值不变。skipPermission
|
||||
// 时跳过这条检查,字段规范化/类型转换本身不受影响。
|
||||
func (dp *dataProcessor) normalizeSaveValue(row db.Record) error {
|
||||
for k, v := range row {
|
||||
if k == "id" || k == "created_user" || k == "created_at" || k == "deleted_at" || k == "updated_at" {
|
||||
delete(row, k)
|
||||
continue
|
||||
}
|
||||
|
||||
field, hasField := dp.res.GetField(k)
|
||||
if hasField && !dp.skipPermission && !hasFieldAccess(field, dp.u) {
|
||||
delete(row, k)
|
||||
continue
|
||||
}
|
||||
|
||||
if val, ok := v.(db.Expression); ok {
|
||||
row[k] = val
|
||||
} else if hasField {
|
||||
row[k] = field.ToValue(v)
|
||||
} else {
|
||||
delete(row, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyDefaultValue 填充默认数据
|
||||
func (dp *dataProcessor) applyDefaultValue(row db.Record, forInsert, forUpdate bool) error {
|
||||
for k, v := range dp.getFieldsDefaultValue(forInsert, forUpdate) {
|
||||
if _, ok := row[k]; !ok {
|
||||
row[k] = v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) getFieldsDefaultValue(forInsert, forUpdate bool) map[string]db.Expression {
|
||||
defs := map[string]db.Expression{}
|
||||
|
||||
if forInsert {
|
||||
for _, item := range dp.res.fields {
|
||||
if item.GetCode() == "updated_at" || item.GetCode() == "created_user" || item.GetCode() == "owned_user" {
|
||||
continue
|
||||
}
|
||||
raw := item.GetRawDefault()
|
||||
if raw != nil {
|
||||
if lit, ok := raw.(exp.LiteralExpression); ok && strings.HasPrefix(strings.ToLower(lit.Literal()), "sql:") {
|
||||
defs[item.GetCode()] = raw
|
||||
} else if item.GetDataType() == req.ResJson {
|
||||
defs[item.GetCode()] = raw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := defs["owned_user"]; !ok {
|
||||
defs["owned_user"] = db.V(dp.u.Uuid())
|
||||
}
|
||||
defs["created_user"] = db.V(dp.u.Uuid())
|
||||
}
|
||||
|
||||
if forUpdate && isLocalDB(dp.res.DB().Dialect()) {
|
||||
defs["updated_at"] = db.L("CURRENT_TIMESTAMP")
|
||||
}
|
||||
|
||||
return defs
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeSelectDataset(sd *db.SelectDataset) error {
|
||||
sub, ex := dp.applyIntercept(ResEventSelect)
|
||||
if ex != nil {
|
||||
*sd = *sd.Where(ex)
|
||||
} else if sub != nil {
|
||||
table := sd.GetClauses().From().Columns()[0]
|
||||
if alias, ok := table.(exp.AliasedExpression); ok {
|
||||
*sd = *sd.From(db.V(sub.Expression()).As(alias.GetAs()))
|
||||
} else {
|
||||
*sd = *sd.From(db.V(sub.Expression()).As(table))
|
||||
}
|
||||
}
|
||||
return dp.maskSelect(sd)
|
||||
}
|
||||
|
||||
// maskSelect 按字段级权限(ResField.GetRoles)替换 SELECT 列表里当前用户没有权限查看的字段:
|
||||
// - 没有显式选择过列(裸的 db.Star()),或者显式选了限定到当前资源自己别名的 db.T(alias).All()
|
||||
// ——两种写法效果一样,都是"选当前资源自己的全部列":展开成资源自己的全部字段,逐个按权限
|
||||
// 判断要不要替换成脱敏值。只覆盖当前资源自己的字段,JOIN 进来的字段没有被展开进来,覆盖不到。
|
||||
// - 其它情况下显式选择了列(通常来自 orm-v2 QueryField.ToSql()):只处理其中被 MaskField(...)
|
||||
// 显式标记过的列,未标记的列一律原样保留——标记是显式的,不去猜哪一列对应哪个字段。这时候才
|
||||
// 真正解析 MaskField 标记的别名对应哪个资源、哪个字段(见 resolveLazyMask),解析失败说明
|
||||
// 调用方标记的表名/字段名写错了,是编程错误,直接报错而不是静默跳过。
|
||||
func (dp *dataProcessor) maskSelect(sd *db.SelectDataset) error {
|
||||
if dp.res.IsVirtual() || dp.skipPermission {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !dp.hasRestrictedFields(sd) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dp.isDefaultSelect(sd) {
|
||||
if len(dp.res.fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
cols := make([]interface{}, len(dp.res.fields))
|
||||
changed := false
|
||||
for i, field := range dp.res.fields {
|
||||
if hasFieldAccess(field, dp.u) {
|
||||
cols[i] = db.C(field.GetCode())
|
||||
} else {
|
||||
cols[i] = dp.res.mask(field).As(field.GetCode())
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
*sd = *sd.Select(cols...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
old := sd.GetClauses().Select().Columns()
|
||||
cols := make([]interface{}, len(old))
|
||||
changed := false
|
||||
for i, c := range old {
|
||||
resolved, colChanged, err := dp.resolveMaskedExpr(sd, c, true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cols[i] = resolved
|
||||
if colChanged {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
*sd = *sd.Select(cols...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasRestrictedFields 判断这次查询可能碰到的字段——当前资源自己的,加上 FROM/JOIN 子句里所有
|
||||
// 能解析到具体资源的——里有没有任何一个配置了 Roles。一个都没有的话,maskSelect 后面所有的工作
|
||||
// (SELECT * 展开、显式 MaskField 列解析)都不会有任何效果,可以在最前面直接跳过整个函数,不用
|
||||
// 再管是不是默认 SELECT、SELECT 里有没有标记过的列。
|
||||
//
|
||||
// JOIN 的表如果解析不到具体资源(不是这套资源系统管理的表、容器没注册 req.GetResource、或者
|
||||
// 这张表在 FROM/JOIN 里的写法认不出真实表名),不能当成"没有受限字段"直接放过——万一这张表上
|
||||
// 真的有 MaskField(...) 标记,那本该在 resolveLazyField 里报错(表名/字段名配置错误),提前跳过
|
||||
// 就变成静默放过,比现在的报错行为更危险。这些拿不准的情况一律返回 true(不跳过),退回到老路径
|
||||
// 让后面该报错报错、该处理处理。
|
||||
//
|
||||
// 有个前提:这里只看 sd 真实的 FROM/JOIN 子句。MaskField("alias.col") 里 alias 指向一个压根没
|
||||
// JOIN 进这次查询的资源时(resolveLazyField 会把 alias 当 code/table/uuid 兜底直接查一次),这
|
||||
// 里扫不到那个资源,如果它自己没有受限字段又恰好没被扫进来,会被误判成"整个查询不用脱敏"直接跳过
|
||||
// ——这种查询本来就没有实际意义(列不属于 FROM 里的任何一张表,真跑数据库会报错),MaskField 的
|
||||
// alias 必须对应查询里真实 JOIN 过的表,是这个快速跳过路径的前提。
|
||||
//
|
||||
// 超级管理员(HasUserRoles() 不带参数调用,只命中超管角色那一支)对任何字段都有权限,不管查了
|
||||
// 哪些表、字段配置了什么 Roles,脱敏最终都不会生效(hasFieldAccess 对超管永远返回 true)——直接
|
||||
// 跳过整个解析过程,既是性能优化,也避免了系统自己在资源初始化阶段用系统用户递归发起查询时,
|
||||
// req.GetResource 还没注册好导致的 DI 循环依赖报错(跟 framework-v2 的 res_scope.go 里
|
||||
// getResUserRolesCondition 对同类问题的处理是一个思路)。
|
||||
func (dp *dataProcessor) hasRestrictedFields(sd *db.SelectDataset) bool {
|
||||
if dp.u.HasUserRoles() {
|
||||
return false
|
||||
}
|
||||
|
||||
if dp.fieldsHaveRoles(dp.res.fields) {
|
||||
return true
|
||||
}
|
||||
|
||||
getResource, err := do.Invoke[req.GetResource](dp.res.container)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
clauses := sd.GetClauses()
|
||||
tables := make([]exp.Expression, 0, 1+len(clauses.Joins()))
|
||||
if from := clauses.From().Columns(); len(from) > 0 {
|
||||
tables = append(tables, from[0])
|
||||
}
|
||||
for _, j := range clauses.Joins() {
|
||||
tables = append(tables, j.Table())
|
||||
}
|
||||
|
||||
for _, t := range tables {
|
||||
aliased, ok := t.(exp.AliasedExpression)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
table, ok := dp.realTableOf(aliased.Aliased())
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
res, ok := getResource(table)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if dp.fieldsHaveRoles(res.GetFields()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// fieldsHaveRoles 判断 fields 里有没有任何一个配置了 Roles(hasRestrictedFields 对当前资源自己
|
||||
// 和每个能解析到的 JOIN 资源都要做一次这个判断)
|
||||
func (dp *dataProcessor) fieldsHaveRoles(fields []req.ResField) bool {
|
||||
for _, field := range fields {
|
||||
if len(field.GetRoles()) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveMaskedExpr 递归处理 c 内部被 MaskField 标记的列,包括嵌套在 db.Func(...) 参数里的情况
|
||||
// (比如 db.Func("CONCAT", resx.MaskField("t.a"), resx.MaskField("t.b")))——顶层 SELECT 列表
|
||||
// 只按类型断言直接标记的列,标记被函数包一层之后顶层类型就变成了 SQLFunctionExpression,断言不
|
||||
// 出来,得递归进 Args() 里找。
|
||||
//
|
||||
// topLevel 为 true 表示 c 本身就是一整个 SELECT 列,这时候没权限要换成 "mask值 AS 别名";
|
||||
// topLevel 为 false 表示 c 是嵌在别的表达式(目前只处理 db.Func 的参数)里面,这时候只能换成
|
||||
// 裸的 mask 值,不能带 AS——SQL 函数参数没法单独起别名。
|
||||
//
|
||||
// 目前只递归处理 SQLFunctionExpression(db.Func(...))和包着它的 AliasedExpression
|
||||
// (db.Func(...).As(...)):这是这个代码库里唯一会把 MaskField 包起来用的场景(聚合/计算字段),
|
||||
// 没有覆盖 CastExpression/CaseExpression/WindowExpression 等目前用不到的包装类型。
|
||||
func (dp *dataProcessor) resolveMaskedExpr(sd *db.SelectDataset, c interface{}, topLevel bool) (interface{}, bool, error) {
|
||||
if r, ok := c.(lazyMaskResolver); ok {
|
||||
field, unresolved, err := dp.resolveLazyField(sd, r)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if unresolved {
|
||||
// req.GetResource 本身解析失败(见 resolveLazyField 的说明),没有具体字段可参考
|
||||
// 数据类型,退到 db.V(nil) 兜底——跟 defaultMask 认不出数据类型时的兜底分支一致。
|
||||
if !topLevel {
|
||||
return db.V(nil), true, nil
|
||||
}
|
||||
return db.V(nil).As(dp.fallbackAlias(c, r)), true, nil
|
||||
}
|
||||
|
||||
if hasFieldAccess(field, dp.u) {
|
||||
return c, false, nil
|
||||
}
|
||||
if !topLevel {
|
||||
return dp.res.mask(field), true, nil
|
||||
}
|
||||
return dp.res.mask(field).As(dp.fallbackAlias(c, r)), true, nil
|
||||
}
|
||||
|
||||
if fn, ok := c.(exp.SQLFunctionExpression); ok {
|
||||
args := fn.Args()
|
||||
newArgs := make([]interface{}, len(args))
|
||||
changed := false
|
||||
for i, a := range args {
|
||||
resolved, argChanged, err := dp.resolveMaskedExpr(sd, a, false)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if argChanged {
|
||||
changed = true
|
||||
newArgs[i] = resolved
|
||||
} else {
|
||||
newArgs[i] = a
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return c, false, nil
|
||||
}
|
||||
return db.Func(fn.Name(), newArgs...), true, nil
|
||||
}
|
||||
|
||||
if aliased, ok := c.(exp.AliasedExpression); ok {
|
||||
inner, innerChanged, err := dp.resolveMaskedExpr(sd, aliased.Aliased(), false)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !innerChanged {
|
||||
return c, false, nil
|
||||
}
|
||||
return inner.(exp.Aliaseable).As(aliased.GetAs()), true, nil
|
||||
}
|
||||
|
||||
return c, false, nil
|
||||
}
|
||||
|
||||
// fallbackAlias 在拿不到真正的 req.ResField(没法用 field.GetCode())时,给替换后的表达式起个
|
||||
// 别名:优先用调用方自己指定的别名(MaskField(...).As("xxx")),没有的话退到列名本身
|
||||
// (lazyMaskField() 的第二个返回值)。
|
||||
func (dp *dataProcessor) fallbackAlias(c interface{}, r lazyMaskResolver) string {
|
||||
if aliased, ok := c.(exp.AliasedExpression); ok {
|
||||
if col, ok := aliased.GetAs().GetCol().(string); ok && col != "" {
|
||||
return col
|
||||
}
|
||||
}
|
||||
_, code := r.lazyMaskField()
|
||||
return code
|
||||
}
|
||||
|
||||
// resolveLazyField 把 MaskField(...) 标记的"待解析"列解析成真正的 req.ResField:codeResource
|
||||
// 为空表示当前资源自己的字段,直接用 dp.res;否则把 codeResource 当 SQL 别名,在 sd 的 FROM/JOIN
|
||||
// 子句里换成真实表名(见 resolveJoinTable),换不到就把它本身当 code/table/uuid 兜底试一次,再用
|
||||
// req.GetResource 查资源。
|
||||
// unresolved=true 表示 req.GetResource 本身解析失败——常见原因是资源系统自己还在初始化过程中
|
||||
// 递归触发了一次带 JOIN 的查询,形成真正的循环依赖(不是调用方标记写错了表名/字段名,那种情况
|
||||
// 会走下面别的报错分支)。这种情况不能让整个查询因为一次 DI 时序问题报错崩掉:调用方
|
||||
// resolveMaskedExpr 拿到 unresolved=true 后会保守地当成需要脱敏处理,宁可多脱敏一列,也不能
|
||||
// 打崩查询。
|
||||
func (dp *dataProcessor) resolveLazyField(sd *db.SelectDataset, r lazyMaskResolver) (field req.ResField, unresolved bool, err error) {
|
||||
codeResource, code := r.lazyMaskField()
|
||||
|
||||
var res req.Resource
|
||||
if codeResource == "" || codeResource == dp.res.GetCode() || codeResource == dp.fromAlias(sd) {
|
||||
// 裸列名,或者 codeResource 就是当前资源自己(不管是 dp.res 自己的 code,还是这次查询给它
|
||||
// 起的 FROM 别名):这两种情况都不是真的 JOIN 进来的字段,直接用 dp.res,完全不用碰
|
||||
// req.GetResource——DataListField 之类的配置记录里,CodeResource 对"master/自身资源"的
|
||||
// 字段本来就常常直接填资源自己的 code,不能一律当成外部资源硬查 DI,否则会把当前资源自己
|
||||
// 的所有字段都误判成"需要走 JOIN 解析",一旦 req.GetResource 暂时不可用,会把整行字段
|
||||
// 全部误脱敏,而不是只脱敏真的配置了 Roles 的那几个。
|
||||
res = dp.res
|
||||
} else {
|
||||
// 到这里 codeResource 已经不是资源自己的标识了,是这次查询里用的 SQL 别名,去 FROM/JOIN
|
||||
// 子句里当别名搜——只有这个语境下才适合叫 alias。
|
||||
alias := codeResource
|
||||
table, ok := dp.resolveJoinTable(sd, alias)
|
||||
if !ok {
|
||||
table = alias
|
||||
}
|
||||
|
||||
getResource, err := do.Invoke[req.GetResource](dp.res.container)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
found, ok := getResource(table)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("resx: MaskField 标记的列 %q 找不到对应的资源(别名/表名 %q)", code, table)
|
||||
}
|
||||
res = found
|
||||
}
|
||||
|
||||
field, ok := res.GetField(code)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("resx: 资源 %q 没有字段 %q", res.GetCode(), code)
|
||||
}
|
||||
|
||||
return field, false, nil
|
||||
}
|
||||
|
||||
// resolveJoinTable 在 sd 的 FROM/JOIN 子句里找别名是 alias 的那一个,返回它的真实表名。
|
||||
// MaskField("alias.col") 里的 alias 通常是资源自己的 code(约定,但不保证——比如同一个资源自
|
||||
// 连接时两个别名不可能都等于资源 code),只有找到真实表名,才能用 req.GetResource 的 table 匹配
|
||||
// 稳定查到资源,不用赌 alias 本身就是 code。
|
||||
func (dp *dataProcessor) resolveJoinTable(sd *db.SelectDataset, alias string) (string, bool) {
|
||||
clauses := sd.GetClauses()
|
||||
|
||||
if from := clauses.From().Columns(); len(from) > 0 {
|
||||
if t, ok := dp.aliasedRealTable(from[0], alias); ok {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
|
||||
for _, j := range clauses.Joins() {
|
||||
if t, ok := dp.aliasedRealTable(j.Table(), alias); ok {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// aliasIdentOf 从 AliasedExpression 里取出别名字符串,兼容两种起别名的路径(同 fromAlias 的说明:
|
||||
// 落在 GetAs() 的 Table 字段还是 Col 字段,取决于当初是直接在 identifier 上调 As(),还是像
|
||||
// GetTableExpr() 那样先包一层字面量再 As())。
|
||||
func (dp *dataProcessor) aliasIdentOf(as exp.IdentifierExpression) string {
|
||||
if t := as.GetTable(); t != "" {
|
||||
return t
|
||||
}
|
||||
if c, ok := as.GetCol().(string); ok {
|
||||
return c
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// aliasedRealTable 如果 e 是别名为 alias 的表引用,返回它包着的真实表名。
|
||||
func (dp *dataProcessor) aliasedRealTable(e exp.Expression, alias string) (string, bool) {
|
||||
aliased, ok := e.(exp.AliasedExpression)
|
||||
if !ok || dp.aliasIdentOf(aliased.GetAs()) != alias {
|
||||
return "", false
|
||||
}
|
||||
return dp.realTableOf(aliased.Aliased())
|
||||
}
|
||||
|
||||
// realTableOf 从"被起别名的那个表达式"里取出真实表名,不管别名是什么。兼容两种构造路径:
|
||||
// db.T(table).As(alias) 时 Aliased() 直接就是原始 identifier;res.GetTableExpr().As(alias)
|
||||
// (GetTableExpr 用 db.V(db.T(table)) 包了一层字面量)时 Aliased() 是字面量,真实的 identifier
|
||||
// 在它的 Args()[0] 里。
|
||||
func (dp *dataProcessor) realTableOf(e exp.Expression) (string, bool) {
|
||||
switch v := e.(type) {
|
||||
case exp.IdentifierExpression:
|
||||
return v.GetTable(), true
|
||||
case exp.LiteralExpression:
|
||||
args := v.Args()
|
||||
if len(args) != 1 {
|
||||
return "", false
|
||||
}
|
||||
if ident, ok := args[0].(exp.IdentifierExpression); ok {
|
||||
return ident.GetTable(), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// isDefaultSelect 判断 SELECT 列表是不是"选当前资源自己的全部列":要么是裸的 db.Star()
|
||||
// (clauses.IsDefaultSelect()),要么是显式限定到当前资源自己别名的 db.T(alias).All()——两种
|
||||
// 写法效果一样,都应该走同一套"展开成显式字段列表"的逻辑。限定到别的(比如 JOIN 进来的)别名
|
||||
// 的 T().All() 不算,那是另一个资源的全部列,这里够不到。
|
||||
func (dp *dataProcessor) isDefaultSelect(sd *db.SelectDataset) bool {
|
||||
clauses := sd.GetClauses()
|
||||
if clauses.IsDefaultSelect() {
|
||||
return true
|
||||
}
|
||||
|
||||
cols := clauses.Select().Columns()
|
||||
if len(cols) != 1 {
|
||||
return false
|
||||
}
|
||||
ident, ok := cols[0].(exp.IdentifierExpression)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
lit, ok := ident.GetCol().(exp.LiteralExpression)
|
||||
if !ok || lit.Literal() != "*" {
|
||||
return false
|
||||
}
|
||||
return ident.GetTable() == "" || ident.GetTable() == dp.fromAlias(sd)
|
||||
}
|
||||
|
||||
// fromAlias 取出 FROM 子句里当前资源自己的别名(GetDBTable 默认用资源 code 做别名,也可能被
|
||||
// ResOption 的 Alias 覆盖),取不到就返回空字符串。
|
||||
//
|
||||
// 别名字符串具体落在 GetAs() 的 Table 字段还是 Col 字段,取决于当初是怎么 As() 出来的:
|
||||
// GetTableExpr() 内部用 db.V(db.T(table)) 包了一层字面量,literal.As() 走的是
|
||||
// NewAliasExpression -> ParseIdentifier,单个词(不带 ".")会被解析成 col,不是 table;而
|
||||
// JOIN 场景常见的 db.T(code).As(alias) 是直接在 identifier 上调用 As(),走的是另一条路径,
|
||||
// 会把别名存进 table 字段。两种都要认。
|
||||
func (dp *dataProcessor) fromAlias(sd *db.SelectDataset) string {
|
||||
table := sd.GetClauses().From().Columns()[0]
|
||||
aliased, ok := table.(exp.AliasedExpression)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return dp.aliasIdentOf(aliased.GetAs())
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeUpdateDataset(ud *db.UpdateDataset) error {
|
||||
if dp.res.IsVirtual() {
|
||||
return nil
|
||||
}
|
||||
sub, ex := dp.applyIntercept(ResEventUpdate)
|
||||
if ex != nil {
|
||||
*ud = *ud.Where(ex)
|
||||
} else if sub != nil {
|
||||
table := ud.GetClauses().Table()
|
||||
if alias, ok := table.(exp.AliasedExpression); ok {
|
||||
*ud = *ud.Where(alias.GetAs().Col("id").In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
|
||||
} else {
|
||||
*ud = *ud.Where(db.L("?.id", table).In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
|
||||
}
|
||||
}
|
||||
|
||||
if dp.onChange != nil {
|
||||
dp.captureSnapshot(ud.GetClauses().Where())
|
||||
}
|
||||
|
||||
if ud.GetClauses().HasSetValues() {
|
||||
udv := ud.GetClauses().SetValues()
|
||||
|
||||
var data db.Record
|
||||
switch v := udv.(type) {
|
||||
case map[string]any:
|
||||
data = v
|
||||
case db.Record:
|
||||
data = v
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %T", udv)
|
||||
}
|
||||
|
||||
if err := dp.normalizeSaveValue(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dp.onChange != nil {
|
||||
dp.buildChanges(data)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp *dataProcessor) beforeDeleteDataset(dd *db.DeleteDataset) error {
|
||||
if dp.res.IsVirtual() {
|
||||
return nil
|
||||
}
|
||||
sub, ex := dp.applyIntercept(ResEventDelete)
|
||||
if ex != nil {
|
||||
*dd = *dd.Where(ex)
|
||||
} else if sub != nil {
|
||||
table := dd.GetClauses().From().Columns()[0]
|
||||
if alias, ok := table.(exp.AliasedExpression); ok {
|
||||
*dd = *dd.Where(alias.GetAs().Col("id").In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
|
||||
} else {
|
||||
*dd = *dd.Where(db.L("?.id", table).In(sub.Select(db.T(dp.res.GetCode()).Col("id"))))
|
||||
}
|
||||
}
|
||||
|
||||
if dp.onChange != nil {
|
||||
dp.captureSnapshot(dd.GetClauses().Where())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user