Feature/ogc 3157 current user none crash#2471
Conversation
…er() Adds OrgRequest.get_current_user() which captures a Sentry warning and raises HTTPForbidden when current_user is None despite a valid Redis session. Replaces all bare asserts and inline None checks across feriennet, org, town6 and translator_directory. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests that get_current_user() returns the user when present, and raises HTTPForbidden while sending a Sentry warning when current_user is None. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR aims to prevent crashes when request.current_user is unexpectedly None by introducing a safer accessor on OrgRequest and replacing a number of assert request.current_user is not None call sites with either request.get_current_user() or guard clauses.
Changes:
- Added
OrgRequest.get_current_user()which raisesHTTPForbiddenand attempts to emit a Sentry warning whencurrent_useris unexpectedly missing. - Replaced multiple
assert request.current_user is not Noneusages withrequest.get_current_user()across org/feriennet/translator_directory views and helpers. - Added unit tests for the new
get_current_user()behavior.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/onegov/org/test_request.py | Adds tests for OrgRequest.get_current_user() success and failure behavior. |
| src/onegov/org/request.py | Introduces get_current_user() and adds Sentry logging + HTTPForbidden on missing user. |
| src/onegov/org/custom.py | Avoids crashing in global tools ticket section when current_user is None. |
| src/onegov/org/utils.py | Switches second-factor lookup to use get_current_user() in can_change_username. |
| src/onegov/org/views/ticket.py | Uses get_current_user() when building “My” ticket filter. |
| src/onegov/org/views/resource.py | Uses get_current_user() when closing reservation tickets. |
| src/onegov/org/views/form_registration_window.py | Uses get_current_user() when closing tickets during submission cancellation. |
| src/onegov/org/views/form_definition.py | Uses get_current_user() when closing tickets related to submissions. |
| src/onegov/translator_directory/views/time_report.py | Uses get_current_user() when accepting time reports and closing tickets. |
| src/onegov/translator_directory/generate_docx.py | Uses get_current_user() to derive signature lookup criteria. |
| src/onegov/translator_directory/custom.py | Adds guard to avoid current_user-None crash in tickets tools. |
| src/onegov/town6/custom.py | Adds guard to avoid current_user-None crash in chat staff tools. |
| src/onegov/feriennet/views/occasion.py | Uses get_current_user() when recording TOS acceptance. |
| src/onegov/feriennet/views/invoice.py | Uses get_current_user() for invoice/donation flows dependent on the logged-in user. |
| src/onegov/feriennet/views/booking.py | Uses get_current_user() for “my bookings” when not admin. |
| src/onegov/feriennet/forms/attendee.py | Uses get_current_user() for TOS field handling based on user data. |
| src/onegov/feriennet/custom.py | Adds guard to avoid current_user-None crash in personal tools. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
Additional details and impacted files
Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Embed the username directly in the message string instead of using the non-existent extras= parameter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…up fails Anonymous requests should raise HTTPForbidden silently; the warning is only meaningful when a valid session identity exists but current_user is unexpectedly None. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ogin links Replace early return guard with an inline condition so only the tickets block is skipped when current_user is None, not the rest of the generator. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename get_current_user() result to current_user to distinguish the requester from the user argument being inspected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@Daverball Is this a good approach to prevent assertion errors at runtime but try to identify the root cause of |
…user NO_IDENTITY is not None, so the previous guard would trigger a spurious Sentry warning for unauthenticated requests. Use is_logged_in instead, which correctly checks identity is not NO_IDENTITY. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MagicMock auto-creates truthy attributes, so setting identity=None had no effect on the is_logged_in check. Explicitly set is_logged_in=True/False to reflect the actual condition the code under test checks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replacing the silent early return with get_current_user() ensures a stale-session case (is_logged_in True but current_user None) is reported to Sentry and handled consistently with the rest of the codebase. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sions Same fix as town6: replace the silent early return with get_current_user() so a stale-session case is reported to Sentry and handled consistently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the silent current_user is not None guard with get_current_user() so stale-session inconsistencies are reported to Sentry consistently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The method raises HTTPForbidden for any None current_user, not just the stale-session case. Clarify that anonymous requests are also forbidden, and that Sentry is only notified for the logged-in-but-missing-user case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ning current_username is an email address (PII) and was sent unconditionally, bypassing the should_send_default_pii() gate used elsewhere in the Sentry integration. Switch to identity.uid which is the non-PII Sentry user ID already attached to every event via the event processor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Asserts can be optimised away with -O and split(' ') would crash on
names with more than one space. Use an explicit guard and split(' ', 1)
with a length check instead, returning None for missing or malformed names.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the silent current_user None guard in translator_directory/custom.py with get_current_user(), consistent with the other nav fixes. Also add get_current_user() to AgencyApp's DummyRequest test fixture. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
strip() removes leading/trailing whitespace, split(None, 1) handles multiple spaces between first and last name, and the non-empty check on both parts prevents overly broad LIKE '%%' queries. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The root cause of this exception is most likely that changing a translator's e-mail address does not log them out. So they're still logged in with their old e-mail, which no longer has an associated account, so So I don't think we should make any large sweeping changes to make the application not crash, when this happens. Since after all it is a real problem if we didn't log that user out. It might be worth adding a Although we probably could change |
Is that related to LDAP? |
It shouldn't be an issue during external login, although I'm not sure what happens if they have multiple sessions and their LDAP username gets changed during external login, I'm not sure if we remembered to terminate all the other sessions, so only the one with the new name remains. But with translators it's also possible for admins to change the name of the translator, which gets mirrored back to the user if I'm not mistaken and we could be forgetting to log the user out there as well. There's also the LDAP synchronization cronjob, although I'm not sure if we have that for translator directory or if it's only running for FSI. Either way there are quite a few ways the username can change, and we probably didn't remember to log out the active session for some of the ways. That's why adding a |
| @@ -37,6 +40,8 @@ def remember( | |||
| ) -> None: | |||
| for key in self.required_keys: | |||
| request.browser_session[key] = getattr(identity, key) | |||
| if (uid := getattr(identity, 'uid', None)) is not None: | |||
| request.browser_session['uid'] = uid | |||
There was a problem hiding this comment.
Just add uid to required_keys instead. I think I just forgot to do that, when I added uid to Identity.
Daverball
left a comment
There was a problem hiding this comment.
Looks good overall, but we can simplify some of the uid vs userid logic.
| uid = getattr(self.identity, 'uid', None) | ||
| if uid: | ||
| self._current_user = ( | ||
| self.session.query(User) | ||
| .filter(User.id == UUID(uid)) | ||
| .first() | ||
| ) | ||
| if ( | ||
| self._current_user is not None | ||
| and self.identity.userid != self._current_user.username | ||
| ): | ||
| # stale username in session — fix it for subsequent | ||
| # requests | ||
| self.browser_session['userid'] = ( | ||
| self._current_user.username) | ||
| else: | ||
| self._current_user = ( | ||
| self.session.query(User) | ||
| .filter_by(username=self.identity.userid) | ||
| .first() | ||
| ) |
There was a problem hiding this comment.
This branching should not be necessary, valid identities always should have a uid. The only reason it sometimes didn't before, is because we forgot to put it in the cache along with the other identity attributes.
| user = self.current_user | ||
| if user is not None: | ||
| return user.username |
There was a problem hiding this comment.
I'm not sure I'm entirely comfortable with this change. It is a little bit more robust, but it means we always have to load the entire user object on every request where we access this property, even though we have the username cached and it should always be correct, if we didn't leave an old session hanging around, that should've been terminated.
I think we should try to get away with the old implementation, but we could check if _current_user already exist, when it does we can return _current_user.username instead. But I don't think we should emit an extra database query, unless we definitely need it.
| uid = getattr(identity, 'uid', None) | ||
| if uid: | ||
| user = self.users.by_id(UUID(uid)) | ||
| if user is not None: | ||
| return user |
There was a problem hiding this comment.
Once again, this branch should be unnecessary and we should skip the query if identity.uid is None, I think you also don't need to convert to UUID, SQLAlchemy should automatically coerce the value to the correct type.
| def test_user_change_username() -> None: | ||
| user = User(username='old@example.org') | ||
|
|
||
| with patch('onegov.user.models.user.remembered') as remembered: | ||
| with patch('onegov.user.models.user.forget') as forget: | ||
| remembered.return_value = True | ||
|
|
||
| user.save_current_session(DummyRequest('sess1')) # type: ignore[arg-type] | ||
| assert user.sessions is not None | ||
| assert 'sess1' in user.sessions | ||
|
|
||
| user.change_username('new@example.org', DummyRequest('zzz').app) # type: ignore[arg-type] | ||
|
|
||
| assert call(None, 'sess1') in forget.mock_calls | ||
| assert user.username == 'new@example.org' |
There was a problem hiding this comment.
| def test_user_change_username() -> None: | |
| user = User(username='old@example.org') | |
| with patch('onegov.user.models.user.remembered') as remembered: | |
| with patch('onegov.user.models.user.forget') as forget: | |
| remembered.return_value = True | |
| user.save_current_session(DummyRequest('sess1')) # type: ignore[arg-type] | |
| assert user.sessions is not None | |
| assert 'sess1' in user.sessions | |
| user.change_username('new@example.org', DummyRequest('zzz').app) # type: ignore[arg-type] | |
| assert call(None, 'sess1') in forget.mock_calls | |
| assert user.username == 'new@example.org' | |
| def test_user_change_username() -> None: | |
| user = User(username='old@example.org') | |
| with ( | |
| patch('onegov.user.models.user.remembered') as remembered, | |
| patch('onegov.user.models.user.forget') as forget | |
| ): | |
| remembered.return_value = True | |
| user.save_current_session(DummyRequest('sess1')) # type: ignore[arg-type] | |
| assert user.sessions is not None | |
| assert 'sess1' in user.sessions | |
| user.change_username('new@example.org', DummyRequest('zzz').app) # type: ignore[arg-type] | |
| assert call(None, 'sess1') in forget.mock_calls | |
| assert user.username == 'new@example.org' |
You can invoke multiple context managers in a single with statement, so you don't need to indent twice.
Org: Replace
assert current_user is not Nonewith a Sentry warning andHTTPForbiddenAdds
OrgRequest.get_current_user()which captures a Sentry warning and raisesHTTPForbiddenwhencurrent_user is Nonedespite a valid session. Replaces all bare asserts across feriennet, org, town6 and translator_directory.Sentry issue: https://seantis-gmbh.sentry.io/issues/7467148569/
TYPE: Feature
LINK: ogc-3157