测试: GetDBTable 显式传 WithPermission,补充权限组合覆盖

Permission 不传时按零值处理,行级过滤和字段脱敏都会跳过(见 options.go 里
Permission 字段的注释),这是既有设计,不是这次改的。但 resx 包自己的测试
之前几乎都没传 WithPermission,实际上一直跑在"权限检查关闭"的状态下——
字段脱敏、写权限过滤、行级过滤相关的断言全部失效但没有暴露出来。

给 res_mask_field_test.go/res_interceptor_test.go 里所有 GetDBTable 调用
显式加上 WithPermission(ResAll),让这些测试真正跑在权限检查打开的状态下。
另外新增 res_permission_test.go,单独验证 WithPermission 各个取值(不传/
显式传 0/单独 ResRow/单独 ResMask/ResAll/按位组合)分别独立生效,作为这个
默认行为的回归防护。
This commit is contained in:
2026-08-20 15:59:24 +08:00
parent 5cdf1cc00d
commit f39adfa46c
3 changed files with 177 additions and 47 deletions
+21 -21
View File
@@ -84,14 +84,14 @@ func (t *hooksTest) TestOnBefore_RolesCondition_Cond() {
// 实际生成的 SQL // 实际生成的 SQL
// SELECT * FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000') // SELECT * FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000')
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Select(db.Star()).Executor().ToSQL() sql, _, _ := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user") t.Contains(sql, "owned_user")
t.Contains(sql, "00000000-0000-0000-0000-000000000000") t.Contains(sql, "00000000-0000-0000-0000-000000000000")
mock.ExpectQuery("SELECT"). mock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "张三")) WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "张三"))
rows, err := res.GetDBTable(base.GetAnonymous()).Select(db.Star()).Executor().GetRecords() rows, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Select(db.Star()).Executor().GetRecords()
t.NoError(err) t.NoError(err)
t.Len(rows, 1) t.Len(rows, 1)
t.NoError(mock.ExpectationsWereMet()) t.NoError(mock.ExpectationsWereMet())
@@ -111,13 +111,13 @@ func (t *hooksTest) TestOnBefore_RolesCondition_Sub() {
// 实际生成的 SQL // 实际生成的 SQL
// SELECT * FROM (SELECT * FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000')) AS `User` // SELECT * FROM (SELECT * FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000')) AS `User`
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Select(db.Star()).Executor().ToSQL() sql, _, _ := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user") t.Contains(sql, "owned_user")
mock.ExpectQuery("SELECT"). mock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(2, "李四")) WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(2, "李四"))
rows, err := res.GetDBTable(base.GetAnonymous()).Select(db.Star()).Executor().GetRecords() rows, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Select(db.Star()).Executor().GetRecords()
t.NoError(err) t.NoError(err)
t.Len(rows, 1) t.Len(rows, 1)
t.NoError(mock.ExpectationsWereMet()) t.NoError(mock.ExpectationsWereMet())
@@ -153,7 +153,7 @@ func (t *hooksTest) TestOnBefore_NeedsCapture_True() {
mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit() mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Update(). _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Update().
Set(db.Record{"name": "新名字"}). Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)). Where(db.C("id").Eq(1)).
Executor().Exec() Executor().Exec()
@@ -185,7 +185,7 @@ func (t *hooksTest) TestOnBefore_NeedsCapture_False() {
// 实际生成的 SQLUPDATE `users` AS `User` SET `name`='新名字' WHERE (`id` = 1) // 实际生成的 SQLUPDATE `users` AS `User` SET `name`='新名字' WHERE (`id` = 1)
mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1))
_, err := res.GetDBTable(base.GetAnonymous()).Update(). _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Update().
Set(db.Record{"name": "新名字"}). Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)). Where(db.C("id").Eq(1)).
Executor().Exec() Executor().Exec()
@@ -220,7 +220,7 @@ func (t *hooksTest) TestOnAfter_RowsContent_Insert() {
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(10, 1)) mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(10, 1))
mock.ExpectCommit() mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Insert(). _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Insert().
Rows(db.Record{"name": "张三"}). Rows(db.Record{"name": "张三"}).
Executor().Exec() Executor().Exec()
t.NoError(err) t.NoError(err)
@@ -256,7 +256,7 @@ func (t *hooksTest) TestOnAfter_UnconditionalOnWrite() {
// 实际生成的 SQLDELETE FROM `users` AS `User` WHERE (`id` = 1) // 实际生成的 SQLDELETE FROM `users` AS `User` WHERE (`id` = 1)
mock.ExpectExec("DELETE").WillReturnResult(sqlmock.NewResult(0, 3)) mock.ExpectExec("DELETE").WillReturnResult(sqlmock.NewResult(0, 3))
_, err := res.GetDBTable(base.GetAnonymous()).Delete(). _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Delete().
Where(db.C("id").Eq(1)). Where(db.C("id").Eq(1)).
Executor().Exec() Executor().Exec()
t.NoError(err) t.NoError(err)
@@ -300,7 +300,7 @@ func (t *hooksTest) TestInsertFromQuery_FetchesInsertedRowsAfterward() {
) )
mock.ExpectCommit() mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Insert().FromQuery(sub).Executor().Exec() _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Insert().FromQuery(sub).Executor().Exec()
t.NoError(err) t.NoError(err)
t.NoError(mock.ExpectationsWereMet()) t.NoError(mock.ExpectationsWereMet())
@@ -335,7 +335,7 @@ func (t *hooksTest) TestBatchInsertIds_Mysql() {
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(100, 3)) mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(100, 3))
mock.ExpectCommit() mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Insert().Rows( _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Insert().Rows(
db.Record{"name": "A"}, db.Record{"name": "B"}, db.Record{"name": "C"}, db.Record{"name": "A"}, db.Record{"name": "B"}, db.Record{"name": "C"},
).Executor().Exec() ).Executor().Exec()
t.NoError(err) t.NoError(err)
@@ -385,7 +385,7 @@ func (t *hooksTest) TestBatchInsertIds_LocalDB() {
), ),
) )
_, err := res.GetDBTable(base.GetAnonymous()).Insert().Rows( _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Insert().Rows(
db.Record{"name": "A"}, db.Record{"name": "B"}, db.Record{"name": "C"}, db.Record{"name": "A"}, db.Record{"name": "B"}, db.Record{"name": "C"},
).Executor().Exec() ).Executor().Exec()
t.Require().NoError(err) t.Require().NoError(err)
@@ -413,7 +413,7 @@ func (t *hooksTest) TestUpdate_RolesConditionSub() {
// 实际生成的 SQL // 实际生成的 SQL
// UPDATE `users` AS `User` SET `name`='新名字' WHERE ((`id` = 1) AND (`id` IN ((SELECT `User`.`id` FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000'))))) // UPDATE `users` AS `User` SET `name`='新名字' WHERE ((`id` = 1) AND (`id` IN ((SELECT `User`.`id` FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000')))))
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Update(). sql, _, _ := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Update().
Set(db.Record{"name": "新名字"}). Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)). Where(db.C("id").Eq(1)).
Executor().ToSQL() Executor().ToSQL()
@@ -438,7 +438,7 @@ func (t *hooksTest) TestDelete_RolesConditionSub() {
// 实际生成的 SQL // 实际生成的 SQL
// DELETE FROM `users` AS `User` WHERE ((`id` = 1) AND (`id` IN ((SELECT `User`.`id` FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000'))))) // DELETE FROM `users` AS `User` WHERE ((`id` = 1) AND (`id` IN ((SELECT `User`.`id` FROM `users` AS `User` WHERE (`owned_user` = '00000000-0000-0000-0000-000000000000')))))
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Delete(). sql, _, _ := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Delete().
Where(db.C("id").Eq(1)). Where(db.C("id").Eq(1)).
Executor().ToSQL() Executor().ToSQL()
@@ -474,7 +474,7 @@ func (t *hooksTest) TestDelete_CapturesSnapshot() {
mock.ExpectExec("DELETE").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("DELETE").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit() mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Delete(). _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Delete().
Where(db.C("id").Eq(5)). Where(db.C("id").Eq(5)).
Executor().Exec() Executor().Exec()
t.NoError(err) t.NoError(err)
@@ -510,7 +510,7 @@ func (t *hooksTest) TestInsertColsVals_Capture() {
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit() mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Insert().Cols("name").Vals( _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Insert().Cols("name").Vals(
db.Vals{"赵六"}, db.Vals{"赵六"},
).Executor().Exec() ).Executor().Exec()
t.NoError(err) t.NoError(err)
@@ -528,7 +528,7 @@ func (t *hooksTest) TestInsertColsVals_DoesNotStripSystemColumns() {
// 实际生成的 SQLcreated_user 没有被剔除/改写): // 实际生成的 SQLcreated_user 没有被剔除/改写):
// INSERT INTO `users` (`name`, `created_user`, `owned_user`) VALUES ('赵六', 'attacker-uuid', '00000000-0000-0000-0000-000000000000') // INSERT INTO `users` (`name`, `created_user`, `owned_user`) VALUES ('赵六', 'attacker-uuid', '00000000-0000-0000-0000-000000000000')
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Insert().Cols("name", "created_user").Vals( sql, _, _ := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Insert().Cols("name", "created_user").Vals(
db.Vals{"赵六", "attacker-uuid"}, db.Vals{"赵六", "attacker-uuid"},
).Executor().ToSQL() ).Executor().ToSQL()
@@ -572,7 +572,7 @@ func (t *hooksTest) TestVirtualResource_SkipsRolesConditionAndOnAfter() {
// 实际生成的 SQL(虚拟资源拿子查询拼表名,且没有 owned_user 权限条件): // 实际生成的 SQL(虚拟资源拿子查询拼表名,且没有 owned_user 权限条件):
// UPDATE (SELECT * FROM `users`) AS `User` SET `name`='新名字' WHERE (`id` = 1) // UPDATE (SELECT * FROM `users`) AS `User` SET `name`='新名字' WHERE (`id` = 1)
sql, _, _ := res.GetDBTable(base.GetAnonymous()).Update(). sql, _, _ := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Update().
Set(db.Record{"name": "新名字"}). Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)). Where(db.C("id").Eq(1)).
Executor().ToSQL() Executor().ToSQL()
@@ -580,7 +580,7 @@ func (t *hooksTest) TestVirtualResource_SkipsRolesConditionAndOnAfter() {
// 没有安排 Begin:虚拟资源在 UseTx 里直接短路,不会自动开事务 // 没有安排 Begin:虚拟资源在 UseTx 里直接短路,不会自动开事务
mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1))
_, err = res.GetDBTable(base.GetAnonymous()).Update(). _, err = res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Update().
Set(db.Record{"name": "新名字"}). Set(db.Record{"name": "新名字"}).
Where(db.C("id").Eq(1)). Where(db.C("id").Eq(1)).
Executor().Exec() Executor().Exec()
@@ -620,7 +620,7 @@ func (t *hooksTest) TestBuildChanges_ResolvesNonPlaceholderLiteral() {
mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("UPDATE").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit() mock.ExpectCommit()
_, err := res.GetDBTable(base.GetAnonymous()).Update(). _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Update().
Set(db.Record{"name": db.L("NOW()")}). Set(db.Record{"name": db.L("NOW()")}).
Where(db.C("id").Eq(1)). Where(db.C("id").Eq(1)).
Executor().Exec() Executor().Exec()
@@ -650,7 +650,7 @@ func (t *hooksTest) TestUseTx_RollsBackOnChangeError() {
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectRollback() mock.ExpectRollback()
_, err := res.GetDBTable(base.GetAnonymous()).Insert(). _, err := res.GetDBTable(base.GetAnonymous(), req.WithPermission(req.ResAll)).Insert().
Rows(db.Record{"name": "赵六"}). Rows(db.Record{"name": "赵六"}).
Executor().Exec() Executor().Exec()
@@ -684,7 +684,7 @@ func (t *hooksTest) TestUseTx_ReusesCallerProvidedTx() {
// 这里不再有第二次 ExpectBegin:证明 UseTx 发现 dp.tx 已经非 nil 就不会再自己开一个 // 这里不再有第二次 ExpectBegin:证明 UseTx 发现 dp.tx 已经非 nil 就不会再自己开一个
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(1, 1))
_, err = res.GetDBTable(base.GetAnonymous(), req.WithTx(tx)).Insert(). _, err = res.GetDBTable(base.GetAnonymous(), req.WithTx(tx), req.WithPermission(req.ResAll)).Insert().
Rows(db.Record{"name": "赵六"}). Rows(db.Record{"name": "赵六"}).
Executor().Exec() Executor().Exec()
t.NoError(err) t.NoError(err)
+26 -26
View File
@@ -178,7 +178,7 @@ func (t *maskFieldTest) newStarJoinApp() (user, dept, manager req.Resource) {
func (t *maskFieldTest) TestDefaultSelect_MasksRestrictedField() { func (t *maskFieldTest) TestDefaultSelect_MasksRestrictedField() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{}).Select(db.Star()).Executor().ToSQL() sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "`name`") t.Contains(sql, "`name`")
t.Contains(sql, "-999999999 AS `salary`", "受限字段应该被替换成内置的数字哨兵值,而不是裸列") t.Contains(sql, "-999999999 AS `salary`", "受限字段应该被替换成内置的数字哨兵值,而不是裸列")
@@ -188,7 +188,7 @@ func (t *maskFieldTest) TestDefaultSelect_MasksRestrictedField() {
func (t *maskFieldTest) TestDefaultSelect_RoleMatchShowsRealColumn() { func (t *maskFieldTest) TestDefaultSelect_RoleMatchShowsRealColumn() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{roles: []string{"hr"}}).Select(db.Star()).Executor().ToSQL() sql, _, _ := res.GetDBTable(testUser{roles: []string{"hr"}}, req.WithPermission(req.ResAll)).Select(db.Star()).Executor().ToSQL()
// 没有字段需要脱敏,直接维持原来的 SELECT *,不会被改写成展开的列表 // 没有字段需要脱敏,直接维持原来的 SELECT *,不会被改写成展开的列表
t.Contains(sql, "SELECT *") t.Contains(sql, "SELECT *")
@@ -199,7 +199,7 @@ func (t *maskFieldTest) TestDefaultSelect_RoleMatchShowsRealColumn() {
func (t *maskFieldTest) TestDefaultSelect_NoRestrictedFieldsLeavesWildcard() { func (t *maskFieldTest) TestDefaultSelect_NoRestrictedFieldsLeavesWildcard() {
res, _ := t.newResource(nameField) res, _ := t.newResource(nameField)
sql, _, _ := res.GetDBTable(testUser{}).Select(db.Star()).Executor().ToSQL() sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "SELECT *") t.Contains(sql, "SELECT *")
} }
@@ -210,7 +210,7 @@ func (t *maskFieldTest) TestDefaultSelect_NoRestrictedFieldsLeavesWildcard() {
func (t *maskFieldTest) TestExplicitColumn_MaskFieldMarkedColumnGetsMasked() { func (t *maskFieldTest) TestExplicitColumn_MaskFieldMarkedColumnGetsMasked() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{}). sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
Select(db.C("name"), resx.MaskField("salary").As("salary")). Select(db.C("name"), resx.MaskField("salary").As("salary")).
Executor().ToSQL() Executor().ToSQL()
@@ -223,7 +223,7 @@ func (t *maskFieldTest) TestExplicitColumn_MaskFieldMarkedColumnGetsMasked() {
func (t *maskFieldTest) TestExplicitColumn_UnmarkedColumnNeverMasked() { func (t *maskFieldTest) TestExplicitColumn_UnmarkedColumnNeverMasked() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{}). sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
Select(db.C("salary")). Select(db.C("salary")).
Executor().ToSQL() Executor().ToSQL()
@@ -237,7 +237,7 @@ func (t *maskFieldTest) TestExplicitColumn_UnmarkedColumnNeverMasked() {
func (t *maskFieldTest) TestQualifiedWildcard_OwnAliasExpandsAndMasks() { func (t *maskFieldTest) TestQualifiedWildcard_OwnAliasExpandsAndMasks() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{}). sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
Select(db.T("User").All()). Select(db.T("User").All()).
Executor().ToSQL() Executor().ToSQL()
@@ -251,7 +251,7 @@ func (t *maskFieldTest) TestQualifiedWildcard_OwnAliasExpandsAndMasks() {
func (t *maskFieldTest) TestQualifiedWildcard_OtherAliasNeverMasked() { func (t *maskFieldTest) TestQualifiedWildcard_OtherAliasNeverMasked() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{}). sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
Select(db.T("Department").All()). Select(db.T("Department").All()).
Executor().ToSQL() Executor().ToSQL()
@@ -263,7 +263,7 @@ func (t *maskFieldTest) TestQualifiedWildcard_OtherAliasNeverMasked() {
func (t *maskFieldTest) TestExplicitColumn_CustomAliasPreserved() { func (t *maskFieldTest) TestExplicitColumn_CustomAliasPreserved() {
res, _ := t.newResource(salaryField) res, _ := t.newResource(salaryField)
sql, _, _ := res.GetDBTable(testUser{}). sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
Select(resx.MaskField("salary").As("employee_salary")). Select(resx.MaskField("salary").As("employee_salary")).
Executor().ToSQL() Executor().ToSQL()
@@ -279,7 +279,7 @@ func (t *maskFieldTest) TestExplicitColumn_CustomAliasPreserved() {
func (t *maskFieldTest) TestExplicitColumn_JoinedResourceFieldMasked() { func (t *maskFieldTest) TestExplicitColumn_JoinedResourceFieldMasked() {
user, dept := t.newJoinedApp() user, dept := t.newJoinedApp()
sql, _, _ := user.GetDBTable(testUser{}). sql, _, _ := user.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))). LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))).
Select(db.C("name"), resx.MaskField("Department.budget").As("budget")). Select(db.C("name"), resx.MaskField("Department.budget").As("budget")).
Executor().ToSQL() Executor().ToSQL()
@@ -294,7 +294,7 @@ func (t *maskFieldTest) TestExplicitColumn_JoinedResourceFieldMasked() {
func (t *maskFieldTest) TestExplicitColumn_JoinedResourceFieldRoleMatchNotMasked() { func (t *maskFieldTest) TestExplicitColumn_JoinedResourceFieldRoleMatchNotMasked() {
user, dept := t.newJoinedApp() user, dept := t.newJoinedApp()
sql, _, _ := user.GetDBTable(testUser{roles: []string{"finance"}}). sql, _, _ := user.GetDBTable(testUser{roles: []string{"finance"}}, req.WithPermission(req.ResAll)).
LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))). LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))).
Select(db.C("name"), resx.MaskField("Department.budget").As("budget")). Select(db.C("name"), resx.MaskField("Department.budget").As("budget")).
Executor().ToSQL() Executor().ToSQL()
@@ -310,7 +310,7 @@ func (t *maskFieldTest) TestExplicitColumn_JoinedResourceFieldRoleMatchNotMasked
func (t *maskFieldTest) TestExplicitColumn_DirectIdentifierAliasJoinResolves() { func (t *maskFieldTest) TestExplicitColumn_DirectIdentifierAliasJoinResolves() {
user, _ := t.newJoinedApp() user, _ := t.newJoinedApp()
sql, _, _ := user.GetDBTable(testUser{}). sql, _, _ := user.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
LeftJoin(db.T("departments").As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))). LeftJoin(db.T("departments").As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))).
Select(db.C("name"), resx.MaskField("Department.budget").As("budget")). Select(db.C("name"), resx.MaskField("Department.budget").As("budget")).
Executor().ToSQL() Executor().ToSQL()
@@ -331,7 +331,7 @@ func (t *maskFieldTest) TestExplicitColumn_SelfJoinDifferentAliasesResolveIndepe
) )
register(category) register(category)
sql, _, _ := category.GetDBTable(testUser{}, req.WithAlias("Child")). sql, _, _ := category.GetDBTable(testUser{}, req.WithAlias("Child"), req.WithPermission(req.ResAll)).
LeftJoin(category.GetTableExpr().As("Parent"), db.On(db.I("Child.pid").Eq(db.I("Parent.id")))). LeftJoin(category.GetTableExpr().As("Parent"), db.On(db.I("Child.pid").Eq(db.I("Parent.id")))).
Select( Select(
resx.MaskField("Child.secret").As("child_secret"), resx.MaskField("Child.secret").As("child_secret"),
@@ -349,7 +349,7 @@ func (t *maskFieldTest) TestExplicitColumn_SelfJoinDifferentAliasesResolveIndepe
func (t *maskFieldTest) TestExplicitColumn_ChainedJoinResolvesThirdTable() { func (t *maskFieldTest) TestExplicitColumn_ChainedJoinResolvesThirdTable() {
user, dept, company := t.newChainedJoinApp() user, dept, company := t.newChainedJoinApp()
sql, _, _ := user.GetDBTable(testUser{}). sql, _, _ := user.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))). LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))).
LeftJoin(company.GetTableExpr().As("Company"), db.On(db.I("Department.company_id").Eq(db.I("Company.id")))). LeftJoin(company.GetTableExpr().As("Company"), db.On(db.I("Department.company_id").Eq(db.I("Company.id")))).
Select(db.C("name"), resx.MaskField("Company.budget").As("budget")). Select(db.C("name"), resx.MaskField("Company.budget").As("budget")).
@@ -366,7 +366,7 @@ func (t *maskFieldTest) TestExplicitColumn_ChainedJoinResolvesThirdTable() {
func (t *maskFieldTest) TestExplicitColumn_StarJoinResolvesEachBranchIndependently() { func (t *maskFieldTest) TestExplicitColumn_StarJoinResolvesEachBranchIndependently() {
user, dept, manager := t.newStarJoinApp() user, dept, manager := t.newStarJoinApp()
sql, _, _ := user.GetDBTable(testUser{roles: []string{"finance"}}). sql, _, _ := user.GetDBTable(testUser{roles: []string{"finance"}}, req.WithPermission(req.ResAll)).
LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))). LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))).
LeftJoin(manager.GetTableExpr().As("Manager"), db.On(db.I("User.manager_id").Eq(db.I("Manager.id")))). LeftJoin(manager.GetTableExpr().As("Manager"), db.On(db.I("User.manager_id").Eq(db.I("Manager.id")))).
Select( Select(
@@ -386,7 +386,7 @@ func (t *maskFieldTest) TestExplicitColumn_StarJoinResolvesEachBranchIndependent
func (t *maskFieldTest) TestExplicitColumn_FieldNotFoundOnResourceErrors() { func (t *maskFieldTest) TestExplicitColumn_FieldNotFoundOnResourceErrors() {
user, dept := t.newJoinedApp() user, dept := t.newJoinedApp()
_, _, err := user.GetDBTable(testUser{}). _, _, err := user.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))). LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))).
Select(db.C("name"), resx.MaskField("Department.no_such_field").As("x")). Select(db.C("name"), resx.MaskField("Department.no_such_field").As("x")).
Executor().ToSQL() Executor().ToSQL()
@@ -399,7 +399,7 @@ func (t *maskFieldTest) TestExplicitColumn_FieldNotFoundOnResourceErrors() {
func (t *maskFieldTest) TestExplicitColumn_UnresolvableAliasErrors() { func (t *maskFieldTest) TestExplicitColumn_UnresolvableAliasErrors() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
_, _, err := res.GetDBTable(testUser{}). _, _, err := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
Select(db.C("name"), resx.MaskField("NoSuchResource.budget").As("budget")). Select(db.C("name"), resx.MaskField("NoSuchResource.budget").As("budget")).
Executor().ToSQL() Executor().ToSQL()
@@ -427,7 +427,7 @@ func (t *maskFieldTest) TestExplicitColumn_GetResourceUnavailableErrors() {
resx.WithFields(nameField), resx.WithFields(nameField),
) )
_, _, sqlErr := res.GetDBTable(testUser{}). _, _, sqlErr := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
LeftJoin(db.T("departments").As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))). LeftJoin(db.T("departments").As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))).
Select(db.C("name"), resx.MaskField("Department.budget").As("budget")). Select(db.C("name"), resx.MaskField("Department.budget").As("budget")).
Executor().ToSQL() Executor().ToSQL()
@@ -459,7 +459,7 @@ func (t *maskFieldTest) TestExplicitColumn_OwnAliasSkipsGetResourceEntirely() {
resx.WithFields(nameField, salaryField), resx.WithFields(nameField, salaryField),
) )
sql, _, sqlErr := res.GetDBTable(testUser{}). sql, _, sqlErr := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
Select(resx.MaskField("User.name").As("name"), resx.MaskField("User.salary").As("salary")). Select(resx.MaskField("User.name").As("name"), resx.MaskField("User.salary").As("salary")).
Executor().ToSQL() Executor().ToSQL()
@@ -477,7 +477,7 @@ func (t *maskFieldTest) TestExplicitColumn_OwnAliasSkipsGetResourceEntirely() {
func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideFuncArgGetsMasked() { func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideFuncArgGetsMasked() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{}). sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
Select(db.Func("CONCAT", resx.MaskField("salary"), db.L("' extra'")).As("computed")). Select(db.Func("CONCAT", resx.MaskField("salary"), db.L("' extra'")).As("computed")).
Executor().ToSQL() Executor().ToSQL()
@@ -489,7 +489,7 @@ func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideFuncArgGetsMasked() {
func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideFuncArgRoleMatchNotMasked() { func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideFuncArgRoleMatchNotMasked() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{roles: []string{"hr"}}). sql, _, _ := res.GetDBTable(testUser{roles: []string{"hr"}}, req.WithPermission(req.ResAll)).
Select(db.Func("CONCAT", resx.MaskField("salary"), db.L("' extra'")).As("computed")). Select(db.Func("CONCAT", resx.MaskField("salary"), db.L("' extra'")).As("computed")).
Executor().ToSQL() Executor().ToSQL()
@@ -503,7 +503,7 @@ func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideFuncArgRoleMatchNotMas
func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideNestedFuncArgGetsMasked() { func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideNestedFuncArgGetsMasked() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{}). sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
Select(db.Func("UPPER", db.Func("CONCAT", resx.MaskField("salary"), db.L("' extra'"))).As("computed")). Select(db.Func("UPPER", db.Func("CONCAT", resx.MaskField("salary"), db.L("' extra'"))).As("computed")).
Executor().ToSQL() Executor().ToSQL()
@@ -515,7 +515,7 @@ func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideNestedFuncArgGetsMaske
func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideJoinedFuncArgGetsMasked() { func (t *maskFieldTest) TestExplicitColumn_MaskFieldInsideJoinedFuncArgGetsMasked() {
user, dept := t.newJoinedApp() user, dept := t.newJoinedApp()
sql, _, _ := user.GetDBTable(testUser{}). sql, _, _ := user.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).
LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))). LeftJoin(dept.GetTableExpr().As("Department"), db.On(db.I("User.dept_id").Eq(db.I("Department.id")))).
Select(db.Func("CONCAT", resx.MaskField("Department.budget"), db.L("' extra'")).As("computed")). Select(db.Func("CONCAT", resx.MaskField("Department.budget"), db.L("' extra'")).As("computed")).
Executor().ToSQL() Executor().ToSQL()
@@ -545,7 +545,7 @@ func (t *maskFieldTest) TestCustomMaskFunc_Override() {
resx.WithFields(salaryField), resx.WithFields(salaryField),
) )
sql, _, _ := customRes.GetDBTable(testUser{}).Select(db.Star()).Executor().ToSQL() sql, _, _ := customRes.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "REDACTED") t.Contains(sql, "REDACTED")
t.NotContains(sql, "-999999999") t.NotContains(sql, "-999999999")
@@ -557,7 +557,7 @@ func (t *maskFieldTest) TestCustomMaskFunc_Override() {
func (t *maskFieldTest) TestInsert_DropsFieldWithoutWriteAccess() { func (t *maskFieldTest) TestInsert_DropsFieldWithoutWriteAccess() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{}).Insert(). sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).Insert().
Rows(db.Record{"name": "张三", "salary": 999999}). Rows(db.Record{"name": "张三", "salary": 999999}).
Executor().ToSQL() Executor().ToSQL()
@@ -569,7 +569,7 @@ func (t *maskFieldTest) TestInsert_DropsFieldWithoutWriteAccess() {
func (t *maskFieldTest) TestInsert_KeepsFieldWithWriteAccess() { func (t *maskFieldTest) TestInsert_KeepsFieldWithWriteAccess() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{roles: []string{"hr"}}).Insert(). sql, _, _ := res.GetDBTable(testUser{roles: []string{"hr"}}, req.WithPermission(req.ResAll)).Insert().
Rows(db.Record{"name": "张三", "salary": 999999}). Rows(db.Record{"name": "张三", "salary": 999999}).
Executor().ToSQL() Executor().ToSQL()
@@ -580,7 +580,7 @@ func (t *maskFieldTest) TestInsert_KeepsFieldWithWriteAccess() {
func (t *maskFieldTest) TestUpdate_DropsFieldWithoutWriteAccess() { func (t *maskFieldTest) TestUpdate_DropsFieldWithoutWriteAccess() {
res, _ := t.newResource(nameField, salaryField) res, _ := t.newResource(nameField, salaryField)
sql, _, _ := res.GetDBTable(testUser{}).Update(). sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).Update().
Set(db.Record{"name": "李四", "salary": 888888}). Set(db.Record{"name": "李四", "salary": 888888}).
Where(db.C("id").Eq(1)). Where(db.C("id").Eq(1)).
Executor().ToSQL() Executor().ToSQL()
+130
View File
@@ -0,0 +1,130 @@
package resx_test
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/samber/do/v2"
"github.com/stretchr/testify/suite"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/engine"
"git.fsdpf.net/go/req"
"git.fsdpf.net/go/req/resx"
_ "git.fsdpf.net/go/db/dialect/mysql"
)
// permissionTest 验证 req.WithPermission(req.ResFlags) 各个取值(不传/传 0/单独 ResRow/单独
// ResMask/ResAll)分别独立生效:ResRow 控制行级过滤条件要不要拼进查询,ResMask 控制字段级脱敏
// 要不要生效,两者互不影响彼此。GetDBTable 不传 WithPermission 时按文档约定的零值处理(两者都
// 跳过),这里也显式测出来,作为这个默认行为的回归防护——之后如果谁想改这个默认值,这些测试会
// 先失败提醒。
type permissionTest struct {
suite.Suite
}
func TestPermission(t *testing.T) {
suite.Run(t, new(permissionTest))
}
// newPermissionResource 构造一个既配置了受限字段(salary,Roles=hr)、又注册了行级过滤条件
// owned_user = 当前用户)的资源,用同一份数据分别验证 ResRow/ResMask 两个开关。
func (t *permissionTest) newPermissionResource() (req.Resource, sqlmock.Sqlmock) {
mockDB, mock, err := sqlmock.New()
t.Require().NoError(err)
app := do.New()
do.Provide(app, func(i do.Injector) (engine.Engine, error) {
return engine.Mock(map[string]engine.MockDBConfig{
"default": {Driver: "mysql", Mock: mockDB},
}), nil
})
do.Provide(app, func(i do.Injector) (resx.DataInterceptor, error) {
return func(res req.Resource, u req.User, category resx.ResEventType) (*db.SelectDataset, db.Expression, resx.ResChangeRowFunc) {
return nil, db.C("owned_user").Eq(u.Uuid()), nil
}, nil
})
res := resx.New(app, "User", "users",
resx.WithConn("default"),
resx.WithFields(nameField, salaryField),
)
return res, mock
}
// TestNoPermissionOption_SkipsBothRowFilterAndMask 不传 WithPermissionPermission 零值,行级
// 过滤和字段脱敏都跳过——这是 options.go 里 Permission 字段注释明确写的默认行为
func (t *permissionTest) TestNoPermissionOption_SkipsBothRowFilterAndMask() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}).Select(db.Star()).Executor().ToSQL()
t.NotContains(sql, "owned_user", "不传 WithPermission 时行级过滤应该被跳过")
t.NotContains(sql, "-999999999", "不传 WithPermission 时字段脱敏应该被跳过")
}
// TestExplicitZeroPermission_SkipsBothRowFilterAndMask 显式传 WithPermission(0),效果跟不传一样
func (t *permissionTest) TestExplicitZeroPermission_SkipsBothRowFilterAndMask() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(0)).Select(db.Star()).Executor().ToSQL()
t.NotContains(sql, "owned_user")
t.NotContains(sql, "-999999999")
}
// TestResRowOnly_EnablesRowFilterOnlyNotMask 只传 ResRow:行级过滤生效,字段脱敏依旧跳过——两个
// 开关相互独立,不是绑在一起的
func (t *permissionTest) TestResRowOnly_EnablesRowFilterOnlyNotMask() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResRow)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user", "传了 ResRow,行级过滤应该生效")
t.NotContains(sql, "-999999999", "没传 ResMask,字段脱敏不该生效")
}
// TestResMaskOnly_EnablesMaskOnlyNotRowFilter 只传 ResMask:字段脱敏生效,行级过滤依旧跳过
func (t *permissionTest) TestResMaskOnly_EnablesMaskOnlyNotRowFilter() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResMask)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "-999999999 AS `salary`", "传了 ResMask,字段脱敏应该生效")
t.NotContains(sql, "owned_user", "没传 ResRow,行级过滤不该生效")
}
// TestResAll_EnablesBothRowFilterAndMask 传 ResAll:行级过滤和字段脱敏都生效
func (t *permissionTest) TestResAll_EnablesBothRowFilterAndMask() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResAll)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user")
t.Contains(sql, "-999999999 AS `salary`")
}
// TestResRowOrResMaskBitwiseCombo_SameAsResAll 手动按位组合 ResRow|ResMask,效果跟直接传 ResAll
// 一样——ResAll 只是这个组合的一个别名,不是什么特殊值
func (t *permissionTest) TestResRowOrResMaskBitwiseCombo_SameAsResAll() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{}, req.WithPermission(req.ResRow|req.ResMask)).Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "owned_user")
t.Contains(sql, "-999999999 AS `salary`")
}
// TestUserHasRole_ResMaskStillOnButFieldPassesThrough ResMask 开启时,用户角色命中字段 Roles 的话
// 字段本身按真实值查询——ResMask 只是"开启检查",不是"无条件脱敏",命中权限时检查结果是放行
func (t *permissionTest) TestUserHasRole_ResMaskStillOnButFieldPassesThrough() {
res, _ := t.newPermissionResource()
sql, _, _ := res.GetDBTable(testUser{roles: []string{"hr"}}, req.WithPermission(req.ResMask)).
Select(db.Star()).Executor().ToSQL()
t.Contains(sql, "SELECT *", "没有字段需要脱敏时不会展开成显式列表")
t.NotContains(sql, "-999999999")
}