This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+90
View File
@@ -61,6 +61,13 @@ func (r *permFakeRepo) GetSessionByID(_ context.Context, id uuid.UUID) (*acp.Ses
out := *s
return &out, nil
}
func (r *permFakeRepo) GetSessionByStepID(context.Context, uuid.UUID) (*acp.Session, error) {
return nil, errs.New(errs.CodeAcpSessionNotFound, "not found")
}
func (r *permFakeRepo) GetCrashedSessionForResume(context.Context, uuid.UUID) (*acp.Session, error) {
return nil, errs.New(errs.CodeAcpSessionNotFound, "not found")
}
func (r *permFakeRepo) ResetSessionForResume(context.Context, uuid.UUID) error { return nil }
func (r *permFakeRepo) DecidePermissionRequest(_ context.Context, id uuid.UUID, status acp.PermissionStatus, chosen *string, decidedBy *uuid.UUID) (*acp.PermissionRequest, bool, error) {
r.mu.Lock()
@@ -89,6 +96,24 @@ func (r *permFakeRepo) ExpirePendingPermissionRequestsBySession(_ context.Contex
return n, nil
}
func (r *permFakeRepo) ListPendingPermissionRequestsByUser(_ context.Context, userID uuid.UUID) ([]*acp.PermissionRequest, error) {
r.mu.Lock()
defer r.mu.Unlock()
var out []*acp.PermissionRequest
for _, p := range r.reqs {
if p.Status != acp.PermissionPending {
continue
}
sess, ok := r.sessions[p.SessionID]
if !ok || sess.UserID != userID {
continue
}
cp := *p
out = append(out, &cp)
}
return out, nil
}
func (r *permFakeRepo) statusOf(id uuid.UUID) acp.PermissionStatus {
r.mu.Lock()
defer r.mu.Unlock()
@@ -310,3 +335,68 @@ func TestPermission_CancelSession_UnblocksDecide(t *testing.T) {
}
require.Equal(t, acp.PermissionExpired, repo.statusOf(repo.onlyReqID()))
}
// fakeMaintainer 满足 acp.ProjectMaintainer:仅当 projectID 命中 allow 集合时放行。
type fakeMaintainer struct {
allow map[uuid.UUID]bool
}
func (f fakeMaintainer) CanWriteProject(_ context.Context, callerID uuid.UUID, isAdmin bool, projectID uuid.UUID) (bool, error) {
if isAdmin {
return true, nil
}
return f.allow[projectID], nil
}
// 注入 ProjectMaintainer 后:会话所属 project 的任意 maintainer(非会话 owner)
// 也可处理待审权限请求(autonomy roadmap §11)。
func TestPermission_Resolve_ProjectMaintainer_Allowed(t *testing.T) {
repo := newPermFakeRepo()
owner := uuid.New()
maintainer := uuid.New()
pid := uuid.New()
sid := uuid.New()
repo.sessions[sid] = &acp.Session{ID: sid, UserID: owner, ProjectID: pid}
svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
svc.SetProjectMaintainer(fakeMaintainer{allow: map[uuid.UUID]bool{pid: true}})
sess := handlers.SessionContext{SessionID: sid, UserID: owner}
go svc.Decide(context.Background(), sess, "req-1",
[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
var reqID uuid.UUID
require.Eventually(t, func() bool {
reqID = repo.onlyReqID()
return reqID != uuid.Nil && repo.statusOf(reqID) == acp.PermissionPending
}, time.Second, 5*time.Millisecond)
// 非 owner 但是 project maintainer → 可处理。
require.NoError(t, svc.Resolve(context.Background(), acp.Caller{UserID: maintainer}, reqID, true, "allow_once"))
require.Equal(t, acp.PermissionApproved, repo.statusOf(reqID))
}
// 非 maintainer 的外部用户仍被拒(fail-closed);项目不在 allow 集合时也拒。
func TestPermission_Resolve_NonMaintainer_Forbidden(t *testing.T) {
repo := newPermFakeRepo()
owner := uuid.New()
pid := uuid.New()
otherPid := uuid.New()
sid := uuid.New()
repo.sessions[sid] = &acp.Session{ID: sid, UserID: owner, ProjectID: pid}
svc := acp.NewPermissionService(repo, nil, time.Minute, nil)
// maintainer 仅对 otherPid 有权,而会话属于 pid。
svc.SetProjectMaintainer(fakeMaintainer{allow: map[uuid.UUID]bool{otherPid: true}})
sess := handlers.SessionContext{SessionID: sid, UserID: owner}
go svc.Decide(context.Background(), sess, "req-1",
[]byte(`{"name":"danger.tool"}`), optionsAllowReject())
var reqID uuid.UUID
require.Eventually(t, func() bool {
reqID = repo.onlyReqID()
return reqID != uuid.Nil && repo.statusOf(reqID) == acp.PermissionPending
}, time.Second, 5*time.Millisecond)
err := svc.Resolve(context.Background(), acp.Caller{UserID: uuid.New()}, reqID, true, "allow_once")
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeAcpSessionNotOwned, ae.Code)
}