WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
@@ -0,0 +1,335 @@
package accesscontrol
import (
"context"
"encoding/json"
"errors"
"path/filepath"
"testing"
"github.com/Tencent/WeKnora/internal/accesspolicy"
"github.com/Tencent/WeKnora/internal/application/repository"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/workbench"
"github.com/google/uuid"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func versionContext(tenant uint64, user string, admin bool) context.Context {
ctx := context.WithValue(context.Background(), types.TenantIDContextKey, tenant)
ctx = context.WithValue(ctx, types.UserIDContextKey, user)
role := types.TenantRoleContributor
if admin {
role = types.TenantRoleAdmin
}
return context.WithValue(ctx, types.TenantRoleContextKey, role)
}
func versionDB(t *testing.T) *Service {
t.Helper()
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "versions.db")), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&Role{}, &Assignment{}, &Publication{}, &Grant{}, &ResourceFork{}, &ResourceSubmission{}, &ResourceHidden{}, &types.CustomAgent{}, &types.TenantSkillCatalogEntity{}, &types.DigitalHuman{}, &types.DigitalHumanVersion{}, &types.DigitalHumanBinding{}, &types.DigitalHumanJob{}, &workbench.Resource{}, &workbench.Revision{}, &workbench.Job{}, &types.AuditLog{}, &types.KnowledgeBase{}, &types.Knowledge{}); err != nil {
t.Fatal(err)
}
sql, _ := db.DB()
if err = db.Exec("CREATE UNIQUE INDEX uq_test_instruction_name ON tenant_skill_catalog(tenant_id,created_by,name,version_source_id) WHERE deleted_at IS NULL AND instruction_only = 1").Error; err != nil {
t.Fatal(err)
}
if err = db.Exec("CREATE TABLE users(id TEXT PRIMARY KEY, username TEXT, deleted_at DATETIME)").Error; err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sql.Close() })
return NewService(db)
}
func seedGlobal(t *testing.T, s *Service, kind string) string {
t.Helper()
id := uuid.NewString()
var err error
switch kind {
case "agents":
err = s.db.Create(&types.CustomAgent{ID: id, TenantID: 1, CreatedBy: "owner", Name: "原资源", Visibility: "global", Enabled: true, Config: types.CustomAgentConfig{SystemPrompt: "original"}}).Error
case "skills":
err = s.db.Create(&types.TenantSkillCatalogEntity{ID: id, TenantID: 1, CreatedBy: "owner", Name: "原资源", Visibility: "global", Enabled: true, InstructionOnly: true, Instructions: "original"}).Error
case "flows":
err = s.db.Create(&workbench.Resource{ID: id, TenantID: 1, OwnerID: "owner", Kind: "flows", Name: "原资源", Version: 1, Data: workbench.JSON{"nodes": []any{}, "edges": []any{}}}).Error
case "digital-humans":
err = repository.NewDigitalHumanRepository(s.db).Create(versionContext(1, "owner", true), &types.DigitalHuman{ID: id, TenantID: 1, OwnerID: "owner", Name: "原资源", Status: "draft", LatestVersion: 1, Revision: 1, PrimaryAgentID: uuid.NewString()}, map[string]string{"tone": "original"})
}
if err != nil {
t.Fatal(err)
}
if err = s.db.Create(&Publication{TenantID: 1, Kind: kind, ResourceID: id, Global: true}).Error; err != nil {
t.Fatal(err)
}
return id
}
func TestPersonalVersionsPreserveOriginalAndApproveImmutableSubmission(t *testing.T) {
for _, kind := range []string{"agents", "skills", "flows", "digital-humans"} {
t.Run(kind, func(t *testing.T) {
s := versionDB(t)
id := seedGlobal(t, s, kind)
ctx := versionContext(1, "alice", false)
admin := versionContext(1, "owner", true)
f, err := s.forkResource(ctx, kind, id)
if err != nil {
t.Fatal(err)
}
if !f.NoExport || f.PersonalID == id || f.OwnerID != "alice" {
t.Fatal("fork provenance missing")
}
again, err := s.forkResource(ctx, kind, id)
if err != nil || again.ID != f.ID {
t.Fatal("fork must be idempotent", err)
}
bob, err := s.forkResource(versionContext(1, "bob", false), kind, id)
if err != nil || bob.PersonalID == f.PersonalID {
t.Fatal("personal copies shared", err)
}
body, _, err := s.resourceSnapshot(s.db, ctx, kind, f.PersonalID)
if err != nil {
t.Fatal(err)
}
var p map[string]any
_ = json.Unmarshal(body, &p)
p["name"] = "个人修改"
body, _ = json.Marshal(p)
if err = s.applyVersionResource(s.db, ctx, kind, f.PersonalID, body); err != nil {
t.Fatal(err)
}
orig, _, _ := s.resourceSnapshot(s.db, ctx, kind, id)
var original map[string]any
_ = json.Unmarshal(orig, &original)
if original["name"] != "原资源" {
t.Fatal("fork changed global resource")
}
r, err := s.submitResourceVersion(ctx, f.ID, "修改说明")
if err != nil {
t.Fatal(err)
}
if _, err = s.submitResourceVersion(ctx, f.ID, "duplicate"); !errors.Is(err, errConflict) {
t.Fatal("duplicate pending submission accepted", err)
}
p["name"] = "提交后的私有修改"
body, _ = json.Marshal(p)
if err = s.applyVersionResource(s.db, ctx, kind, f.PersonalID, body); err != nil {
t.Fatal(err)
}
if err = s.reviewResourceVersion(ctx, r.ID, true, ""); !errors.Is(err, errVersionDenied) {
t.Fatal("ordinary user approved", err)
}
if err = s.reviewResourceVersion(admin, r.ID, true, "通过"); err != nil {
t.Fatal(err)
}
updated, _, _ := s.resourceSnapshot(s.db, ctx, kind, id)
_ = json.Unmarshal(updated, &original)
if original["name"] != "个人修改" {
t.Fatal("approval did not use immutable snapshot", string(updated))
}
if err = s.reviewResourceVersion(admin, r.ID, true, ""); !errors.Is(err, errConflict) {
t.Fatal("replayed approval accepted", err)
}
})
}
}
func TestSkillPersonalVersionPreservesNameForOwnerAndExistingImport(t *testing.T) {
s := versionDB(t)
id := seedGlobal(t, s, "skills")
if err := s.db.Create(&types.TenantSkillCatalogEntity{ID: uuid.NewString(), TenantID: 1, CreatedBy: "alice", Name: "原资源", InstructionOnly: true, Instructions: "independent import"}).Error; err != nil {
t.Fatal(err)
}
for _, user := range []string{"owner", "alice"} {
ctx := versionContext(1, user, user == "owner")
fork, err := s.forkResource(ctx, "skills", id)
if err != nil {
t.Fatal(err)
}
var personal types.TenantSkillCatalogEntity
if err = s.db.First(&personal, "id=?", fork.PersonalID).Error; err != nil {
t.Fatal(err)
}
if personal.Name != "原资源" || personal.VersionSourceID != id {
t.Fatal("personal version lost original name or source")
}
}
}
func TestDeletedSkillPersonalVersionCanBeRecreatedWithoutLosingHistory(t *testing.T) {
s := versionDB(t)
id := seedGlobal(t, s, "skills")
ctx := versionContext(1, "owner", true)
fork, err := s.forkResource(ctx, "skills", id)
if err != nil {
t.Fatal(err)
}
submission, err := s.submitResourceVersion(ctx, fork.ID, "history")
if err != nil {
t.Fatal(err)
}
if err = s.db.Where("id=?", fork.PersonalID).Delete(&types.TenantSkillCatalogEntity{}).Error; err != nil {
t.Fatal(err)
}
if _, err = s.forkResource(ctx, "skills", id); !errors.Is(err, errConflict) {
t.Fatal("pending submission must block recreation", err)
}
if err = s.reviewResourceVersion(ctx, submission.ID, false, "withdraw old version"); err != nil {
t.Fatal(err)
}
recreated, err := s.forkResource(ctx, "skills", id)
if err != nil {
t.Fatal(err)
}
if recreated.PersonalID == fork.PersonalID || recreated.ID != fork.ID {
t.Fatal("recreation must keep provenance and create a fresh resource")
}
if recreated.CreatedAt.IsZero() || !recreated.CreatedAt.Equal(fork.CreatedAt) {
t.Fatal("recreation must preserve the original provenance timestamp")
}
var saved ResourceSubmission
if err = s.db.First(&saved, "id=?", submission.ID).Error; err != nil {
t.Fatal(err)
}
if saved.PersonalID != fork.PersonalID || saved.Status != "rejected" {
t.Fatal("recreation changed historical submission")
}
}
func TestPersonalVersionsRejectConflictsAndForeignScope(t *testing.T) {
s := versionDB(t)
id := seedGlobal(t, s, "skills")
ctx := versionContext(1, "alice", false)
if _, err := s.forkResource(versionContext(2, "alice", false), "skills", id); !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatal("cross tenant fork accepted", err)
}
denied := accesspolicy.WithSnapshot(ctx, &accesspolicy.Snapshot{Restricted: true, Permissions: map[string]bool{"agents.read": true}})
if _, err := s.forkResource(denied, "skills", id); err == nil {
t.Fatal("denied module fork accepted")
}
f, err := s.forkResource(ctx, "skills", id)
if err != nil {
t.Fatal(err)
}
if _, err = s.submitResourceVersion(versionContext(1, "bob", false), f.ID, ""); !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatal("other user's fork submitted", err)
}
r, err := s.submitResourceVersion(ctx, f.ID, "")
if err != nil {
t.Fatal(err)
}
s.db.Model(&types.TenantSkillCatalogEntity{}).Where("id=?", id).Update("instructions", "new global")
if err = s.reviewResourceVersion(versionContext(1, "owner", true), r.ID, true, ""); !errors.Is(err, errConflict) {
t.Fatal("stale global overwritten", err)
}
var stored types.TenantSkillCatalogEntity
s.db.First(&stored, "id=?", id)
if stored.Instructions != "new global" {
t.Fatal("conflict changed source")
}
if err = s.reviewResourceVersion(versionContext(2, "admin", true), r.ID, false, ""); !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatal("cross tenant review accepted", err)
}
if err = s.reviewResourceVersion(versionContext(1, "owner", true), r.ID, false, "已有新版本"); err != nil {
t.Fatal(err)
}
}
func TestPersonalVersionsRefusePrivateGlobalDependencies(t *testing.T) {
s := versionDB(t)
source := seedGlobal(t, s, "agents")
ctx := versionContext(1, "alice", false)
privateSkill := types.TenantSkillCatalogEntity{ID: uuid.NewString(), TenantID: 1, CreatedBy: "alice", Name: "private", Visibility: "private", InstructionOnly: true, Enabled: true, Instructions: "private"}
if err := s.db.Create(&privateSkill).Error; err != nil {
t.Fatal(err)
}
f, err := s.forkResource(ctx, "agents", source)
if err != nil {
t.Fatal(err)
}
var a types.CustomAgent
s.db.First(&a, "id=?", f.PersonalID)
a.Config.InstructionSkillIDs = []string{privateSkill.ID}
s.db.Save(&a)
if _, err = s.submitResourceVersion(ctx, f.ID, ""); !errors.Is(err, errInvalid) {
t.Fatal("private dependency published", err)
}
}
func TestPersonalAccessHidingAndExportProvenance(t *testing.T) {
s := versionDB(t)
source := seedGlobal(t, s, "skills")
ctx := versionContext(1, "alice", false)
f, err := s.forkResource(ctx, "skills", source)
if err != nil {
t.Fatal(err)
}
if err = s.db.Create(&ResourceHidden{TenantID: 1, OwnerID: "alice", Kind: "skills", ResourceID: source}).Error; err != nil {
t.Fatal(err)
}
for _, user := range []string{"alice", "bob"} {
v := &accesspolicy.Snapshot{Publications: map[string]bool{"skills:" + source: true}}
if err = s.populatePersonalAccess(versionContext(1, user, false), v); err != nil {
t.Fatal(err)
}
if v.Hidden["skills:"+source] != (user == "alice") {
t.Fatal("hide crossed users")
}
if !v.NoExport["skills:"+f.PersonalID] {
t.Fatal("fork lost provenance")
}
if accesspolicy.Readable(accesspolicy.WithSnapshot(ctx, v), "skills", source, false, true) == (user == "alice") {
t.Fatal("hidden shared resource readable")
}
}
}
func TestHiddenDigitalHumanAlsoHidesItsDefaultAgent(t *testing.T) {
s := versionDB(t)
id := seedGlobal(t, s, "digital-humans")
ctx := versionContext(1, "alice", false)
f, err := s.forkResource(ctx, "digital-humans", id)
if err != nil {
t.Fatal(err)
}
var human types.DigitalHuman
if err = s.db.First(&human, "id=?", f.PersonalID).Error; err != nil {
t.Fatal(err)
}
if err = s.db.Create(&ResourceHidden{TenantID: 1, OwnerID: "alice", Kind: "digital-humans", ResourceID: f.PersonalID}).Error; err != nil {
t.Fatal(err)
}
snapshot := &accesspolicy.Snapshot{}
if err = s.populatePersonalAccess(ctx, snapshot); err != nil {
t.Fatal(err)
}
ctx = accesspolicy.WithSnapshot(ctx, snapshot)
if accesspolicy.Readable(ctx, "agents", human.PrimaryAgentID, true, false) {
t.Fatal("hidden digital human default Agent remains visible")
}
_, err = repository.NewDigitalHumanRepository(s.db).Get(ctx, types.DigitalHumanScope{TenantID: 1, OwnerID: "alice"}, f.PersonalID)
if !errors.Is(err, types.ErrDigitalHumanNotFound) {
t.Fatal("hidden owned digital human remains readable", err)
}
if _, err = repository.NewDigitalHumanRepository(s.db).Get(versionContext(1, "owner", true), types.DigitalHumanScope{TenantID: 1, OwnerID: "owner", ReadAll: true}, f.PersonalID); err != nil {
t.Fatal("personal hiding removed administrator access", err)
}
}
func TestBoundAgentForkRequiresDigitalHumanVersion(t *testing.T) {
s := versionDB(t)
id := seedGlobal(t, s, "digital-humans")
var human types.DigitalHuman
if err := s.db.First(&human, "id=?", id).Error; err != nil {
t.Fatal(err)
}
if err := s.db.Create(&Publication{TenantID: 1, Kind: "agents", ResourceID: human.PrimaryAgentID, Global: true}).Error; err != nil {
t.Fatal(err)
}
if _, err := s.forkResource(versionContext(1, "alice", false), "agents", human.PrimaryAgentID); !errors.Is(err, errInvalid) {
t.Fatal("bound Agent cloned without its digital human relationship", err)
}
}