Commit Graph

75 Commits

Author SHA1 Message Date
kolaente b0bd8ab888 fix(mcp): allow update wrappers to clear booleans and numerics
copyByJSONTag previously skipped any IsZero value, which made it
impossible for tasks_update / projects_update to flip done from true
to false, reset priority/percent_done to 0, or unarchive a project.

A non-nil pointer src is now the unambiguous "caller supplied this"
signal: dereferenced values are written through even when zero, while
value-typed src fields keep the partial-update semantics. The
affected wrapper fields (Done, IsArchived, IsFavorite, Priority,
PercentDone, RepeatAfter, RepeatMode, BucketID,
CoverImageAttachmentID, ParentProjectID, Position) move to pointer
types so the JSON Schema still marks them optional.
2026-05-30 14:49:19 +02:00
kolaente ecd4d786f7 feat(mcp): expose remaining v1 resources via mcp tools
Registers tasks, labels, teams, task_comments and task_assignees through
the MCP tool surface, completing the v1 resource list from the plan:

  * tasks    : create / read_one / update / delete (read_all omitted;
               models.Task.ReadAll is a stub — TaskCollection is OOS)
  * labels   : full CRUD
  * teams    : full CRUD
  * tasks_comments  : full CRUD, install-time gated on
                      config.ServiceEnableTaskComments
  * tasks_assignees : create / read_all / delete only (REST exposes no
                      read_one or update)

Per-resource input wrappers carry the path-param fields (task_id,
user_id) explicitly so MCP callers can provide them as JSON args.
installToolsForToken fans out to one installer per resource; the
generics-bound addTool keeps per-(resource, op) call sites at compile
time. The api_tokens.yml fixture extends token 11 to cover the new
scopes; token count stays at 5 for user 1 so existing token-listing
tests are unaffected.

Integration tests per resource cover tools/list visibility, at least
one successful create or read_all, and a permission denial scenario.
2026-05-27 00:11:29 +02:00
kolaente 8fbc6b62a2 feat(mcp): enforce per-tool api token scopes
Filter MCP tool visibility and invocation by the requesting API token's
(group, permission) scopes. tools/list now returns only the tools the
token's APIPermissions authorise; tools/call additionally re-checks the
scope in the dispatcher as defence-in-depth, so a session created with
one token cannot be reused to invoke tools that token never had access to.

The per-session filter runs at session-init via the StreamableHTTPHandler
getServer factory (which the SDK calls once per session, before caching
the *mcp.Server). The dispatcher check runs on every tools/call and
returns ErrScopeDenied, which the AddTool wrapper renders as an IsError
tool result.
2026-05-26 23:54:02 +02:00
kolaente e423167ce1 feat(mcp): expose projects via mcp tools
Wires the projects resource into the MCP server end-to-end. The five
project tools (create, read_one, read_all, update, delete) are now
visible in tools/list and dispatch through handler.Do* like the REST
layer.

- Add ProjectCreateInput / ProjectUpdateInput in inputs.go with
  jsonschema tags covering only the writable fields the model honours
  (title, description, identifier, hex_color, parent_project_id,
  position, is_archived, is_favorite); computed fields like Owner and
  MaxPermission are intentionally absent so the SDK-reflected schema
  stays narrow.
- Add resources.go with a sync.Once-guarded RegisterResources(), and an
  installTools helper that registers tools per (resource, op) on the
  *mcp.Server via a generic addTool[In inputAdapter] helper. The
  handler maps domain failures (permission denials, missing rows,
  validation) to IsError tool results per the SDK convention.
- Add DispatchTyped in dispatcher.go so the AddTool handler can hand a
  pre-unmarshalled wrapper to the dispatcher without a JSON
  round-trip. The existing Dispatch (raw JSON path) delegates to a
  shared dispatchPrepared.
- Wire RegisterResources() + installTools() into newServer() so each
  new MCP session inherits the static tool set.
- Add fixture token 11 (mcp:access + projects:*) for the full-scope
  integration tests; bump TestAPIToken_ReadAll's expected count.
- Refresh TestMCP_ToolsListEmpty into
  TestMCP_ToolsListReturnsRegisteredResources, asserting the five
  projects_* tools are present (Task 6 will introduce scope-based
  filtering of this list).
- Add pkg/webtests/mcp_projects_test.go covering tools/list,
  create/read_one/read_all/update/delete happy paths, schema-validation
  failure on missing required title, permission denial on a forbidden
  project, and nonexistent-id lookup.
2026-05-26 23:43:59 +02:00
kolaente 3ec2d89543 feat(mcp): add streamable-http endpoint skeleton
Mount /api/v1/mcp (and /api/v1/mcp/*) inside the authenticated route
group. Reject JWT-authed requests with 401 (token-only policy), reject
API tokens without the mcp:access scope with 403, and propagate the
authed *user.User + *models.APIToken to r.Context() via typed keys so
downstream tool handlers can pull them out without depending on Echo.

The MCP protocol — JSON-RPC framing, Mcp-Session-Id management, SSE
streaming — is delegated to github.com/modelcontextprotocol/go-sdk
v1.6.1. tools/list returns {"tools": []} since no tools are registered
yet.
2026-05-26 23:08:45 +02:00
kolaente 49934adaaf feat(mcp): register mcp:access api token scope
Adds the mcp scope group with a single access permission so it shows up
in GET /api/v1/routes (and therefore in the frontend token form).
Adds APIToken.HasMCPAccess() mirroring the caldav/feeds helpers.

The MCP endpoint will use POST, GET, and DELETE on the same path for the
streamable-HTTP transport, which CanDoAPIRoute's exact (method, path)
match cannot gate. The token middleware therefore skips the route check
for /api/v1/mcp and any sub-path; the actual authorization is delegated
to an inline HasMCPAccess() call in the MCP handler (added in the next
task).

Fixtures gain two MCP tokens for user 1: one mcp-only and one with
mcp:access plus projects read scopes for the per-tool scope filter tests.
2026-05-26 22:58:53 +02:00
kolaente 3498dfe7fb test(admin): add webtests for /admin/* endpoints and share bypass 2026-04-20 18:55:06 +00:00
kolaente 8578fe3468 feat(api): add GET /projects/:project/tasks/by-index/:index endpoint 2026-04-11 20:44:28 +00:00
kolaente 75e1f72c6e fix(security): move reparent Admin gate into UpdateProject
GHSA-2vq4-854f-5c72 / CVE-2026-35595: the recursive permission CTE
cascades Admin from any owned ancestor, so a user with Write on a
shared project could reparent it under an attacker-owned root and
resolve as Admin on the moved project via the new parent.

Require Admin on both the moved project and the new parent whenever
parent_project_id is set to a non-zero value that differs from the
stored value. The gate lives in UpdateProject rather than CanUpdate
because CanUpdate is reused by permission-check-only callers
(buckets, webhooks, task ops) that pass stub &Project{ID:...} values
with ParentProjectID=0 and never commit a reparent — gating there
would spuriously trip the check for every such call.

Only non-zero ParentProjectID is gated: the generic update handler
binds a fresh struct, so an omitted parent_project_id is
indistinguishable from an explicit 0. Detach-to-root via the generic
endpoint is therefore out of scope for this fix and is tracked as a
follow-up (needs a pointer field to disambiguate).
2026-04-09 16:47:35 +00:00
kolaente 6ca0151d02 test(webtests): add end-to-end TOTP lockout test
Drives the login endpoint through 11 failed TOTP attempts against user10
and asserts the account ends up locked in the database, then verifies a
subsequent login with a valid TOTP code is rejected with
ErrCodeAccountLocked. Exercises the GHSA-fgfv-pv97-6cmj regression
against the real handler path.
2026-04-09 16:08:26 +00:00
kolaente 879462d717 fix(caldav): enforce URL project match in GetResourcesByList
Multiget REPORT requests would happily return tasks from projects
different from the one in the href, even though GetTasksByUIDs now
filters by access. Drop any returned task whose real project_id does
not match the project ID parsed from the href path segment.

Hardening for GHSA-48ch-p4gq-x46x.
2026-04-09 16:07:32 +00:00
kolaente 200b787c16 fix(caldav): reject GetResource when URL project mismatches task project
Even with the GetTasksByUIDs authz filter in place, a user with access
to multiple projects could read a task from project B by requesting it
under project A's URL. Enforce that the task's real project_id matches
the project ID parsed from the CalDAV URL path and 404 otherwise.

Adjusts the Delete Subtask test to use the correct URL project for
uid-caldav-test-child-task-2 (which lives in project 38, not 36);
the previous URL only worked because of the authz gap being closed.

Hardening for GHSA-48ch-p4gq-x46x.
2026-04-09 16:07:32 +00:00
kolaente 379d8a5c19 test(security): webtest that a deleted link share rejects its still-valid JWT
End-to-end regression test for GHSA-96q5-xm3p-7m84 / CVE-2026-35594: mints
a JWT for a link share via the real helper, then deletes the share row and
invokes the real ReadAllWeb handler to prove the full request path (not
just the unit-tested GetLinkShareFromClaims) surfaces the revocation.

Also fixes a pre-existing stale literal in the TestLinkSharing test fixture
struct: linkshareRead declared Hash="test1" while the actual fixture row
id=1 uses Hash="test". The old code never looked at the DB so the mismatch
went unnoticed; after the fix it would cause every link-share webtest that
used linkshareRead to fail hash validation.
2026-04-09 15:38:07 +00:00
kolaente 6a0f39b252 fix(security): enforce HTTP method and path in scoped API token matcher
CanDoAPIRoute's non-CRUD fallback branch compared a path-derived
permission name to the token's permission strings without checking
the request method. A token with projects.background (registered for
GET /projects/:project/background) could therefore invoke DELETE on
the same path. The same method-confusion affected the whole
/projects/:project/views/:view/buckets[/:bucket] cluster, where a
token with projects.views_buckets (registered for GET) authorized
PUT, POST, and DELETE on any accessible view's buckets.

The matcher also leaked CRUD permissions between parent and nested
sub-resource groups. When the request targeted a nested CRUD resource
(e.g. projects_teams, projects_shares, projects_users, projects_views,
projects_webhooks, projects_views_tasks, tasks_assignees, tasks_labels,
tasks_comments, tasks_relations, tasks_attachments, teams_members),
the matcher fell back from the specific group to the parent's permission
list but then looked the permission name up inside the sub-resource's
RouteDetail map. The effect was that a token holding only projects.read_all
also authorized GET on every nested projects_* list endpoint, and the
same held for create/update/delete and for the tasks.* family.

Rewrite the matcher to iterate the token's own permissions and accept
only when the stored RouteDetail's (Path, Method) matches the request.
This removes all the path-derived group guessing and makes the stored
detail the single source of truth. Preserve the tasks.read_all quirk
(one permission, two list endpoints) as an explicit two-path allowlist
inside the loop.

Extract a GetAPITokenRoutes accessor so the new property-based webtest
can consume the same snapshot served by GET /api/v1/routes.

Add TestAPITokenMethodMatching in pkg/webtests: using the live echo
router and the live apiTokenRoutes map, it iterates every advertised
permission against every registered route and asserts the matcher
accepts iff the stored (Path, Method) matches. Any future collision
introduced by a new non-CRUD route on a shared path will be caught.

After this change, previously-dead permissions like
projects.background_delete, projects.views_buckets_{put,post,delete},
other.avatar, other.ws and caldav.access start working as their UI
labels imply. Tokens that relied on the over-broad background /
views_buckets grants, or on cross-cluster CRUD bleed-through, will
lose the extra access - that is the fix.

Refs: GHSA-v479-vf79-mg83
2026-04-09 15:17:20 +00:00
kolaente 76790348f7 test: verify background removal preserves project title
Regression test for #2552. Deletes the background of project 35 (owned by
testuser6) and then fetches the project to confirm the title is still
'Test35 with background'.
2026-04-08 09:07:15 +00:00
kolaente 88c2f0a289 fix(project): remove non-existent columns from UpdateProject column list
The UpdateProject function referenced done_bucket_id and default_bucket_id
in its column update list, but these columns belong to the project_views
table, not the projects table. This caused SQL errors when archiving or
updating a project on MySQL/PostgreSQL.

Also adds a test for archiving a non-archived project.

Fixes #2459
2026-04-03 16:59:05 +00:00
kolaente 390957b3f5 test: verify caldav permission group appears in /routes 2026-03-30 12:09:53 +00:00
kolaente 194bec8b9f test: add integration tests for CalDAV API token auth 2026-03-30 12:09:53 +00:00
kolaente 83bac15841
feat: rename ServiceJWTSecret to ServiceSecret with deprecation (#2502) 2026-03-30 12:07:01 +02:00
kolaente 649043aceb test: add tests for OAuth 2.0 authorization flow
Add web tests covering the authorize endpoint, token exchange, PKCE
verification, single-use codes, and refresh token rotation. Add unit
tests for redirect URI validation and PKCE. Add E2E test for the full
browser-based authorization code flow with login redirect.

Extract setupApiUrl helper for E2E tests to avoid duplication.
2026-03-27 23:05:04 +00:00
surfingbytes 8e8ffac016
fix(caldav): add tags and sync token to collections (#2482)
Fixes #2401
2026-03-26 10:42:39 +00:00
kolaente 13be01de9f test: update expected results for archived project propagation
Adjust test assertions to reflect that projects inheriting archived
state from parents are now correctly filtered out of ReadAll results,
task collections, and search results across all database backends.
2026-03-25 09:06:33 +00:00
kolaente 0b04768d83 test(auth): add comprehensive disabled/locked user auth tests
Add locked user fixture (user18, status=3) and test that both disabled
and locked users are rejected across all auth paths: API tokens,
CalDAV basic auth, CheckUserCredentials.

Ref: GHSA-94xm-jj8x-3cr4
2026-03-23 16:37:26 +00:00
kolaente 751ab2c635 test: add failing test for webhook BasicAuth credential exposure 2026-03-23 16:35:47 +00:00
kolaente a0478a0d96 fix: correct error message assertion in linkshare ReadAll tests
The ErrGenericForbidden HTTP message is "You're not allowed to do this.",
not "Forbidden". Match on "not allowed" instead.
2026-03-23 16:34:40 +00:00
kolaente 9efe1fadba fix: block link share users from listing link shares in ReadAll
Link share authenticated users could call ReadAll on link shares,
which leaked hash credentials for other shares on the same project.
This allowed permission escalation from read-only to write/admin.

Add a check at the top of ReadAll() that rejects link-share-authenticated
callers, mirroring the pattern in CanRead() and canDoLinkShare().
Update tests to expect 403 Forbidden for all link share permission levels.

Fixes GHSA-8hp8-9fhr-pfm9
2026-03-23 16:34:40 +00:00
kolaente 3111f3d70c test: add IDOR test for task attachment ReadOne (GHSA-jfmm-mjcp-8wq2) 2026-03-23 16:34:07 +00:00
kolaente 595002bf96 fix: update ParadeDB search test count for new fixture
Project 40 (archived child project) is pulled into ParadeDB fuzzy
search results via the recursive CTE.
2026-03-23 14:13:53 +00:00
kolaente cd6148511a fix(auth): reject disabled/locked users in API token middleware
checkAPITokenAndPutItInContext now returns 401 Unauthorized when the
token owner's account is disabled or locked, instead of a 500 error.
Also fixes the API token test to match the actual middleware behavior.
2026-03-23 12:06:16 +00:00
kolaente 8b614a4cb3 test: verify disabled user is rejected via CalDAV auth
Also fix BasicAuth to check for status errors from checkUserCaldavTokens
before falling through to password-based auth.
2026-03-23 12:06:16 +00:00
kolaente e4379eff10 test: verify disabled user's API token is rejected 2026-03-23 12:06:16 +00:00
kolaente 1f2aef776c test: verify CalDAV token auth bypasses TOTP check
Add a CalDAV token fixture (kind=4) for user10 who has TOTP enabled,
and implement the previously-skipped test proving token-based auth
still works when TOTP is active.
2026-03-20 12:22:27 +00:00
kolaente 1ed813caf0 fix: update TOTP fixtures and tests to avoid conflicts with existing enrollment tests
- user10 gets enabled TOTP (for CalDAV 2FA test)
- user1 gets enrolled-but-not-enabled TOTP (for existing QR/settings tests)
- TOTP enrollment test uses user2 (no TOTP fixture) instead of user1
2026-03-20 12:22:27 +00:00
kolaente 659e73af05 fix: use user10 instead of user1 for TOTP fixture to avoid breaking login tests 2026-03-20 12:22:27 +00:00
kolaente bda16e770f test: add failing test for CalDAV 2FA bypass via basic auth 2026-03-20 12:22:27 +00:00
kolaente b7a1408098 fix: use require.Error instead of assert.Error for error assertions 2026-03-20 11:41:28 +00:00
kolaente f60f3af70b test: add failing test for project background delete with read-only access
Proves that a user with read-only access to a project can delete its
background image. The test expects a 403 Forbidden but the operation
proceeds because RemoveProjectBackground only checks CanRead.

Adds fixture entry giving user 15 read-only access to project 35
(which has a background_file_id).

Ref: GHSA-564f-wx8x-878h
2026-03-20 11:41:28 +00:00
kolaente 2da89258e5 test: add failing test for task comment IDOR
Proves that a user can read a comment from an inaccessible task by
supplying an accessible task ID in the URL. Comment 18 belongs to
task 34 (owned by user 13), but testuser1 can read it via task 1.

Ref: GHSA-mr3j-p26x-72x4
2026-03-20 11:41:28 +00:00
kolaente 2260d763b5 test: add web test for disabled user password reset rejection 2026-03-20 11:23:21 +00:00
Henry Cole e7f1e99878
fix(caldav): use /dav/projects/ as home to make iOS/MacOS reminders work (#2417)
Resolves issue #475 by modifying CalDAV discovery so Apple Reminders can
use /dav/projects/ as the home set without exposing that synthetic path
as a real task list, preserving the existing principal-based flow. This
is because Apple Reminders defaults back to the /dav/projects/ URL,
rather than accepting the /dav/principals/username/ URL specified in
Vikunja.

Resolves #475
2026-03-20 09:33:56 +00:00
Tink ada2ebab9e
fix: preserve CalDAV inverse relations when parent has no RELATED-TO (#2389)
- Fixes `removeStaleRelations` in CalDAV storage provider to only remove
relations of kinds explicitly declared in the incoming VTODO's
`RELATED-TO` properties
- When a VTODO has no `RELATED-TO` at all (e.g., a parent task from
Tasks.org), no relations are removed — they were auto-created as
inverses by child tasks
- When a VTODO declares specific relation kinds (e.g.,
`RELATED-TO;RELTYPE=PARENT`), only relations of that kind are checked
for staleness; other kinds (like auto-created `subtask` inverses) are
preserved

Fixes #2383

---------

Co-authored-by: kolaente <k@knt.li>
2026-03-11 09:40:09 +01:00
kolaente 675dfb3ea4 test: add web tests for bulk label task endpoint 2026-03-10 23:58:44 +01:00
kolaente d36ac9ddda test: fix ParadeDB project search count to 27
The recursive CTE pulls in child projects of matched parents,
resulting in 27 total results, not 12.
2026-03-05 13:57:05 +01:00
kolaente df0e3a84a9 test: fix non-ParadeDB project search count assertion
ILIKE '%Test1%' matches Test1, Test10, Test11, Test19 + favorites = 5,
not 2. Also use 'Test2"' pattern to avoid matching Test20/Test21.
2026-03-05 13:57:05 +01:00
kolaente c7c63e8ead test: add result count assertions for ParadeDB search tests
Address review feedback: assert exact result counts when ParadeDB is
active. fuzzy(1, prefix=true) broadens matches via edit distance,
returning 6 projects for "TEST10", 14 tasks for "number #17", and
12 projects for "Test1".
2026-03-05 13:57:05 +01:00
kolaente b69705e64b test: fix lint and adjust project search test for ParadeDB fuzzy matching
- Use require.NotEmpty instead of require.Greater for testifylint
- Skip exclusion assertions in web project search test when ParadeDB is
  active, since fuzzy(1, prefix=true) on "Test1" also matches Test2, Test3
2026-03-05 13:57:05 +01:00
kolaente 892b38b3b6 test: add web tests for prefix/substring search (#2346) 2026-03-05 13:57:05 +01:00
kolaente 06617891fa test: verify email masking for external team name search 2026-03-04 20:32:11 +01:00
kolaente 3a730165bc test: add tests for external team user discoverability bypass 2026-03-04 20:32:11 +01:00
kolaente 4d494ba442 test: add web integration tests for task duplication 2026-03-04 17:20:26 +01:00