package executionlog import ( "context" _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "testing" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" ) func TestPrincipalIsolation(t *testing.T) { ctx := context.Background() if _, _, err := List(ctx, nil); err == nil { t.Fatal("guest allowed") } if _, err := Summary(ctx, "mine"); err == nil { t.Fatal("guest summary allowed") } if _, _, err := Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: "mine"}); err == nil { t.Fatal("guest details allowed") } if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}}); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = g.DB().Close(ctx) }) for _, sql := range []string{ "CREATE TABLE batch_main (id INTEGER PRIMARY KEY, batch_no TEXT, batch_type INTEGER, source_batch_no TEXT, source_channel TEXT, execution_mode INTEGER, source_type INTEGER, batch_async_status INTEGER, total_count INTEGER, run_result INTEGER, fail_reason TEXT, created_at TEXT, start_time TEXT, end_time TEXT, tenant_id TEXT, created_by INTEGER, created_dept INTEGER DEFAULT 10, target_dept_id INTEGER DEFAULT 0, batch_info TEXT)", "CREATE TABLE batch_detail (id INTEGER PRIMARY KEY, batch_no TEXT, row_no INTEGER, item_id TEXT, detail_status INTEGER, error_code TEXT, error_reason TEXT, tenant_id TEXT, created_by INTEGER, created_dept INTEGER DEFAULT 10, snapshot TEXT)", "INSERT INTO batch_main(id,batch_no,batch_type,tenant_id,created_by,source_channel,execution_mode,source_type) VALUES (1,'mine',101,'A',7,'mini_program',1,0),(2,'other-user',101,'A',8,'console',2,3),(3,'other-tenant',101,'B',7,'console',2,3)", "INSERT INTO batch_detail(id,batch_no,row_no,item_id,detail_status,error_code,error_reason,tenant_id,created_by) VALUES(1,'mine',1,'card1',3,'ERR','套餐已过期','A',7),(2,'mine',2,'hidden',3,'ERR','hidden','B',7)", } { if _, err := g.DB().Exec(ctx, sql); err != nil { t.Fatal(err) } } ctx = contexts.Set(ctx, &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 7, DeptId: 10, TenantId: "A", Roles: []model.Role{{RoleId: 82, DataScope: consts.SysRoleDataScopePersonal}}}}}) list, total, err := List(ctx, nil) if err != nil || total != 1 || len(list) != 1 || list[0].BatchNo != "mine" { t.Fatalf("list=%+v total=%d err=%v", list, total, err) } for _, no := range []string{"other-user", "other-tenant", "missing"} { if _, err := Summary(ctx, no); err == nil { t.Fatalf("summary exposed %s", no) } if _, _, err := Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: no}); err == nil { t.Fatalf("details exposed %s", no) } } details, total, err := Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: "mine"}) if err != nil || total != 1 || len(details) != 1 || details[0].ItemId != "card1" { t.Fatalf("details=%+v total=%d err=%v", details, total, err) } source := 0 list, total, err = List(ctx, &model.AppExecutionLogListParam{SourceType: &source, SourceChannel: "mini_program"}) if err != nil || total != 1 || len(list) != 1 { t.Fatal("zero filter failed", err, total) } source = 3 _, total, err = List(ctx, &model.AppExecutionLogListParam{SourceType: &source}) if err != nil || total != 0 { t.Fatal("foreign records leaked through filter", err, total) } t.Run("控制台数据范围", func(t *testing.T) { for _, sql := range []string{ "CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, parent_id INTEGER, dept_type INTEGER, ancestors TEXT,tenant_id TEXT,dept_key TEXT,dept_name TEXT,deleted_at TEXT)", "CREATE TABLE sys_role_dept (tenant_id TEXT, role_id INTEGER, dept_id INTEGER)", "INSERT INTO sys_dept(dept_id,parent_id,dept_type,ancestors) VALUES(10,0,1,',0,'),(20,10,1,',0,10,'),(30,20,1,',0,10,20,'),(40,0,1,',0,')", "UPDATE sys_dept SET tenant_id='A',dept_name='部门',dept_key='key'", "INSERT INTO sys_role_dept VALUES('A',82,20),('B',82,40)", "UPDATE batch_main SET created_dept=20 WHERE batch_no='other-user'", "INSERT INTO batch_main(id,batch_no,batch_type,tenant_id,created_by,created_dept) VALUES(20,'grandchild',101,'A',9,30),(21,'outside',101,'A',10,40)", "INSERT INTO batch_detail(id,batch_no,row_no,item_id,detail_status,tenant_id,created_by,created_dept) VALUES(50,'other-user',1,'other-card',2,'A',8,20)", } { if _, err := g.DB().Exec(ctx, sql); err != nil { t.Fatal(err) } } t.Cleanup(func() { _, _ = g.DB().Exec(ctx, "DELETE FROM batch_main WHERE id IN (20,21)") _, _ = g.DB().Exec(ctx, "DELETE FROM batch_detail WHERE id=50") }) cases := []struct { name, roleScope, menuScope string dept, role int64 count int super bool }{ {name: "仅本人", roleScope: "5", dept: 10, role: 82, count: 1}, {name: "本部门", roleScope: "3", dept: 10, role: 82, count: 1}, {name: "本部门及下级", roleScope: "4", dept: 10, role: 82, count: 3}, {name: "自定义部门", roleScope: "2", dept: 10, role: 82, count: 1}, {name: "全部仍隔离租户", roleScope: "1", dept: 10, role: 82, count: 4}, {name: "部门及下级或本人", roleScope: "6", dept: 40, role: 82, count: 2}, {name: "本组织及下一级", roleScope: "7", dept: 10, role: 82, count: 2}, {name: "本组织下一级", roleScope: "8", dept: 10, role: 82, count: 1}, {name: "无下一级", roleScope: "8", dept: 40, role: 82, count: 0}, {name: "自定义空集合", roleScope: "2", dept: 10, role: 99, count: 0}, {name: "菜单收窄角色", roleScope: "1", menuScope: "5", dept: 10, role: 82, count: 1}, {name: "菜单放宽角色", roleScope: "5", menuScope: "1", dept: 10, role: 82, count: 4}, {name: "无角色", dept: 10, count: 0}, {name: "未知范围", roleScope: "unknown", dept: 10, role: 82, count: 0}, {name: "超管仍隔离租户", dept: 10, role: consts.SuperAdminRoleId, count: 4, super: true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { roles := []model.Role{} if tc.role > 0 { roles = append(roles, model.Role{RoleId: tc.role, DataScope: tc.roleScope}) } testCtx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 7, DeptId: tc.dept, TenantId: "A", Roles: roles}}}) if tc.menuScope != "" { contexts.SetDataValue(testCtx, "currentRoleId", "82") contexts.SetDataValue(testCtx, "currentRoleDataScope", tc.menuScope) } list, total, err := List(testCtx, nil) if err != nil || total != tc.count { t.Fatalf("total=%d want=%d err=%v", total, tc.count, err) } for _, item := range list { if _, err := Summary(testCtx, item.BatchNo); err != nil { t.Fatal("列表与概览范围不一致", err) } } if _, err := Summary(testCtx, "other-tenant"); err == nil { t.Fatal("泄露其他租户") } if _, _, err := Details(testCtx, &model.AppExecutionLogDetailParam{BatchNo: "other-tenant"}); err == nil { t.Fatal("泄露其他租户明细") } visibleOther := false for _, item := range list { if item.BatchNo == "other-user" { visibleOther = true } } rows, total, err := Details(testCtx, &model.AppExecutionLogDetailParam{BatchNo: "other-user"}) if visibleOther { if err != nil || total != 1 || rows[0].ItemId != "other-card" { t.Fatalf("可见部门明细缺失: %v %d %v", rows, total, err) } } else if err == nil { t.Fatal("不可见批次明细被放行") } }) } }) for _, sql := range []string{ `INSERT INTO batch_main(id,batch_no,batch_type,source_batch_no,batch_info,tenant_id,created_by) VALUES (4,'PARENT',3,'','{"jobRole":"allocate_parent"}','A',7),(5,'CHILD',3,'PARENT','{"jobRole":"allocate_chunk"}','A',7),(6,'FOREIGN-CHILD',3,'PARENT','{"jobRole":"allocate_chunk"}','A',8),(7,'RELATED',3,'PARENT','{}','A',7)`, "INSERT INTO batch_detail(id,batch_no,row_no,item_id,detail_status,error_code,error_reason,tenant_id,created_by) VALUES(3,'PARENT',1,'parent-card',2,'','','A',7),(4,'CHILD',1,'mine-child',2,'','','A',7),(5,'FOREIGN-CHILD',1,'foreign-child',2,'','','A',8),(6,'RELATED',1,'related-child',2,'','','A',7)", } { if _, err := g.DB().Exec(ctx, sql); err != nil { t.Fatal(err) } } for _, sql := range []string{ `UPDATE batch_main SET target_dept_id=20,batch_info='{"jobRole":"allocate_parent","sourceDeptName":"提交时划拨单位","targetDeptName":"提交时接收单位"}' WHERE batch_no='PARENT'`, `UPDATE batch_detail SET snapshot='{"sourceDeptId":10,"targetDeptId":20,"sourceDeptName":"明细划拨单位","targetDeptName":"明细接收单位"}' WHERE id=4`, } { if _, err := g.DB().Exec(ctx, sql); err != nil { t.Fatal(err) } } summary, err := Summary(ctx, "PARENT") if err != nil || summary.SourceDeptName != "提交时划拨单位" || summary.TargetDeptName != "提交时接收单位" { t.Fatal("概览未优先使用历史名称", summary, err) } details, total, err = Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: "PARENT"}) if err != nil || total != 3 || len(details) != 3 { t.Fatalf("child isolation: %+v %d %v", details, total, err) } seen := map[string]bool{} for _, detail := range details { if detail.ItemId == "mine-child" && (detail.SourceDeptName != "明细划拨单位" || detail.TargetDeptName != "明细接收单位") { t.Fatal("明细快照名称被覆盖", detail) } if detail.ItemId != "mine-child" && detail.TargetDeptName != "提交时接收单位" { t.Fatal("明细未继承批次历史接收单位", detail) } seen[detail.ItemId] = true } for _, itemID := range []string{"parent-card", "mine-child", "related-child"} { if !seen[itemID] { t.Fatalf("parent details missing %s: %+v", itemID, details) } } list, total, err = List(ctx, nil) if err != nil || total != 2 { t.Fatalf("children appeared as submissions: %+v %d %v", list, total, err) } if _, err := AllocateSummary(ctx, "PARENT"); err != nil { t.Fatal("allocate result rejected", err) } if _, total, err := AllocateDetails(ctx, &model.AppExecutionLogDetailParam{BatchNo: "PARENT"}); err != nil || total != 3 { t.Fatal("allocate details rejected", total, err) } allCtx := contexts.Set(context.Background(), &model.Context{User: &model.Identity{BaseClaims: model.BaseClaims{ID: 8, DeptId: 20, TenantId: "A", Roles: []model.Role{{RoleId: 82, DataScope: consts.SysRoleDataScopeAll}}}}}) if _, err := Summary(allCtx, "PARENT"); err != nil { t.Fatal("全部范围不能查看他人批次", err) } if _, err := AllocateSummary(allCtx, "PARENT"); err == nil { t.Fatal("办理恢复接口泄露他人任务") } if _, _, err := AllocateDetails(allCtx, &model.AppExecutionLogDetailParam{BatchNo: "PARENT"}); err == nil { t.Fatal("办理明细接口泄露他人任务") } for _, no := range []string{"mine", "CHILD", "RELATED", "other-user", "other-tenant", "missing"} { if _, err := AllocateSummary(ctx, no); err == nil { t.Fatalf("allocate summary exposed %s", no) } if _, _, err := AllocateDetails(ctx, &model.AppExecutionLogDetailParam{BatchNo: no}); err == nil { t.Fatalf("allocate details exposed %s", no) } } for _, no := range []string{"CHILD", "RELATED"} { if _, err := Summary(ctx, no); err == nil { t.Fatalf("child exposed as a standalone submission: %s", no) } } if _, err := g.DB().Exec(ctx, "INSERT INTO batch_main(id,batch_no,batch_type,source_batch_no,tenant_id,created_by) VALUES (8,'SELF',3,'SELF','A',7)"); err != nil { t.Fatal(err) } if _, err := g.DB().Exec(ctx, "INSERT INTO batch_detail(id,batch_no,row_no,item_id,detail_status,error_code,error_reason,tenant_id,created_by) VALUES(7,'SELF',1,'self-card',2,'','','A',7)"); err != nil { t.Fatal(err) } if _, err := Summary(ctx, "SELF"); err != nil { t.Fatal("self-referencing allocation hidden", err) } details, total, err = Details(ctx, &model.AppExecutionLogDetailParam{BatchNo: "SELF"}) if err != nil || total != 1 || len(details) != 1 || details[0].ItemId != "self-card" { t.Fatalf("self allocation details lost: %+v %d %v", details, total, err) } } func TestSafeReason(t *testing.T) { for _, s := range []string{"token=abc", "dial tcp 192.0.2.10", "secret=abc", "panic\nstack", "https://internal"} { if safeReason(s) == s { t.Fatalf("sensitive reason exposed: %s", s) } } if safeReason("套餐已过期") != "套餐已过期" { t.Fatal("business explanation lost") } p, n := paging(0, 5000) if p != 1 || n != 100 { t.Fatal(p, n) } }