Skip to content
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

feat(auth): Add bulk get/delete methods #400

Merged
merged 20 commits into from
May 12, 2020
Merged

Conversation

rsgowman
Copy link
Member

@rsgowman rsgowman commented Jan 30, 2020

This PR allows callers to retrieve a list of users by unique identifier (uid, email, phone, federated provider uid) as well as to delete a list of users.

RELEASE NOTE: Added get_users() and delete_users() APIs for retrieving and deleting user accounts in bulk.

@rsgowman rsgowman self-assigned this Jan 30, 2020
last_refresh_at_millis = None
last_refresh_at_iso8601 = self._data.get('lastRefreshAt', None)
if last_refresh_at_iso8601 is not None:
last_refresh_at_millis = iso8601.parse_date(last_refresh_at_iso8601).timestamp() * 1000
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datetime.timestamp() is python 3.3. But I think that's ok now.

requirements.txt Outdated Show resolved Hide resolved
integration/test_auth.py Show resolved Hide resolved
@rsgowman rsgowman assigned hiranya911 and unassigned rsgowman Jan 30, 2020
Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good. I pointed out a few areas that can be improved.

firebase_admin/_identifier.py Outdated Show resolved Hide resolved
firebase_admin/_identifier.py Outdated Show resolved Hide resolved
firebase_admin/_user_mgt.py Outdated Show resolved Hide resolved
firebase_admin/_user_mgt.py Outdated Show resolved Hide resolved
firebase_admin/_user_mgt.py Outdated Show resolved Hide resolved
integration/test_auth.py Outdated Show resolved Hide resolved
integration/test_auth.py Outdated Show resolved Hide resolved
tests/test_user_mgt.py Outdated Show resolved Hide resolved
tests/test_user_mgt.py Show resolved Hide resolved
tests/test_user_mgt.py Show resolved Hide resolved
@hiranya911 hiranya911 assigned rsgowman and unassigned hiranya911 Jan 31, 2020
@rsgowman rsgowman assigned hiranya911 and unassigned rsgowman Feb 5, 2020
Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I think we just need to expose one of the new types in the public API.

datestr_modified = re.sub(r'(\d\d):(\d\d)$', r'\1\2', datestr_modified)

try:
print("trying with micros")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the print statements.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops.

firebase_admin/_user_mgt.py Outdated Show resolved Hide resolved
firebase_admin/_user_mgt.py Outdated Show resolved Hide resolved
firebase_admin/_user_mgt.py Outdated Show resolved Hide resolved
firebase_admin/auth.py Show resolved Hide resolved
@hiranya911
Copy link
Contributor

Adding @egilmorez to review the documentation bits in auth.py

@rsgowman rsgowman assigned hiranya911 and unassigned rsgowman Feb 5, 2020
Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one suggestion about the public API.

I'm also ok with updating the docstring of ErrorInfo to read some like "Error encountered while performing a batch operation such as importing users or deleting multiple user accounts" and then use it instead of the new BatchDeleteErrorInfo.

firebase_admin/auth.py Show resolved Hide resolved

def __init__(self, errors=None):
"""Constructs a BatchDeleteAccountsResponse instance, corresponseing to
the json representing the BatchDeleteAccountsResponse proto.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest "JSON"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"""Represents the results of a delete_users() call."""

def __init__(self, errors=None):
"""Constructs a BatchDeleteAccountsResponse instance, corresponseing to

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest backticks for literal, here and just below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (also fixed spelling typo)


Args:
errors: List of dictionaries, with each dictionary representing a
ErrorInfo instance as returned by the server. None implies an

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest backticks for literal, and "an ErrorInfo instance..."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done/done.


Returns:
list[dict[string, string]]: List of dicts representing the json
UserInfo responses from the server.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest backticks for literal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. (Also json->JSON)

result list is not guaranteed to correspond to the nth entry in the input
parameters list.

Only a maximum of 100 identifiers may be supplied. If more than 100

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest "A maximum . . . "

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

parameters list.

Only a maximum of 100 identifiers may be supplied. If more than 100
identifiers are supplied, this method will immediately raise a ValueError.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest "method raises a ValueError.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

DeleteUserResult.success_count value.

Only a maximum of 1000 identifiers may be supplied. If more than 1000
identifiers are supplied, this method will immediately raise a ValueError.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest "method raises a ValueError.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"""Deletes the users specified by the given identifiers.

Deleting a non-existing user won't generate an error. (i.e. this method is
idempotent.) Non-existing users will be considered to be successfully

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest "are considered"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link

@egilmorez egilmorez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor style suggestions for parsed comments. Thanks!

Copy link
Collaborator

@cbonnie cbonnie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left small comment, otherwise LGTM. (Reviewing on behalf of egilmore@)


@property
def errors(self):
"""A list of ``auth.ErrorInfo`` instances describing the errors that
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking, is this supposed to be in double back ticks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea! It seems both double and single backticks are used throughout. Single backticks are more common though, so I switched to that.

Deleting a non-existing user won't generate an error. (i.e. this method is
idempotent.) Non-existing users are considered to be successfully
deleted, and will therefore be counted in the
`DeleteUserResult.success_count` value.
Copy link
Collaborator

@cbonnie cbonnie Mar 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest:

"Deleting a non-existing user does not generate an error (the method is idempotent). Non-existing users are considered to be successfully deleted and are therefore included in the DeleteUserResult.success_count value."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks quite good. I just made a few suggestions. See if those make sense for this PR.

firebase_admin/_user_identifier.py Show resolved Hide resolved
Args:
uid: A user ID string.
"""
self.uid = uid
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to validate these arguments? (Not empty, not None, string type etc)

Copy link
Member Author

@rsgowman rsgowman May 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't hurt, though implies we'll be doing some of the validation twice. (I don't think that's a problem.) Done. (Also, no longer doing validation twice; see other comment).

Note that we don't validate arguments to (eg) auth.get_user_by_email(), instead, just deferring to the implementation function (user_mgt.get_user). This is a slightly different scenario, since for UserIdentifier, the user can create that ahead of time and possibly use it in some other context.

Another improvement (that I don't want to make in this PR) is to use type annotations. All of our supported python versions now support type annotations and they'd give you some of this for free (and at "compile" time too.)

firebase_admin/_auth_client.py Outdated Show resolved Hide resolved
firebase_admin/_auth_client.py Show resolved Hide resolved
payload = {}
for identifier in identifiers:
if isinstance(identifier, _user_identifier.UidIdentifier):
_auth_utils.validate_uid(identifier.uid, required=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we push this validation and the subsequent marshaling to the corresponding classes?

Each identifier class can expose something like:

def marshal(self, payload):
   payload['localId'] = payload.get('localId', []).append(self.uid)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this (just now) but didn't really like the results. It makes this function shorter, but it alters the UserIdentifier types such that they need to know how the json payload is constructed. (For instance, should it be ProviderIdentifiers job to know that provider_uid maps to rawId on the wire?) It also spreads building the payload across 2 files and 5 classes, instead of having it in just one spot.

One possible improvement we could make here is to eliminate the validation (or at least, refactor it somewhat). As per one of your other comments, I've already moved some validation to the ctors, but that won't stop users from changing the values after construction, but before calling get_users(), so we should keep the validation here. But if we altered (eg) UidIdentifier.uid to be UidIdentifier._uid and added a getter (essentially making these objects immutable) I think that could allow us to skip validation here entirely, which would allow this block to focus solely on creating the payload. I've done that. PTAL.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how validation turned out (and the immutability of the properties). I don't feel strongly about the marshalling part. When I first suggested it, I felt that having each class being in charge of marshalling was a good thing. If we add more implementations of the UserIdentifier interface in the future, that's when this will matter more. I'm ok with leaving this as is for now.

firebase_admin/_user_mgt.py Outdated Show resolved Hide resolved
@@ -30,7 +32,12 @@ def __init__(self, uid):
Args:
uid: A user ID string.
"""
self.uid = uid
_auth_utils.validate_uid(uid, required=True)
self._uid = uid
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can collapse these into a single line:

self._uid = _auth_utils.validate_uid(...)

Same below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

firebase_admin/_user_mgt.py Outdated Show resolved Hide resolved
Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. LGTM with a suggestion.

@rsgowman rsgowman merged commit d1aae52 into master May 12, 2020
@rsgowman rsgowman deleted the rsgowman/bulk_getdelete branch May 12, 2020 20:57
rsgowman added a commit that referenced this pull request May 13, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants