See #152: added permission fields on user model and corresponding API permission
This commit is contained in:
parent
23e27e0dd9
commit
ff65a4b935
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 2.0.4 on 2018-05-17 23:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0005_user_subsonic_api_token'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='permission_federation',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='permission_library',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='permission_settings',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -19,6 +19,13 @@ def get_token():
|
|||
return binascii.b2a_hex(os.urandom(15)).decode('utf-8')
|
||||
|
||||
|
||||
PERMISSIONS = [
|
||||
'federation',
|
||||
'library',
|
||||
'settings',
|
||||
]
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class User(AbstractUser):
|
||||
|
||||
|
@ -28,20 +35,6 @@ class User(AbstractUser):
|
|||
|
||||
# updated on logout or password change, to invalidate JWT
|
||||
secret_key = models.UUIDField(default=uuid.uuid4, null=True)
|
||||
# permissions that are used for API access and that worth serializing
|
||||
relevant_permissions = {
|
||||
# internal_codename : {external_codename}
|
||||
'music.add_importbatch': {
|
||||
'external_codename': 'import.launch',
|
||||
},
|
||||
'dynamic_preferences.change_globalpreferencemodel': {
|
||||
'external_codename': 'settings.change',
|
||||
},
|
||||
'federation.change_library': {
|
||||
'external_codename': 'federation.manage',
|
||||
},
|
||||
}
|
||||
|
||||
privacy_level = fields.get_privacy_field()
|
||||
|
||||
# Unfortunately, Subsonic API assumes a MD5/password authentication
|
||||
|
@ -52,12 +45,24 @@ class User(AbstractUser):
|
|||
subsonic_api_token = models.CharField(
|
||||
blank=True, null=True, max_length=255)
|
||||
|
||||
# permissions
|
||||
permission_federation = models.BooleanField(default=False)
|
||||
permission_library = models.BooleanField(default=False)
|
||||
permission_settings = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
def add_permission(self, codename):
|
||||
p = Permission.objects.get(codename=codename)
|
||||
self.user_permissions.add(p)
|
||||
def get_permissions(self):
|
||||
perms = {}
|
||||
for p in PERMISSIONS:
|
||||
v = self.is_superuser or getattr(self, 'permission_{}'.format(p))
|
||||
perms[p] = v
|
||||
return perms
|
||||
|
||||
def has_permissions(self, *perms):
|
||||
permissions = self.get_permissions()
|
||||
return all([permissions[p] for p in perms])
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('users:detail', kwargs={'username': self.username})
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
from rest_framework.permissions import BasePermission
|
||||
|
||||
|
||||
class HasUserPermission(BasePermission):
|
||||
"""
|
||||
Ensure the request user has the proper permissions.
|
||||
|
||||
Usage:
|
||||
|
||||
class MyView(APIView):
|
||||
permission_classes = [HasUserPermission]
|
||||
required_permissions = ['federation']
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
if not hasattr(request, 'user') or not request.user:
|
||||
return False
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
return request.user.has_permissions(*view.required_permissions)
|
|
@ -1,3 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from funkwhale_api.users import models
|
||||
|
||||
|
||||
def test__str__(factories):
|
||||
user = factories['users.User'](username='hello')
|
||||
|
@ -16,3 +20,33 @@ def test_changing_password_updates_subsonic_api_token(factories):
|
|||
|
||||
assert user.subsonic_api_token is not None
|
||||
assert user.subsonic_api_token != 'test'
|
||||
|
||||
|
||||
def test_get_permissions_superuser(factories):
|
||||
user = factories['users.User'](is_superuser=True)
|
||||
|
||||
perms = user.get_permissions()
|
||||
for p in models.PERMISSIONS:
|
||||
assert perms[p] is True
|
||||
|
||||
|
||||
def test_get_permissions_regular(factories):
|
||||
user = factories['users.User'](permission_library=True)
|
||||
|
||||
perms = user.get_permissions()
|
||||
for p in models.PERMISSIONS:
|
||||
if p == 'library':
|
||||
assert perms[p] is True
|
||||
else:
|
||||
assert perms[p] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize('args,perms,expected', [
|
||||
({'is_superuser': True}, ['federation', 'library'], True),
|
||||
({'is_superuser': False}, ['federation'], False),
|
||||
({'permission_library': True}, ['library'], True),
|
||||
({'permission_library': True}, ['library', 'federation'], False),
|
||||
])
|
||||
def test_has_permissions(args, perms, expected, factories):
|
||||
user = factories['users.User'](**args)
|
||||
assert user.has_permissions(*perms) is expected
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import pytest
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from funkwhale_api.users import permissions
|
||||
|
||||
|
||||
def test_has_user_permission_no_user(api_request):
|
||||
view = APIView.as_view()
|
||||
permission = permissions.HasUserPermission()
|
||||
request = api_request.get('/')
|
||||
assert permission.has_permission(request, view) is False
|
||||
|
||||
|
||||
def test_has_user_permission_anonymous(anonymous_user, api_request):
|
||||
view = APIView.as_view()
|
||||
permission = permissions.HasUserPermission()
|
||||
request = api_request.get('/')
|
||||
setattr(request, 'user', anonymous_user)
|
||||
assert permission.has_permission(request, view) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value', [True, False])
|
||||
def test_has_user_permission_logged_in_single(value, factories, api_request):
|
||||
user = factories['users.User'](permission_federation=value)
|
||||
|
||||
class View(APIView):
|
||||
required_permissions = ['federation']
|
||||
view = View()
|
||||
permission = permissions.HasUserPermission()
|
||||
request = api_request.get('/')
|
||||
setattr(request, 'user', user)
|
||||
result = permission.has_permission(request, view)
|
||||
assert result == user.has_permissions('federation') == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize('federation,library,expected', [
|
||||
(True, False, False),
|
||||
(False, True, False),
|
||||
(False, False, False),
|
||||
(True, True, True),
|
||||
])
|
||||
def test_has_user_permission_logged_in_single(
|
||||
federation, library, expected, factories, api_request):
|
||||
user = factories['users.User'](
|
||||
permission_federation=federation,
|
||||
permission_library=library,
|
||||
)
|
||||
|
||||
class View(APIView):
|
||||
required_permissions = ['federation', 'library']
|
||||
view = View()
|
||||
permission = permissions.HasUserPermission()
|
||||
request = api_request.get('/')
|
||||
setattr(request, 'user', user)
|
||||
result = permission.has_permission(request, view)
|
||||
assert result == user.has_permissions('federation', 'library') == expected
|
Loading…
Reference in New Issue