Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 @@ -100,6 +100,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, str) 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
89 changes: 89 additions & 0 deletions firebase_admin/_rfc3339.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright 2020 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.

"""Parse RFC3339 date strings"""

from datetime import datetime, timezone
import re

def parse_to_epoch(datestr):
"""Parses an RFC3339 date string and return the number of seconds since the
Copy link

@egilmorez egilmorez Feb 18, 2020

Choose a reason for hiding this comment

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

Suggest "Parse . . . and return"

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.

epoch (as a float).

In particular, this method is meant to parse the strings returned by the
JSON mapping of protobuf google.protobuf.timestamp.Timestamp instances:
https://github.com/protocolbuffers/protobuf/blob/4cf5bfee9546101d98754d23ff378ff718ba8438/src/google/protobuf/timestamp.proto#L99

This method has microsecond precision; i.e. nanoseconds will be truncated.

Choose a reason for hiding this comment

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

I think "i.e." can be omitted without losing the sense.

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.


Args:
datestr: A string in RFC3339 format.
Returns:
Float: The number of seconds since the unix epoch.

Choose a reason for hiding this comment

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

I see this as "Unix epoch" on wikipedia, without all caps, but with a leading cap.

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.

Raises:
ValueError: Raised if the datestr is not a valid RFC3339 date string.

Choose a reason for hiding this comment

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

Suggest backticks for code font/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.

"""
return _parse_to_datetime(datestr).timestamp()


def _parse_to_datetime(datestr):
"""Parse an RFC3339 date string and return a python datetime instance.

Args:
datestr: A string in RFC3339 format.
Returns:
datetime: The corresponding datetime (with timezone information).

Choose a reason for hiding this comment

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

Suggest backticks for code font/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.

Raises:
ValueError: Raised if the datestr is not a valid RFC3339 date string.

Choose a reason for hiding this comment

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

Suggest backticks for code font/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.

"""
# If more than 6 digits appear in the fractional seconds position, truncate
# to just the most significant 6. (i.e. we only have microsecond precision;
# nanos are truncated.)
datestr_modified = re.sub(r'(\.\d{6})\d*', r'\1', datestr)

# This format is the one we actually expect to occur from our backend. The
# others are only present because the spec says we *should* accept them.
try:
return datetime.strptime(
datestr_modified, '%Y-%m-%dT%H:%M:%S.%fZ'
).replace(tzinfo=timezone.utc)
except ValueError:
pass

try:
return datetime.strptime(
datestr_modified, '%Y-%m-%dT%H:%M:%SZ'
).replace(tzinfo=timezone.utc)
except ValueError:
pass

# Note: %z parses timezone offsets, but requires the timezone offset *not*
# include a separating ':'. As of python 3.7, this was relaxed.
# TODO(rsgowman): Once python3.7 becomes our floor, we can drop the regex
# replacement.
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.

return datetime.strptime(datestr_modified, '%Y-%m-%dT%H:%M:%S.%f%z')
except ValueError:
pass

try:
print("trying without micros")
return datetime.strptime(datestr_modified, '%Y-%m-%dT%H:%M:%S%z')
except ValueError:
pass

raise ValueError('time data {0} does not match RFC3339 format'.format(datestr))
80 changes: 80 additions & 0 deletions firebase_admin/_user_identifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright 2020 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.

"""Classes to uniquely identify a user."""

class UserIdentifier:
"""Identifies a user to be looked up."""


class UidIdentifier(UserIdentifier):
"""Used for looking up an account by uid.

See ``auth.get_user()``.
"""

def __init__(self, uid):
"""Constructs a new UidIdentifier.

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.)



class EmailIdentifier(UserIdentifier):
"""Used for looking up an account by email.

See ``auth.get_user()``.
"""

def __init__(self, email):
"""Constructs a new EmailIdentifier.

Choose a reason for hiding this comment

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

Suggest backticks for code font/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.


Args:
email: A user email address string.
"""
self.email = email


class PhoneIdentifier(UserIdentifier):
"""Used for looking up an account by phone number.

See ``auth.get_user()``.
"""

def __init__(self, phone_number):
"""Constructs a new PhoneIdentifier.

Args:
phone_number: A phone number string.
"""
self.phone_number = phone_number


class ProviderIdentifier(UserIdentifier):
"""Used for looking up an account by provider.

See ``auth.get_user()``.
"""

def __init__(self, provider_id, provider_uid):
"""Constructs a new ProviderIdentifier.

Choose a reason for hiding this comment

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

Suggest backticks for code font/literal. Maybe even ProviderIdentifier object?

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 (throughout).


  Args:
    provider_id: A provider ID string.
    provider_uid: A provider UID string.
"""
self.provider_id = provider_id
self.provider_uid = provider_uid
Loading