-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
ref(users): move user & user_identity_config & auth serializers to new abode #76896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ref(users): move user & user_identity_config & auth serializers to new abode #76896
Conversation
Codecov ReportAttention: Patch coverage is ✅ All tests successful. No failed tests found.
Additional details and impacted files@@ Coverage Diff @@
## master #76896 +/- ##
===========================================
+ Coverage 64.76% 78.25% +13.49%
===========================================
Files 6894 6900 +6
Lines 306469 306875 +406
Branches 50250 50268 +18
===========================================
+ Hits 198493 240154 +41661
+ Misses 101387 60292 -41095
+ Partials 6589 6429 -160 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced file with import shim for any getsentry refs
from sentry.api.serializers import Serializer, register | ||
from sentry.auth.authenticators.base import AuthenticatorInterface | ||
from sentry.auth.authenticators.recovery_code import RecoveryCodeInterface | ||
from sentry.auth.authenticators.sms import SmsInterface | ||
from sentry.auth.authenticators.totp import TotpInterface | ||
from sentry.auth.authenticators.u2f import U2fInterface | ||
|
||
if TYPE_CHECKING: | ||
from django.utils.functional import _StrOrPromise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From: typeddjango/django-stubs#1804
is it ok if we potentially return/serialize a promise?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it ok if we potentially return/serialize a promise?
If we can we should avoid importing symbols with _
prefixes. We could str()
cast these translation related containers to simplify the typed dict definitions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is actually fine -- _StrOrPromise
is ~kinda public and typing-only
|
||
|
||
def manytoone_to_dict( | ||
queryset: QuerySet[T], key: str, filter_func: Callable[[Any], bool] | None = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typed and made this generic-ish
dateJoined: datetime | ||
lastLogin: datetime | None | ||
has2fa: bool | ||
lastActive: datetime | None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The model says that lastLogin
and lastActive
are nullable fields but the openAPI spec says these are required fields, not sure if there's a source of truth/who to follow for this response
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have casting/fallback in the serializer that removes the None
values, or the api schema could be wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely interpreted the OpenApi failing job wrong, as I didn't realize the OpenApi spec is generated from the model serializer which I guess didn't exist until now (?????)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That job fails anytime there is a delta which isn't great. I took a look at the output and because we've added more accurate types schema generation has better metadata to use and the schema has changed. The properties were always nullable, but our schema didn't reflect that because of limited information.
] | ||
|
||
return d | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got rid of the DetailedUserSerializerResponse
here as it wasn't really adding anything new
from sentry.models.notificationaction import ( | ||
ActionService, | ||
ActionTarget, | ||
ActionTrigger, | ||
NotificationAction, | ||
NotificationActionProject, | ||
) | ||
from sentry.users.api.serializers.user import manytoone_to_dict |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not entirely related to these changes, but this module seems like a strange place for a very generic query utility. We can move it separately later.
@@ -0,0 +1,3 @@ | |||
from .authenticator import * # noqa: F401,F403 | |||
from .user import * # noqa: F401,F403 | |||
from .user_identity_config import * # noqa: F401,F403 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this barrel file? It seems like we could avoid it right now.
from sentry.api.serializers import Serializer, register | ||
from sentry.auth.authenticators.base import AuthenticatorInterface | ||
from sentry.auth.authenticators.recovery_code import RecoveryCodeInterface | ||
from sentry.auth.authenticators.sms import SmsInterface | ||
from sentry.auth.authenticators.totp import TotpInterface | ||
from sentry.auth.authenticators.u2f import U2fInterface | ||
|
||
if TYPE_CHECKING: | ||
from django.utils.functional import _StrOrPromise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it ok if we potentially return/serialize a promise?
If we can we should avoid importing symbols with _
prefixes. We could str()
cast these translation related containers to simplify the typed dict definitions.
dateJoined: datetime | ||
lastLogin: datetime | None | ||
has2fa: bool | ||
lastActive: datetime | None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have casting/fallback in the serializer that removes the None
values, or the api schema could be wrong.
def get_interface_serializer(interface: AuthenticatorInterface) -> AuthenticatorInterfaceSerializer: | ||
if isinstance(interface, SmsInterface): | ||
return SmsInterfaceSerializer() | ||
return AuthenticatorInterfaceSerializer() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the endpoints that are given an ApplicationInterface
, interface, to serialize, this 'getter' is used to make sure we're using the right serializer. It's a weird scenario because registering the serializer in line 76 and 94 should solve this for us, but because we also have that bug of the file not being imported and therefore the serializers not being registered, this redundant function is here.....
Maybe other alternatives could be having barrel file for just the authenticator ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't hate the factory function. In the longer term, I think we should consider removing the @register()
calls as they contribute to import tangle and import side-effects create hidden dependencies between modules.
It's a weird scenario because registering the serializer in line 76 and 94 should solve this for us, but because we also have that bug of the file not being imported and therefore the serializers not being registered, this redundant function is here.....
If the endpoint imports the sentry.users.api.serializers.authenticators
module I would expect that the register()
calls would behave correctly. Is that not the case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah that's correct, but if we import the authenticator
serializer and don't do anything with it, we'll get shaboinked by mypy & linter. OTOH we can't just pass in AuthenticatorInterface for everything because of SmsInterface :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me.
class EnrolledAuthenticatorInterfaceSerializerResponse(TypedDict, total=False): | ||
authId: str | ||
createdAt: datetime | ||
lastUsedAt: datetime | None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way to solve this in future changes would be to use typing.NotRequired
to mark these keys as optional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually that's a good point -- is this actually optional and nullable? (rarely something is)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asottile-sentry like are the "enrolled" fields optional & nullable ? I believe so, because they are some test cases like tests/sentry/users/api/endpoints/test_user_authenticator_enroll.py
where the enrollment in an authenticator interface failed, so the returned user doesn't have any of these fields. Or were you talking about a different field (?) ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as written now there's these possibilities:
{}
{"lastUsedAt": some_datetime}
{"lastUsedAt": None}
which is usually a bug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I dunno, from what I'm seeing from our tests
{}
-> is a real scenario [it seems like this can happen when enrollment fails], so Optional seems to be valid
{"lastUsedAt": some_datetime}
-> also is a real scenario when we mark an authenticator as used.
{"lastUsedAt": None}
-> also is real when a user is newly enrolled to an authenticator
It looks like the three scenarios all happen so probably not a bug, albeit also somewhat confusing to deal with
def get_interface_serializer(interface: AuthenticatorInterface) -> AuthenticatorInterfaceSerializer: | ||
if isinstance(interface, SmsInterface): | ||
return SmsInterfaceSerializer() | ||
return AuthenticatorInterfaceSerializer() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't hate the factory function. In the longer term, I think we should consider removing the @register()
calls as they contribute to import tangle and import side-effects create hidden dependencies between modules.
It's a weird scenario because registering the serializer in line 76 and 94 should solve this for us, but because we also have that bug of the file not being imported and therefore the serializers not being registered, this redundant function is here.....
If the endpoint imports the sentry.users.api.serializers.authenticators
module I would expect that the register()
calls would behave correctly. Is that not the case?
# OrganizationMemberResponse). | ||
identities: list[_Identity] | ||
avatar: SerializedAvatarFields | ||
authenticators: list[Any] # TODO: find out what type this is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting that we have a special serialization format for authenticators here that is different from the representation that authenticator endpoints use. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeahh..... the user_authenticator_enroll.py
endpoint also has it's own serializer /format for each of the interfaces that's different. AuthenticatorInterface serialization in general is like p. inconsistent ;-;
T = TypeVar("T", bound=Model) | ||
|
||
|
||
def manytoone_to_dict( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏
Moves
and their tests to the user's directory :0) Fixes any additional typing along the journey (mainly serializer response dicts) ~
split of (#76822)
ref(issue # here)