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
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:
@@ -0,0 +1,119 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user