diff --git a/api/funkwhale_api/common/management/__init__.py b/api/funkwhale_api/common/management/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/common/management/commands/__init__.py b/api/funkwhale_api/common/management/commands/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/api/funkwhale_api/common/management/commands/script.py b/api/funkwhale_api/common/management/commands/script.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d26a5836d46906967e24662a94bab553fa15880
--- /dev/null
+++ b/api/funkwhale_api/common/management/commands/script.py
@@ -0,0 +1,66 @@
+from django.core.management.base import BaseCommand, CommandError
+
+from funkwhale_api.common import scripts
+
+
+class Command(BaseCommand):
+    help = 'Run a specific script from funkwhale_api/common/scripts/'
+
+    def add_arguments(self, parser):
+        parser.add_argument('script_name', nargs='?', type=str)
+        parser.add_argument(
+            '--noinput', '--no-input', action='store_false', dest='interactive',
+            help="Do NOT prompt the user for input of any kind.",
+        )
+
+    def handle(self, *args, **options):
+        name = options['script_name']
+        if not name:
+            self.show_help()
+
+        available_scripts = self.get_scripts()
+        try:
+            script = available_scripts[name]
+        except KeyError:
+            raise CommandError(
+                '{} is not a valid script. Run python manage.py script for a '
+                'list of available scripts'.format(name))
+
+        self.stdout.write('')
+        if options['interactive']:
+            message = (
+                'Are you sure you want to execute the script {}?\n\n'
+                "Type 'yes' to continue, or 'no' to cancel: "
+            ).format(name)
+            if input(''.join(message)) != 'yes':
+                raise CommandError("Script cancelled.")
+        script['entrypoint'](self, **options)
+
+    def show_help(self):
+        indentation = 4
+        self.stdout.write('')
+        self.stdout.write('Available scripts:')
+        self.stdout.write('Launch with: python manage.py <script_name>')
+        available_scripts = self.get_scripts()
+        for name, script in sorted(available_scripts.items()):
+            self.stdout.write('')
+            self.stdout.write(self.style.SUCCESS(name))
+            self.stdout.write('')
+            for line in script['help'].splitlines():
+                self.stdout.write('     {}'.format(line))
+        self.stdout.write('')
+
+    def get_scripts(self):
+        available_scripts = [
+            k for k in sorted(scripts.__dict__.keys())
+            if not k.startswith('__')
+        ]
+        data = {}
+        for name in available_scripts:
+            module = getattr(scripts, name)
+            data[name] = {
+                'name': name,
+                'help': module.__doc__.strip(),
+                'entrypoint': module.main
+            }
+        return data
diff --git a/api/funkwhale_api/common/permissions.py b/api/funkwhale_api/common/permissions.py
index cab4b699d2a518cffc52aaf0cbad4473dcc28a6f..e9e8b8819f4b70e9ada3a0bcf71c709c9a5ef1de 100644
--- a/api/funkwhale_api/common/permissions.py
+++ b/api/funkwhale_api/common/permissions.py
@@ -3,7 +3,7 @@ import operator
 from django.conf import settings
 from django.http import Http404
 
-from rest_framework.permissions import BasePermission, DjangoModelPermissions
+from rest_framework.permissions import BasePermission
 
 from funkwhale_api.common import preferences
 
@@ -16,17 +16,6 @@ class ConditionalAuthentication(BasePermission):
         return True
 
 
-class HasModelPermission(DjangoModelPermissions):
-    """
-    Same as DjangoModelPermissions, but we pin the model:
-
-        class MyModelPermission(HasModelPermission):
-            model = User
-    """
-    def get_required_permissions(self, method, model_cls):
-        return super().get_required_permissions(method, self.model)
-
-
 class OwnerPermission(BasePermission):
     """
     Ensure the request user is the owner of the object.
diff --git a/api/funkwhale_api/common/scripts/__init__.py b/api/funkwhale_api/common/scripts/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4b2d525202218c43483dae60b6df4f4c6090723e
--- /dev/null
+++ b/api/funkwhale_api/common/scripts/__init__.py
@@ -0,0 +1,2 @@
+from . import django_permissions_to_user_permissions
+from . import test
diff --git a/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py b/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..1bc971f80911de276ef2c210fafddfc052d71ee6
--- /dev/null
+++ b/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py
@@ -0,0 +1,29 @@
+"""
+Convert django permissions to user permissions in the database,
+following the work done in #152.
+"""
+from django.db.models import Q
+from funkwhale_api.users import models
+
+from django.contrib.auth.models import Permission
+
+mapping = {
+    'dynamic_preferences.change_globalpreferencemodel': 'settings',
+    'music.add_importbatch': 'library',
+    'federation.change_library': 'federation',
+}
+
+
+def main(command, **kwargs):
+    for codename, user_permission in sorted(mapping.items()):
+        app_label, c = codename.split('.')
+        p = Permission.objects.get(
+            content_type__app_label=app_label, codename=c)
+        users = models.User.objects.filter(
+            Q(groups__permissions=p) | Q(user_permissions=p)).distinct()
+        total = users.count()
+
+        command.stdout.write('Updating {} users with {} permission...'.format(
+            total, user_permission
+        ))
+        users.update(**{'permission_{}'.format(user_permission): True})
diff --git a/api/funkwhale_api/common/scripts/test.py b/api/funkwhale_api/common/scripts/test.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab401dca4a7a46be53df6ffc1a09cd6657407ea8
--- /dev/null
+++ b/api/funkwhale_api/common/scripts/test.py
@@ -0,0 +1,8 @@
+"""
+This is a test script that does nothing.
+You can launch it just to check how it works.
+"""
+
+
+def main(command, **kwargs):
+    command.stdout.write('Test script run successfully')
diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py
index 37ad9ebfd1a481ae0d882d064ea285f6b7fd4424..06a2cd040cfc0d94fe51ab419bf6159c6e14f631 100644
--- a/api/funkwhale_api/federation/views.py
+++ b/api/funkwhale_api/federation/views.py
@@ -15,8 +15,8 @@ from rest_framework.serializers import ValidationError
 
 from funkwhale_api.common import preferences
 from funkwhale_api.common import utils as funkwhale_utils
-from funkwhale_api.common.permissions import HasModelPermission
 from funkwhale_api.music.models import TrackFile
+from funkwhale_api.users.permissions import HasUserPermission
 
 from . import activity
 from . import actors
@@ -187,16 +187,13 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
         return response.Response(data)
 
 
-class LibraryPermission(HasModelPermission):
-    model = models.Library
-
-
 class LibraryViewSet(
         mixins.RetrieveModelMixin,
         mixins.UpdateModelMixin,
         mixins.ListModelMixin,
         viewsets.GenericViewSet):
-    permission_classes = [LibraryPermission]
+    permission_classes = (HasUserPermission,)
+    required_permissions = ['federation']
     queryset = models.Library.objects.all().select_related(
         'actor',
         'follow',
@@ -291,7 +288,8 @@ class LibraryViewSet(
 class LibraryTrackViewSet(
         mixins.ListModelMixin,
         viewsets.GenericViewSet):
-    permission_classes = [LibraryPermission]
+    permission_classes = (HasUserPermission,)
+    required_permissions = ['federation']
     queryset = models.LibraryTrack.objects.all().select_related(
         'library__actor',
         'library__follow',
diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py
index e6725e24846500f86bfbf9105d55b2a6780dc5dd..b905acd3e6c1ce78811e44f691b17506b041ab6f 100644
--- a/api/funkwhale_api/instance/views.py
+++ b/api/funkwhale_api/instance/views.py
@@ -6,6 +6,7 @@ from dynamic_preferences.api import viewsets as preferences_viewsets
 from dynamic_preferences.registries import global_preferences_registry
 
 from funkwhale_api.common import preferences
+from funkwhale_api.users.permissions import HasUserPermission
 
 from . import nodeinfo
 from . import stats
@@ -18,7 +19,8 @@ NODEINFO_2_CONTENT_TYPE = (
 
 class AdminSettings(preferences_viewsets.GlobalPreferencesViewSet):
     pagination_class = None
-
+    permission_classes = (HasUserPermission,)
+    required_permissions = ['settings']
 
 class InstanceSettings(views.APIView):
     permission_classes = []
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index f2ab72c5a020f4745a932dbbb50c3233030c9497..e71d3555e67a21012ee34d79e2fd56b28354fcf8 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -25,8 +25,8 @@ from rest_framework import permissions
 from musicbrainzngs import ResponseError
 
 from funkwhale_api.common import utils as funkwhale_utils
-from funkwhale_api.common.permissions import (
-    ConditionalAuthentication, HasModelPermission)
+from funkwhale_api.common.permissions import ConditionalAuthentication
+from funkwhale_api.users.permissions import HasUserPermission
 from taggit.models import Tag
 from funkwhale_api.federation import actors
 from funkwhale_api.federation.authentication import SignatureAuthentication
@@ -107,25 +107,22 @@ class ImportBatchViewSet(
               .annotate(job_count=Count('jobs'))
     )
     serializer_class = serializers.ImportBatchSerializer
-    permission_classes = (permissions.DjangoModelPermissions, )
+    permission_classes = (HasUserPermission,)
+    required_permissions = ['library']
     filter_class = filters.ImportBatchFilter
 
     def perform_create(self, serializer):
         serializer.save(submitted_by=self.request.user)
 
 
-class ImportJobPermission(HasModelPermission):
-    # not a typo, perms on import job is proxied to import batch
-    model = models.ImportBatch
-
-
 class ImportJobViewSet(
         mixins.CreateModelMixin,
         mixins.ListModelMixin,
         viewsets.GenericViewSet):
     queryset = (models.ImportJob.objects.all().select_related())
     serializer_class = serializers.ImportJobSerializer
-    permission_classes = (ImportJobPermission, )
+    permission_classes = (HasUserPermission,)
+    required_permissions = ['library']
     filter_class = filters.ImportJobFilter
 
     @list_route(methods=['get'])
@@ -442,7 +439,8 @@ class Search(views.APIView):
 
 class SubmitViewSet(viewsets.ViewSet):
     queryset = models.ImportBatch.objects.none()
-    permission_classes = (permissions.DjangoModelPermissions, )
+    permission_classes = (HasUserPermission,)
+    required_permissions = ['library']
 
     @list_route(methods=['post'])
     @transaction.non_atomic_requests
diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py
index 89b67d3df96de05b9ffd8fff9ce027f67f101318..7e9062a1308a298fb83453fa0cb1c53c5740dd51 100644
--- a/api/funkwhale_api/users/admin.py
+++ b/api/funkwhale_api/users/admin.py
@@ -5,6 +5,7 @@ from django import forms
 from django.contrib import admin
 from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
 from django.contrib.auth.forms import UserChangeForm, UserCreationForm
+from django.utils.translation import ugettext_lazy as _
 
 from .models import User
 
@@ -41,8 +42,33 @@ class UserAdmin(AuthUserAdmin):
         'email',
         'date_joined',
         'last_login',
-        'privacy_level',
+        'is_staff',
+        'is_superuser',
     ]
     list_filter = [
+        'is_superuser',
+        'is_staff',
         'privacy_level',
+        'permission_settings',
+        'permission_library',
+        'permission_federation',
     ]
+
+    fieldsets = (
+        (None, {'fields': ('username', 'password', 'privacy_level')}),
+        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
+        (_('Permissions'), {
+            'fields': (
+                'is_active',
+                'is_staff',
+                'is_superuser',
+                'permission_library',
+                'permission_settings',
+                'permission_federation')}),
+        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
+        (_('Useless fields'), {
+            'fields': (
+                'user_permissions',
+                'groups',
+            )})
+        )
diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py
index 12307f7fd109407f025b05dd364146204a59ae27..cd28f44073ded3ad425ee3c1f84a7c4b6dcabec6 100644
--- a/api/funkwhale_api/users/factories.py
+++ b/api/funkwhale_api/users/factories.py
@@ -1,15 +1,41 @@
 import factory
 
-from funkwhale_api.factories import registry
+from funkwhale_api.factories import registry, ManyToManyFromList
 from django.contrib.auth.models import Permission
 
 
+@registry.register
+class GroupFactory(factory.django.DjangoModelFactory):
+    name = factory.Sequence(lambda n: 'group-{0}'.format(n))
+
+    class Meta:
+        model = 'auth.Group'
+
+    @factory.post_generation
+    def perms(self, create, extracted, **kwargs):
+        if not create:
+            # Simple build, do nothing.
+            return
+
+        if extracted:
+            perms = [
+                Permission.objects.get(
+                    content_type__app_label=p.split('.')[0],
+                    codename=p.split('.')[1],
+                )
+                for p in extracted
+            ]
+            # A list of permissions were passed in, use them
+            self.permissions.add(*perms)
+
+
 @registry.register
 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
+    groups = ManyToManyFromList('groups')
 
     class Meta:
         model = 'users.User'
diff --git a/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py b/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c9ab0fadc99016e8a62f609ace117ea0b941965
--- /dev/null
+++ b/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py
@@ -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),
+        ),
+    ]
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index 8273507c49bb23f1986b6b691fe90cc1fc8fea45..c16cd62b319c01d4a9f1802c531d279f0ef802ed 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -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,32 @@ class User(AbstractUser):
     subsonic_api_token = models.CharField(
         blank=True, null=True, max_length=255)
 
+    # permissions
+    permission_federation = models.BooleanField(
+        'Manage library federation',
+        help_text='Follow other instances, accept/deny library follow requests...',
+        default=False)
+    permission_library = models.BooleanField(
+        'Manage library',
+        help_text='Import new content, manage existing content',
+        default=False)
+    permission_settings = models.BooleanField(
+        'Manage instance-level settings',
+        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})
diff --git a/api/funkwhale_api/users/permissions.py b/api/funkwhale_api/users/permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..2ff49ff3fa6661aecd0616fdb87b5b43f89a95c2
--- /dev/null
+++ b/api/funkwhale_api/users/permissions.py
@@ -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)
diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py
index eadce6154fa12c385f0655d3667b1634442716ec..3a095e78aa727b52ed35153cfffc8419b6142172 100644
--- a/api/funkwhale_api/users/serializers.py
+++ b/api/funkwhale_api/users/serializers.py
@@ -55,16 +55,11 @@ class UserReadSerializer(serializers.ModelSerializer):
             'is_superuser',
             'permissions',
             'date_joined',
-            'privacy_level'
+            'privacy_level',
         ]
 
     def get_permissions(self, o):
-        perms = {}
-        for internal_codename, conf in o.relevant_permissions.items():
-            perms[conf['external_codename']] = {
-                'status': o.has_perm(internal_codename)
-            }
-        return perms
+        return o.get_permissions()
 
 
 class PasswordResetSerializer(PRS):
diff --git a/api/tests/common/test_scripts.py b/api/tests/common/test_scripts.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce478ba048043b6c1da61d0febf72fd91e8c2444
--- /dev/null
+++ b/api/tests/common/test_scripts.py
@@ -0,0 +1,56 @@
+import pytest
+
+from funkwhale_api.common.management.commands import script
+from funkwhale_api.common import scripts
+
+
+@pytest.fixture
+def command():
+    return script.Command()
+
+
+@pytest.mark.parametrize('script_name', [
+    'django_permissions_to_user_permissions',
+    'test',
+])
+def test_script_command_list(command, script_name, mocker):
+    mocked = mocker.patch(
+        'funkwhale_api.common.scripts.{}.main'.format(script_name))
+
+    command.handle(script_name=script_name, interactive=False)
+
+    mocked.assert_called_once_with(
+        command, script_name=script_name, interactive=False)
+
+
+def test_django_permissions_to_user_permissions(factories, command):
+    group = factories['auth.Group'](
+        perms=[
+            'federation.change_library'
+        ]
+    )
+    user1 = factories['users.User'](
+        perms=[
+            'dynamic_preferences.change_globalpreferencemodel',
+            'music.add_importbatch',
+        ]
+    )
+    user2 = factories['users.User'](
+        perms=[
+            'music.add_importbatch',
+        ],
+        groups=[group]
+    )
+
+    scripts.django_permissions_to_user_permissions.main(command)
+
+    user1.refresh_from_db()
+    user2.refresh_from_db()
+
+    assert user1.permission_settings is True
+    assert user1.permission_library is True
+    assert user1.permission_federation is False
+
+    assert user2.permission_settings is False
+    assert user2.permission_library is True
+    assert user2.permission_federation is True
diff --git a/api/tests/conftest.py b/api/tests/conftest.py
index dda537801f3cf66efb7eec4e823816e5dcec8207..b7a7d071ab6e8e548b7026b84ff815c0f2e6e43a 100644
--- a/api/tests/conftest.py
+++ b/api/tests/conftest.py
@@ -14,6 +14,7 @@ from rest_framework.test import APIClient
 from rest_framework.test import APIRequestFactory
 
 from funkwhale_api.activity import record
+from funkwhale_api.users.permissions import HasUserPermission
 from funkwhale_api.taskapp import celery
 
 
@@ -224,3 +225,11 @@ def authenticated_actor(factories, mocker):
         'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor',
         return_value=actor)
     yield actor
+
+
+@pytest.fixture
+def assert_user_permission():
+    def inner(view, permissions):
+        assert HasUserPermission in view.permission_classes
+        assert set(view.required_permissions) == set(permissions)
+    return inner
diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py
index fd6ac2eb2ef07e581cd8b6bd473a0e65a0e30125..10237ed9fd76d156656238edbce11495dc08934b 100644
--- a/api/tests/federation/test_views.py
+++ b/api/tests/federation/test_views.py
@@ -9,9 +9,18 @@ from funkwhale_api.federation import activity
 from funkwhale_api.federation import models
 from funkwhale_api.federation import serializers
 from funkwhale_api.federation import utils
+from funkwhale_api.federation import views
 from funkwhale_api.federation import webfinger
 
 
+@pytest.mark.parametrize('view,permissions', [
+    (views.LibraryViewSet, ['federation']),
+    (views.LibraryTrackViewSet, ['federation']),
+])
+def test_permissions(assert_user_permission, view, permissions):
+    assert_user_permission(view, permissions)
+
+
 @pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys())
 def test_instance_actors(system_actor, db, api_client):
     actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
diff --git a/api/tests/instance/test_views.py b/api/tests/instance/test_views.py
index 6d8dcac3eebe1470da291a783bb7ad97a8b0c127..daf54db51cb32181c380fca8719bc951c97404c6 100644
--- a/api/tests/instance/test_views.py
+++ b/api/tests/instance/test_views.py
@@ -1,5 +1,16 @@
+import pytest
+
 from django.urls import reverse
 
+from funkwhale_api.instance import views
+
+
+@pytest.mark.parametrize('view,permissions', [
+    (views.AdminSettings, ['settings']),
+])
+def test_permissions(assert_user_permission, view, permissions):
+    assert_user_permission(view, permissions)
+
 
 def test_nodeinfo_endpoint(db, api_client, mocker):
     payload = {
@@ -43,7 +54,8 @@ def test_admin_settings_restrict_access(db, logged_in_api_client, preferences):
 def test_admin_settings_correct_permission(
         db, logged_in_api_client, preferences):
     user = logged_in_api_client.user
-    user.add_permission('change_globalpreferencemodel')
+    user.permission_settings = True
+    user.save()
     url = reverse('api:v1:instance:admin-settings-list')
     response = logged_in_api_client.get(url)
 
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index e641b45d517068520101651a98fd6f1318a2954d..030fc3a73eeeabaf0507d5bdbc9949e3d5c4bff5 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -8,6 +8,14 @@ from funkwhale_api.music import views
 from funkwhale_api.federation import actors
 
 
+@pytest.mark.parametrize('view,permissions', [
+    (views.ImportBatchViewSet, ['library']),
+    (views.ImportJobViewSet, ['library']),
+])
+def test_permissions(assert_user_permission, view, permissions):
+    assert_user_permission(view, permissions)
+
+
 @pytest.mark.parametrize('param,expected', [
     ('true', 'full'),
     ('false', 'empty'),
diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py
index c7cd12e9e3ba19a457fa0d66d68e6b3679925c84..49199e0a781b990268431f109ac0c8804f8391ea 100644
--- a/api/tests/users/test_models.py
+++ b/api/tests/users/test_models.py
@@ -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
diff --git a/api/tests/users/test_permissions.py b/api/tests/users/test_permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..1564c761db59239eee5169bffd6ee92c2c148301
--- /dev/null
+++ b/api/tests/users/test_permissions.py
@@ -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
diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py
index fffc762fde7cb6f6533f3781bc73e828fa8e5d56..1bbf8b9a2d065735a017802bba369e817d016992 100644
--- a/api/tests/users/test_views.py
+++ b/api/tests/users/test_views.py
@@ -53,33 +53,24 @@ def test_can_disable_registration_view(preferences, client, db):
     assert response.status_code == 403
 
 
-def test_can_fetch_data_from_api(client, factories):
+def test_can_fetch_data_from_api(api_client, factories):
     url = reverse('api:v1:users:users-me')
-    response = client.get(url)
+    response = api_client.get(url)
     # login required
     assert response.status_code == 401
 
     user = factories['users.User'](
-        is_staff=True,
-        perms=[
-            'music.add_importbatch',
-            'dynamic_preferences.change_globalpreferencemodel',
-        ]
+        permission_library=True
     )
-    assert user.has_perm('music.add_importbatch')
-    client.login(username=user.username, password='test')
-    response = client.get(url)
+    api_client.login(username=user.username, password='test')
+    response = api_client.get(url)
     assert response.status_code == 200
-
-    payload = json.loads(response.content.decode('utf-8'))
-
-    assert payload['username'] == user.username
-    assert payload['is_staff'] == user.is_staff
-    assert payload['is_superuser'] == user.is_superuser
-    assert payload['email'] == user.email
-    assert payload['name'] == user.name
-    assert payload['permissions']['import.launch']['status']
-    assert payload['permissions']['settings.change']['status']
+    assert response.data['username'] == user.username
+    assert response.data['is_staff'] == user.is_staff
+    assert response.data['is_superuser'] == user.is_superuser
+    assert response.data['email'] == user.email
+    assert response.data['name'] == user.name
+    assert response.data['permissions'] == user.get_permissions()
 
 
 def test_can_get_token_via_api(client, factories):
@@ -202,6 +193,8 @@ def test_user_can_get_new_subsonic_token(logged_in_api_client):
     assert response.data == {
         'subsonic_api_token': 'test'
     }
+
+
 def test_user_can_request_new_subsonic_token(logged_in_api_client):
     user = logged_in_api_client.user
     user.subsonic_api_token = 'test'
diff --git a/changes/changelog.d/152.feature b/changes/changelog.d/152.feature
new file mode 100644
index 0000000000000000000000000000000000000000..a10225288fd67723d2af1e337dfe7ad399317e81
--- /dev/null
+++ b/changes/changelog.d/152.feature
@@ -0,0 +1,32 @@
+Simpler permission system (#152)
+
+
+Simpler permission system
+=========================
+
+Starting from this release, the permission system is much simpler. Up until now,
+we were using Django's built-in permission system, which was working, but also
+quite complex to deal with.
+
+The new implementation relies on simpler logic, which will make integration
+on the front-end in upcoming releases faster and easier.
+
+If you have manually given permissions to users on your instance,
+you can migrate those to the new system.
+
+On docker setups:
+
+.. code-block:: shell
+
+    docker-compose run --rm api python manage.py script django_permissions_to_user_permissions --no-input
+
+On non-docker setups:
+
+.. code-block:: shell
+
+    # in your virtualenv
+    python api/manage.py script django_permissions_to_user_permissions --no-input
+
+There is still no dedicated interface to manage user permissions, but you
+can use the admin interface at ``/api/admin/users/user/`` for that purpose in
+the meantime.
diff --git a/docs/configuration.rst b/docs/configuration.rst
index b7df2db42079304fe9956a5460ea6f9beb4e746d..46756bb266ccf918314f57b8ef8b01dcb68409ce 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -117,3 +117,28 @@ Then, the value of :ref:`setting-MUSIC_DIRECTORY_SERVE_PATH` should be
 On non-docker setup, you don't need to configure this setting.
 
 .. note:: This path should not include any trailing slash
+
+User permissions
+----------------
+
+Funkwhale's permission model works as follows:
+
+- Anonymous users cannot do anything unless configured specifically
+- Logged-in users can use the application, but cannot do things that affect
+  the whole instance
+- Superusers can do anything
+
+To make things more granular and allow some delegation of responsability,
+superusers can grant specific permissions to specific users. Available
+permissions are:
+
+- **Manage instance-level settings**: users with this permission can edit instance
+  settings as described in :ref:`instance-settings`
+- **Manage library**: users with this permission can import new music in the
+  instance
+- **Manage library federation**: users with this permission can ask to federate with
+  other instances, and accept/deny federation requests from other intances
+
+There is no dedicated interface to manage users permissions, but superusers
+can login on the Django's admin at ``/api/admin/`` and grant permissions
+to users at ``/api/admin/users/user/``.
diff --git a/front/src/components/About.vue b/front/src/components/About.vue
index b0ae67ef7ae554aa70a2ed6cc595b45fea81b32d..59c8411ac131fe65ec72fa6ed1e4769eed5b361a 100644
--- a/front/src/components/About.vue
+++ b/front/src/components/About.vue
@@ -15,7 +15,7 @@
       </p>
       <router-link
         class="ui button"
-        v-if="$store.state.auth.availablePermissions['settings.change']"
+        v-if="$store.state.auth.availablePermissions['settings']"
         :to="{path: '/manage/settings', hash: 'instance'}">
         <i class="pencil icon"></i>{{ $t('Edit instance info') }}
       </router-link>
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 9fbc5605c8e13923379e879b4b1a6b3322d14333..9f3134c2a1cd38a64a1761c505eac57fa6970666 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -60,7 +60,7 @@
           <div class="menu">
             <router-link
               class="item"
-              v-if="$store.state.auth.availablePermissions['import.launch']"
+              v-if="$store.state.auth.availablePermissions['library']"
               :to="{name: 'library.requests', query: {status: 'pending' }}">
               <i class="download icon"></i>{{ $t('Import requests') }}
               <div
@@ -70,7 +70,7 @@
             </router-link>
             <router-link
               class="item"
-              v-if="$store.state.auth.availablePermissions['federation.manage']"
+              v-if="$store.state.auth.availablePermissions['federation']"
               :to="{path: '/manage/federation/libraries'}">
               <i class="sitemap icon"></i>{{ $t('Federation') }}
               <div
@@ -80,7 +80,7 @@
             </router-link>
             <router-link
               class="item"
-              v-if="$store.state.auth.availablePermissions['settings.change']"
+              v-if="$store.state.auth.availablePermissions['settings']"
               :to="{path: '/manage/settings'}">
               <i class="settings icon"></i>{{ $t('Settings') }}
             </router-link>
@@ -192,8 +192,8 @@ export default {
     }),
     showAdmin () {
       let adminPermissions = [
-        this.$store.state.auth.availablePermissions['federation.manage'],
-        this.$store.state.auth.availablePermissions['import.launch']
+        this.$store.state.auth.availablePermissions['federation'],
+        this.$store.state.auth.availablePermissions['library']
       ]
       return adminPermissions.filter(e => {
         return e
@@ -209,7 +209,7 @@ export default {
       this.fetchFederationImportRequestsCount()
     },
     fetchFederationNotificationsCount () {
-      if (!this.$store.state.auth.availablePermissions['federation.manage']) {
+      if (!this.$store.state.auth.availablePermissions['federation']) {
         return
       }
       let self = this
@@ -218,7 +218,7 @@ export default {
       })
     },
     fetchFederationImportRequestsCount () {
-      if (!this.$store.state.auth.availablePermissions['import.launch']) {
+      if (!this.$store.state.auth.availablePermissions['library']) {
         return
       }
       let self = this
diff --git a/front/src/components/library/Library.vue b/front/src/components/library/Library.vue
index 820d60a5438545f1af49bd9bcf597b8c227534b9..e360ccb1c5c80985e5adc33f203ff316c610f504 100644
--- a/front/src/components/library/Library.vue
+++ b/front/src/components/library/Library.vue
@@ -13,10 +13,10 @@
           exact>
           <i18next path="Requests"/>
         </router-link>
-        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/launch" exact>
+        <router-link v-if="$store.state.auth.availablePermissions['library']" class="ui item" to="/library/import/launch" exact>
           <i18next path="Import"/>
         </router-link>
-        <router-link v-if="$store.state.auth.availablePermissions['import.launch']" class="ui item" to="/library/import/batches">
+        <router-link v-if="$store.state.auth.availablePermissions['library']" class="ui item" to="/library/import/batches">
           <i18next path="Import batches"/>
         </router-link>
       </div>
diff --git a/front/src/components/requests/Card.vue b/front/src/components/requests/Card.vue
index 9d0bf0b771bdaa398c882e84be4f07afb642c6ce..7743bb6d4403653fedbc355983ec0bb4e7adecf1 100644
--- a/front/src/components/requests/Card.vue
+++ b/front/src/components/requests/Card.vue
@@ -22,7 +22,7 @@
       </span>
       <button
         @click="createImport"
-        v-if="request.status === 'pending' && importAction && $store.state.auth.availablePermissions['import.launch']"
+        v-if="request.status === 'pending' && importAction && $store.state.auth.availablePermissions['library']"
         class="ui mini basic green right floated button">{{ $t('Create import') }}</button>
 
     </div>
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index 68a15090b5c289d2825563743f4cad7f2d3cdbf0..87af081d26fdfd09d2d02b16db6196f91d6cee95 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -112,7 +112,7 @@ export default {
         dispatch('playlists/fetchOwn', null, {root: true})
         Object.keys(data.permissions).forEach(function (key) {
           // this makes it easier to check for permissions in templates
-          commit('permission', {key, status: data.permissions[String(key)].status})
+          commit('permission', {key, status: data.permissions[String(key)]})
         })
         return response.data
       }, (response) => {
diff --git a/front/test/unit/specs/store/auth.spec.js b/front/test/unit/specs/store/auth.spec.js
index 3d175e9f9fa0e31b3b51b79314fe182e0516549c..46901cd16edd25ee9be4f7137a8835f1336174fb 100644
--- a/front/test/unit/specs/store/auth.spec.js
+++ b/front/test/unit/specs/store/auth.spec.js
@@ -164,9 +164,7 @@ describe('store/auth', () => {
       const profile = {
         username: 'bob',
         permissions: {
-          admin: {
-            status: true
-          }
+          admin: true
         }
       }
       moxios.stubRequest('users/users/me/', {