329 lines
11 KiB
Python
329 lines
11 KiB
Python
# Copyright 2006 Google LLC. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
"""The User Python datastore class to be used as a datastore data type."""
|
|
|
|
# WARNING: This file is externally viewable by our users. All comments from
|
|
# this file will be stripped. The docstrings will NOT. Do not put sensitive
|
|
# information in docstrings. If you must communicate internal information in
|
|
# this source file, please place them in comments only.
|
|
|
|
|
|
from __future__ import absolute_import
|
|
__author__ = ['jonmac@google.com (Jon McAlister)',
|
|
'ryanb@google.com (Ryan Barrett)']
|
|
|
|
# See user_service.proto.
|
|
|
|
import os
|
|
from googlecloudsdk.appengine._internal import six_subset
|
|
from googlecloudsdk.appengine.api import apiproxy_stub_map
|
|
from googlecloudsdk.appengine.api import user_service_pb
|
|
from googlecloudsdk.appengine.runtime import apiproxy_errors
|
|
|
|
|
|
class Error(Exception):
|
|
"""Base User error type."""
|
|
|
|
|
|
class UserNotFoundError(Error):
|
|
"""No email argument was specified, and no user is logged in."""
|
|
|
|
|
|
class RedirectTooLongError(Error):
|
|
"""The generated redirect URL was too long."""
|
|
|
|
|
|
class NotAllowedError(Error):
|
|
"""The requested redirect URL is not allowed."""
|
|
|
|
|
|
class User(object):
|
|
"""Provides the email address, nickname, and ID for a user.
|
|
|
|
A nickname is a human-readable string that uniquely identifies a Google user,
|
|
akin to a username. For some users, this nickname is an email address, but for
|
|
other users, a different nickname is used.
|
|
|
|
A user is a Google Accounts user.
|
|
|
|
`federated_identity` and `federated_provider` are decommissioned and should
|
|
not be used.
|
|
"""
|
|
|
|
# For more information, go to http://g3doc/apphosting/g3doc/wiki-carryover/unified_theory_of_users.md
|
|
|
|
# If any new properties are added to the object, also add them as class
|
|
# properties here for backwards compat with pickled instances.
|
|
__user_id = None
|
|
__federated_identity = None
|
|
__federated_provider = None
|
|
|
|
def __init__(self, email=None, _auth_domain=None,
|
|
_user_id=None, federated_identity=None, federated_provider=None,
|
|
_strict_mode=True):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
email: An optional string of the user's email address. It defaults to
|
|
the current user's email address.
|
|
federated_identity: Decommissioned, don't use.
|
|
federated_provider: Decommissioned, don't use.
|
|
|
|
Raises:
|
|
UserNotFoundError: If the user is not logged in and both `email` and
|
|
`federated_identity` are empty.
|
|
"""
|
|
# the `_auth_domain argument` is intentionally not publicly documented. It's
|
|
# only intended to be used by the admin console and other trusted apps.
|
|
# The datastore backend will prevent untrusted apps from using it.
|
|
# Similarly, the `_strict_mode` argument is not publicly documented as it is
|
|
# only meant to be used by the `datastore_types` library in order to be more
|
|
# forgiving when delivering data back to the user in case of data corruption
|
|
# that may not pass the constructor validation.
|
|
if _auth_domain is None:
|
|
_auth_domain = os.environ.get('AUTH_DOMAIN')
|
|
assert _auth_domain
|
|
|
|
if email is None and federated_identity is None:
|
|
email = os.environ.get('USER_EMAIL', email)
|
|
_user_id = os.environ.get('USER_ID', _user_id)
|
|
federated_identity = os.environ.get('FEDERATED_IDENTITY',
|
|
federated_identity)
|
|
federated_provider = os.environ.get('FEDERATED_PROVIDER',
|
|
federated_provider)
|
|
|
|
# We set this to maintain compatibility with the
|
|
# `datastore_types.FromPropertyPb` creation of a User object, which will set
|
|
# an empty string for the email (since it is a required field of the
|
|
# underlying data representation of this class in the datastore.
|
|
if email is None:
|
|
email = ''
|
|
|
|
if not email and not federated_identity and _strict_mode:
|
|
# If no valid `email` and no valid `federated_identity`,
|
|
# the user is not valid user.
|
|
raise UserNotFoundError
|
|
|
|
self.__email = email
|
|
self.__federated_identity = federated_identity
|
|
self.__federated_provider = federated_provider
|
|
self.__auth_domain = _auth_domain
|
|
self.__user_id = _user_id or None # User.user_id() should only return a
|
|
# string of length > 0 or None.
|
|
|
|
def nickname(self):
|
|
"""Returns the user's nickname.
|
|
|
|
The nickname will be a unique, human readable identifier for this user with
|
|
respect to this application. It will be an email address for some users,
|
|
and part of the email address for some users.
|
|
|
|
Returns:
|
|
The nickname of the user as a string.
|
|
"""
|
|
if (self.__email and self.__auth_domain and
|
|
self.__email.endswith('@' + self.__auth_domain)):
|
|
suffix_len = len(self.__auth_domain) + 1
|
|
return self.__email[:-suffix_len]
|
|
elif self.__federated_identity:
|
|
return self.__federated_identity
|
|
else:
|
|
return self.__email
|
|
|
|
def email(self):
|
|
"""Returns the user's email address."""
|
|
return self.__email
|
|
|
|
def user_id(self):
|
|
"""Obtains the user ID of the user.
|
|
|
|
Returns:
|
|
A permanent unique identifying string or `None`. If the email address was
|
|
set explicitly, this will return `None`.
|
|
"""
|
|
return self.__user_id
|
|
|
|
def auth_domain(self):
|
|
"""Obtains the user's authentication domain.
|
|
|
|
Returns:
|
|
A string containing the authentication domain. This method is internal and
|
|
should not be used by client applications.
|
|
"""
|
|
return self.__auth_domain
|
|
|
|
def federated_identity(self):
|
|
"""Decommissioned, don't use.
|
|
|
|
Returns:
|
|
A string containing the federated identity of the user. If the user is not
|
|
a federated user, `None` is returned.
|
|
"""
|
|
return self.__federated_identity
|
|
|
|
def federated_provider(self):
|
|
"""Decommissioned, don't use.
|
|
|
|
Returns:
|
|
A string containing the federated provider. If the user is not a federated
|
|
user, `None` is returned.
|
|
"""
|
|
return self.__federated_provider
|
|
|
|
def __unicode__(self):
|
|
return six_subset.text_type(self.nickname())
|
|
|
|
def __str__(self):
|
|
return str(self.nickname())
|
|
|
|
def __repr__(self):
|
|
values = []
|
|
if self.__email:
|
|
values.append("email='%s'" % self.__email)
|
|
if self.__federated_identity:
|
|
values.append("federated_identity='%s'" % self.__federated_identity)
|
|
if self.__user_id:
|
|
values.append("_user_id='%s'" % self.__user_id)
|
|
return 'users.User(%s)' % ','.join(values)
|
|
|
|
def __hash__(self):
|
|
if self.__federated_identity:
|
|
return hash((self.__federated_identity, self.__auth_domain))
|
|
else:
|
|
return hash((self.__email, self.__auth_domain))
|
|
|
|
def __cmp__(self, other):
|
|
if not isinstance(other, User):
|
|
return NotImplemented
|
|
if self.__federated_identity:
|
|
return cmp((self.__federated_identity, self.__auth_domain),
|
|
(other.__federated_identity, other.__auth_domain))
|
|
else:
|
|
return cmp((self.__email, self.__auth_domain),
|
|
(other.__email, other.__auth_domain))
|
|
|
|
|
|
def create_login_url(dest_url=None, _auth_domain=None,
|
|
federated_identity=None):
|
|
"""Computes the login URL for redirection.
|
|
|
|
Args:
|
|
dest_url: String that is the desired final destination URL for the user
|
|
once login is complete. If `dest_url` does not specify a host, the host
|
|
from the current request is used.
|
|
federated_identity: Decommissioned, don't use. Setting this to a non-None
|
|
value raises a NotAllowedError
|
|
|
|
Returns:
|
|
Login URL as a string. The login URL will use Google Accounts.
|
|
|
|
Raises:
|
|
NotAllowedError: If federated_identity is not None.
|
|
"""
|
|
req = user_service_pb.CreateLoginURLRequest()
|
|
resp = user_service_pb.CreateLoginURLResponse()
|
|
if dest_url:
|
|
req.set_destination_url(dest_url)
|
|
else:
|
|
req.set_destination_url('')
|
|
if _auth_domain:
|
|
req.set_auth_domain(_auth_domain)
|
|
if federated_identity:
|
|
raise NotAllowedError('OpenID 2.0 support is decomissioned')
|
|
|
|
try:
|
|
apiproxy_stub_map.MakeSyncCall('user', 'CreateLoginURL', req, resp)
|
|
except apiproxy_errors.ApplicationError as e:
|
|
if (e.application_error ==
|
|
user_service_pb.UserServiceError.REDIRECT_URL_TOO_LONG):
|
|
raise RedirectTooLongError
|
|
elif (e.application_error ==
|
|
user_service_pb.UserServiceError.NOT_ALLOWED):
|
|
raise NotAllowedError
|
|
else:
|
|
raise e
|
|
return resp.login_url()
|
|
|
|
# Backwards compatible alias. TODO(user): kill this.
|
|
CreateLoginURL = create_login_url
|
|
|
|
|
|
# `_auth_domain` will remain undocumented for now; external users can't provide
|
|
# it as an argument
|
|
# pylint: disable=g-doc-args
|
|
def create_logout_url(dest_url, _auth_domain=None):
|
|
"""Computes the logout URL and specified destination URL for the request.
|
|
|
|
This function works for Google Accounts applications.
|
|
|
|
Args:
|
|
dest_url: String that is the desired final destination URL for the user
|
|
after the user has logged out. If `dest_url` does not specify a host,
|
|
the host from the current request is used.
|
|
|
|
Returns:
|
|
Logout URL as a string.
|
|
"""
|
|
req = user_service_pb.CreateLogoutURLRequest()
|
|
resp = user_service_pb.CreateLogoutURLResponse()
|
|
req.set_destination_url(dest_url)
|
|
if _auth_domain:
|
|
req.set_auth_domain(_auth_domain)
|
|
|
|
try:
|
|
apiproxy_stub_map.MakeSyncCall('user', 'CreateLogoutURL', req, resp)
|
|
except apiproxy_errors.ApplicationError as e:
|
|
if (e.application_error ==
|
|
user_service_pb.UserServiceError.REDIRECT_URL_TOO_LONG):
|
|
raise RedirectTooLongError
|
|
else:
|
|
raise e
|
|
return resp.logout_url()
|
|
|
|
# Backwards compatible alias. TODO(user): kill this.
|
|
CreateLogoutURL = create_logout_url
|
|
|
|
|
|
def get_current_user():
|
|
"""Retrieves information associated with the user that is making a request.
|
|
|
|
Returns:
|
|
|
|
"""
|
|
try:
|
|
return User()
|
|
except UserNotFoundError:
|
|
return None
|
|
|
|
# Backwards compatible alias. TODO(user): kill this.
|
|
GetCurrentUser = get_current_user
|
|
|
|
|
|
def is_current_user_admin():
|
|
"""Specifies whether the user making a request is an application admin.
|
|
|
|
Because administrator status is not persisted in the datastore,
|
|
`is_current_user_admin()` is a separate function rather than a member function
|
|
of the `User` class. The status only exists for the user making the current
|
|
request.
|
|
|
|
Returns:
|
|
`True` if the user is an administrator; all other user types return `False`.
|
|
"""
|
|
return (os.environ.get('USER_IS_ADMIN', '0')) == '1'
|
|
|
|
# Backwards compatible alias. TODO(user): kill this.
|
|
IsCurrentUserAdmin = is_current_user_admin
|