Add EXT-19 account lister host capability #181
@@ -0,0 +1,53 @@
|
||||
package app
|
||||
|
||||
// EXT-19 消费测试:inprocHost.ListAccounts——最小目录(owner/valid)、游标分页、
|
||||
// limit 钳制(1-200,缺省 50)。数据来自 users 表(真实 store,非桩)。
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"agent-system-backend/internal/store"
|
||||
)
|
||||
|
||||
func TestInprocHostListAccounts(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
st, err := store.OpenSQLite(dir + "/test.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { st.Close() })
|
||||
for _, o := range []string{"carol", "alice", "bob", "dave"} {
|
||||
if _, err := st.CreateAPIKey(o, "e2e"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
h := inprocHost{caps: hostcaps{st: st}}
|
||||
|
||||
// 首页(limit=2):alice/bob,next=bob。
|
||||
page, next, err := h.ListAccounts("", 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(page) != 2 || page[0].Owner != "alice" || page[1].Owner != "bob" {
|
||||
t.Fatalf("首页不符: %+v", page)
|
||||
}
|
||||
for _, e := range page {
|
||||
if !e.Valid {
|
||||
t.Fatalf("现有账号应 valid: %+v", e)
|
||||
}
|
||||
}
|
||||
if next != "bob" {
|
||||
t.Fatalf("next 应为 bob, got %q", next)
|
||||
}
|
||||
// 次页:carol/dave,next=""(末页)。
|
||||
page2, next2, _ := h.ListAccounts(next, 2)
|
||||
if len(page2) != 2 || page2[0].Owner != "carol" || page2[1].Owner != "dave" || next2 != "" {
|
||||
t.Fatalf("次页不符: %+v next=%q", page2, next2)
|
||||
}
|
||||
// limit 钳制:0 → 50 缺省;超 200 → 200(不炸)。
|
||||
if p, _, _ := h.ListAccounts("", 0); len(p) != 4 {
|
||||
t.Fatalf("limit=0 应取缺省(4 账号全量), got %d", len(p))
|
||||
}
|
||||
if _, _, err := h.ListAccounts("", 999); err != nil {
|
||||
t.Fatalf("limit 超限应钳制不报错: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -171,6 +171,46 @@ func (h inprocHost) IsAdmin() bool {
|
||||
return state.IsAdmin
|
||||
}
|
||||
|
||||
// ListAccounts 实现 EXT-19 AccountLister(最小账号目录):仅授权管理读面用。
|
||||
// 有效态=账号存在且非停用(当前 users 表无停用位——must_change_password 不影响
|
||||
// 有效性;后续加停用位在此映射)。游标=owner 升序偏移。
|
||||
func (h inprocHost) ListAccounts(cursor string, limit int) ([]extension.AccountEntry, string, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 50
|
||||
}
|
||||
users, err := h.caps.st.ListUsersAdmin()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
entries := make([]extension.AccountEntry, 0, len(users))
|
||||
for _, u := range users {
|
||||
entries = append(entries, extension.AccountEntry{Owner: u.Owner, Valid: true})
|
||||
}
|
||||
// 排序+游标(owner 升序;cursor 为上一页末 owner,空=首页)。
|
||||
for i := 0; i < len(entries); i++ {
|
||||
for j := i + 1; j < len(entries); j++ {
|
||||
if entries[j].Owner < entries[i].Owner {
|
||||
entries[i], entries[j] = entries[j], entries[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
start := 0
|
||||
if cursor != "" {
|
||||
for start < len(entries) && entries[start].Owner <= cursor {
|
||||
start++
|
||||
}
|
||||
}
|
||||
end := start + limit
|
||||
if end > len(entries) {
|
||||
end = len(entries)
|
||||
}
|
||||
next := ""
|
||||
if end < len(entries) {
|
||||
next = entries[end-1].Owner
|
||||
}
|
||||
return entries[start:end], next, nil
|
||||
}
|
||||
|
||||
func (h inprocHost) KVPut(key, value string) error {
|
||||
return h.caps.st.PutPluginKV(h.pluginID, h.owner, key, value)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package extension
|
||||
|
||||
// EXT-19 红测(方案 r1 U3/KTD2 最小账号目录):公开面新增可选能力接口
|
||||
// AccountLister(ListAccounts:owner+有效态+游标分页——授权管理所需的最小身份,
|
||||
// 不含姓名/邮箱/联系方式)。红:类型未定义,编译红先行。
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAccountListerSurface(t *testing.T) {
|
||||
if !strings.HasPrefix(SurfaceVersion, "2.11.0") {
|
||||
t.Fatalf("SurfaceVersion 应升至 2.11.0-experimental(新增导出符号须升版),got %s", SurfaceVersion)
|
||||
}
|
||||
// 接口方法签名即契约:游标分页 + 最小字段。
|
||||
var _ AccountLister = (AccountLister)(nil)
|
||||
e := AccountEntry{Owner: "u1", Valid: true}
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(b), `"owner":"u1"`) || !strings.Contains(string(b), `"valid":true`) {
|
||||
t.Fatalf("AccountEntry 序列化不符(最小字段 owner/valid): %s", b)
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,14 @@ import (
|
||||
// 2.8.0-experimental:EXT-17 新增可选能力接口 SurfaceHost(SurfaceSave/Get/List/
|
||||
// Activation;Generative UI U2)。实现由基座 hostcaps 注入;未装配时类型断言
|
||||
// 不成立即明确拒绝,不给必实现 Host 接口加方法(不破坏既有消费者)。
|
||||
// 2.11.0-experimental:EXT-19 新增可选能力接口 AccountLister(ListAccounts:
|
||||
// 最小账号目录——owner+有效态+游标分页,U3/KTD2;不含姓名/联系方式)。
|
||||
// 2.10.0-experimental:EXT-8b 新增 ResourceAuthzQuery/ResourceAuthorizerV2 与
|
||||
// Registry.RegisterResourceAuthorizerV2(资源级授权,U9 首切片)。
|
||||
// 2.9.0-experimental:Registry.AddAdminZone 与 AdminZoneSpec(WP3-I3/KTD4:编译
|
||||
// 插件向基座管理区声明展示分区;分区入口不做基座级权限判定,数据能力由
|
||||
// 分区组件自身的命令面 fail-closed)。
|
||||
const SurfaceVersion = "2.10.0-experimental"
|
||||
const SurfaceVersion = "2.11.0-experimental"
|
||||
|
||||
// ErrNotLinked 表示进程没有空白导入 link 包,Main 无法启动服务。
|
||||
var ErrNotLinked = errors.New("extension: runner not linked; blank-import agent-system-backend/pkg/extension/link")
|
||||
@@ -80,6 +82,20 @@ type Registry interface {
|
||||
AddAdminZone(spec json.RawMessage) error
|
||||
}
|
||||
|
||||
// AccountEntry 最小账号目录行(U3/KTD2):owner+有效态——授权管理所需的最小
|
||||
// 身份;姓名/邮箱/联系方式一律不下发(隐私边界)。
|
||||
type AccountEntry struct {
|
||||
Owner string `json:"owner"`
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
// AccountLister EXT-19 可选能力接口(实现由宿主注入):游标分页列举系统账号。
|
||||
// 未实现时下游回落现行行为(成员添加不做账号有效性校验);实现后仅用于
|
||||
// 授权管理读面——不授予任何写权。
|
||||
type AccountLister interface {
|
||||
ListAccounts(cursor string, limit int) (entries []AccountEntry, next string, err error)
|
||||
}
|
||||
|
||||
// AdminZoneSpec 管理区分区声明(WP3-I3):ID 是分区身份(建议形如
|
||||
// <plugin>.<section>,前端按 ID 映射内置分区组件);Title/Icon/Desc 为展示
|
||||
// 元数据(icon 取基座前端 IconName 词表,未知值前端回退通用图标);Order
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
)
|
||||
|
||||
func TestResourceAuthzV2SurfacePresent(t *testing.T) {
|
||||
if !strings.HasPrefix(SurfaceVersion, "2.10.0") {
|
||||
t.Fatalf("SurfaceVersion 应升至 2.10.0-experimental(新增导出符号须升版),got %s", SurfaceVersion)
|
||||
if SurfaceVersion < "2.10.0" {
|
||||
t.Fatalf("SurfaceVersion 应 ≥2.10.0-experimental(EXT-8b 起生效),got %s", SurfaceVersion)
|
||||
}
|
||||
var _ ResourceAuthorizerV2 = func(q ResourceAuthzQuery) (bool, error) {
|
||||
return q.Actor != "" && q.Action != "" && q.ResourceType != "" && q.ResourceID != "", nil
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
AccountEntry
|
||||
AccountLister
|
||||
AccountLister.ListAccounts
|
||||
AdminProvider
|
||||
AdminProvider.IsAdmin
|
||||
AdminZoneSpec
|
||||
|
||||
Reference in New Issue
Block a user