Beispiel #1
0
class SellerPaypal(Model):
    paypal_id = AESField(blank=True, null=True, aes_key='sellerpaypal:id')
    token = AESField(blank=True, null=True, aes_key='sellerpaypal:token')
    secret = AESField(blank=True, null=True, aes_key='sellerpaypal:secret')
    seller = models.OneToOneField(Seller, related_name='paypal')
    # TODO: currencies.

    # Sellers personal contact information.
    first_name = models.CharField(max_length=255, blank=True)
    last_name = models.CharField(max_length=255, blank=True)
    full_name = models.CharField(max_length=255, blank=True)
    business_name = models.CharField(max_length=255, blank=True)
    country = models.CharField(max_length=64, blank=True)
    address_one = models.CharField(max_length=255, blank=True)
    address_two = models.CharField(max_length=255, blank=True)
    post_code = models.CharField(max_length=128, blank=True)
    city = models.CharField(max_length=128, blank=True)
    state = models.CharField(max_length=64, blank=True)
    phone = models.CharField(max_length=32, blank=True)

    class Meta(Model.Meta):
        db_table = 'seller_paypal'

    @property
    def secret_exists(self):
        return bool(self.secret)

    @property
    def token_exists(self):
        return bool(self.token)
Beispiel #2
0
    def test_settings(self):
        # Warn if the settings are confused and the proxy settings are
        # mixed with non-proxy settings. At this time we can't tell if you
        # are running just the database server or solitude all in one.
        self.status['settings'] = True
        caches = getattr(settings, 'CACHES', {})
        dbs = getattr(settings, 'DATABASES', {})

        if self.is_proxy:
            # As a proxy, we should not have database access.
            for db in dbs.values():
                engine = db.get('ENGINE', '')
                if (db.get('ENGINE', '') not in ['',
                        'django.db.backends.dummy']):
                    log.error('Proxy db set to: %s' % engine)
                    return False

            # There could be an issue if you share a proxy with the database
            # server, a local cache should be fine.
            for cache in caches.values():
                backend = cache.get('BACKEND', '')
                if (backend not in ['',
                        'django.core.cache.backends.dummy.DummyCache',
                        'django.core.cache.backends.locmem.LocMemCache']):
                    log.error('Proxy cache set to: %s' % backend)
                    return False

        else:
            # Tuck the encrypt test into settings.
            test = AESField(aes_key='bango:signature')
            if test._decrypt(test._encrypt('foo')) != 'foo':
                return False

        return True
Beispiel #3
0
    def test_settings(self):
        # Warn if the settings are confused and the proxy settings are
        # mixed with non-proxy settings. At this time we can't tell if you
        # are running just the database server or solitude all in one.
        self.status['settings'] = True
        caches = getattr(settings, 'CACHES', {})
        dbs = getattr(settings, 'DATABASES', {})

        if self.is_proxy:
            # As a proxy, we should not have database access.
            for db in dbs.values():
                engine = db.get('ENGINE', '')
                if (db.get('ENGINE', '')
                        not in ['', 'django.db.backends.dummy']):
                    log.error('Proxy db set to: %s' % engine)
                    return False

            # There could be an issue if you share a proxy with the database
            # server, a local cache should be fine.
            for cache in caches.values():
                backend = cache.get('BACKEND', '')
                if (backend not in [
                        '', 'django.core.cache.backends.dummy.DummyCache',
                        'django.core.cache.backends.locmem.LocMemCache'
                ]):
                    log.error('Proxy cache set to: %s' % backend)
                    return False

        else:
            # Tuck the encrypt test into settings.
            test = AESField(aes_key='bango:signature')
            if test._decrypt(test._encrypt('foo')) != 'foo':
                return False

        return True
    def test_settings(self):
        # Warn if the settings are confused and the proxy settings are
        # mixed with non-proxy settings. At this time we can't tell if you
        # are running just the database server or solitude all in one.
        self.status["settings"] = True
        caches = getattr(settings, "CACHES", {})
        dbs = getattr(settings, "DATABASES", {})

        if self.is_proxy:
            # As a proxy, we should not have database access.
            for db in dbs.values():
                engine = db.get("ENGINE", "")
                if db.get("ENGINE", "") not in ["", "django.db.backends.dummy"]:
                    log.error("Proxy db set to: %s" % engine)
                    return False

            # There could be an issue if you share a proxy with the database
            # server, a local cache should be fine.
            for cache in caches.values():
                backend = cache.get("BACKEND", "")
                if backend not in [
                    "",
                    "django.core.cache.backends.dummy.DummyCache",
                    "django.core.cache.backends.locmem.LocMemCache",
                ]:
                    log.error("Proxy cache set to: %s" % backend)
                    return False

        else:
            # Tuck the encrypt test into settings.
            test = AESField(aes_key="bango:signature")
            if test._decrypt(test._encrypt("foo")) != "foo":
                return False

        return True
    def test_get_key(self, settings):
        key = 'some-super-secret-key'
        temporary_file = tempfile.NamedTemporaryFile()
        temporary_file.write(force_bytes(key))
        temporary_file.flush()

        settings.AES_KEYS = {'default': temporary_file.name}
        assert AESField().get_aes_key() == 'some-super-secret-key'
Beispiel #6
0
class Access(ModelBase):
    key = models.CharField(max_length=255, unique=True)
    secret = AESField(max_length=255, aes_key='api:access:secret')
    user = models.ForeignKey('auth.User')
    redirect_uri = models.CharField(max_length=255)
    app_name = models.CharField(max_length=255)

    class Meta:
        db_table = 'api_access'
Beispiel #7
0
class SellerProduct(Model):

    """
    The key to a seller's generic product.
    """
    # An identifier for this product that corresponds to the
    # seller's catalog. This is only guaranteed to be unique
    # per seller, not per all products.
    external_id = models.CharField(max_length=255, db_index=True)
    # A publicly visible id used in in-app payments so that we
    # can identify the seller. This will be the iss field in JWT.
    public_id = models.CharField(max_length=255, db_index=True, unique=True)
    seller = models.ForeignKey(Seller, related_name='product')
    # A generic secret field that can be used for this product, regardless
    # of backend.
    secret = AESField(blank=True, null=True,
                      aes_key='sellerproduct:secret')
    # The type of access this product key has.
    access = models.PositiveIntegerField(choices=ACCESS_CHOICES,
                                         default=ACCESS_PURCHASE)

    class Meta(Model.Meta):
        db_table = 'seller_product'
        unique_together = (('seller', 'external_id'),)

    def supported_providers(self):
        # This will provide the seller_uuids for each supported
        # payment provider on the product.  This is a temporary solution
        # that should not be expanded upon but rather refactored to be
        # more generic, see bug
        # https://bugzilla.mozilla.org/show_bug.cgi?id=1001018
        providers = {}
        provider_fields = (
            ('bango', 'product', 'seller_bango'),
            ('reference', 'product_reference', 'seller_reference'),
        )

        for provider_name, product_field, provider_field in provider_fields:
            try:
                providers[provider_name] = getattr(
                    getattr(self, product_field),
                    provider_field
                ).seller.uuid
            except ObjectDoesNotExist:
                providers[provider_name] = None

        return providers

    def get_uri(self):
        return reverse('generic:sellerproduct-detail', kwargs={'pk': self.pk})
Beispiel #8
0
class Access(ModelBase):
    key = models.CharField(max_length=255, unique=True)
    secret = AESField(max_length=255, aes_key='api:access:secret')
    user = models.ForeignKey(UserProfile)
    redirect_uri = models.CharField(max_length=255)
    app_name = models.CharField(max_length=255)

    class Meta:
        db_table = 'api_access'

    @classmethod
    def create_for_user(cls, user):
        key = 'mkt:%s:%s:%s' % (user.pk, user.email,
                                Access.objects.filter(user=user).count())
        return Access.objects.create(
            key=key, user=user, secret=get_random_string().encode('ascii'))
Beispiel #9
0
class BuyerPaypal(Model):
    key = AESField(blank=True, null=True, aes_key='buyerpaypal:key')
    expiry = models.DateField(blank=True, null=True)
    currency = models.CharField(max_length=3, blank=True, null=True)
    buyer = models.OneToOneField(Buyer, related_name='paypal')

    class Meta(Model.Meta):
        db_table = 'buyer_paypal'

    @property
    def key_exists(self):
        return bool(self.key)

    @key_exists.setter
    def key_exists(self, value):
        # This is bit warped. But we need to be able to remove the key
        # from the buyer. But we should never be setting this value. But we do
        # need to remove it. So if you pass an empty string, we ignore it.
        # Otherwise we leave it alone.
        self.key = None if not value else self.key
Beispiel #10
0
class SellerProduct(Model):
    """
    The key to a seller's generic product.
    """
    # An identifier for this product that corresponds to the
    # seller's catalog.
    external_id = models.CharField(max_length=255, db_index=True)
    # An publily visible id used in in-app payments so that we
    # can identify the seller. This will be the iss field in JWT.
    public_id = models.CharField(max_length=255, db_index=True, unique=True)
    seller = models.ForeignKey(Seller, related_name='product')
    # A generic secret field that can be used for this product, regardless
    # of backend.
    secret = AESField(blank=True, null=True, aes_key='sellerproduct:secret')
    # The type of access this product key has.
    access = models.PositiveIntegerField(choices=ACCESS_CHOICES,
                                         default=ACCESS_PURCHASE)

    unique_error_message = lambda *args: EXTERNAL_PRODUCT_ID_IS_NOT_UNIQUE

    class Meta(Model.Meta):
        db_table = 'seller_product'
        unique_together = (('seller', 'external_id'), )
Beispiel #11
0
class Buyer(Model):
    uuid = models.CharField(max_length=255, db_index=True, unique=True)
    pin = HashField(blank=True, null=True)
    pin_confirmed = models.BooleanField(default=False)
    pin_failures = models.IntegerField(default=0)
    pin_locked_out = models.DateTimeField(blank=True, null=True)
    pin_was_locked_out = models.BooleanField(default=False)
    active = models.BooleanField(default=True, db_index=True)
    new_pin = HashField(blank=True, null=True)
    needs_pin_reset = models.BooleanField(default=False)
    email = AESField(blank=True, null=True, aes_key='buyeremail:key')
    # Because the email field is encrypted we can't do lookups in mysql on it.
    # This allows us to use a field for lookups, without exposing anything in
    # the database.
    email_sig = ConsistentSigField(blank=True, null=True)
    locale = models.CharField(max_length=255, blank=True, null=True)
    # When this is True it means the buyer was created by some trusted
    # authentication mechanism such as with a verified Firefox Account
    # email. This is True by default for historic reasons since most
    # clients only create authenticated users.
    authenticated = models.BooleanField(default=True)

    close_signal = Signal(providing_args=['buyer'])

    class Meta(Model.Meta):
        db_table = 'buyer'

    @property
    def locked_out(self):
        if not self.pin_locked_out:
            return False

        if ((datetime.now() - self.pin_locked_out).seconds >
                settings.PIN_FAILURE_LENGTH):
            self.clear_lockout()
            return False

        return True

    def clear_lockout(self, clear_was_locked=False):
        self.pin_failures = 0
        self.pin_locked_out = None
        if clear_was_locked:
            self.pin_was_locked_out = False
        self.save()

    def incr_lockout(self):
        # Use F to avoid race conditions, although this means an extra
        # query to check if we've gone over the limit.
        self.pin_failures = models.F('pin_failures') + 1
        self.save()

        failing = self.reget()
        if failing.pin_failures >= settings.PIN_FAILURES:
            failing.pin_locked_out = datetime.now()
            failing.pin_was_locked_out = True
            failing.save()
            # Indicate to the caller that we are now locked out.
            return True

        return False

    def close(self):
        """
        An explicit close command that goes and changes all the associated
        payment providers for this account and assumes that anything they
        need to do happens.

        Warning:

        This is performing multiple actions across the multiple payment
        providers. Some actions are irreversible. If the action fails, then
        the entire transaction in solitude will be rolled back. Leaving us in a
        confusing state.
        """
        log.warning('Anonymising account starting: {}'.format(self.pk))
        if self.uuid.startswith(ANONYMISED):
            raise ValueError('Account already anonymised.')

        self.close_signal.send(
            buyer=self,
            dispatch_uid='close_account_signal_{}'.format(self.pk),
            sender=self.__class__
        )

        # All succeeds, so go ahead and anonymise the account.
        self.active = False
        self.email = ''
        self.email_sig = None
        self.uuid = ANONYMISED + str(uuid.uuid4())
        self.save()
        log.warning('Anonymising account complete: {}'.format(self.pk))

    def get_uri(self):
        return reverse('generic:buyer-detail', kwargs={'pk': self.pk})

    def save(self, *args, **kw):
        # Each time the email field gets set, set the sig field to the same
        # value. However the email_sig will pass through ConsistentSigField on
        # the way to the database.
        self.email_sig = self.email
        super(Buyer, self).save(*args, **kw)
Beispiel #12
0
class FrozenAPIAccess(amo.models.ModelBase):
    secret = AESField(max_length=255, aes_key='api:access:secret')

    class Meta:
        db_table = 'api_access'
Beispiel #13
0
class APIKey(ModelBase):
    """
    A developer's key/secret pair to access the API.
    """
    user = models.ForeignKey(UserProfile, related_name='api_keys')
    is_active = models.BooleanField(default=True)
    type = models.PositiveIntegerField(choices=dict(
        zip(API_KEY_TYPES, API_KEY_TYPES)).items(),
                                       default=0)
    key = models.CharField(max_length=255, db_index=True, unique=True)
    # TODO: use RSA public keys instead? If we were to use JWT RSA keys
    # then we'd only need to store the public key.
    secret = AESField(aes_key='api_key:secret')

    class Meta:
        db_table = 'api_key'

    def __unicode__(self):
        return (
            u'<{cls} user={user}, type={type}, key={key} secret=...>'.format(
                cls=self.__class__.__name__,
                key=self.key,
                type=self.type,
                user=self.user))

    @classmethod
    def get_jwt_key(cls, **query):
        """
        return a single APIKey instance for a JWT key matching the query.
        """
        query.setdefault('is_active', True)
        return cls.objects.get(type=SYMMETRIC_JWT_TYPE, **query)

    @classmethod
    def new_jwt_credentials(cls, user, **attributes):
        """
        Generates a new key/secret pair suitable for symmetric JWT signing.

        This method must be run within a db transaction.
        Returns an instance of APIKey.
        """
        key = cls.get_unique_key('user:{}:'.format(user.pk))
        return cls.objects.create(key=key,
                                  secret=cls.generate_secret(32),
                                  type=SYMMETRIC_JWT_TYPE,
                                  user=user,
                                  is_active=True,
                                  **attributes)

    @classmethod
    def get_unique_key(cls, prefix, try_count=1, max_tries=1000):
        if try_count >= max_tries:
            raise RuntimeError(
                'a unique API key could not be found after {} tries'.format(
                    max_tries))

        key = '{}{}'.format(prefix, random.randint(0, 999))
        if cls.objects.filter(key=key).exists():
            return cls.get_unique_key(prefix,
                                      try_count=try_count + 1,
                                      max_tries=max_tries)
        return key

    @staticmethod
    def generate_secret(byte_length):
        """
        Return a true random ascii string containing byte_length of randomness.

        The resulting key is suitable for cryptography.
        The key will be hex encoded which means it will be twice as long
        as byte_length, i.e. 40 random bytes yields an 80 byte string.

        byte_length must be at least 32.
        """
        if byte_length < 32:  # at least 256 bit
            raise ValueError(
                '{} is too short; secrets must be longer than 32 bytes'.format(
                    byte_length))
        return os.urandom(byte_length).encode('hex')
Beispiel #14
0
class Buyer(Model):
    uuid = models.CharField(max_length=255, db_index=True, unique=True)
    pin = HashField(blank=True, null=True)
    pin_confirmed = models.BooleanField(default=False)
    pin_failures = models.IntegerField(default=0)
    pin_locked_out = models.DateTimeField(blank=True, null=True)
    pin_was_locked_out = models.BooleanField(default=False)
    active = models.BooleanField(default=True, db_index=True)
    new_pin = HashField(blank=True, null=True)
    needs_pin_reset = models.BooleanField(default=False)
    email = AESField(blank=True, null=True, aes_key='buyeremail:key')
    locale = models.CharField(max_length=255, blank=True, null=True)

    close_signal = Signal(providing_args=['buyer'])

    class Meta(Model.Meta):
        db_table = 'buyer'

    @property
    def locked_out(self):
        if not self.pin_locked_out:
            return False

        if ((datetime.now() - self.pin_locked_out).seconds >
                settings.PIN_FAILURE_LENGTH):
            self.clear_lockout()
            return False

        return True

    def clear_lockout(self, clear_was_locked=False):
        self.pin_failures = 0
        self.pin_locked_out = None
        if clear_was_locked:
            self.pin_was_locked_out = False
        self.save()

    def incr_lockout(self):
        # Use F to avoid race conditions, although this means an extra
        # query to check if we've gone over the limit.
        self.pin_failures = models.F('pin_failures') + 1
        self.save()

        failing = self.reget()
        if failing.pin_failures >= settings.PIN_FAILURES:
            failing.pin_locked_out = datetime.now()
            failing.pin_was_locked_out = True
            failing.save()
            # Indicate to the caller that we are now locked out.
            return True

        return False

    def close(self):
        """
        An explicit close command that goes and changes all the associated
        payment providers for this account and assumes that anything they
        need to do happens.

        Warning:

        This is performing multiple actions across the multiple payment
        providers. Some actions are irreversible. If the action fails, then
        the entire transaction in solitude will be rolled back. Leaving us in a
        confusing state.
        """
        log.warning('Anonymising account starting: {}'.format(self.pk))
        if self.uuid.startswith(ANONYMISED):
            raise ValueError('Account already anonymised.')

        self.close_signal.send(buyer=self,
                               dispatch_uid='close_account_signal_{}'.format(
                                   self.pk),
                               sender=self.__class__)

        # All succeeds, so go ahead and anonymise the account.
        self.active = False
        self.email = ''
        self.uuid = ANONYMISED + str(uuid.uuid4())
        self.save()
        log.warning('Anonymising account complete: {}'.format(self.pk))

    def get_uri(self):
        return reverse('generic:buyer-detail', kwargs={'pk': self.pk})
Beispiel #15
0
class APIKey(ModelBase):
    """
    A developer's key/secret pair to access the API.
    """
    id = PositiveAutoField(primary_key=True)
    user = models.ForeignKey(UserProfile, related_name='api_keys')

    # A user can only have one active key at the same time, it's enforced by
    # a unique db constraint. Since we keep old inactive keys though, nulls
    # need to be allowed (and we need to always set is_active=None instead of
    # is_active=False when revoking keys).
    is_active = models.NullBooleanField(default=True)
    type = models.PositiveIntegerField(choices=dict(
        zip(API_KEY_TYPES, API_KEY_TYPES)).items(),
                                       default=0)
    key = models.CharField(max_length=255, db_index=True, unique=True)
    # TODO: use RSA public keys instead? If we were to use JWT RSA keys
    # then we'd only need to store the public key.
    secret = AESField(aes_key='api_key:secret')

    class Meta:
        db_table = 'api_key'
        unique_together = (('user', 'is_active'), )

    def __str__(self):
        return (
            u'<{cls} user={user}, type={type}, key={key} secret=...>'.format(
                cls=self.__class__.__name__,
                key=self.key,
                type=self.type,
                user=self.user))

    @classmethod
    def get_jwt_key(cls, **kwargs):
        """
        Return a single active APIKey instance for a given user or key.
        """
        kwargs['is_active'] = True
        return cls.objects.get(type=SYMMETRIC_JWT_TYPE, **kwargs)

    @classmethod
    def new_jwt_credentials(cls, user):
        """
        Generates a new key/secret pair suitable for symmetric JWT signing.

        This method must be run within a db transaction.
        Returns an instance of APIKey.
        """
        key = cls.get_unique_key('user:{}:'.format(user.pk))
        return cls.objects.create(key=key,
                                  secret=cls.generate_secret(32),
                                  type=SYMMETRIC_JWT_TYPE,
                                  user=user,
                                  is_active=True)

    @classmethod
    def get_unique_key(cls, prefix, try_count=1, max_tries=1000):
        if try_count >= max_tries:
            raise RuntimeError(
                'a unique API key could not be found after {} tries'.format(
                    max_tries))

        key = '{}{}'.format(prefix, random.randint(0, 999))
        if cls.objects.filter(key=key).exists():
            return cls.get_unique_key(prefix,
                                      try_count=try_count + 1,
                                      max_tries=max_tries)
        return key

    @staticmethod
    def generate_secret(byte_length):
        """
        Return a true random ascii string containing byte_length of randomness.

        The resulting key is suitable for cryptography.
        The key will be hex encoded which means it will be twice as long
        as byte_length, i.e. 40 random bytes yields an 80 byte string.

        byte_length must be at least 32.
        """
        if byte_length < 32:  # at least 256 bit
            raise ValueError(
                '{} is too short; secrets must be longer than 32 bytes'.format(
                    byte_length))
        return force_text(binascii.b2a_hex(os.urandom(byte_length)))
Beispiel #16
0
class AESTestModel(models.Model):
    key = AESField(max_length=255, aes_prefix=u'new-aes:')
    def test_reinitialize_with_deconstruct(self):
        test_model = AESTestModel()
        field = test_model._meta.get_field('key')
        name, path, args, kwargs = field.deconstruct()

        AESField(*args, **kwargs)
 def test_no_prefix(self):
     with pytest.raises(ValueError):
         AESField(aes_prefix='')
class AESTestModel(models.Model):
    key = AESField(max_length=255)