package accesscontrol import ( "net/http" "net/http/httptest" "slices" "strings" "testing" "github.com/Tencent/WeKnora/internal/types" "github.com/gin-gonic/gin" ) func TestGranularActionsDoNotRequireGenericWrite(t *testing.T) { for _, tc := range []struct{ method, route, path, permission string }{ {"POST", "/skills/instructions/mount", "/skills/instructions/mount", "agents.skills"}, {"POST", "/memory/consolidate", "/memory/consolidate", "agents.memory"}, {"DELETE", "/agents/:id", "/agents/owned", "agents.delete"}, {"POST", "/digital-humans/:id/distill", "/digital-humans/owned/distill", "digital-humans.distill"}, {"POST", "/digital-humans/:id/review", "/digital-humans/owned/review", "digital-humans.review"}, {"DELETE", "/digital-humans/:id", "/digital-humans/owned", "digital-humans.delete"}, {"POST", "/wechat/qrcode", "/wechat/qrcode", "digital-humans.wechat"}, {"PATCH", "/workbench/operations/consumption", "/workbench/operations/consumption", "knowledge.consumption"}, {"DELETE", "/workbench/resources/:kind/:id", "/workbench/resources/flows/owned", "flows.delete"}, {"POST", "/workbench/resources/:kind/:id/actions/:action", "/workbench/resources/training-maps/owned/actions/generate", "content.maps"}, {"POST", "/workbench/resources/:kind/:id/actions/:action", "/workbench/resources/content-tasks/owned/actions/decompose", "content.generate"}, {"POST", "/workbench/resources/:kind/:id/actions/:action", "/workbench/resources/content-tasks/owned/actions/compose", "content.review"}, } { t.Run(tc.permission, func(t *testing.T) { s := versionDB(t) module, _, _ := strings.Cut(tc.permission, ".") permissions := types.StringArray{module + ".read", tc.permission} if !validPermissions(permissions) || validPermissions([]string{tc.permission}) { t.Fatal("action must require only its module read prerequisite") } role := Role{ID: "precise", TenantID: 1, Name: "precise", PermissionVersion: currentPermissionVersion, Permissions: permissions} if err := s.db.Create(&role).Error; err != nil { t.Fatal(err) } if err := s.db.Create(&Assignment{TenantID: 1, UserID: "alice", RoleID: role.ID}).Error; err != nil { t.Fatal(err) } calls := 0 router := gin.New() router.Use(s.Middleware()) router.Handle(tc.method, "/api/v1"+tc.route, func(c *gin.Context) { calls++; c.Status(http.StatusNoContent) }) request := func() int { req := httptest.NewRequest(tc.method, "/api/v1"+tc.path, nil).WithContext(versionContext(1, "alice", false)) res := httptest.NewRecorder() router.ServeHTTP(res, req) return res.Code } if status := request(); status != http.StatusNoContent { t.Fatalf("explicit action blocked: %d", status) } if err := s.db.Model(&role).Update("permissions", types.StringArray{module + ".read"}).Error; err != nil { t.Fatal(err) } if status := request(); status != http.StatusForbidden || calls != 1 { t.Fatalf("revocation ignored: status=%d calls=%d", status, calls) } }) } } func TestManagementEndpointsCannotBeGrantedToOrdinaryUsers(t *testing.T) { for _, tc := range []struct{ method, route, path string }{ {"GET", "/evaluation", "/evaluation"}, {"GET", "/tenants/:id/members", "/tenants/1/members"}, {"PUT", "/tenants/kv/:key", "/tenants/kv/config"}, {"GET", "/me/env-vars", "/me/env-vars"}, {"GET", "/workbench/operations/records/:recordKind", "/workbench/operations/records/usage"}, } { t.Run(tc.path, func(t *testing.T) { s := versionDB(t) role := Role{ID: "attempted-admin", TenantID: 1, Name: "attempted-admin", PermissionVersion: currentPermissionVersion, Permissions: types.StringArray{"settings.read", "settings.write", "usage.read"}} if err := s.db.Create(&role).Error; err != nil { t.Fatal(err) } if err := s.db.Create(&Assignment{TenantID: 1, UserID: "alice", RoleID: role.ID}).Error; err != nil { t.Fatal(err) } router := gin.New() router.Use(s.Middleware()) router.Handle(tc.method, "/api/v1"+tc.route, func(c *gin.Context) { c.Status(http.StatusNoContent) }) for _, admin := range []bool{false, true} { req := httptest.NewRequest(tc.method, "/api/v1"+tc.path, nil).WithContext(versionContext(1, "alice", admin)) res := httptest.NewRecorder() router.ServeHTTP(res, req) want := http.StatusForbidden if admin { want = http.StatusNoContent } if res.Code != want { t.Fatalf("admin=%v got=%d want=%d", admin, res.Code, want) } } }) } } func TestLegacyRolesExpandOnlyUntilSavedAsGranular(t *testing.T) { role := Role{Permissions: types.StringArray{"content.read", "content.write", "agents.read", "agents.write", "digital-humans.read", "digital-humans.write"}} legacy := effectiveRolePermissions(role) for _, permission := range []string{"content.generate", "content.maps", "content.delete", "agents.skills", "agents.memory", "agents.delete", "digital-humans.distill", "digital-humans.review", "digital-humans.wechat", "digital-humans.delete"} { if !slices.Contains(legacy, permission) { t.Fatalf("lost legacy capability %s", permission) } } for _, permission := range []string{"content.review", "content.deliver", "settings.write"} { if slices.Contains(legacy, permission) { t.Fatalf("unexpected permission %s", permission) } } role.PermissionVersion = currentPermissionVersion if !slices.Equal(effectiveRolePermissions(role), role.Permissions) { t.Fatal("granular selections were broadened") } }