Commit Graph
615 Commits
Author SHA1 Message Date
kolaente 01c3884bb1 feat(auth): sync provider avatars in the background after login
Address review decision: the post-commit avatar sync still blocked the
LDAP/OIDC login response for the download and up to 30s of decode
queueing. It now runs in a goroutine on a context that outlives the
request, with panics recovered and failures logged.
2026-10-20 23:05:30 +02:00
kolaente 819455ca12 fix(avatar): cancel an avatar decode when the client goes away
Address pr-swarm findings: avatar GETs queued for decode capacity on
context.Background(), so an abandoned request still waited 30s. The
request context now reaches the providers; gravatar's shared fetch keeps
running for other waiters when one disconnects. Busy shedding is logged
as a warning instead of an error.
2026-10-20 23:05:30 +02:00
kolaente f31acee824 fix(avatar): flush avatar caches only after the replacement commits
Address pr-swarm finding: the flush ran before commit while the old blob
stays readable until then, so a queued avatar GET could re-cache the old
image with no expiry. The flush now runs on commit and the cache key
carries the file id.
2026-10-20 23:05:30 +02:00
kolaente 891d11da34 fix(avatar): keep the shared default avatar when replacing it
Address pr-swarm finding: users start on defaultsettings.avatar_file_id,
so the first avatar upload or provider sync deleted that shared file for
everyone. Replacing now skips it, through the same check account
deletion uses.
2026-10-20 23:05:30 +02:00
kolaente d2ec3fefc5 fix(avatar): remove a replaced avatar's blob only after commit
Address review decision: SaveAvatar removed the old file's blob from
storage inside the transaction, so a later rollback left the user on an
avatar whose content was gone. The files row is still deleted in the
session; the blob and its previews go once the session commits.
2026-10-20 23:05:30 +02:00
kolaente 98abca499a fix(auth): sync OIDC and LDAP avatars after the login commits
Address review decision: the provider avatar was decoded inside the login
transaction after the user row was written, holding it while queued for
decode capacity, and a later rollback (TOTP, disabled user) discarded the
work. Logins now commit first; the avatar is decoded without a session
and saved in its own short transaction.
2026-10-20 23:05:30 +02:00
kolaente e7a2daa3ff fix(background): decode backgrounds before opening the write session
Address review decision: a background upload queued for decode capacity
held its transaction for up to 30s. The permission check now runs in a
short read session, the image is decoded and encoded without a session,
and a fresh write session re-checks permission and stores the result.
2026-10-20 23:05:30 +02:00
kolaente c8b816bdfa refactor(imageutils)!: answer every busy decode with code 4037
Address review decision: previews (4037) and other decodes (4040) had two
busy errors for one condition. ErrDecodeBusy now carries 4037 and the
preview-only error is gone.
2026-10-20 23:05:30 +02:00
kolaente 9467ad5787 fix(avatar): decode avatars before opening a database session
Address review decision: a decode queued for capacity held the request's
transaction for up to 30s, so on SQLite a concurrent write made the
upload fail with database is locked. Uploads and avatar reads now decode
and encode first; only the encoded PNG enters the transaction.
2026-10-20 23:05:30 +02:00
kolaente 4f8b06499e feat(avatar)!: store every avatar as a centered square
Address review decision: uploads skipped the square crop synced avatars
got, so non-square API uploads were stored as-is although avatars render
square. Non-square uploads are now center-cropped.
2026-10-20 23:05:30 +02:00
kolaente a1865b0151 docs(api): document decode failures on avatar and background uploads
Address pr-swarm finding: the routes did not mention 400/4032 for
undecodable or oversized images, or 503/4040 when decoding is busy.
2026-10-20 23:05:30 +02:00
kolaente b764cf77eb fix(avatar): sync LDAP and OIDC avatars through one decode
Address pr-swarm finding: both logins copied crop, store and flush, had
diverged, decoded every avatar twice, and imaging.Fill cloned images
thinner than 100 px at full resolution outside the decode budget.
2026-10-20 23:05:30 +02:00
kolaente dc2b7ae37d fix(files): drop the file row when storing its content fails
Address pr-swarm finding: avatar sync on login no longer rolls back on a
store error, so a blobless files row would be committed.
2026-10-20 23:05:30 +02:00
kolaente 36136362a2 fix(background): name the right mime type when rejecting an image
Address pr-swarm finding: SaveBackgroundFile sniffed an already-read
stream, so the 400 reported text/plain.
2026-10-20 23:05:30 +02:00
kolaente f773755997 fix(background): answer 400 for any image the decoder rejects
Address pr-swarm finding: only "unknown format" errors mapped to 400, so
truncated data or a header past the read limit answered 500.
2026-10-20 23:05:30 +02:00
kolaente 34434f9434 fix(avatar): answer 400 for uploads the decoder rejects
Address pr-swarm finding: StoreUploadedAvatar ran its own header check
without the read limit, and anything it let through that Decode then
rejected (truncated data, header past 12 MiB) answered 500.
2026-10-20 23:05:30 +02:00
kolaente aa830d8c9b fix(avatar): delete the old avatar only after the new one is saved
Address pr-swarm finding: a failed file create or user update rolled back
avatar_file_id but the old blob was already gone.
2026-10-20 23:05:30 +02:00
kolaente d879cb5c19 fix(ldap): keep the login when storing the synced avatar fails
Address pr-swarm finding: a busy decode budget made StoreAvatarFile fail
the whole LDAP login with 503, while OIDC skips the avatar.
2026-10-20 23:05:30 +02:00
kolaente 1ce3df2d60 refactor(imageutils): queue decodes on the caller's context
Address pr-swarm finding: avatar, background and OIDC/LDAP decodes passed
context.Background(), so a disconnected client stayed queued for up to 30s.
2026-10-20 23:05:30 +02:00
kolaente 1edabda41c refactor(imageutils): drop ValidateReader
Every decode site validates through imageutils.Decode now.
2026-10-20 23:05:30 +02:00
kolaente eaecaa53fe fix(oidc): bound avatar decode memory
Identity-provider avatars are cropped through imageutils.Decode. When the
server is busy the avatar is skipped and logged, like any other avatar
sync error, so the login still succeeds.
2026-10-20 23:05:30 +02:00
kolaente 96496e1dce fix(background): bound decode memory
Project background uploads (and migrated backgrounds) are decoded through
imageutils.Decode, which also covers the blur hash computed from the
decoded image. A busy server answers 503. Drops the unused CreateBlurHash,
which decoded without any bound.
2026-10-20 23:05:30 +02:00
kolaente bcdd4c0cba fix(avatar): bound decode memory
Uploaded avatars are decoded through imageutils.Decode on upload and when
resizing per size, so a busy server answers 503 instead of decoding in
parallel. The old avatar file is removed only after the new one decoded,
since a storage delete is not rolled back with the session.
2026-10-20 23:05:30 +02:00
kolaente f930d895c9 refactor(imageutils): move pixel-weighted decode semaphore
Attachment previews now decode through imageutils.Decode, which holds an
image's pixels against one process-wide budget until the caller is done
with the decoded image. Other decode sites move onto it next.
2026-10-20 23:05:30 +02:00
kolaente 01b0b10a73 fix(migration): count vikunja export items before decoding data.json
Address pr-swarm finding: the item cap ran only after data.json was fully
decoded into tasks, so a file of millions of bare objects still exhausted
memory.
2026-10-20 15:13:02 +02:00
kolaente da41604b64 fix(auth): reject user JWTs whose session was revoked on the HTTP path 2026-10-20 15:13:02 +02:00
kolaente 866789ebad fix(user): enforce the email confirmation cooldown on redis
Redis returns integers as decimal strings, so the cooldown read 0 and was
never enforced. Add keyvalue.GetInt64 that reads either form.
2026-10-20 15:13:02 +02:00
kolaente a0454051cb fix(auth): refuse email-unconfirmed accounts on every credential path 2026-10-20 15:13:02 +02:00
kolaente a89f7dd596 fix(migration): bind oauth migration callbacks to the user who requested the auth url 2026-10-20 15:13:02 +02:00
kolaente 147379947b fix(dump): create the dump archive 0600 and refuse existing paths
The archive holds the config, the full database and every blob, but was
created with the umask mode and followed or clobbered an existing path.

Refs GHSA-rp64-xg8g-9mpr
2026-10-20 15:13:02 +02:00
kolaente 22374d73a5 fix(migration): bound todoist and microsoft todo api responses and item counts 2026-10-20 15:13:02 +02:00
kolaente bf881194ca fix(migration): cap trello and todoist downloads at files.maxsize 2026-10-20 15:13:02 +02:00
kolaente 1852e3b873 fix(migration): reject wekan boards with duplicate ids or too many items 2026-10-20 15:13:02 +02:00
kolaente 2dd4ab14be fix(avatar): round requested size up to a fixed allowlist
Providers cache each size without eviction, so arbitrary, zero and
negative sizes minted unbounded cache entries.

Resolves GHSA-3x3m-2j93-w46j
2026-10-20 15:13:02 +02:00
kolaente bf8859bab3 fix(migration): cap the number of items one import may create 2026-10-20 15:13:02 +02:00
kolaente c7bd5efacc feat(keyvalue): add atomic IncrByWithTTL for windowed counters 2026-10-20 15:13:02 +02:00
kolaente 757f00910e fix(migration): cap the entry count of vikunja export archives 2026-10-20 15:13:02 +02:00
kolaente dc3853feef fix(migration): dispatch events after importing saved filters 2026-10-20 13:34:51 +02:00
kolaente f5e6632b93 fix(migration): escape planka names in generated markdown
Checklist, custom field and link attachment names were concatenated raw
into the description markdown, so brackets, parens or HTML in names broke
links or were dropped.
2026-10-19 21:06:10 +02:00
kolaente 78288f9cbd fix(migration): escape trello link attachment url and name
Link attachments were concatenated raw into the description HTML, so a
quote in the URL or < / & in the name mangled the description.
2026-10-19 21:06:10 +02:00
kolaenteandTink 685c31a1ae refactor(attachments): cut comment noise
Address pr-swarm comment sweep.
2026-10-19 20:33:06 +02:00
kolaenteandTink c32b0e41f2 perf(keyvalue): unlink keys per scan page when deleting a prefix
Address pr-swarm finding: the legacy preview cleanup collected every key and freed them in one blocking DEL.
2026-10-19 20:33:06 +02:00
kolaente c9b6d29383 fix(openid): copy oauth2 config before setting per-request redirect URL
The memory keyvalue store only shallow-copies Provider, so Oauth2Config
was shared between the cache and all requests. Concurrent callbacks
overwrote each other's RedirectURL, causing intermittent login failures.
2026-10-19 17:51:16 +02:00
kolaente d617a7a807 fix(migration): escape checklist titles in trello and wekan imports
Checklist and item titles were concatenated raw into the task description
HTML, so titles containing < or & were mangled.
2026-10-19 17:33:57 +02:00
kolaente 13f7a89a09 fix(openid): reject non-http(s) end_session_endpoint
A provider's discovery document could advertise a javascript: URL as
end_session_endpoint, which the frontend then navigated to on logout,
running script on Vikunja's origin. Only absolute http(s) URLs are now
accepted, both when caching the discovered endpoint and when building
the logout URL. Invalid endpoints are dropped so logout stays local.
2026-10-19 16:58:05 +02:00
kolaente 9803f8687a fix(dump): log only env variable names on restore, not their values 2026-10-19 16:05:26 +02:00
kolaente 12ed4036c8 fix(unsplash): close pingback response body
The pingback discarded the response, leaking the body and its
connection on every pingback.
2026-10-19 14:50:05 +02:00
kolaenteandTink 33bd1c9afe feat(ldap): match users and groups by an immutable directory id
Add auth.ldap.attribute.id. When set (e.g. objectGUID on AD, entryUUID
on OpenLDAP), users and teams are matched by that attribute instead of
the login name and the group DN, so renaming or moving them in the
directory no longer creates duplicate accounts or new teams without the
old shares.

AD's binary objectGUID is formatted as a canonical GUID string; other
attributes must be printable text. Existing accounts and teams are
switched to the new id on the next login. An empty option keeps the old
behaviour.
2026-10-19 13:42:19 +02:00
kolaenteandTink a2875739e9 fix(ldap): match legacy group members against the ldap username
Address pr-swarm finding.
2026-10-19 13:42:19 +02:00
kolaenteandTink eff486337f feat(ldap): query only the user's groups during group sync
Support {userdn} and {username} placeholders in auth.ldap.groupsyncfilter.
When present, every matching group is a membership and the member
attribute is not fetched, so the cost scales with the user's own
memberships and nested AD groups can be resolved with
LDAP_MATCHING_RULE_IN_CHAIN. Filters without a placeholder keep the
previous client-side member matching.

The group search now uses paged results in both modes so directories
with more groups than the server's size limit no longer fail the login.
2026-10-19 13:42:19 +02:00