Fixes#2495, fixes#2400
When tasks are created, setTaskInBucketInViews() assigns positions
without checking for conflicts. If two tasks get the same position value,
their order scrambles on reload. This adds a conflict check after
bulk-inserting positions, reusing the existing resolution logic from the
update path.
Add resolvePositionConflictsAfterInsert() which checks newly inserted
task positions for duplicate position values within the same view and
resolves them using existing conflict resolution logic.
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
When expand[]=subtasks is used, the LEFT JOIN on task_relations filters
out same-project subtasks. If a parent task was deleted, the JOIN
produces NULL for parent_tasks.id/project_id, and since NULL != value
evaluates to NULL (not TRUE) in SQL, these tasks were incorrectly
excluded.
Add builder.IsNull{"parent_tasks.id"} to the OR condition so tasks
whose parent was deleted are still included in results.
Add the OAuthCode model for storing short-lived authorization codes
with PKCE challenges. Codes are hashed (SHA-256) before storage and
are single-use with a 10-minute expiry. Add the database migration
and OAuth-specific error types.
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.
Replace the Go-side propagateArchivedState function with in-CTE
propagation. The recursive SELECT uses (ap.is_archived OR p.is_archived)
to inherit archived state from parent projects. The outer query uses
GROUP BY with MAX(CAST(is_archived AS int)) to handle projects
accessible via both direct permissions and parent traversal. When
getArchived=false, a HAVING clause filters out archived projects.
The is_archived filter is removed from getUserProjectsStatement so
archived parents enter the CTE and propagation works correctly.
Previously, any user with read access to a project could list all link
shares including their hashes via GET /projects/{id}/shares. This allowed
read-only collaborators to obtain write or admin link share hashes and
escalate their privileges. Now ReadAll requires admin access to the
project.
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
The SSRF protection is now tested at the shared utility level in
pkg/utils/httpclient_test.go. The webhook-specific SSRF tests were
duplicating the same checks since getWebhookHTTPClient() delegates
to NewSSRFSafeHTTPClient().
Add config-level exclusions for G117 (secret-named struct fields),
G101 in test files, G702/G704 in magefile, and goheader in plugins.
Add inline #nosec comments for specific G703/G704 false positives
in export, dump/restore, migration, and avatar code.
CheckIsArchived() previously skipped checking a child project's own
IsArchived flag when ParentProjectID > 0, immediately recursing to
only check the parent. This allowed write operations on individually
archived child projects whose parent was not archived.
Now the function loads the project from the database first, checks its
own IsArchived flag, and only then recurses to check parent projects.
Make isErrUserStatusError public and replace all verbose
!IsErrAccountDisabled(err) && !IsErrAccountLocked(err) checks
with the shorter IsErrUserStatusError(err) call.
Internal callers (reactions) look up comments by ID without knowing
the task. The IDOR protection is still effective because ReadOne
always has TaskID set from the URL parameter.
Add task_id check to getTaskCommentSimple so that a comment can only
be loaded if it actually belongs to the task specified in the URL.
Previously, any valid comment ID could be read through any accessible
task endpoint.
Ref: GHSA-mr3j-p26x-72x4
File.File is now io.ReadCloser (no Seek). Buffer the file bytes
once via io.ReadAll, then use bytes.NewReader for both DecodeConfig
and Decode. Test updated to use io.NopCloser instead of afero.
Replace the github.com/spf13/afero dependency with a purpose-built
FileStorage interface (Open, Write, Stat, Remove, MkdirAll) with three
implementations: localStorage (with basePath), s3Storage (with key
prefix), and memStorage (for tests).
Each implementation owns its base path — callers pass only file IDs.
Delete s3fs.go, change File.File from afero.File to io.ReadCloser,
and fix duplication flows to buffer content for seeking.
Block webhook requests to non-globally-routable IP addresses by default.
Uses net.Dialer.Control hook to validate resolved IPs against IANA
Special Purpose Registries after DNS resolution, preventing DNS rebinding.
Configurable via webhooks.allownonroutableips (default: false).
The `expand` query parameter only supported the `expand[]=foo` array
format, but the swagger docs described it as a plain string parameter.
This adds support for both formats (`expand=foo` and `expand[]=foo`),
matching the existing pattern used by `sort_by` and `order_by`
parameters.
Closes#2408
---------
Co-authored-by: kolaente <k@knt.li>
The tasks_labels_bulk route was not recognized as a CRUD route by
isStandardCRUDRoute, causing it to be processed as a non-CRUD route
and registered in the wrong apiTokenRoutes group. API tokens with
tasks_labels permissions could not access the bulk endpoint, resulting
in a 401 error.
Fixes https://github.com/go-vikunja/vikunja/issues/2375