Ports the v1 DatabaseNotifications routes to the Huma /api/v2 API:
- GET /notifications lists the caller's own notifications (paginated)
- PUT /notifications/{notificationid} marks one (un-)read
- POST /notifications is a custom action marking all as read; the
link-share guard, session and commit live in the handler since there
is no CRUDable Do* for a bulk mark.
Adds fixture rows and a webtest matrix mirroring the v1 model behaviour
(own-only visibility, mark-(un)read, link-share refusal on every route).
Port the Subscription resource from /api/v1 to the Huma-backed /api/v2:
POST /subscriptions/{entity}/{entityID} subscribes, DELETE unsubscribes.
The {entity} discriminator is bound as a string path param with an
enum:"project,task" tag; the model's CanCreate/CanDelete derive the numeric
EntityType from it and reject unknown kinds. Permissions and the
already-subscribed/forbidden checks come from the shared model via DoCreate/
DoDelete, identical to v1's generic handler. Mark the model's server-controlled
fields readOnly and add doc tags for the v2 schema.
Port the v1 webhook webtest to /api/v2 and extend it to the full
permission gradient the model enforces: list needs read access while
create/update/delete need write (Project.CanWrite), exercised across an
owned project and read/write/admin shares plus a no-access project. Also
assert credential masking, events-only updates, the 412 validation path,
and that the routes 404 when webhooks.enabled is false.
Add fixture webhooks 2-5 in projects 9/10/11/2 to back the matrix; they
do not collide with the e2e tests, which scope to project 1.
Port the v1 /tasks/{projecttask}/assignees routes to the Huma-backed
/api/v2. The resource self-registers (RegisterTaskAssigneeRoutes) and
reuses the model's Can* methods via the generic Do* handlers:
- POST /tasks/{projecttask}/assignees → assign a user (body: user_id)
- GET /tasks/{projecttask}/assignees → list assignees (as users)
- DELETE /tasks/{projecttask}/assignees/{user} → un-assign
The list element type is []*user.User (assignees are returned as the
assigned users), which differs from the create body (a TaskAssginee
carrying user_id); the list handler type-asserts to []*user.User.
create/delete require write access to the task's project, list requires
read — enforced at the model level.
The webtest re-proves the full v1 permission matrix on the v2 surface
(read-only shares forbidden, write/admin allowed for create and delete;
already-assigned, no-project-access, missing-user, and missing-task
error codes) so v1's routes can be removed later.
LinkSharing.CanRead resolved the parent project from the share hash, but a
by-id read (GET /projects/{project}/shares/{share}) only carries the numeric
id, never the hash — so the project lookup returned ErrProjectShareDoesNotExist
and every read-one 404'd, even for the share's owner. This affected both v1 and
v2.
Resolve the project from ProjectID when it is set (the by-id read path), keeping
the hash lookup as a fallback for resolving a share purely by its public hash.
The permission semantic is unchanged — you can read a share if you can read its
parent project; only the project lookup changes. ReadOne still scopes by
id AND project_id, so a share id from another project the caller can access is
not leaked (404, no IDOR).
Flips the v2 webtest's pinned 404 cases to assert success and adds the
cross-project IDOR and non-member negatives.
Port the LinkSharing resource from /api/v1 to the Huma-backed /api/v2 under
/projects/{project}/shares. Self-registers via AddRouteRegistrar and is gated
on ServiceEnableLinkSharing, checked inside the registrar so a disabled
instance exposes no routes.
There is no update operation, mirroring v1: a share is created, read, listed
or deleted, never modified in place. Permissions stay at the model level via
the generic Do* handlers (project write to create read/write shares and to
delete; project admin to create an admin share and to list).
ReadOne is ported faithfully including a latent v1 quirk: CanRead resolves the
parent project from the share hash, which the by-id route never carries, so a
by-id read always 404s. The webtest pins this so a future fix is deliberate.
Port the BotUser resource from /api/v1's /user/bots routes to the
Huma-backed /api/v2, preserving every v1 behavior:
- Full CRUD at /user/bots and /user/bots/{bot} with v2 verbs (POST
creates, PUT updates; PATCH is synthesised by AutoPatch).
- ReadAll returns only the caller's own bots; read/update/delete of an
unowned or missing bot is refused with 403, since ownership is resolved
by loading the user (no existence disclosure, no 404 branch).
- Create requires a real user account and rejects link shares, the
bot- username prefix is enforced, and bots are created without an
email or password — all delegated to the unchanged model layer.
- ReadOne surfaces max_permission via the shared value-embed pattern and
carries an ETag for conditional requests.
doc/readOnly tags are added to the exposed user.User fields the bot
response surfaces, and to BotUser.Status, so the v2 OpenAPI schema is
documented. The model and v1 routes are untouched.
The webtest ports the v1 model-level permission matrix to the v2 HTTP
surface and adds the v2-only ETag/304 and merge-patch coverage.
Port the APIToken resource from /api/v1 to the Huma-backed /api/v2 at
top-level /tokens. List/create/delete only — no ReadOne, no Update,
matching v1. The list operation accepts an owner_id query param to list
a caller-owned bot's tokens; create returns the cleartext token exactly
once. Permissions are enforced by the model via the shared Do* helpers.
The webtest ports the v1 model-level permission matrix onto the v2 HTTP
surface (owner isolation, exact list cardinality, bot-owner authz,
validation, forbidden delete) so v2 proves the contract independently.
Port the LabelTask resource (labels attached to a task) from the frozen
/api/v1 to the Huma-backed /api/v2 as nested routes under
/tasks/{projecttask}/labels:
- GET list the labels on a task (read access to the task)
- POST attach a label to a task (write access to the task + access to the label)
- DELETE detach a label from a task (write access to the task)
There is no read-one or update for a label-task relation, so no
max_permission. Adds doc tags and marks the server-set created timestamp
readOnly on the shared model. Permissions stay enforced at the model
layer via the existing Can* methods through handler.Do*.
Ports the Session resource from /api/v1 to the Huma-backed /api/v2 with
list and delete only — sessions are created by the login flow, not CRUD,
so there is no create/read-one/update (and no max_permission or AutoPatch).
The delete path param is a string UUID (path:"session"), not an int64 id,
mapping to the model's string ID. ReadAll is type-asserted to
[]*models.Session; permissions stay at the model level via DoReadAll/DoDelete.
The v2 webtest mirrors v1's session-CRUD matrix (list own vs others',
delete own, non-owner forbidden, nonexistent 404). The login/refresh
auth-flow cases stay on v1.
Assert the read-one body carries max_permission, and add
TestHumaTaskComment_ETagReflectsPermission proving two users with different
permission on a comment's parent task (project 9: owner user6 vs read-share
user1, comment 6 on task 18) receive different ETags.
Decode the ReadOne/Normal body and assert MaxPermission equals the real
permission (admin for the owner) instead of substring-matching, so a
regression to 0 or null is caught precisely.
Add TestHumaProject_NullMaxPermissionRoundTrips: create/update return
max_permission:null, and PUTting that response body back verbatim must
succeed (200, not 422). max_permission is readOnly so Huma ignores it on
write, and Permission.UnmarshalJSON treats JSON null as a no-op.
The project test port had added db.LoadFixtures() into the shared
webHandlerTestV2.serve(), reloading fixtures before every request. That
wiped runtime-created rows between requests within a test, breaking the
create-then-read-back contract every v2 resource relies on (e.g.
TestHumaTeam/Create/Public read its freshly-created team back and got 403).
Revert that shared-harness change and isolate the project/archived tests
the way the team and label tests do: each subtest builds its own handler
via handlerFor, so it runs against freshly loaded fixtures (setupTestEnv
reloads once per handler), while a create-then-read-back sequence reuses
one handler within the subtest.
Read-one now returns a projectReadBody embedding models.Project with
max_permission always populated from CanRead, matching the labels/views
value-embed pattern instead of gating it behind expand=permissions.
CanRead yields a real permission for every readable project (Favorites
pseudo-project and saved-filter-backed ones included), so the field is
always meaningful on a read.
Project remains the no-ETag exception: the response carries user-scoped
favorite/subscription state that changes without bumping Updated, so it
is served fresh.
Update routes its body through the read shape so AutoPatch's GET→PUT echo
of the read-only max_permission validates. Create/Update return null for
max_permission (not computed there) rather than a misleading 0 (=read).
Bring the v2 project webtest to 1:1 parity with v1's TestProject and
TestArchived so the v2 routes independently prove everything v1 proved:
- Full sharing matrix on ReadOne/Update/Delete across team, user,
parent-team and parent-user shares x read/write/admin, asserting
allow/deny and (for ReadOne) the granted max_permission level via
expand=permissions (v2's replacement for v1's x-max-permission header).
- Create permission matrix via parent_project_id (forbidden parent,
parent-team/user write+admin allowed, read-only denied), nonexistent
parent (404), and title-too-long (422) on both Create and Update.
- Create response assertions (owner echo, description, tasks not embedded).
- ReadAll search (q=) with exact cardinality and archived propagation to
child project 21.
- New TestHumaArchived ports the HTTP-observable archived behaviours:
no edit/unarchive under an archived parent, self-archived edit denied
but unarchive allowed, and archiving a project (412 / ErrCodeProjectIsArchived).
Make webHandlerTestV2.serve reload fixtures per request, mirroring v1's
per-request fixture reload, so mutating subtests don't leak state across
the shared Echo instance.
The project read response is enriched with user-scoped, derived state
(subscription, favorite, views, computed archived state) that can change
without bumping project.Updated. An ETag built only from Updated would
therefore hand out stale 304s and hide those changes from the client.
Serve project reads fresh on every call by returning the no-ETag
singleBody envelope and dropping the conditional.Params input. Labels
keep their ETag because their response has no such volatile derived
fields. Update the ReadOne/Normal webtest to assert no ETag is sent.
The project read handler left MaxPermission at its zero value when
expand=permissions was not requested, which serialised as 0
(PermissionRead) instead of being omitted. Force PermissionUnknown so
the field marshals as null, matching the list operation's behaviour and
avoiding a misleading read permission for projects the caller may own.
Assert the null shape in the ReadOne/Normal webtest.
Add a simple /{id} CRUD resource for projects on the Huma-backed /api/v2,
mirroring labels.go. Exposes the expand query param (value "permissions")
which surfaces the caller's max permission per project on both list and read.
The handler stays standard (DoReadAll/DoReadOne/DoCreate/DoUpdate/DoDelete);
the model's ReadOne keeps handling the Favorites pseudo-project and
saved-filter-backed projects.
Self-registers via init() -> AddRouteRegistrar; no routes.go change.
projectusers is intentionally out of scope.
Read/update use a per-resource struct that embeds the model by value and adds a
readOnly max_permission field (labelReadBody, projectViewReadBody); Go and Huma
promote the embedded fields, so the body stays flat with no custom marshaler and
nothing on the shared models. The handler passes the model's Updated and the
permission to conditionalReadResponse, which folds the permission into the ETag.
Adds a webtest asserting two callers with different permission on the same label
get different ETags, plus max_permission presence assertions.
Assert the specific domain error code (ErrCodeTaskDoesNotExist) on the
nonexistent-source-task case, matching v1's TestTaskDuplicate. v2 carries
the code as the numeric `code` field of the RFC 9457 problem+json body,
so the test now checks that field instead of only the 404 status.
Bring the v2 avatar webtest to 1:1 parity with the v1 avatar tests so
the v1 routes and tests can be removed without losing coverage:
- link-share auth path: a request authenticated as a link-share user
(not a regular JWT) returns 200 + non-empty image bytes, porting
v1's TestLinkShareAvatar.
- bot user: the botmarble provider path returns 200 + SVG bytes, a
distinct rendering v1 never exercised; asserts the marble mask id so
it cannot silently fall through to the default placeholder.
- non-numeric size: rejected with 422 (Huma's int64 query validation)
rather than v1's 400 ErrInvalidModel, both being client errors that
refuse the malformed input.
Cut narration a reader can infer from the code (envelope element type,
path-param binding, per-case test descriptions). Keep the non-obvious
rationale: IDOR scoping, RFC 9110 etag quoting, why the feature gate sits
in the registrar, and the author-only fixture crux.
The Forbidden non-author update/delete cases used user6, who also lacks access
to task 1, so they only proved access denial, not the author-only restriction.
Add cases driven by testuser1 against comment 4 on task 16 (project 7): user1
has write access via team 3 but did not author the comment (user6 did), so a
403 there genuinely exercises the authorship branch. Keep the user6 cases as
the no-access negatives, relabelled for clarity.
Add TaskComment CRUD on /api/v2 under /tasks/{task}/comments, mirroring
the project_views nested-resource shape. The resource is feature-gated by
config.ServiceEnableTaskComments, checked inside the registrar so it runs
after config has loaded. Self-registers via init()+AddRouteRegistrar; no
routes.go change. ReadAll exposes the order_by (asc/desc) query param.
Adds doc:/readOnly: tags to the shared TaskComment model fields and a
TestHumaTaskComment webtest covering list/read/create/update/delete plus
negatives (non-author forbidden, comment under the wrong task -> 404).
Bring TestHumaAdminProjects to 1:1 parity with v1 TestAdmin_ListProjects
by asserting owner hydration ("username":"user1", never "owner":null)
and project field presence ("id":, "title":) on the response body, in
addition to the existing gate personas and ownership/archived visibility
cardinality checks.
Bring the merged v2 Label webtest (TestHumaLabel) to 1:1 parity with the
model-level matrix in pkg/models/label_test.go so the v2 HTTP surface
independently proves the full visibility/permission contract once v1's
routes and tests are removed.
Added scenarios:
- ReadAll asserts the EXACT visible set for user1 = {1,2,4,7,8}, with #3
(other owner, unattached), #5 (other owner, inaccessible task) and #6
(GHSA private fixture) explicitly absent — not just contains/not-contains.
- ReadOne: #3 forbidden (other owner, unattached); #6 forbidden (GHSA
private); #4 ALLOWED (other owner but visible via an accessible task);
#7 allowed (own, unattached); #8 allowed (own, only on inaccessible task).
- Update/Delete: #4 forbidden (GHSA-hj5c-mhh2-g7jq read-vs-write: readable
but not writable by the non-owner); #3 forbidden; #6 forbidden.
- Create asserts hex-color normalization (#aabbcc -> aabbcc).
Keeps the existing ETag/304 and merge-patch subtests.
Adds Team CRUD on /api/v2 mirroring the labels reference resource:
list, read, create, update, delete under /teams[/{id}].
- The list op exposes an include_public query param bound onto the
model so Team.ReadAll can surface public teams (gated by the instance
public-teams setting).
- Read ops emit an ETag and honor If-None-Match (304).
- Model fields gain doc: tags; server-controlled fields are marked
readOnly:true.
- Self-registers via init()/AddRouteRegistrar; no routes.go change.
- New webtest TestHumaTeam (named to avoid clashing with the v1 model
TestTeam) covers list/read/create/update/delete plus negatives
(non-member 403, nonexistent 403/404) and ETag/304.
MaxBodyBytes was set to exactly the configured max file size, but a
multipart request carries extra bytes (boundary, part headers) on top of
the file, so a file at the limit could be rejected by Huma before the
handler runs. Mirror the +2 MB overhead that Echo's global BodyLimit
middleware already allows so a max-sized avatar isn't rejected.
Browsers set a real image Content-Type (image/png, image/jpeg, ...) on
the multipart avatar part, while programmatic clients often send
application/octet-stream. The part contentType tag is an allow-list for
Huma's MimeTypeValidator, which runs before the handler; broaden it so
both cases are accepted instead of being rejected with a 422.
The byte-level mimetype.DetectReader check in the handler remains the
real security gate and is unchanged.
Extend the webtest with a case that sends a part declared as image/png
and asserts it reaches the handler successfully.
Add PUT /api/v2/user/settings/avatar, the first multipart/form-data file
upload on the Huma-backed v2 API. Reuses v1's byte-level mime validation
(mimetype.DetectReader) and storage (upload.StoreAvatarFile), modeling the
request as a huma.MultipartFormFiles input so it renders as multipart/form-data
in the OpenAPI spec instead of being read off the raw echo context.
Flips the user's avatar provider to "upload" on success. Authenticated (JWT).
Add GET /api/v2/avatar/{username}, the v2 reference for a binary response
modeled in the OpenAPI spec. Reuses the v1 avatar provider logic (provider
lookup, size clamp to config.ServiceMaxAvatarSize, runtime content-type) and
returns raw image bytes via Huma's []byte body + dynamic Content-Type header
idiom, advertised in the spec as application/octet-stream.
The endpoint is authenticated under the global security like every other v2
route (an anonymous request gets a 401); it is not public.
Add the admin + license gate for /api/v2 and ship the first gated
resource, GET /api/v2/admin/projects (AdminProjectList).
The gate reuses the existing v1 middleware functions unchanged —
RequireFeature(license.FeatureAdminPanel) and RequireInstanceAdmin(),
both of which serve 404 on failure. Rather than splitting the single
v2 Huma API into a separate gated sub-group (which would split the
OpenAPI spec and drop admin operations from /api/v2/openapi.json), the
gate is applied as a path-scoped Echo middleware on the shared /api/v2
group, firing only for /api/v2/admin/* and after the token middleware.
This preserves v1's 404-not-403 semantics and keeps admin routes in the
unified v2 spec and Scalar docs.
AdminProjectList lists every project on the instance (archived
included), behind the gate. Adds doc:/readOnly: tags to the shared
Project model so it documents correctly as a v2 schema.
Tests in pkg/webtests/huma_admin_test.go (TestHumaAdminProjects) cover
all three personas: non-admin -> 404, admin without feature -> 404,
admin with feature -> 200 list, plus unauthenticated -> 401.
Add ProjectView CRUD on /api/v2 under the nested path
/projects/{project}/views[/{view}], establishing the two-path-param
binding pattern for sub-resources. Mirrors the labels.go handler shape
and reuses handler.Do* so permission checks stay at the model layer.
Both {project} and {view} are bound on every operation; {project} is
threaded onto ProjectView.ProjectID (ReadOne resolves via
GetProjectViewByIDAndProject, which needs the parent id). List wraps the
[]*models.ProjectView slice in the shared Paginated envelope, read sends
an ETag for If-None-Match/304, and AutoPatch synthesises PATCH.
Also:
- Tag exposed ProjectView / ProjectViewBucketConfiguration / nested
TaskCollection fields with doc: descriptions; mark server-controlled
fields (id, project_id, created, updated) readOnly. Safe for v1.
- Give ProjectViewKind and BucketConfigurationModeKind a huma.SchemaProvider
so the string-serialised enums reflect as string schemas instead of
Huma's default integer schema (which rejected the string form with 422).
Routes registered in registerAPIRoutesV2 before EnableAutoPatch.
Seven integration tests covering the Label pilot:
- Create_Read_Update_Delete — full round-trip through POST/GET/PUT/
DELETE, asserts body + status at each step.
- List_ReturnsItems — GET /labels, asserts items[] is non-empty and
contains a known fixture; this is the regression catcher for the
generic-any silent-empty trap the spike hit.
- ForbiddenErrorShape — user1 reading user13's private label returns
403 problem+json with the RFC 9457 type/title/status/detail shape.
- ValidationErrorShape — POST with empty title fails Huma's
minLength:1 check with 422 problem+json + structured per-field
errors locating `title`.
- ETagReturns304 — first GET captures ETag, second GET with
If-None-Match returns 304.
- PATCHMergePatch — AutoPatch-synthesised PATCH with partial
application/merge-patch+json body updates one field and leaves
the others untouched; a follow-up GET confirms preservation.
- OpenAPISpecDescribesAllFive — the unauthenticated
/api/v2/openapi.json surfaces GET+POST on /labels and GET+PUT+
DELETE on /labels/{id}.
Switches the input normalisation from lower- to uppercase so identifiers
canonicalise the same way GitHub-style refs do (e.g. "PROJ-42"). The
positive identifier tests are dropped for now because the existing
fixtures store identifiers as lowercase ("test1") and the SQL comparison
remains case-sensitive — once the column-side case-insensitive match
lands, full coverage can be reinstated.
Normalises the input side so GitHub-style references like "TEST1-42" and
"test1-42" resolve to the same project. The SQL comparison itself remains
case-sensitive for now; case-insensitive matching on the column will be
addressed separately.
Allows GET /projects/{project}/tasks/by-index/{index} to resolve {project}
as either a numeric id or a project identifier (e.g. "PROJ"), so callers
can build GitHub-style task references like "PROJ-42" without first
looking up the project's numeric id. Pure-digit values remain interpreted
as ids, which makes identifiers consisting solely of digits unreachable
via this route.
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).
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.
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.
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.
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.
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
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'.
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
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.
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.
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
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
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.
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.
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
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
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
- 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>
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".
- 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
- Add httpCodeGetter interface to handle ValidationHTTPError in test helper
- Add test case for password too short in password reset
- Add test case for password too short in password update
- Fix existing test data to use valid 8+ char passwords