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
143 lines
5.8 KiB
Go
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
|
|
}
|