diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py index 0af155e77..12307f7fd 100644 --- a/api/funkwhale_api/users/factories.py +++ b/api/funkwhale_api/users/factories.py @@ -9,6 +9,7 @@ class UserFactory(factory.django.DjangoModelFactory): username = factory.Sequence(lambda n: 'user-{0}'.format(n)) email = factory.Sequence(lambda n: 'user-{0}@example.com'.format(n)) password = factory.PostGenerationMethodCall('set_password', 'test') + subsonic_api_token = None class Meta: model = 'users.User' diff --git a/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py b/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py new file mode 100644 index 000000000..689b3ef77 --- /dev/null +++ b/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.3 on 2018-05-08 09:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_user_privacy_level'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='subsonic_api_token', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index 572fa9ddc..773d60f38 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals, absolute_import import uuid +import secrets from django.conf import settings from django.contrib.auth.models import AbstractUser @@ -38,6 +39,13 @@ class User(AbstractUser): privacy_level = fields.get_privacy_field() + # Unfortunately, Subsonic API assumes a MD5/password authentication + # scheme, which is weak in terms of security, and not achievable + # anyway since django use stronger schemes for storing passwords. + # Users that want to use the subsonic API from external client + # should set this token and use it as their password in such clients + subsonic_api_token = models.CharField( + blank=True, null=True, max_length=255) def __str__(self): return self.username @@ -49,9 +57,15 @@ class User(AbstractUser): self.secret_key = uuid.uuid4() return self.secret_key + def update_subsonic_api_token(self): + self.subsonic_api_token = secrets.token_hex(32) + return self.subsonic_api_token + def set_password(self, raw_password): super().set_password(raw_password) self.update_secret_key() + if self.subsonic_api_token: + self.update_subsonic_api_token() def get_activity_url(self): return settings.FUNKWHALE_URL + '/@{}'.format(self.username) diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py index 57793f494..c7cd12e9e 100644 --- a/api/tests/users/test_models.py +++ b/api/tests/users/test_models.py @@ -2,3 +2,17 @@ def test__str__(factories): user = factories['users.User'](username='hello') assert user.__str__() == 'hello' + + +def test_changing_password_updates_subsonic_api_token_no_token(factories): + user = factories['users.User'](subsonic_api_token=None) + user.set_password('new') + assert user.subsonic_api_token is None + + +def test_changing_password_updates_subsonic_api_token(factories): + user = factories['users.User'](subsonic_api_token='test') + user.set_password('new') + + assert user.subsonic_api_token is not None + assert user.subsonic_api_token != 'test'