# Copyright 2018 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Firebase user import sub module.""" import base64 import json from firebase_admin import _auth_utils def b64_encode(bytes_value): return base64.urlsafe_b64encode(bytes_value).decode() class UserProvider: """Represents a user identity provider that can be associated with a Firebase user. One or more providers can be specified in an ``ImportUserRecord`` when importing users via ``auth.import_users()``. Args: uid: User's unique ID assigned by the identity provider. provider_id: ID of the identity provider. This can be a short domain name or the identifier of an OpenID identity provider. email: User's email address (optional). display_name: User's display name (optional). photo_url: User's photo URL (optional). """ def __init__(self, uid, provider_id, email=None, display_name=None, photo_url=None): self.uid = uid self.provider_id = provider_id self.email = email self.display_name = display_name self.photo_url = photo_url @property def uid(self): return self._uid @uid.setter def uid(self, uid): self._uid = _auth_utils.validate_uid(uid, required=True) @property def provider_id(self): return self._provider_id @provider_id.setter def provider_id(self, provider_id): self._provider_id = _auth_utils.validate_provider_id(provider_id, required=True) @property def email(self): return self._email @email.setter def email(self, email): self._email = _auth_utils.validate_email(email) @property def display_name(self): return self._display_name @display_name.setter def display_name(self, display_name): self._display_name = _auth_utils.validate_display_name(display_name) @property def photo_url(self): return self._photo_url @photo_url.setter def photo_url(self, photo_url): self._photo_url = _auth_utils.validate_photo_url(photo_url) def to_dict(self): payload = { 'rawId': self.uid, 'providerId': self.provider_id, 'displayName': self.display_name, 'email': self.email, 'photoUrl': self.photo_url, } return {k: v for k, v in payload.items() if v is not None} class ImportUserRecord: """Represents a user account to be imported to Firebase Auth. Must specify the ``uid`` field at a minimum. A sequence of ``ImportUserRecord`` objects can be passed to the ``auth.import_users()`` function, in order to import those users into Firebase Auth in bulk. If the ``password_hash`` is set on a user, a hash configuration must be specified when calling ``import_users()``. Args: uid: User's unique ID. Must be a non-empty string not longer than 128 characters. email: User's email address (optional). email_verified: A boolean indicating whether the user's email has been verified (optional). display_name: User's display name (optional). phone_number: User's phone number (optional). photo_url: User's photo URL (optional). disabled: A boolean indicating whether this user account has been disabled (optional). user_metadata: An ``auth.UserMetadata`` instance with additional user metadata (optional). provider_data: A list of ``auth.UserProvider`` instances (optional). custom_claims: A ``dict`` of custom claims to be set on the user account (optional). password_hash: User's password hash as a ``bytes`` sequence (optional). password_salt: User's password salt as a ``bytes`` sequence (optional). Raises: ValueError: If provided arguments are invalid. """ def __init__(self, uid, email=None, email_verified=None, display_name=None, phone_number=None, photo_url=None, disabled=None, user_metadata=None, provider_data=None, custom_claims=None, password_hash=None, password_salt=None): self.uid = uid self.email = email self.display_name = display_name self.phone_number = phone_number self.photo_url = photo_url self.password_hash = password_hash self.password_salt = password_salt self.email_verified = email_verified self.disabled = disabled self.user_metadata = user_metadata self.provider_data = provider_data self.custom_claims = custom_claims @property def uid(self): return self._uid @uid.setter def uid(self, uid): self._uid = _auth_utils.validate_uid(uid, required=True) @property def email(self): return self._email @email.setter def email(self, email): self._email = _auth_utils.validate_email(email) @property def display_name(self): return self._display_name @display_name.setter def display_name(self, display_name): self._display_name = _auth_utils.validate_display_name(display_name) @property def phone_number(self): return self._phone_number @phone_number.setter def phone_number(self, phone_number): self._phone_number = _auth_utils.validate_phone(phone_number) @property def photo_url(self): return self._photo_url @photo_url.setter def photo_url(self, photo_url): self._photo_url = _auth_utils.validate_photo_url(photo_url) @property def password_hash(self): return self._password_hash @password_hash.setter def password_hash(self, password_hash): self._password_hash = _auth_utils.validate_bytes(password_hash, 'password_hash') @property def password_salt(self): return self._password_salt @password_salt.setter def password_salt(self, password_salt): self._password_salt = _auth_utils.validate_bytes(password_salt, 'password_salt') @property def user_metadata(self): return self._user_metadata @user_metadata.setter def user_metadata(self, user_metadata): created_at = user_metadata.creation_timestamp if user_metadata is not None else None last_login_at = user_metadata.last_sign_in_timestamp if user_metadata is not None else None self._created_at = _auth_utils.validate_timestamp(created_at, 'creation_timestamp') self._last_login_at = _auth_utils.validate_timestamp( last_login_at, 'last_sign_in_timestamp') self._user_metadata = user_metadata @property def provider_data(self): return self._provider_data @provider_data.setter def provider_data(self, provider_data): if provider_data is not None: try: if any([not isinstance(p, UserProvider) for p in provider_data]): raise ValueError('One or more provider data instances are invalid.') except TypeError: raise ValueError('provider_data must be iterable.') self._provider_data = provider_data @property def custom_claims(self): return self._custom_claims @custom_claims.setter def custom_claims(self, custom_claims): json_claims = json.dumps(custom_claims) if isinstance( custom_claims, dict) else custom_claims self._custom_claims_str = _auth_utils.validate_custom_claims(json_claims) self._custom_claims = custom_claims def to_dict(self): """Returns a dict representation of the user. For internal use only.""" payload = { 'localId': self.uid, 'email': self.email, 'displayName': self.display_name, 'phoneNumber': self.phone_number, 'photoUrl': self.photo_url, 'emailVerified': (bool(self.email_verified) if self.email_verified is not None else None), 'disabled': bool(self.disabled) if self.disabled is not None else None, 'customAttributes': self._custom_claims_str, 'createdAt': self._created_at, 'lastLoginAt': self._last_login_at, 'passwordHash': b64_encode(self.password_hash) if self.password_hash else None, 'salt': b64_encode(self.password_salt) if self.password_salt else None, } if self.provider_data: payload['providerUserInfo'] = [p.to_dict() for p in self.provider_data] return {k: v for k, v in payload.items() if v is not None} class UserImportHash: """Represents a hash algorithm used to hash user passwords. An instance of this class must be specified when importing users with passwords via the ``auth.import_users()`` API. Use one of the provided class methods to obtain new instances when required. Refer to `documentation`_ for more details. .. _documentation: https://firebase.google.com/docs/auth/admin/import-users """ def __init__(self, name, data=None): self._name = name self._data = data def to_dict(self): payload = {'hashAlgorithm': self._name} if self._data: payload.update(self._data) return payload @classmethod def _hmac(cls, name, key): data = { 'signerKey': b64_encode(_auth_utils.validate_bytes(key, 'key', required=True)) } return UserImportHash(name, data) @classmethod def hmac_sha512(cls, key): """Creates a new HMAC SHA512 algorithm instance. Args: key: Signer key as a byte sequence. Returns: UserImportHash: A new ``UserImportHash``. """ return cls._hmac('HMAC_SHA512', key) @classmethod def hmac_sha256(cls, key): """Creates a new HMAC SHA256 algorithm instance. Args: key: Signer key as a byte sequence. Returns: UserImportHash: A new ``UserImportHash``. """ return cls._hmac('HMAC_SHA256', key) @classmethod def hmac_sha1(cls, key): """Creates a new HMAC SHA1 algorithm instance. Args: key: Signer key as a byte sequence. Returns: UserImportHash: A new ``UserImportHash``. """ return cls._hmac('HMAC_SHA1', key) @classmethod def hmac_md5(cls, key): """Creates a new HMAC MD5 algorithm instance. Args: key: Signer key as a byte sequence. Returns: UserImportHash: A new ``UserImportHash``. """ return cls._hmac('HMAC_MD5', key) @classmethod def md5(cls, rounds): """Creates a new MD5 algorithm instance. Args: rounds: Number of rounds. Must be an integer between 0 and 8192. Returns: UserImportHash: A new ``UserImportHash``. """ return UserImportHash( 'MD5', {'rounds': _auth_utils.validate_int(rounds, 'rounds', 0, 8192)}) @classmethod def sha1(cls, rounds): """Creates a new SHA1 algorithm instance. Args: rounds: Number of rounds. Must be an integer between 1 and 8192. Returns: UserImportHash: A new ``UserImportHash``. """ return UserImportHash( 'SHA1', {'rounds': _auth_utils.validate_int(rounds, 'rounds', 1, 8192)}) @classmethod def sha256(cls, rounds): """Creates a new SHA256 algorithm instance. Args: rounds: Number of rounds. Must be an integer between 1 and 8192. Returns: UserImportHash: A new ``UserImportHash``. """ return UserImportHash( 'SHA256', {'rounds': _auth_utils.validate_int(rounds, 'rounds', 1, 8192)}) @classmethod def sha512(cls, rounds): """Creates a new SHA512 algorithm instance. Args: rounds: Number of rounds. Must be an integer between 1 and 8192. Returns: UserImportHash: A new ``UserImportHash``. """ return UserImportHash( 'SHA512', {'rounds': _auth_utils.validate_int(rounds, 'rounds', 1, 8192)}) @classmethod def pbkdf_sha1(cls, rounds): """Creates a new PBKDF SHA1 algorithm instance. Args: rounds: Number of rounds. Must be an integer between 0 and 120000. Returns: UserImportHash: A new ``UserImportHash``. """ return UserImportHash( 'PBKDF_SHA1', {'rounds': _auth_utils.validate_int(rounds, 'rounds', 0, 120000)}) @classmethod def pbkdf2_sha256(cls, rounds): """Creates a new PBKDF2 SHA256 algorithm instance. Args: rounds: Number of rounds. Must be an integer between 0 and 120000. Returns: UserImportHash: A new ``UserImportHash``. """ return UserImportHash( 'PBKDF2_SHA256', {'rounds': _auth_utils.validate_int(rounds, 'rounds', 0, 120000)}) @classmethod def scrypt(cls, key, rounds, memory_cost, salt_separator=None): """Creates a new Scrypt algorithm instance. This is the modified Scrypt algorithm used by Firebase Auth. See ``standard_scrypt()`` function for the standard Scrypt algorith, Args: key: Signer key as a byte sequence. rounds: Number of rounds. Must be an integer between 1 and 8. memory_cost: Memory cost as an integer between 1 and 14. salt_separator: Salt separator as a byte sequence (optional). Returns: UserImportHash: A new ``UserImportHash``. """ data = { 'signerKey': b64_encode(_auth_utils.validate_bytes(key, 'key', required=True)), 'rounds': _auth_utils.validate_int(rounds, 'rounds', 1, 8), 'memoryCost': _auth_utils.validate_int(memory_cost, 'memory_cost', 1, 14), } if salt_separator: data['saltSeparator'] = b64_encode(_auth_utils.validate_bytes( salt_separator, 'salt_separator')) return UserImportHash('SCRYPT', data) @classmethod def bcrypt(cls): """Creates a new Bcrypt algorithm instance. Returns: UserImportHash: A new ``UserImportHash``. """ return UserImportHash('BCRYPT') @classmethod def standard_scrypt(cls, memory_cost, parallelization, block_size, derived_key_length): """Creates a new standard Scrypt algorithm instance. Args: memory_cost: Memory cost as a non-negaive integer. parallelization: Parallelization as a non-negative integer. block_size: Block size as a non-negative integer. derived_key_length: Derived key length as a non-negative integer. Returns: UserImportHash: A new ``UserImportHash``. """ data = { 'memoryCost': _auth_utils.validate_int(memory_cost, 'memory_cost', low=0), 'parallelization': _auth_utils.validate_int(parallelization, 'parallelization', low=0), 'blockSize': _auth_utils.validate_int(block_size, 'block_size', low=0), 'dkLen': _auth_utils.validate_int(derived_key_length, 'derived_key_length', low=0), } return UserImportHash('STANDARD_SCRYPT', data) class ErrorInfo: """Represents an error encountered while performing a batch operation such as importing users or deleting multiple user accounts. """ # TODO(rsgowman): This class used to be specific to importing users (hence # it's home in _user_import.py). It's now also used by bulk deletion of # users. Move this to a more common location. def __init__(self, error): self._index = error['index'] self._reason = error['message'] @property def index(self): return self._index @property def reason(self): return self._reason class UserImportResult: """Represents the result of a bulk user import operation. See ``auth.import_users()`` API for more details. """ def __init__(self, result, total): errors = result.get('error', []) self._success_count = total - len(errors) self._failure_count = len(errors) self._errors = [ErrorInfo(err) for err in errors] @property def success_count(self): """Returns the number of users successfully imported.""" return self._success_count @property def failure_count(self): """Returns the number of users that failed to be imported.""" return self._failure_count @property def errors(self): """Returns a list of ``auth.ErrorInfo`` instances describing the errors encountered.""" return self._errors