Commit Graph

206 Commits

Author SHA1 Message Date
kolaente ac5e94252b feat(api/v2): add totp qr code endpoint
Port GET /user/settings/totp/qrcode to v2 as an image/jpeg blob, modeled in
the OpenAPI spec. Extract the qr-to-jpeg encoding into user.GetTOTPQrCodeAsJpegForUser
so v1 and v2 share it; refactor v1 onto it. The handler reuses the existing
local-account guard, rejecting non-local users with 412.
2026-06-17 18:39:38 +00:00
kolaente f33cde82e2 feat(audit): attribute failed logins to the originating request
Thread the request context through CheckUserCredentials so the
LoginFailedEvent carries IP, user agent and request id — without it,
failed logins were the one auth event useless for brute-force tracing.
All four callers have the request at hand.
2026-06-12 08:56:08 +00:00
kolaente 5f4a21a4c5 feat(events): add auth boundary events
LoginSucceededEvent fires from NewUserAuthTokenResponse (the chokepoint
where local, LDAP and OIDC logins converge), LoginFailedEvent from
handleFailedPassword on every failed password check, LogoutEvent from
the logout handler, and APIToken issued/revoked/used events from the
token model and auth middleware. The token events carry IDs only since
the freshly created token struct holds the raw token string and the
poison queue logs message payloads.

None of these events have a listener yet — the audit registration adds
them. Dispatching to a topic without subscribers is a no-op.
2026-06-12 08:56:08 +00:00
kolaente 5807f2e7b4 refactor(user): share user-search logic between v1 and v2
Extract the duplicated user-search business logic into two helpers both API
versions call, and refactor v1's handlers onto them:
- user.SearchUsers wraps ListUsers + email obfuscation (global search)
- models.SearchUsersForProject wraps the project read check + ListUsersFromProject

Each handler keeps its own forbidden mapping (v1 echo.ErrForbidden vs v2
huma) so v1 stays byte-identical on the wire.
2026-06-11 20:07:43 +00:00
kolaente 46b07a019c refactor(user): extract shared account orchestration into models/user/shared for v1+v2
Pull the business logic out of the v1 current-user account/settings handlers
into reusable functions so both v1 and the upcoming v2 handlers call one
implementation. No behavior change — the v1 handlers keep their HTTP-layer
quirks (input binding, validation, error mapping); only orchestration moves.

Homes are forced by the import graph:
- shared.GetAuthProviderName  (new pkg/routes/api/shared, above openid+user so it
                              can combine both without a cycle; routes-only helper)
- user.ChangeUserEmail        (CheckUserCredentials + UpdateEmail, both in user)
- models.ChangeUserPassword   (needs models.DeleteAllUserSessions; user can't import models)
- models.UpdateUserGeneralSettings / UpdateUserAvatarProvider
                              (need avatar.FlushAllCaches; user can't import avatar)

The general settings get a single shared wire struct, models.UserGeneralSettings
(tagged for both swaggo/govalidator and Huma): it is the update request body and
the nested settings on GET /user for v1 (replacing v1's UserSettings) and v2.
ExtraSettingsLinks is readOnly — populated from the user on read, ignored on
write. A dedicated struct is required because user.User's settings fields are
json:"-" so they don't leak when it is embedded in other responses.
2026-06-11 07:02:31 +00:00
kolaente 4afcfa4441 docs(api/v2): tag TOTP fields for the v2 schema 2026-06-10 17:58:16 +00:00
kolaente da3bf0e7cd docs(api/v2): tag CalDAV token fields for the v2 schema 2026-06-10 17:55:52 +00:00
kolaente cae89caef2 feat(api/v2): add bot user CRUD on /api/v2
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.
2026-06-05 08:51:39 +00:00
kolaente 78ca1904b5
docs(api/v2): mark server-controlled label and user fields read-only 2026-05-31 15:27:44 +02:00
kolaente 9a810f7632 refactor(user): remove the now-empty listeners file
The user package no longer registers any event listeners, so drop the
empty RegisterListeners hook and its caller.
2026-05-30 13:48:01 +00:00
kolaente 06000b7a03 refactor(metrics): drop the user count listener
The user count is now counted on demand, so the increment-on-create
listener is no longer needed.
2026-05-30 13:48:01 +00:00
Claude d9a5958bb8 feat: always enable bot users
Removes the `service.enablebotusers` config flag, the matching
`bot_users_enabled` field on /info, and the now-unused
`ErrBotUsersDisabled` error. Bot user routes and the frontend
settings tab are now always available.

https://claude.ai/code/session_01VhAR6xnoCdG1fpX52bzaCC
2026-05-04 10:38:53 +00:00
kolaente 22d82e292b feat(user): always include own bots in user search
User search previously filtered bots only when they happened to match the
search string. That produced two bad behaviours:

1. Bots owned by other users could surface on an exact-username match,
   leaking them into assignee pickers and similar UI.
2. A user could not reliably find their own bots by typing a partial
   name, so bots became awkward to assign to tasks.

Change ListUsers to treat bot ownership explicitly: the existing match
branch excludes rows owned by someone else, and a second branch always
returns bots owned by the calling user. The own-bots branch also
respects any AdditionalCond passed in so project-scoped listings don't
start leaking bots from outside the project.
2026-05-01 14:44:10 +00:00
kolaente 3415981d1c feat(models): add BotUser CRUD wrapper 2026-05-01 14:44:10 +00:00
kolaente 1637ecd0c7 feat(user): add CreateBotUser 2026-05-01 14:44:10 +00:00
kolaente 506bfa2549 feat(user): reserve bot- username prefix for regular signup 2026-05-01 14:44:10 +00:00
kolaente a262c6a848 feat(user): add bot-related error types 2026-05-01 14:44:10 +00:00
kolaente 83c5190c9b feat(user): add BotOwnerID field and IsBot helper 2026-05-01 14:44:10 +00:00
kolaente af8beb5758 fix(user): skip last-admin guard when target is already unreachable
GuardLastAdmin counted only active, non-deletion-scheduled admins, but gated only on target.IsAdmin. Demoting or deleting an already-disabled or deletion-scheduled admin would then be blocked whenever exactly one active admin remained, even though removing a user who isn't in the reachable set can't reduce the count. Return early when the target isn't part of the counted set.
2026-04-20 18:55:06 +00:00
kolaente d24b96b99c feat(user): extract last-admin guard and close invariant gaps 2026-04-20 18:55:06 +00:00
kolaente 7c7e060d16 feat(auth): include is_admin in JWT claims 2026-04-20 18:55:06 +00:00
kolaente deccc9d29b feat(user): add IsAdmin field to User struct 2026-04-20 18:55:06 +00:00
kolaente 75629158cb test(user): cover TOTP lockout persistence and password-reset unlock
Verifies that HandleFailedTOTPAuth locks the account after 10 rolled-back
caller sessions (the regression from GHSA-fgfv-pv97-6cmj), and that the
persisted password reset token can unlock the account via ResetPassword.
2026-04-09 16:08:26 +00:00
kolaente d435c50df3 fix(security): persist TOTP lockout across login rollback
The failed-TOTP handler shared the login request's xorm session, and the
login handler rolled that session back after a failed login. The status
change to StatusAccountLocked was silently discarded, so the account was
never locked regardless of how many failed TOTP attempts arrived.

HandleFailedTOTPAuth now opens its own session and commits independently
of the caller. The login handler rolls back its session before invoking
the handler so the lockout write can acquire a write lock on SQLite
shared-cache.

Also handles the Redis keyvalue backend returning the attempt counter as
a string instead of int64, which would have prevented the lockout path
from ever running on Redis.

See GHSA-fgfv-pv97-6cmj.
2026-04-09 16:08:26 +00:00
kolaente 39e16653aa fix: add ORDER BY to ListUsers query for deterministic ordering
The query had no ORDER BY clause, causing non-deterministic result
ordering on PostgreSQL where row order is not guaranteed.
2026-03-27 23:05:04 +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 033922309f fix(auth): reject disabled/locked users in CheckUserCredentials
Defense-in-depth: CheckUserCredentials now checks user status after
validating credentials. While current callers are already protected
by upstream checks, this prevents future auth bypass if new code
calls CheckUserCredentials without a subsequent status check.

Ref: GHSA-94xm-jj8x-3cr4
2026-03-23 16:37:26 +00:00
kolaente c7740fc4aa fix(user): use unique error code for ErrCodeAccountLocked
Was 1025 which collides with ErrorCodeInvalidTimezone. Changed to 1026.
2026-03-23 12:06:16 +00:00
kolaente 37394fb336 fix(user): use getUser directly for uniqueness checks in UpdateUser
The username and email uniqueness checks don't need status filtering —
they just need to know if the name/email exists regardless of account
status. Use getUser (which skips the status check) instead of the
public wrappers, reducing cyclomatic complexity back under the threshold.
2026-03-23 12:06:16 +00:00
kolaente 8409bdb120 refactor(user): export IsErrUserStatusError for use across packages
Make isErrUserStatusError public and replace all verbose
!IsErrAccountDisabled(err) && !IsErrAccountLocked(err) checks
with the shorter IsErrUserStatusError(err) call.
2026-03-23 12:06:16 +00:00
kolaente 525f5ee407 test: verify GetUserByID rejects disabled users and returns user with error 2026-03-23 12:06:16 +00:00
kolaente 91c0f386c6 fix(user): handle status errors in pkg/user callers, remove redundant checks 2026-03-23 12:06:16 +00:00
kolaente 04704e0fde fix(user): reject disabled/locked users in getUser by default
getUser now returns ErrAccountDisabled or ErrAccountLocked (alongside
the full user object) for users with StatusDisabled or StatusAccountLocked.
Callers that need disabled/locked users discard the error; all others
propagate it automatically.

GHSA-94xm-jj8x-3cr4
2026-03-23 12:06:16 +00:00
kolaente be771289db feat(user): add ErrAccountLocked error type
Separate from ErrAccountDisabled so callers can distinguish between
admin-disabled accounts (no recovery) and locked accounts (recoverable
via password reset).
2026-03-23 12:06:16 +00:00
kolaente 0f98c19ab6 fix: add TTL-based expiry and cleanup for used TOTP passcode entries
Store a unix timestamp instead of a boolean, and treat entries older
than 90 seconds as expired. A background goroutine lazily cleans up
expired keys after each successful validation to prevent unbounded
growth in the keyvalue store.
2026-03-23 10:34:49 +00:00
kolaente acafa6db10 fix: update TOTP reuse test to use user10 matching rebased fixture 2026-03-23 10:34:49 +00:00
kolaente 5f06e1dce5 fix: prevent TOTP passcode reuse within validity window
Store used TOTP passcodes in the keyvalue store after successful
validation. On subsequent validation attempts, check if the passcode
was already used for the same user and reject it with
ErrTOTPPasscodeUsed. This prevents replay attacks where an intercepted
TOTP code could be reused within its 30-second validity window.
2026-03-23 10:34:49 +00:00
kolaente 5591ca94ba test: add failing test for TOTP passcode reuse prevention
Add TestTOTPPasscodeCannotBeReused which verifies that a valid TOTP
passcode cannot be used twice within its validity window. Also add
ErrTOTPPasscodeUsed error type for the new behavior.
2026-03-23 10:34:49 +00:00
kolaente de58f630ee test: add TOTP fixture and load it in user test bootstrap
Add a TOTP fixture for user1 with a known secret to enable
testing TOTP validation logic. Update InitTests to load the
totp fixture alongside users and user_tokens.
2026-03-23 10:34:49 +00:00
kolaente 89923ebe70 fix: update test expectations for new disabled user fixture
- TestListUsers expects 17 users (was 16)
- TestCleanupOldTokens expects 3 old tokens deleted (was 2)
2026-03-20 11:23:21 +00:00
kolaente 049f4a6be4 fix: prevent email confirmation from re-enabling admin-disabled accounts 2026-03-20 11:23:21 +00:00
kolaente 241b0e80b6 test: add tests for disabled user password reset prevention 2026-03-20 11:23:21 +00:00
kolaente 708ccab895 fix: reject password reset token requests for disabled users 2026-03-20 11:23:21 +00:00
kolaente d8570c603d fix: prevent password reset from re-enabling admin-disabled accounts 2026-03-20 11:23:21 +00:00
kolaente 7792bf6cea refactor: use StatusAccountLocked for TOTP lockouts 2026-03-20 11:23:21 +00:00
kolaente f42a045bdc feat: add StatusAccountLocked user status for TOTP lockouts 2026-03-20 11:23:21 +00:00
kolaente 54c7c4aef2 refactor: move ListUsers tests from pkg/user to pkg/models
The ListUsers function now references team_members and teams tables
via a subquery for external team discoverability. The pkg/user test
environment only syncs user tables, so these tests need to run in
pkg/models which has the full schema and all fixtures.

Also adds new tests for the external team discoverability bypass
directly in the models package alongside the moved tests.
2026-03-04 20:32:11 +01:00
kolaente 28b913f29f feat: bypass discoverability settings for external team members 2026-03-04 20:32:11 +01:00
kolaente 3dd2ba4aa4 feat: register Vikunja tables with db package at init 2026-03-04 15:37:54 +01:00
kolaente 1f363dbd43 fix(events): defer event dispatch for user creation and task positions
Refs #2315
2026-03-03 12:46:34 +01:00