Repository: bookwyrm-social/bookwyrm

Issue:
User permission levels
```
                          admin   moderator   editor   user
edit instance settings     ✔️         -          -       -
change user level          ✔️         -          -       -
manage federation          ✔️         ✔️          -       -
issue invites              ✔️         ✔️          -       -
deactivate users           ✔️         ✔️          -       -
delete posts               ✔️         ✔️          -       -
edit books                 ✔️         ✔️          ✔️       -
upload covers              ✔️         ✔️          ✔️       ✔️
```



Discussion:
plan:
- define non-editable django groups for admin, moderator, editor, and user
- define non-editable permissions sets that correspond to types of activities in the app
- assign default permissions to groups (as defined in the table above), and allow an admin to swap permissions into various groups
- store a user's group in the user table, with user as default
- control access to entire views with the `@permisson_required` decorator
- control access to parts of the UI with a template variable check

Resolving patch:
diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py
--- a/bookwyrm/view_actions.py
+++ b/bookwyrm/view_actions.py
@@ -3,7 +3,7 @@
 from PIL import Image
 
 from django.contrib.auth import authenticate, login, logout
-from django.contrib.auth.decorators import login_required
+from django.contrib.auth.decorators import login_required, permission_required
 from django.core.files.base import ContentFile
 from django.http import HttpResponseBadRequest, HttpResponseNotFound
 from django.shortcuts import redirect
@@ -141,6 +141,7 @@ def resolve_book(request):
 
 
 @login_required
+@permission_required('bookwyrm.edit_book', raise_exception=True)
 def edit_book(request, book_id):
     ''' edit a book cool '''
     if not request.method == 'POST':
@@ -433,7 +434,9 @@ def import_data(request):
         return redirect('/import_status/%d' % (job.id,))
     return HttpResponseBadRequest()
 
+
 @login_required
+@permission_required('bookwyrm.create_invites', raise_exception=True)
 def create_invite(request):
     ''' creates a user invite database entry '''
     form = forms.CreateInviteForm(request.POST)
diff --git a/bookwyrm/views.py b/bookwyrm/views.py
--- a/bookwyrm/views.py
+++ b/bookwyrm/views.py
@@ -1,7 +1,7 @@
 ''' views for pages you can go to in the application '''
 import re
 
-from django.contrib.auth.decorators import login_required
+from django.contrib.auth.decorators import login_required, permission_required
 from django.db.models import Avg, Count, Q
 from django.http import HttpResponseBadRequest, HttpResponseNotFound,\
         JsonResponse
@@ -228,6 +228,7 @@ def invite_page(request, code):
     return TemplateResponse(request, 'invite.html', data)
 
 @login_required
+@permission_required('bookwyrm.create_invites', raise_exception=True)
 def manage_invites(request):
     ''' invite management page '''
     data = {
@@ -453,6 +454,7 @@ def book_page(request, book_id):
 
 
 @login_required
+@permission_required('bookwyrm.edit_book', raise_exception=True)
 def edit_book_page(request, book_id):
     ''' info about a book '''
     book = books_manager.get_edition(book_id)
diff --git a/init_db.py b/init_db.py
--- a/init_db.py
+++ b/init_db.py
@@ -1,24 +1,60 @@
 ''' starter data '''
+from django.contrib.auth.models import Group, Permission
+from django.contrib.contenttypes.models import ContentType
+
 from bookwyrm.models import Connector, User
 from bookwyrm.settings import DOMAIN
 
-User.objects.create_user('mouse', 'mouse.reeve@gmail.com', 'password123')
-User.objects.create_user(
-    'rat', 'rat@rat.com', 'ratword',
-    manually_approves_followers=True
-)
 
-User.objects.get(id=1).followers.add(User.objects.get(id=2))
+groups = ['admin', 'moderator', 'editor']
+for group in groups:
+    Group.objects.create(name=group)
+
+permissions = [{
+        'codename': 'edit_instance_settings',
+        'name': 'change the instance info',
+        'groups': ['admin',]
+    }, {
+        'codename': 'set_user_group',
+        'name': 'change what group a user is in',
+        'groups': ['admin', 'moderator']
+    }, {
+        'codename': 'control_federation',
+        'name': 'control who to federate with',
+        'groups': ['admin', 'moderator']
+    }, {
+        'codename': 'create_invites',
+        'name': 'issue invitations to join',
+        'groups': ['admin', 'moderator']
+    }, {
+        'codename': 'moderate_user',
+        'name': 'deactivate or silence a user',
+        'groups': ['admin', 'moderator']
+    }, {
+        'codename': 'moderate_post',
+        'name': 'delete other users\' posts',
+        'groups': ['admin', 'moderator']
+    }, {
+        'codename': 'edit_book',
+        'name': 'edit book info',
+        'groups': ['admin', 'moderator', 'editor']
+    }]
+
+content_type = ContentType.objects.get_for_model(User)
+for permission in permissions:
+    permission_obj = Permission.objects.create(
+        codename=permission['codename'],
+        name=permission['name'],
+        content_type=content_type,
+    )
+    # add the permission to the appropriate groups
+    for group_name in permission['groups']:
+        Group.objects.get(name=group_name).permissions.add(permission_obj)
+
+# while the groups and permissions shouldn't be changed because the code
+# depends on them, what permissions go with what groups should be editable
+
 
-Connector.objects.create(
-    identifier='openlibrary.org',
-    name='OpenLibrary',
-    connector_file='openlibrary',
-    base_url='https://openlibrary.org',
-    books_url='https://openlibrary.org',
-    covers_url='https://covers.openlibrary.org',
-    search_url='https://openlibrary.org/search?q=',
-)
 
 Connector.objects.create(
     identifier=DOMAIN,
@@ -31,3 +67,13 @@
     search_url='https://%s/search?q=' % DOMAIN,
     priority=1,
 )
+
+Connector.objects.create(
+    identifier='openlibrary.org',
+    name='OpenLibrary',
+    connector_file='openlibrary',
+    base_url='https://openlibrary.org',
+    books_url='https://openlibrary.org',
+    covers_url='https://covers.openlibrary.org',
+    search_url='https://openlibrary.org/search?q=',
+)


Test patch: