Why: RDM delegated rename rebuilt the creator Host and could commit a Backend project rename after the delegated operator was disabled.
Evidence: SQLite and isolated PostgreSQL targeted checks cover operator/creator disable order, two processes, sorted locks, auth-off local compatibility, and HTTP/command Host wiring; exact committed-head replay remains pending.
Impact: Adds a request-bound single-project extension capability and checks operator plus creator in the final Backend transaction. Global ScopedHost cannot derive delegated authority; RDM cross-database atomicity remains outside the promise.
Refs: Approved Documents delegated-projection amendment 5783940, U3 A1-A4 candidate adcd8fe, Dev feature/iam-u3-account-directory-recovery.
123 lines
3.8 KiB
Go
123 lines
3.8 KiB
Go
package app
|
|
|
|
import (
|
|
"agent-system-backend/internal/store"
|
|
"agent-system-backend/pkg/extension"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
// requestBoundHost is created only for a current lifecycle or command request.
|
|
// A reconstructed global ScopedHost is a plain inprocHost and cannot expose
|
|
// the delegated projection method by type assertion.
|
|
type requestBoundHost struct{ inprocHost }
|
|
|
|
// ProjectProjectionForCreator only derives from a live request Host of the
|
|
// registered RDM provider. The operator is inherited from that Host, never
|
|
// accepted as a caller-supplied field. RDM's project.settings decision remains
|
|
// its own domain authority; this capability fences the Backend projection.
|
|
func (bound requestBoundHost) ProjectProjectionForCreator(creator, projectID string) (extension.ProjectHost, error) {
|
|
h := bound.inprocHost
|
|
if h.actor == "" || h.actor != h.owner || h.pluginID != "rdm" ||
|
|
h.caps.projects == nil || h.caps.st == nil || creator == "" || projectID == "" {
|
|
return nil, store.ErrAccountForbidden
|
|
}
|
|
slot := h.caps.projects
|
|
slot.mu.Lock()
|
|
registered := slot.provider != nil && slot.holder == h.pluginID
|
|
slot.mu.Unlock()
|
|
if !registered {
|
|
return nil, store.ErrAccountForbidden
|
|
}
|
|
if err := h.requireEffective(); err != nil {
|
|
return nil, err
|
|
}
|
|
if h.caps.accountGuard != nil {
|
|
if err := h.caps.accountGuard(creator); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
project, err := h.caps.st.ProjectMetadata(projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if string(project.Owner) != creator || string(project.ID) != projectID {
|
|
return nil, store.ErrNotFound
|
|
}
|
|
return delegatedProjectProjection{caps: h.caps, pluginID: h.pluginID,
|
|
actor: h.actor, creator: creator, projectID: projectID}, nil
|
|
}
|
|
|
|
// delegatedProjectProjection exposes only ProjectHost, and every method is
|
|
// confined to the bound project. It cannot be cast to the broader Host.
|
|
type delegatedProjectProjection struct {
|
|
caps hostcaps
|
|
pluginID string
|
|
actor string
|
|
creator string
|
|
projectID string
|
|
}
|
|
|
|
var _ extension.ProjectHost = delegatedProjectProjection{}
|
|
|
|
func (p delegatedProjectProjection) Owner() string { return p.creator }
|
|
|
|
func (p delegatedProjectProjection) check() error {
|
|
if p.caps.accountGuard == nil {
|
|
return nil
|
|
}
|
|
if err := p.caps.accountGuard(p.actor); err != nil {
|
|
return err
|
|
}
|
|
return p.caps.accountGuard(p.creator)
|
|
}
|
|
|
|
func (p delegatedProjectProjection) target(id string) error {
|
|
if id == "" || id != p.projectID {
|
|
return store.ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p delegatedProjectProjection) OwnedProjects() ([]core.Project, error) {
|
|
return nil, store.ErrAccountForbidden
|
|
}
|
|
|
|
func (p delegatedProjectProjection) OwnedProject(string) (core.Project, error) {
|
|
return core.Project{}, store.ErrAccountForbidden
|
|
}
|
|
|
|
func (p delegatedProjectProjection) EnsureProject(string, string) (core.Project, error) {
|
|
return core.Project{}, store.ErrAccountForbidden
|
|
}
|
|
|
|
func (p delegatedProjectProjection) ProjectMetadata(id string) (core.Project, error) {
|
|
if err := p.target(id); err != nil {
|
|
return core.Project{}, err
|
|
}
|
|
if err := p.check(); err != nil {
|
|
return core.Project{}, err
|
|
}
|
|
view := inprocHost{caps: p.caps, pluginID: p.pluginID, owner: p.creator, actor: p.actor}
|
|
return view.ProjectMetadata(id)
|
|
}
|
|
|
|
func (p delegatedProjectProjection) ProjectNameAvailable(name, exceptID string) (bool, error) {
|
|
if err := p.target(exceptID); err != nil {
|
|
return false, err
|
|
}
|
|
if err := p.check(); err != nil {
|
|
return false, err
|
|
}
|
|
return p.caps.st.ProjectNameAvailable(name, exceptID)
|
|
}
|
|
|
|
func (p delegatedProjectProjection) RenameOwnedProject(id, name string) (core.Project, error) {
|
|
if err := p.target(id); err != nil {
|
|
return core.Project{}, err
|
|
}
|
|
if err := p.check(); err != nil {
|
|
return core.Project{}, err
|
|
}
|
|
return p.caps.st.RenameOwnedProjectAccountChecked(p.actor, p.creator, p.projectID, name, p.caps.allowImplicitLocal)
|
|
}
|