Take resolver scope from authorize, not the GID

The authorize/Authorize helpers (GraphQL and MCP) already return the
*coredata.Scope resolved from the resource's organization_id attribute,
but several resolvers discarded it and rebuilt the scope with
coredata.NewScopeFromObjectID(...) right after. NewScopeFromObjectID
only reads the tenant encoded in the GID, while the authorizer derives
the scope from loaded resource attributes, so the two silently drift if
the resource lookup ever changes.

Capture scope from authorize and feed it straight to the service/coredata
layer. For the LinkX/UnlinkX MCP tools, move the per-case Authorize
inside the switch and drop the shared scope so each case owns its own
authorization result. Document the rule in contrib/claude/authorization.md
and add a matching .cursor/rules/go-authorize-scope.mdc, including the
narrow exception for global-catalog authorize calls (e.g. identity-scoped
ActionCommonThirdPartyList) where downstream services take no scope.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-28 14:29:02 +02:00
parent af4b8a3476
commit 88d7961ac6
7 changed files with 230 additions and 137 deletions

View File

@@ -683,14 +683,13 @@ func (r *employeeDocumentResolver) Signed(ctx context.Context, obj *types.Employ
// ApprovalState is the resolver for the approvalState field.
func (r *employeeDocumentResolver) ApprovalState(ctx context.Context, obj *types.EmployeeDocument) (*coredata.DocumentVersionApprovalDecisionState, error) {
if _, err := r.authorize(ctx, obj.ID, probo.ActionEmployeeDocumentGet); err != nil {
scope, err := r.authorize(ctx, obj.ID, probo.ActionEmployeeDocumentGet)
if err != nil {
return nil, err
}
identity := authn.IdentityFromContext(ctx)
scope := coredata.NewScopeFromObjectID(obj.ID)
state, err := r.probo.Documents.GetViewerApprovalState(ctx, scope, obj.ID, identity.ID)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
@@ -791,12 +790,12 @@ func (r *employeeDocumentVersionResolver) Signed(ctx context.Context, obj *types
// ApprovalDecision is the resolver for the approvalDecision field.
func (r *employeeDocumentVersionResolver) ApprovalDecision(ctx context.Context, obj *types.EmployeeDocumentVersion) (*types.DocumentVersionApprovalDecision, error) {
if _, err := r.authorize(ctx, obj.DocumentID, probo.ActionEmployeeDocumentGet); err != nil {
scope, err := r.authorize(ctx, obj.DocumentID, probo.ActionEmployeeDocumentGet)
if err != nil {
return nil, err
}
identity := authn.IdentityFromContext(ctx)
scope := coredata.NewScopeFromObjectID(obj.ID)
decision, err := r.probo.DocumentApprovals.GetViewerDecision(ctx, scope, obj.ID, identity.ID)
if err != nil {