Skip to content
This repository has been archived by the owner on Nov 5, 2019. It is now read-only.

Deserialization error (Invalid Padding) after django_orm.CredentialsField.to_python #142

Closed
qcaron opened this issue Mar 7, 2015 · 17 comments

Comments

@qcaron
Copy link

qcaron commented Mar 7, 2015

Hi guys !

I am using google-api-python-client to deal with Google Analytics API. google-api-python-client uses oauth2client to deal with Google's OAuth 2 authentication tokens (which are saved in my Django PostgreSQL DB).

I have created tests which used to pass with Django==1.6.5 and google-api-python-client==1.2 but now fail. I run my test with a fixture containing a model using a oauth2client.django_orm.CredentialsField field. Here is my full stack trace generated when the test is run:

Traceback (most recent call last):
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/test/testcases.py", line 182, in __call__
    self._pre_setup()
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/test/testcases.py", line 754, in _pre_setup
    self._fixture_setup()
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/test/testcases.py", line 907, in _fixture_setup
    'skip_checks': True,
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/core/management/__init__.py", line 115, in call_command
    return klass.execute(*args, **defaults)
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/core/management/base.py", line 338, in execute
    output = self.handle(*args, **options)
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/modeltranslation/management/commands/loaddata.py", line 50, in handle
    return super(Command, self).handle(*fixture_labels, **options)
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/core/management/commands/loaddata.py", line 61, in handle
    self.loaddata(fixture_labels)
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/core/management/commands/loaddata.py", line 91, in loaddata
    self.load_label(fixture_label)
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/core/management/commands/loaddata.py", line 142, in load_label
    for obj in objects:
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/core/serializers/json.py", line 81, in Deserializer
    six.reraise(DeserializationError, DeserializationError(e), sys.exc_info()[2])
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/core/serializers/json.py", line 75, in Deserializer
    for obj in PythonDeserializer(objects, **options):
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/django/core/serializers/python.py", line 145, in Deserializer
    data[field.name] = field.to_python(field_value)
  File "/Users/quentin/Python/virtualenvs/mtanalytics/lib/python2.7/site-packages/oauth2client/django_orm.py", line 47, in to_python
    return pickle.loads(base64.b64decode(value))
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/base64.py", line 76, in b64decode
    raise TypeError(msg)
DeserializationError: Problem installing fixture '/Users/quentin/Python/projects/mtanalytics/test/fixtures/cron_fixture.json': Incorrect padding

The code line from oauth2client.django_orm.CredentialsField.to_python (line 47):

return pickle.loads(base64.b64decode(value))

which should work fine with this code from oauth2client.django_orm.CredentialsField.get_db_prep_value (line 52):

return base64.b64encode(pickle.dumps(value))

My Django text fixture is created using manage.py dumpdata. I am using oauth2client==1.4.6.

Thanks for your support!!

@qcaron
Copy link
Author

qcaron commented Mar 7, 2015

Oh, here is the serialization of my Credentials Django model that stores the oauth2client.django_orm.CredentialsField value:

"fields": {
        "credential": "<oauth2client.client.OAuth2Credentials object at 0x10d0fd4d0>"
    },
    "model": "digger.credentialsmodel",
    "pk": 1

So pretty hard to deserialize...

Shouldn't my credential field be dumped to JSON using

base64.b64encode(pickle.dumps(value))

from oauth2client.django_orm.CredentialsField.get_db_prep_value ???

@qcaron
Copy link
Author

qcaron commented Mar 12, 2015

Okay I figured out that the CredentialsField field requires a value_to_string method so Django 1.7 knows how to dump it correctly.

I ended up with the following code to get it working:

def value_to_string(self, obj):
    value = self._get_val_from_obj(obj)
    return self.get_prep_value(value)

and changed get_db_prep_value to get_prep_value :

def get_prep_value(self, value):
    if value is None:
      return None
    return base64.b64encode(pickle.dumps(value))

I have to test this update in a dedicated virtualenv before making a pull request. If this looks wrong to anyone please let me know ;)

Cheers,
Quentin

@csssaz
Copy link

csssaz commented Apr 29, 2015

Hey, I'm having the same problem but couldn't make it work with the solution you point out. Did you just changed those two functions??? Please, provide some more details

@qcaron
Copy link
Author

qcaron commented Apr 29, 2015

Hello Alejandro,

Here is my field full code for the CredentialsField:

class CredentialsField(models.Field):

  __metaclass__ = models.SubfieldBase

  def __init__(self, *args, **kwargs):
    if 'null' not in kwargs:
      kwargs['null'] = True
    super(CredentialsField, self).__init__(*args, **kwargs)

  def get_internal_type(self):
    return "TextField"

  def to_python(self, value):
    if value is None:
      return None
    if isinstance(value, oauth2client.client.Credentials):
      return value
    return pickle.loads(base64.b64decode(value))

  def get_prep_value(self, value):
    if value is None:
      return None
    return base64.b64encode(pickle.dumps(value))

  def value_to_string(self, obj):
    value = self._get_val_from_obj(obj)
    return self.get_prep_value(value)

  #def deconstruct(self):
  #    name, path, args, kwargs = super(CommaSepField, self).deconstruct()
  #    return name, path, args, kwargs

Hope this helps :)

Cheers,
Quentin

@csssaz
Copy link

csssaz commented Apr 30, 2015

Hello Quentin, thanks for the quick response! I tried using your code, but still get an incorrect padding error.

I'm using Django 1.7.3 with python 3.4.3. Because of this reason, I had to change the metaclass definition from:

class CredentialsField(models.Field):
     __metaclass__ = models.SubfieldBase

to

class CredentialsField(models.Field, metaclass=models.SubfieldBase):

This because of python 3 change in metaclass definition: https://docs.python.org/3/whatsnew/3.0.html#changed-syntax

Do you know what may be happening, or what should I try to do?

Many thanks in advance.

alejandro.

@apragacz
Copy link
Contributor

Hello Alejandro,

I had the same problem. I'm using Django 1.8 with Python 3.4 and the padding errors occured when I used the PostgreSQL backend (for some strange reason the padding errors didn't happen when SQLite backend was used).

I ended up with following solution, which seems to work for both PostgresSQL and SQLite backends:

class FixedCredentialsField(models.Field, metaclass=models.SubfieldBase):
    '''
    currently, CredentialsField does not work correctly because:
    * __metaclass__ attribute is no longer used in Python 3.
    * using get_db_prep_value instead of get_prep_value may cause problems,
      see: https://github.com/google/oauth2client/issues/142 for details.
    * the data needs to be converted to bytes before processing via base64
      (this affects mostly Python 3).
    '''

    def __init__(self, *args, **kwargs):
        if 'null' not in kwargs:
            kwargs['null'] = True
        super().__init__(*args, **kwargs)

    def get_internal_type(self):
        return 'TextField'

    def to_python(self, value):
        if value is None:
            return None
        if isinstance(value, Credentials):
            return value
        # Ensure that the string input is converted to bytes
        if isinstance(value, str):
            value = bytes(value, 'utf-8')
        return pickle.loads(base64.b64decode(value))

    def get_prep_value(self, value):
        if value is None:
            return None
        # Ensure that the representation is a string
        return base64.b64encode(pickle.dumps(value)).decode('utf-8')

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_prep_value(value)

Please keep in mind this is Python 3 only and wouldn't work on Python 2.

Hope it helps anyone.

@dhermes
Copy link
Contributor

dhermes commented Sep 25, 2015

Where does the fix occur? What version of oauth2client are you using?

@apragacz
Copy link
Contributor

@dhermes

  • the fix (in comparison to the Quentin solution) contains additional type conversions (from str to bytes, when it is passed to base64 functions, and one bytes to str conversion in get_prep_value. My guess is that Python 2 handles the unicode/str types in more liberal manner so the problem does not occur here.
  • I'm using oauth2client==1.5.1.

@dhermes
Copy link
Contributor

dhermes commented Sep 28, 2015

@szopu Can you provide a diff with the current source or point to a fork that you're currently using?

apragacz added a commit to apragacz/oauth2client that referenced this issue Sep 30, 2015
apragacz added a commit to apragacz/oauth2client that referenced this issue Oct 1, 2015
@apragacz
Copy link
Contributor

apragacz commented Oct 1, 2015

@dhermes here is the diff (the autogenerated link above is to outdated commit):

master...szopu:django-credentials-field-py3-fix

I refactored it to make it also Python 2 compatible, and also used smart_text, smart_bytes django util functions.

I can create PR if that's OK.

@dhermes
Copy link
Contributor

dhermes commented Oct 1, 2015

Please do. Note also that there are _to_bytes and _from_bytes helpers in oauth2client._helpers (rather than using the raw unicode.encode() and str.decode() methods)

apragacz added a commit to apragacz/oauth2client that referenced this issue Oct 1, 2015
apragacz added a commit to apragacz/oauth2client that referenced this issue Oct 4, 2015
apragacz added a commit to apragacz/oauth2client that referenced this issue Oct 4, 2015
apragacz added a commit to apragacz/oauth2client that referenced this issue Oct 5, 2015
dhermes added a commit that referenced this issue Oct 6, 2015
@dhermes dhermes closed this as completed Oct 6, 2015
@nagyv
Copy link

nagyv commented Oct 8, 2015

I just run into this issue. When is the next release expected?

@dhermes
Copy link
Contributor

dhermes commented Oct 9, 2015

It's up to @nathanielmanistaatgoogle

Here is our most recent release schedule:

               oauth2client-1.4.11.zip    2015-05-19         5339
            oauth2client-1.4.11.tar.gz    2015-05-19       384765
         oauth2client-1.4.11-py2.7.egg    2015-05-19       744492
               oauth2client-1.4.12.zip    2015-07-08         3064
            oauth2client-1.4.12.tar.gz    2015-07-08       995647
         oauth2client-1.4.12-py2.7.egg    2015-07-08       752731
             oauth2client-1.5.0.tar.gz    2015-09-03        83136
             oauth2client-1.5.1.tar.gz    2015-09-16       138414

@craigcitro Our last two releases have been automatic via Travis.

  1. Is it important to have a .egg and .zip release file?
  2. What were you doing to generate and upload them?

@craigcitro
Copy link
Contributor

@dhermes

  • I think .egg is outdated as long as you've got wheels.
  • I always did the .zip because at some point, that made installing on Windows easier. If that's no longer true (maybe subsumed by wheel?), then no worries; my trusty python setup.py sdist --formats=gztar,zip bdist_wheel always pushed the zip.

@dhermes
Copy link
Contributor

dhermes commented Oct 9, 2015

Gotcher. I'll leave it alone for now, but will update if people request other formats.

Thanks!

@nathanielmanistaatgoogle
Copy link
Contributor

@nagyv: to the extent that "it is up to [me]", we release when we are told it would be helpful to release, and/or when we feel like it. I take it that it would be helpful to you for a release to be made? I therefore decree: release!

@dhermes
Copy link
Contributor

dhermes commented Oct 14, 2015

I'm super busy until about next Friday (October 23). Someone else can do the release / release notes if it's desired before then.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants