Skip to content

relationships - target side validation #145

@matt-beanland

Description

@matt-beanland

Background

ValidateRelationshipPermitted currently enforces source-side permissions only — it calls permitted_source_roles/0 on the changeset's own resource and rejects the action if the role isn't allowed.

Target-side enforcement is missing: a resource with target :none can still be related to by any source, because the change only runs on the source's update action and has no way to resolve the target's resource module from its record ID alone.

The gap

When relate_shelf runs with a CardInstance as the target, we call ShelfInstance.permitted_source_roles/0 ✓. We do not call CardInstance.permitted_target_roles/0 ✗. A CardInstance with target :none would silently accept incoming relationships.

Implementation plan — runtime introspection, no registry needed

All the information we need is available at runtime. No compile-time registry required.

How it works

At the point ValidateRelationshipPermitted runs, we have changeset.domain. The approach:

  1. Call Ash.Domain.Info.resources(changeset.domain) to enumerate all resource modules in the domain.
  2. Filter to those that export permitted_target_roles/0 (via function_exported?(module, :permitted_target_roles, 0)) — these are all Instance resources using the provider extension.
  3. For each relationship in the :relationships argument, try Ash.get(module, rel.id, domain: domain, authorize?: false) across candidate modules until one returns {:ok, record}.
  4. Call module.permitted_target_roles/0 on the resolved module and check the relationship alias against it.
  5. If no module resolves the ID, fail with a clear error.

Cost

O(N queries) in the number of Instance resource types in the domain per relationship — in practice a small, bounded number. Only fires on update actions that carry argument :relationships, {:array, :struct}.

Future optimisation (not required)

If the O(N) cost becomes a concern, build a %{spec_uuid => module} lookup map at startup from all modules that export specification_id/0. The Specification instance data is the natural registry key since it is already the stable identifier for a resource kind. This would make resolution O(1). Defer until benchmarking shows it is needed.

Acceptance criteria

  • A resource with target :none rejects incoming relationships at the source's relate action.
  • A resource with target :all accepts them.
  • A resource with target [:consumes] only accepts relationships whose alias matches.
  • Unit test: relate_shelf targeting a CardInstance with target :none fails with "not permitted as target".
  • Unit test: same action targeting a CardInstance with target :all succeeds.
  • Existing source-side tests continue to pass unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions