The password was checked through the login lookup by username or email, which could resolve to a different account, and the email of that account was then changed. Load the account by its ID and check its own password, keeping the re-authentication throttle and the lockout counter.
Checking the old password directly skipped the failed-attempt counter that login and the email-change prompt use. Extract CheckUserPasswordCountingFailures from CheckUserCredentials and use it for the password change too.
ShouldNotify dropped every notification for locked accounts, including the reset email needed to get out of the lock. It now receives the notification and lets password resets through for locked accounts; disabled accounts, bots and all other notifications stay suppressed. The admin reset action no longer refuses locked accounts either.
Consume the exact confirmation token and only apply the update when the pending address is still the one that was read, so a concurrent email change cannot be confirmed by an older token.
ILIKE treated _ and \ in the search term as pattern characters, so the exact-username lookup could match other usernames. Compare case-insensitively with LOWER() instead.
Mirrors the TOTP lockout: issue a password reset token, send a mail, set
the account to locked and revoke its sessions. Counts every wrong password
checked by CheckUserCredentials, including the password and email change
re-auth prompts, so a stolen session can no longer guess the password
indefinitely.
The 3-attempt warning mail no longer resets the counter, otherwise it
could never reach the lockout threshold.
GetUserByID and GetUsersByIDs now set BotOwner on bots, loading all
owners in one memoized query. Owners skip the disabled/locked gate so a
bot's creator stays discoverable after the owner is disabled.
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.
The auto-lock after ten failed TOTP attempts left sessions and sockets
alive until token expiry. Locking now dispatches an account-locked event
whose listener deletes the user's sessions.
- Owners can manage bots in any status (disabled or locked); unowned,
missing and non-bot ids all return 404 so existence never leaks.
- PUT /api/v2/user/bots/{bot} requires status, so a partial body can no
longer re-enable a disabled bot. A bot_status validator tag limits it
to active/disabled.
- BotUser.Status is a pointer: the v1 update route keeps the current
status when the body omits it instead of unlocking the bot.
- Owner status changes dispatch bot.status.changed for the audit log.
- Regenerate the frontend client and adapt the bot update callers.
Refs #3999
1026 was already taken by ErrorCodeUsernameReserved, which is older, so a
locked account was reported as a reserved username in clients.
BREAKING CHANGE: a locked account now returns 1040 instead of 1026.
1025 was already taken by ErrorCodeInvalidTimezone, which is older, so
reusing a TOTP passcode (e.g. logging in right after enabling 2FA) showed
"The timezone '' is invalid." in the frontend.
Refs #3900
BREAKING CHANGE: a reused TOTP passcode now returns 1039 instead of 1025.
One GET /tasks/{id} loaded the task twice (Task.CanRead, then ReadOne), its
project twice and users by id several times, each a round trip.
GetTaskByIDSimple, GetProjectSimpleByID, GetProjectsMapByIDs,
user.GetUserByID and user.GetUsersByIDs now go through the per-session memo,
which drops itself as soon as that session writes anything.
db.Remember and db.RememberEach hold the get-or-fetch dance, mirroring
keyvalue.Remember; the two pre-existing memo sites in project_access.go use
them too. Callers get copies: Project.memoCopy re-allocates ParentProjectID,
and the user memo holds the raw row so the disabled/locked gate runs on
every hit rather than only on the first load.
1022 was already taken by ErrCodeUsernameMustNotContainSpaces, which is
the older of the two, so a malformed OIDC scope rendered "The username
must not contain spaces." in clients.
BREAKING CHANGE: a malformed OIDC custom scope now returns 1038 instead
of 1022.
Both helpers compared a.GetID() against users.id, but *LinkSharing also
satisfies web.Auth and its GetID() is a link_shares.id - a different id
namespace. Passing one would have emitted users.id = <share id> and lent
that share another user's identity, which is the confusion behind
GHSA-vvcv-vpph-h844. Guarded at every call site, but the helpers moved
out of models and into user, so the guard drifted away from the code
needing it. Taking *User makes the case unrepresentable.
Also fail closed on a zero id: humans store bot_owner_id = 0, so an
unguarded zero would have matched all of them.
The doc claimed JWT and CalDAV auth carry no BotOwnerID, but bots can
reach neither path, and claimed bots-cannot-own-bots as an invariant when
the guard is inert for JWT principals. Reworded to chain-safety by
construction, which holds regardless.
The label access condition covered owner-to-bot and bot-to-owner, so two bots
of the same human could not use each other's labels: bot A seeded a label and
bot B got a 403 on it.
Replacing it with SameBotIdentityCond resolves every caller to an identity root
- a bot's owner, or a human themselves - and matches everyone under that root,
which covers all three directions in a single condition. Write access is
unchanged: only the creator and the human owner can update or delete a label.
Replaces the hand-rolled timestamp check and cleanup goroutine. The old
int64 type assertion never matched on redis (Get returns a string), so
passcode replay protection was silently disabled there.
Gives the account owner an out-of-band signal if someone with their
password starts moving the login address, with a link to the settings
page where the change can be cancelled. Sent after the confirmation mail
so a throttled request mails nothing.
Changing the email no longer overwrites the address and flips the account
to StatusEmailConfirmationRequired. The new address is stored as
pending_email and only applied when the confirm token is used (valid 24h),
so a lost or misaddressed confirmation mail can no longer lock the user
out. The change can be repeated to fix typos, cancelled, or the mail
resent; confirmation mails are throttled to one per minute per user, and
stale change tokens are swept by the cleanup cron.
Fixes#3481
The default-project creation triggered by the user-created event wrote the
stale in-memory user back with a full UpdateUser, racing registration and
sometimes resetting the status from "email confirmation required" back to
active. Only update the default_project_id column and keep the returned
user's status in sync with what was persisted.