| # -*- coding: utf-8 -*- |
| """ |
| webapp2_extras.appengine.auth.models |
| ==================================== |
| |
| Auth related models. |
| |
| :copyright: 2011 by tipfy.org. |
| :license: Apache Sotware License, see LICENSE for details. |
| """ |
| import time |
| |
| try: |
| from ndb import model |
| except ImportError: # pragma: no cover |
| from google.appengine.ext.ndb import model |
| |
| from webapp2_extras import auth |
| from webapp2_extras import security |
| |
| |
| class Unique(model.Model): |
| """A model to store unique values. |
| |
| The only purpose of this model is to "reserve" values that must be unique |
| within a given scope, as a workaround because datastore doesn't support |
| the concept of uniqueness for entity properties. |
| |
| For example, suppose we have a model `User` with three properties that |
| must be unique across a given group: `username`, `auth_id` and `email`:: |
| |
| class User(model.Model): |
| username = model.StringProperty(required=True) |
| auth_id = model.StringProperty(required=True) |
| email = model.StringProperty(required=True) |
| |
| To ensure property uniqueness when creating a new `User`, we first create |
| `Unique` records for those properties, and if everything goes well we can |
| save the new `User` record:: |
| |
| @classmethod |
| def create_user(cls, username, auth_id, email): |
| # Assemble the unique values for a given class and attribute scope. |
| uniques = [ |
| 'User.username.%s' % username, |
| 'User.auth_id.%s' % auth_id, |
| 'User.email.%s' % email, |
| ] |
| |
| # Create the unique username, auth_id and email. |
| success, existing = Unique.create_multi(uniques) |
| |
| if success: |
| # The unique values were created, so we can save the user. |
| user = User(username=username, auth_id=auth_id, email=email) |
| user.put() |
| return user |
| else: |
| # At least one of the values is not unique. |
| # Make a list of the property names that failed. |
| props = [name.split('.', 2)[1] for name in uniques] |
| raise ValueError('Properties %r are not unique.' % props) |
| |
| Based on the idea from http://goo.gl/pBQhB |
| """ |
| |
| @classmethod |
| def create(cls, value): |
| """Creates a new unique value. |
| |
| :param value: |
| The value to be unique, as a string. |
| |
| The value should include the scope in which the value must be |
| unique (ancestor, namespace, kind and/or property name). |
| |
| For example, for a unique property `email` from kind `User`, the |
| value can be `User.email:me@myself.com`. In this case `User.email` |
| is the scope, and `me@myself.com` is the value to be unique. |
| :returns: |
| True if the unique value was created, False otherwise. |
| """ |
| entity = cls(key=model.Key(cls, value)) |
| txn = lambda: entity.put() if not entity.key.get() else None |
| return model.transaction(txn) is not None |
| |
| @classmethod |
| def create_multi(cls, values): |
| """Creates multiple unique values at once. |
| |
| :param values: |
| A sequence of values to be unique. See :meth:`create`. |
| :returns: |
| A tuple (bool, list_of_keys). If all values were created, bool is |
| True and list_of_keys is empty. If one or more values weren't |
| created, bool is False and the list contains all the values that |
| already existed in datastore during the creation attempt. |
| """ |
| # Maybe do a preliminary check, before going for transactions? |
| # entities = model.get_multi(keys) |
| # existing = [entity.key.id() for entity in entities if entity] |
| # if existing: |
| # return False, existing |
| |
| # Create all records transactionally. |
| keys = [model.Key(cls, value) for value in values] |
| entities = [cls(key=key) for key in keys] |
| func = lambda e: e.put() if not e.key.get() else None |
| created = [model.transaction(lambda: func(e)) for e in entities] |
| |
| if created != keys: |
| # A poor man's "rollback": delete all recently created records. |
| model.delete_multi(k for k in created if k) |
| return False, [k.id() for k in keys if k not in created] |
| |
| return True, [] |
| |
| @classmethod |
| def delete_multi(cls, values): |
| """Deletes multiple unique values at once. |
| |
| :param values: |
| A sequence of values to be deleted. |
| """ |
| return model.delete_multi(model.Key(cls, v) for v in values) |
| |
| |
| class UserToken(model.Model): |
| """Stores validation tokens for users.""" |
| |
| created = model.DateTimeProperty(auto_now_add=True) |
| updated = model.DateTimeProperty(auto_now=True) |
| user = model.StringProperty(required=True, indexed=False) |
| subject = model.StringProperty(required=True) |
| token = model.StringProperty(required=True) |
| |
| @classmethod |
| def get_key(cls, user, subject, token): |
| """Returns a token key. |
| |
| :param user: |
| User unique ID. |
| :param subject: |
| The subject of the key. Examples: |
| |
| - 'auth' |
| - 'signup' |
| :param token: |
| Randomly generated token. |
| :returns: |
| ``model.Key`` containing a string id in the following format: |
| ``{user_id}.{subject}.{token}.`` |
| """ |
| return model.Key(cls, '%s.%s.%s' % (str(user), subject, token)) |
| |
| @classmethod |
| def create(cls, user, subject, token=None): |
| """Creates a new token for the given user. |
| |
| :param user: |
| User unique ID. |
| :param subject: |
| The subject of the key. Examples: |
| |
| - 'auth' |
| - 'signup' |
| :param token: |
| Optionally an existing token may be provided. |
| If None, a random token will be generated. |
| :returns: |
| The newly created :class:`UserToken`. |
| """ |
| user = str(user) |
| token = token or security.generate_random_string(entropy=128) |
| key = cls.get_key(user, subject, token) |
| entity = cls(key=key, user=user, subject=subject, token=token) |
| entity.put() |
| return entity |
| |
| @classmethod |
| def get(cls, user=None, subject=None, token=None): |
| """Fetches a user token. |
| |
| :param user: |
| User unique ID. |
| :param subject: |
| The subject of the key. Examples: |
| |
| - 'auth' |
| - 'signup' |
| :param token: |
| The existing token needing verified. |
| :returns: |
| A :class:`UserToken` or None if the token does not exist. |
| """ |
| if user and subject and token: |
| return cls.get_key(user, subject, token).get() |
| |
| assert subject and token, \ |
| 'subject and token must be provided to UserToken.get().' |
| return cls.query(cls.subject == subject, cls.token == token).get() |
| |
| |
| class User(model.Expando): |
| """Stores user authentication credentials or authorization ids.""" |
| |
| #: The model used to ensure uniqueness. |
| unique_model = Unique |
| #: The model used to store tokens. |
| token_model = UserToken |
| |
| created = model.DateTimeProperty(auto_now_add=True) |
| updated = model.DateTimeProperty(auto_now=True) |
| # ID for third party authentication, e.g. 'google:username'. UNIQUE. |
| auth_ids = model.StringProperty(repeated=True) |
| # Hashed password. Not required because third party authentication |
| # doesn't use password. |
| password = model.StringProperty() |
| |
| def get_id(self): |
| """Returns this user's unique ID, which can be an integer or string.""" |
| return self._key.id() |
| |
| def add_auth_id(self, auth_id): |
| """A helper method to add additional auth ids to a User |
| |
| :param auth_id: |
| String representing a unique id for the user. Examples: |
| |
| - own:username |
| - google:username |
| :returns: |
| A tuple (boolean, info). The boolean indicates if the user |
| was saved. If creation succeeds, ``info`` is the user entity; |
| otherwise it is a list of duplicated unique properties that |
| caused creation to fail. |
| """ |
| self.auth_ids.append(auth_id) |
| unique = '%s.auth_id:%s' % (self.__class__.__name__, auth_id) |
| ok = self.unique_model.create(unique) |
| if ok: |
| self.put() |
| return True, self |
| else: |
| return False, ['auth_id'] |
| |
| @classmethod |
| def get_by_auth_id(cls, auth_id): |
| """Returns a user object based on a auth_id. |
| |
| :param auth_id: |
| String representing a unique id for the user. Examples: |
| |
| - own:username |
| - google:username |
| :returns: |
| A user object. |
| """ |
| return cls.query(cls.auth_ids == auth_id).get() |
| |
| @classmethod |
| def get_by_auth_token(cls, user_id, token): |
| """Returns a user object based on a user ID and token. |
| |
| :param user_id: |
| The user_id of the requesting user. |
| :param token: |
| The token string to be verified. |
| :returns: |
| A tuple ``(User, timestamp)``, with a user object and |
| the token timestamp, or ``(None, None)`` if both were not found. |
| """ |
| token_key = cls.token_model.get_key(user_id, 'auth', token) |
| user_key = model.Key(cls, user_id) |
| # Use get_multi() to save a RPC call. |
| valid_token, user = model.get_multi([token_key, user_key]) |
| if valid_token and user: |
| timestamp = int(time.mktime(valid_token.created.timetuple())) |
| return user, timestamp |
| |
| return None, None |
| |
| @classmethod |
| def get_by_auth_password(cls, auth_id, password): |
| """Returns a user object, validating password. |
| |
| :param auth_id: |
| Authentication id. |
| :param password: |
| Password to be checked. |
| :returns: |
| A user object, if found and password matches. |
| :raises: |
| ``auth.InvalidAuthIdError`` or ``auth.InvalidPasswordError``. |
| """ |
| user = cls.get_by_auth_id(auth_id) |
| if not user: |
| raise auth.InvalidAuthIdError() |
| |
| if not security.check_password_hash(password, user.password): |
| raise auth.InvalidPasswordError() |
| |
| return user |
| |
| @classmethod |
| def validate_token(cls, user_id, subject, token): |
| """Checks for existence of a token, given user_id, subject and token. |
| |
| :param user_id: |
| User unique ID. |
| :param subject: |
| The subject of the key. Examples: |
| |
| - 'auth' |
| - 'signup' |
| :param token: |
| The token string to be validated. |
| :returns: |
| A :class:`UserToken` or None if the token does not exist. |
| """ |
| return cls.token_model.get(user=user_id, subject=subject, |
| token=token) is not None |
| |
| @classmethod |
| def create_auth_token(cls, user_id): |
| """Creates a new authorization token for a given user ID. |
| |
| :param user_id: |
| User unique ID. |
| :returns: |
| A string with the authorization token. |
| """ |
| return cls.token_model.create(user_id, 'auth').token |
| |
| @classmethod |
| def validate_auth_token(cls, user_id, token): |
| return cls.validate_token(user_id, 'auth', token) |
| |
| @classmethod |
| def delete_auth_token(cls, user_id, token): |
| """Deletes a given authorization token. |
| |
| :param user_id: |
| User unique ID. |
| :param token: |
| A string with the authorization token. |
| """ |
| cls.token_model.get_key(user_id, 'auth', token).delete() |
| |
| @classmethod |
| def create_signup_token(cls, user_id): |
| entity = cls.token_model.create(user_id, 'signup') |
| return entity.token |
| |
| @classmethod |
| def validate_signup_token(cls, user_id, token): |
| return cls.validate_token(user_id, 'signup', token) |
| |
| @classmethod |
| def delete_signup_token(cls, user_id, token): |
| cls.token_model.get_key(user_id, 'signup', token).delete() |
| |
| @classmethod |
| def create_user(cls, auth_id, unique_properties=None, **user_values): |
| """Creates a new user. |
| |
| :param auth_id: |
| A string that is unique to the user. Users may have multiple |
| auth ids. Example auth ids: |
| |
| - own:username |
| - own:email@example.com |
| - google:username |
| - yahoo:username |
| |
| The value of `auth_id` must be unique. |
| :param unique_properties: |
| Sequence of extra property names that must be unique. |
| :param user_values: |
| Keyword arguments to create a new user entity. Since the model is |
| an ``Expando``, any provided custom properties will be saved. |
| To hash a plain password, pass a keyword ``password_raw``. |
| :returns: |
| A tuple (boolean, info). The boolean indicates if the user |
| was created. If creation succeeds, ``info`` is the user entity; |
| otherwise it is a list of duplicated unique properties that |
| caused creation to fail. |
| """ |
| assert user_values.get('password') is None, \ |
| 'Use password_raw instead of password to create new users.' |
| |
| assert not isinstance(auth_id, list), \ |
| 'Creating a user with multiple auth_ids is not allowed, ' \ |
| 'please provide a single auth_id.' |
| |
| if 'password_raw' in user_values: |
| user_values['password'] = security.generate_password_hash( |
| user_values.pop('password_raw'), length=12) |
| |
| user_values['auth_ids'] = [auth_id] |
| user = cls(**user_values) |
| |
| # Set up unique properties. |
| uniques = [('%s.auth_id:%s' % (cls.__name__, auth_id), 'auth_id')] |
| if unique_properties: |
| for name in unique_properties: |
| key = '%s.%s:%s' % (cls.__name__, name, user_values[name]) |
| uniques.append((key, name)) |
| |
| ok, existing = cls.unique_model.create_multi(k for k, v in uniques) |
| if ok: |
| user.put() |
| return True, user |
| else: |
| properties = [v for k, v in uniques if k in existing] |
| return False, properties |