23 Commits
Author SHA1 Message Date
Guarzo 6240f16154 fix(map): copy signatures during duplication + harden endpoint guard test
Two polish follow-ups on the /api/v1 authz branch:

1. Map duplication never actually copied signatures. get_all_map_signatures
   called `MapSystemSignature.by_system_id_all(%{system_id: id})`, but the
   code interface is defined with `args: [:system_id]` and every other call
   site passes the id positionally. The map form raised
   Ash.Error.Query.InvalidArgument on :system_id, which the old
   `{:error, _} -> []` swallow silently turned into "no signatures", so
   duplication reported success while copying none. Fixed the call to pass
   the id positionally; the surrounding reduce now propagates a read failure
   as {:error, {:signature_read_failed, _}} and rolls back rather than
   returning an incomplete duplicate as success.

2. Added a custom-endpoint test asserting the systems_and_connections IDOR
   guard is not loosened into case-folding: an upper-cased own-map id must
   still 404, confirming to_string/1 normalization guards rather than matches
   loosely.

Verified: all /api/v1 authz suites, map_scoped units, and the four map
duplication suites pass (157/0 at seed 0).
2026-08-01 19:24:33 +00:00
GuarzoandClaude Opus 5 4239930de5 docs: drop internal planning-artifact references from comments
The rationale in these comments is worth keeping, but the citations
(task-N, task-1-report.md, fix-round-1) point at branch-local planning
artifacts that no reader of the repository can resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 18:38:55 +00:00
Guarzo 0c0c90dce4 test(api): internal non-breakage + non-vacuous includes coverage
Two guards closing out the authz work.

internal_authz_regression_test.exs exercises the real internal call sites
that pass an `actor:`, so tightening a policy later cannot silently break the
app's own paths. Each test names the production site it mirrors.

Two corrections to the planned tests, both from reading the callers:

  * Map.duplicate is invoked with conn.assigns[:current_character], a
    Character (map_api_controller.ex:1276), and the :duplicate action sets
    owner_id from context.actor.id. Map.owner is a belongs_to Character, so a
    User actor would write a User id into a Character FK. The test uses a
    Character and asserts owner_id comes back correct.
  * :available is never called with a Character. Its only callers
    (Maps.get_available_maps/1, Acls.get_available_acls/1) pass a User, and
    FilterMapsByRoles reads actor.characters, which a Character struct lacks.
    The suite reloads the User with `load: :characters` the way
    CheckJsonApiAuth does, and covers the Character bypass via the write and
    duplicate tests instead.

authz_includes_leak_test.exs covers `include=`, the one path that reaches a
resource without going through its own route. Every refutation is paired
with a positive control so it cannot pass vacuously.

Note Character is NOT an AshJsonApi resource (extensions: [AshCloak] only),
so ?include=owner emits a resource identifier with "type": null and an empty
attributes object -- no Character field is serialized at all. The negative
assertion is therefore about sensitive attributes being absent rather than
about the type name, which the planned test would have asserted and which
would never have matched.
2026-08-01 17:15:33 +00:00
Guarzo 8379126a7f feat(api): guard + actor-propagate custom systems_and_connections endpoint
GET /api/v1/maps/:map_id/systems_and_connections is a hand-written
controller, so it inherits none of the AshJsonApi route policies. It had two
independent holes, both fixed here:

  1. No path-vs-token check. Any valid token could read ANY map's systems and
     connections by passing that map's id in the path. Confirmed before the
     fix: requesting another map's id returned 200 with its data.
  2. Actor-less reads. `Ash.read!/1` was called with no actor, and the domain
     gate is `authorize :when_requested`, which authorizes only when an
     `actor:` key is present (ash/lib/ash/actions/helpers.ex:390) -- so the
     reads ran entirely unauthorized.

A path/token mismatch returns 404 rather than 403 so the endpoint does not
confirm that another map exists, consistent with the filter-check behaviour
elsewhere on this surface.

The two fixes are verified independently by mutation, since either could
mask the other: reverting the guard alone fails 2 tests, and dropping
`actor:` alone fails 1. Note the second failure is the OWN-map positive
control going empty -- MapSystem's FilterSystemsByActorMap preparation
fails closed without an actor -- which is exactly why that test asserts
membership of a real id rather than just a 200.

Full suite: 392 tests, 14 failures, all in the two known-flaky pre-existing
suites; zero new.
2026-08-01 17:15:29 +00:00
Guarzo 1119e45893 feat(api): deny-by-default policies for zero-operation v1 resources
MapSolarSystem, MapState, ShipTypeInfo and User expose no /api/v1 routes,
but they remain reachable as relationship `includes` and would become
reachable if a route were ever added. Enforce the guarantee at the Ash layer
rather than relying on the route list staying empty: token actors are
forbidden outright, trusted internal actors pass via the bypass.

Safe for internal callers. The domain gate is `authorize :when_requested`,
which authorizes only when an `actor:` key is present
(ash/lib/ash/actions/helpers.ex:390). Every internal caller of these four
reads them without an actor (e.g. ShipTypeInfo.read/0 in cached_info.ex), so
those paths skip authorization entirely. A test asserts this direction
explicitly so a future change that breaks production reads is caught.

Also replaces MapScoped.Trusted's struct pattern matching with is_struct/2.
A struct pattern is expanded at COMPILE time, so matching %Api.User{} made
the policy module depend on Api.User; once User itself gained a policy
referencing that module, the cycle closed and `mix compile --force` failed
with "deadlocked waiting on struct WandererApp.Api.User" (reproduced twice,
24 occurrences each). is_struct/2 checks the module at runtime and is
otherwise equivalent. MapScoped's 24 unit tests still pass unchanged.

Tests require %Ash.Error.Forbidden{} exclusively, never {:ok, []}, and seed
a row per resource first so no denial can pass vacuously over an empty
table. Verified non-vacuous by mutation: flipping forbid_if to authorize_if
fails the deny test.

Full suite: 386 tests, 14 failures, all in the two known-flaky pre-existing
suites; zero new.
2026-08-01 17:15:24 +00:00
Guarzo 67c10eb682 feat(api): token-read-only ACLs and denied user_activity on v1
Add the AclScoped policy module and apply policies to the ACL resources and
the user_activity audit log.

ACLs join to maps through MapAccessList rather than carrying a map_id, so
scoping tests for the existence of a join row: `exists(map_access_lists,
map_id == ^map_id)`. A literal expr is safe here because the relationship
path is static; the nested-keyword-list workaround in MapScoped exists only
because that module builds paths dynamically from an opt.

Status codes, deliberately mixed:

  * ACL/member reads are FilterChecks -> an unlinked ACL is invisible
    (list omits it, GET is 404).
  * ACL/member writes are `forbid_if always()` -> 403 even for a LINKED
    ACL. Unlike map-owned resources there is no "your own ACL" a map token
    may legitimately mutate, so this is a hard denial rather than scoping.
  * user_activity is hard-forbidden on every action -> 403. Deliberately not
    a filter, which would return an empty 200 and imply the endpoint is
    legitimately available to tokens.

AccessList gained `has_many :map_access_lists` (the missing inverse of
MapAccessList.access_list) so the read filter can resolve.

Tests pair each denial with a positive control and assert internal User
actors still read and write through the bypass. Verified non-vacuous by
mutation: neutering all scoped checks and forbids fails 11 of 16 tests. The
user_activity suite seeds a real row so its 403s are not a vacuous pass over
an empty table.

Full integration + web suite: 13 failures, identical to the pre-branch
baseline (zero new).
2026-08-01 17:15:20 +00:00
Guarzo 9fc6401c50 feat(api): scope system-child v1 resources per real route matrix
Add Ash policies to the three `system`-path /api/v1 resources, scoping each
to the routes it actually exposes:

  * map_system_signature  - read + destroy
  * map_system_structure  - full CRUD
  * map_system_comment    - read only

Read/update/destroy use FilterChecks (in_token_map / parent_in_token_map)
across the [:system, :map_id] path, so out-of-scope rows are invisible
rather than rejected: lists omit them and GET/PATCH/DELETE return 404. This
follows the amended global constraint from Task 2 -- filter checks keep the
:atomic strategy eligible and avoid confirming that a foreign row exists.

Structure create has no existing row to filter, so it uses the
CreateParentInTokenMap SimpleCheck to look up the parent system and returns
403 for a foreign parent.

Tests pair every denial with an own-map positive control. Verified
non-vacuous by mutation: replacing all six scoped checks with
`authorize_if always()` fails 11 of the 15 tests.

The shared `insert(:map_system_comment, ...)` factory is stale (it passes
map_id/solar_system_id/position_* which MapSystemComment's :create no longer
accepts); the test creates comments directly rather than expanding scope to
fix the factory.
2026-08-01 17:15:14 +00:00
Guarzo aa49da53f5 feat(api): enable domain authz gate + policies for direct-map_id resources
Turns on `authorize :when_requested` on the WandererApp.Api domain and
attaches Ash.Policy.Authorizer policies to every route-exposed resource
scoped by a direct `map_id` (plus `map` itself, scoped by `:id`).

Composition is `bypass Trusted` (internal %User{}/%Character{} actors)
followed by action policies, per the design. Reads are filter-scoped;
creates are rejected by SimpleChecks.

Two behaviours worth calling out for reviewers:

BREAKING API CHANGE: POST /api/v1/map_systems (and map_connections) with
a client-supplied foreign map_id now returns 403 and creates nothing.
Previously InjectMapFromActor silently overrode it and returned 201.
Omitting map_id still injects the token's map and returns 201 -- that is
the path real clients use, and it now has an explicit positive-control
test. InjectMapFromActor itself is unmodified.

Foreign PATCH/DELETE returns 404, not 403. Update/destroy use the FILTER
check rather than the `write_direct/1` SimpleCheck: a SimpleCheck with
`requires_original_data?` disqualifies Ash's :atomic bulk strategy, which
broke atomic updates (NoMatchingBulkStrategy -> 400) and forced the
:stream destroy path, whose `Ash.Query.select([])` re-read returns
pkey-only records that then crash Jason encoding on %Ash.NotLoaded{}.
Filter-scoping makes out-of-scope rows invisible, so Ash reports
not_found. This deviates from the plan's "writes -> 403" constraint and
was accepted deliberately; it also avoids confirming that foreign rows
exist.

Adds simple_sat, required at runtime by Ash's policy authorizer.

Verified: 25/25 new authz matrix tests; full integration suite shows the
same 13 pre-existing failures as before this branch (no new failures);
no new compile warnings.
2026-08-01 17:15:07 +00:00
Guarzo 20a50e8db0 fix: apiV1 default fields updates 2025-12-02 17:55:05 +00:00
Dmitry Popov 9e9dc39200 Merge pull request #556 from guarzo/guarzo/ticker2andsse
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
fix: sse enable checkbox, and kills ticker
2025-11-25 15:33:05 +04:00
Dmitry Popov abd7e4e15c chore: fix tests issues 2025-11-25 12:28:31 +01:00
Dmitry Popov 01e291daf4 chore: fix tests issues 2025-11-24 23:57:52 +01:00
Dmitry Popov a872561b18 chore: fix tests issues 2025-11-24 11:33:08 +01:00
Dmitry Popov 857608f8ef chore: fix tests issues 2025-11-23 22:43:59 +01:00
Guarzo 7a74ae566b fix: sse enable checkbox, and kills ticker 2025-11-23 18:04:30 +00:00
Guarzo 5764c41d23 pr feedback 2025-11-18 20:46:06 +00:00
Guarzo 09444596ff fix: apiv1 token auth and structure fixes 2025-11-18 20:10:06 +00:00
guarzo 6fb3edbfd6 fix: update system signature api to return correct system id 2025-08-28 01:30:37 +00:00
guarzo 956a5a04ca fix: test updates 2025-07-16 23:17:47 +00:00
guarzo affeb7c624 feat: apiv1 and tests 2025-07-16 20:39:30 +00:00
guarzo 33f710127c feature: add duplicate map api 2025-07-13 21:12:32 -04:00
guarzo 7b9e2c4fd9 fix: add test coverage for api 2025-07-12 22:28:59 +00:00
guarzo d390455cf2 refactor: add tests and ci gates for quality 2025-07-09 01:47:24 -04:00