Skip to content

Add API to look up users by provider user ID. #388

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ def validate_provider_id(provider_id, required=True):
'string.'.format(provider_id))
return provider_id

def validate_provider_uid(provider_uid, required=True):
if provider_uid is None and not required:
return None
if not isinstance(provider_uid, six.string_types) or not provider_uid:
raise ValueError(
'Invalid provider uid: "{0}". Provider uid must be a non-empty '
'string.'.format(provider_uid))
return provider_uid

def validate_photo_url(photo_url, required=False):
"""Parses and validates the given URL string."""
if photo_url is None and not required:
Expand Down
18 changes: 18 additions & 0 deletions firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,24 @@ def get_user(self, **kwargs):
elif 'phone_number' in kwargs:
key, key_type = kwargs.pop('phone_number'), 'phone number'
payload = {'phoneNumber' : [_auth_utils.validate_phone(key, required=True)]}
elif 'provider_id' in kwargs and 'provider_uid' in kwargs:
provider_id = kwargs.pop('provider_id')
provider_uid = kwargs.pop('provider_uid')
if provider_id == 'phone':
key, key_type = provider_uid, 'phone number'
payload = {'phoneNumber' : [_auth_utils.validate_phone(key, required=True)]}
elif provider_id == 'password':
key, key_type = provider_uid, 'email'
payload = {'email' : [_auth_utils.validate_email(key, required=True)]}
else:
key, key_type = {
'provider_id': provider_id, 'provider_uid': provider_uid
}, 'provider_user_id'
payload = {
'federated_user_id' : [{
Copy link
Member

Choose a reason for hiding this comment

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

Double check the spelling in the json payload. Normally, the json part needs to be camelCase. (i.e. like 470). But maybe the backend accepts both?

'provider_id': _auth_utils.validate_provider_id(key['provider_id']),
'raw_id': _auth_utils.validate_provider_id(key['provider_uid'])}]
}
Copy link
Member

Choose a reason for hiding this comment

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

Optional:

elif 'provider_id' in kwargs:
  raise ValueError('provider_uid keyword argument must be supplied when setting provider_id argument')
elif 'provider_uid' in kwargs:
  raise ValueError('provider_id keyword argument must be supplied when setting provider_uid argument')

Alternatively, change the elif condition on 471 to be 'or' instead of 'and'. Then, let kwargs.pop throw a KeyError if one of them is missing. You won't get as nice of an error message though, so you may want to catch-and-rethrow at which point, you might be better off with the elif's.

else:
raise TypeError('Unsupported keyword arguments: {0}.'.format(kwargs))

Expand Down
23 changes: 23 additions & 0 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
'get_user',
'get_user_by_email',
'get_user_by_phone_number',
'get_user_by_provider_user_id',
'import_users',
'list_users',
'revoke_refresh_tokens',
Expand Down Expand Up @@ -308,6 +309,28 @@ def get_user_by_phone_number(phone_number, app=None):
response = user_manager.get_user(phone_number=phone_number)
return UserRecord(response)

def get_user_by_provider_user_id(provider_id, provider_uid, app=None):
Copy link
Member

Choose a reason for hiding this comment

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

naming: get_user_by_provider_uid. (A recent change; sorry. :( )

"""Gets the user data corresponding to the specified provider identifier.

Args:
provider_id: Identifier for the given provider, for example,
"google.com" for the Google provider.
provider_uid: The user identifier with the given provider.
app: An App instance (optional).

Returns:
UserRecord: A UserRecord instance.

Raises:
ValueError: If the given provider identifier is None or empty, or if
the given provider user identifier is None or empty.
UserNotFoundError: If no user exists by the specified identifiers.
FirebaseError: If an error occurs while retrieving the user.
"""
user_manager = _get_auth_service(app).user_manager
response = user_manager.get_user(
provider_id=provider_id, provider_uid=provider_uid)
return UserRecord(response)

def list_users(page_token=None, max_results=_user_mgt.MAX_LIST_USERS_RESULTS, app=None):
"""Retrieves a page of user accounts from a Firebase project.
Expand Down
54 changes: 42 additions & 12 deletions integration/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,27 @@ def new_user_with_params():
yield user
auth.delete_user(user.uid)

@pytest.fixture
def imported_user_with_params():
random_id, email = _random_id()
phone = _random_phone()
user = auth.ImportUserRecord(
uid=random_id,
email=email,
phone_number=phone,
display_name='Random User',
photo_url='https://example.com/photo.png',
user_metadata=auth.UserMetadata(100, 150),
Copy link
Member

Choose a reason for hiding this comment

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

I couldn't figure out what 100,150 were without looking up UserMetadata. Named parameters would make this more obvious, i.e.

auth.UserMetadata(creation_timestamp=100, last_sign_in_timestamp=150)

Optional.

password_hash=b'password', password_salt=b'NaCl', custom_claims={'admin': True},
email_verified=True,
disabled=False,
provider_data=[auth.UserProvider(uid='test', provider_id='google.com')])
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as before: uid=test might cause this to fail if the test doesn't get properly cleaned up.

hash_alg = auth.UserImportHash.scrypt(
b'key', rounds=8, memory_cost=14, salt_separator=b'sep')
auth.import_users([user], hash_alg=hash_alg)
yield user
auth.delete_user(user.uid)

@pytest.fixture
def new_user_list():
users = [
Expand All @@ -200,24 +221,33 @@ def new_user_email_unverified():
yield user
auth.delete_user(user.uid)

def test_get_user(new_user_with_params):
user = auth.get_user(new_user_with_params.uid)
assert user.uid == new_user_with_params.uid
def test_get_user(imported_user_with_params):
user = auth.get_user(imported_user_with_params.uid)
assert user.uid == imported_user_with_params.uid
assert user.display_name == 'Random User'
assert user.email == new_user_with_params.email
assert user.phone_number == new_user_with_params.phone_number
assert user.email == imported_user_with_params.email
assert user.phone_number == imported_user_with_params.phone_number
assert user.photo_url == 'https://example.com/photo.png'
assert user.email_verified is True
assert user.disabled is False
assert len(user.provider_data) == 3
provider_ids = sorted([provider.provider_id for provider in user.provider_data])
assert provider_ids == ['google.com', 'password', 'phone']

user = auth.get_user_by_email(new_user_with_params.email)
assert user.uid == new_user_with_params.uid
user = auth.get_user_by_phone_number(new_user_with_params.phone_number)
assert user.uid == new_user_with_params.uid
user = auth.get_user_by_email(imported_user_with_params.email)
assert user.uid == imported_user_with_params.uid

assert len(user.provider_data) == 2
provider_ids = sorted([provider.provider_id for provider in user.provider_data])
assert provider_ids == ['password', 'phone']
user = auth.get_user_by_phone_number(imported_user_with_params.phone_number)
assert user.uid == imported_user_with_params.uid

user = auth.get_user_by_provider_user_id('phone', imported_user_with_params.phone_number)
assert user.uid == imported_user_with_params.uid

user = auth.get_user_by_provider_user_id('password', imported_user_with_params.email)
assert user.uid == imported_user_with_params.uid

user = auth.get_user_by_provider_user_id('google.com', 'test')
assert user.uid == imported_user_with_params.uid

def test_list_users(new_user_list):
err_msg_template = (
Expand Down
34 changes: 34 additions & 0 deletions tests/test_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,19 @@ def test_get_user_by_phone(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, MOCK_GET_USER_RESPONSE)
_check_user_record(auth.get_user_by_phone_number('+1234567890', user_mgt_app))

def test_invalid_get_user_empty_provider_id(self, user_mgt_app):
with pytest.raises(ValueError):
auth.get_user_by_provider_user_id("", "test_provider_uid", app=user_mgt_app)

def test_invalid_get_user_empty_provider_uid(self, user_mgt_app):
with pytest.raises(ValueError):
auth.get_user_by_provider_user_id("google.com", "", app=user_mgt_app)

def test_get_user_by_provider_user_id(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, MOCK_GET_USER_RESPONSE)
_check_user_record(
auth.get_user_by_provider_user_id('google.com', 'test_google_id', user_mgt_app))

def test_get_user_non_existing(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, '{"users":[]}')
with pytest.raises(auth.UserNotFoundError) as excinfo:
Expand Down Expand Up @@ -247,6 +260,17 @@ def test_get_user_by_phone_non_existing(self, user_mgt_app):
assert excinfo.value.http_response is not None
assert excinfo.value.cause is None

def test_get_user_by_provider_user_id_non_existing(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, '{"users":[]}')
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user_by_provider_user_id('google.com', 'test_google_id', user_mgt_app)
error_msg = 'No user record found for the provided provider_user_id: %s.' % str(
{'provider_id': 'google.com', 'provider_uid': 'test_google_id'})
assert excinfo.value.code == exceptions.NOT_FOUND
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is None

def test_get_user_http_error(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":{"message": "USER_NOT_FOUND"}}')
with pytest.raises(auth.UserNotFoundError) as excinfo:
Expand Down Expand Up @@ -293,6 +317,16 @@ def test_get_user_by_phone_http_error(self, user_mgt_app):
assert excinfo.value.http_response is not None
assert excinfo.value.cause is not None

def test_get_user_by_provider_user_id_http_error(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":{"message": "USER_NOT_FOUND"}}')
with pytest.raises(auth.UserNotFoundError) as excinfo:
auth.get_user_by_provider_user_id('google.com', 'test_google_id', user_mgt_app)
error_msg = 'No user record found for the given identifier (USER_NOT_FOUND).'
assert excinfo.value.code == exceptions.NOT_FOUND
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is not None


class TestCreateUser:

Expand Down