Files
xinmi/internal/accesscontrol/permission_version_migration_test.go
yawan f7fc992f57
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
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

143 lines
5.8 KiB
Go

package accesscontrol
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/Tencent/WeKnora/internal/types"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestRolePermissionVersionMigrationPreservesLegacyAndAllowsGranularSave(t *testing.T) {
for _, dialect := range []string{"sqlite", "postgres"} {
t.Run(dialect, func(t *testing.T) {
db := permissionMigrationDB(t, dialect)
migrationDir := "sqlite"
if dialect == "postgres" {
migrationDir = "versioned"
applyPermissionMigration(t, db, migrationDir, "000196_workbench_access.up.sql")
} else {
require.NoError(t, db.Exec(`CREATE TABLE workbench_access_roles (
id TEXT PRIMARY KEY, tenant_id INTEGER NOT NULL, name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '', permissions TEXT NOT NULL DEFAULT '[]',
version INTEGER NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (tenant_id, name))`).Error)
}
require.False(t, db.Migrator().HasColumn(&Role{}, "permission_version"))
require.NoError(t, db.Exec(`INSERT INTO workbench_access_roles
(id, tenant_id, name, permissions, version) VALUES (?, ?, ?, ?, ?)`,
"legacy", 1, "legacy", types.StringArray{"agents.read", "agents.write"}, 7).Error)
applyPermissionMigration(t, db, migrationDir, "000215_access_role_permission_version.up.sql")
var legacy Role
require.NoError(t, db.First(&legacy, "id = ?", "legacy").Error)
require.Equal(t, 0, legacy.PermissionVersion)
require.Equal(t, 7, legacy.Version)
require.Equal(t, types.StringArray{"agents.read", "agents.write"}, legacy.Permissions)
require.Contains(t, effectiveRolePermissions(legacy), "agents.skills")
require.Contains(t, effectiveRolePermissions(legacy), "agents.memory")
// Only unrelated audit infrastructure uses AutoMigrate: the role schema
// must come from the shipped SQL to catch production upgrade omissions.
require.NoError(t, db.AutoMigrate(&types.AuditLog{}))
s := NewService(db)
router := gin.New()
s.Register(router.Group("/api/v1"))
for _, action := range []struct{ method, path, body string }{
{"POST", "/api/v1/access/roles", `{"name":"new-granular","permissions":["agents.read","agents.write"]}`},
{"PUT", "/api/v1/access/roles/legacy", `{"name":"legacy","permissions":["agents.read","agents.write"],"version":7}`},
} {
req := httptest.NewRequest(action.method, action.path, strings.NewReader(action.body)).WithContext(versionContext(1, "admin", true))
req.Header.Set("Content-Type", "application/json")
res := httptest.NewRecorder()
router.ServeHTTP(res, req)
require.Equal(t, http.StatusOK, res.Code, res.Body.String())
}
var roles []Role
require.NoError(t, db.Order("name").Find(&roles).Error)
require.Len(t, roles, 2)
for _, role := range roles {
require.Equal(t, currentPermissionVersion, role.PermissionVersion)
require.NotContains(t, effectiveRolePermissions(role), "agents.skills")
require.NotContains(t, effectiveRolePermissions(role), "agents.memory")
}
if dialect == "postgres" {
// The production hotfix can precede the next deployment; rerunning
// migration 215 must not reset already saved granular selections.
applyPermissionMigration(t, db, migrationDir, "000215_access_role_permission_version.up.sql")
require.NoError(t, db.First(&legacy, "id = ?", "legacy").Error)
require.Equal(t, currentPermissionVersion, legacy.PermissionVersion)
require.Equal(t, 8, legacy.Version)
}
applyPermissionMigration(t, db, migrationDir, "000215_access_role_permission_version.down.sql")
require.False(t, db.Migrator().HasColumn(&Role{}, "permission_version"))
var count int64
require.NoError(t, db.Table("workbench_access_roles").Count(&count).Error)
require.EqualValues(t, 2, count)
})
}
}
func applyPermissionMigration(t *testing.T, db *gorm.DB, dialect, name string) {
t.Helper()
sql, err := os.ReadFile(filepath.Join("..", "..", "migrations", dialect, name))
require.NoError(t, err)
require.NoError(t, db.Exec(string(sql)).Error)
}
func permissionMigrationDB(t *testing.T, dialect string) *gorm.DB {
t.Helper()
var dialector gorm.Dialector
if dialect == "sqlite" {
dialector = sqlite.Open(filepath.Join(t.TempDir(), "roles.db"))
} else {
bin := os.Getenv("POSTGRES_TEST_BIN")
if bin == "" {
bin = "/opt/homebrew/opt/postgresql@16/bin"
}
if _, err := os.Stat(filepath.Join(bin, "initdb")); err != nil {
t.Skip("isolated PostgreSQL binaries unavailable; set POSTGRES_TEST_BIN")
}
dir, err := os.MkdirTemp("/tmp", "role-migration-pg-")
require.NoError(t, err)
run := func(name string, args ...string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
output, err := exec.CommandContext(ctx, filepath.Join(bin, name), args...).CombinedOutput()
require.NoError(t, err, "%s: %s", name, output)
}
data := filepath.Join(dir, "data")
running := false
t.Cleanup(func() {
if running {
run("pg_ctl", "-D", data, "-m", "fast", "-w", "stop")
}
require.NoError(t, os.RemoveAll(dir))
})
run("initdb", "-D", data, "-U", "fixture", "--auth=trust", "--no-locale", "--encoding=UTF8")
run("pg_ctl", "-D", data, "-l", filepath.Join(dir, "postgres.log"), "-o",
fmt.Sprintf("-F -c listen_addresses='' -k %s -p 55461", dir), "-w", "start")
running = true
dialector = postgres.Open(fmt.Sprintf("host=%s port=55461 user=fixture dbname=postgres sslmode=disable", dir))
}
db, err := gorm.Open(dialector, &gorm.Config{})
require.NoError(t, err)
sqlDB, err := db.DB()
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
return db
}