diff --git a/course/admin.py b/course/admin.py
index a025637283e76a3971ddc64505d3ff0d9dab9764..6ba71b40ec51988424adff6958015967354acf07 100644
--- a/course/admin.py
+++ b/course/admin.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-
+from __future__ import absolute_import
 __copyright__ = "Copyright (C) 2014 Andreas Kloeckner"
 
 __license__ = """
@@ -47,6 +47,8 @@ from course.constants import (
         exam_ticket_states
         )
 
+from rules.contrib.admin import ObjectPermissionsModelAdmin
+
 if False:
     from typing import Any  # noqa
 
@@ -101,7 +103,7 @@ class CourseAdminForm(forms.ModelForm):
         exclude = ()
 
 
-class CourseAdmin(admin.ModelAdmin):
+class CourseAdmin(ObjectPermissionsModelAdmin):
     list_display = (
             "identifier",
             "number",
@@ -150,6 +152,7 @@ class CourseAdmin(admin.ModelAdmin):
         return _filter_courses_for_user(qs, request.user)
 
     # }}}
+    pass
 
 
 admin.site.register(Course, CourseAdmin)
@@ -159,7 +162,7 @@ admin.site.register(Course, CourseAdmin)
 
 # {{{ events
 
-class EventAdmin(admin.ModelAdmin):
+class EventAdmin(ObjectPermissionsModelAdmin):
     list_display = (
             "course",
             "kind",
@@ -182,7 +185,7 @@ class EventAdmin(admin.ModelAdmin):
     if six.PY3:
         __str__ = __unicode__
 
-    list_editable = ("ordinal", "time", "end_time", "shown_in_calendar")
+#    list_editable = ("ordinal", "time", "end_time", "shown_in_calendar")
 
     # {{{ permissions
 
@@ -199,6 +202,23 @@ class EventAdmin(admin.ModelAdmin):
 
     # }}}
 
+    # Overrides for row-level permission {{{
+
+    def get_form(self, request, *args, **kwargs):
+        form = super(EventAdmin, self).get_form(request, *args, **kwargs)
+        form.base_fields['creator'].initial = request.user
+        form.base_fields['creator'].widget = forms.HiddenInput()
+        return form
+
+    def save_model(self, request, obj, form, change):
+        obj.creator = request.user
+        super(EventAdmin, self).save_model(request, obj, form, change)
+        pass
+
+    pass
+
+    # }}}
+
 
 admin.site.register(Event, EventAdmin)
 
diff --git a/course/migrations/0102_add_event_creator.py b/course/migrations/0102_add_event_creator.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c1464af6ce666966752cf843222ed295890801b
--- /dev/null
+++ b/course/migrations/0102_add_event_creator.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-05-30 02:06
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('course', '0101_add_ticket_facility_and_validity'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='event',
+            name='creator',
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterField(
+            model_name='participationpermission',
+            name='permission',
+            field=models.CharField(choices=[('edit_course', 'Edit course'), ('use_admin_interface', 'Use admin interface'), ('impersonate_role', 'Impersonate role'), ('set_fake_time', 'Set fake time'), ('set_pretend_facility', 'Pretend to be in facility'), ('edit_course_permissions', 'Edit course permissions'), ('view_hidden_course_page', 'View hidden course page'), ('view_calendar', 'View calendar'), ('send_instant_message', 'Send instant message'), ('access_files_for', 'Access files for'), ('included_in_grade_statistics', 'Included in grade statistics'), ('edit_exam', 'Edit exam'), ('issue_exam_ticket', 'Issue exam ticket'), ('batch_issue_exam_ticket', 'Batch issue exam ticket'), ('view_participant_masked_profile', "View participants' masked profile only"), ('view_flow_sessions_from_role', 'View flow sessions from role'), ('view_gradebook', 'View gradebook'), ('edit_grading_opportunity', 'Edit grading opportunity'), ('assign_grade', 'Assign grade'), ('view_grader_stats', 'View grader stats'), ('batch_import_grade', 'Batch-import grades'), ('batch_export_grade', 'Batch-export grades'), ('batch_download_submission', 'Batch-download submissions'), ('impose_flow_session_deadline', 'Impose flow session deadline'), ('batch_impose_flow_session_deadline', 'Batch-impose flow session deadline'), ('end_flow_session', 'End flow session'), ('batch_end_flow_session', 'Batch-end flow sessions'), ('regrade_flow_session', 'Regrade flow session'), ('batch_regrade_flow_session', 'Batch-regrade flow sessions'), ('recalculate_flow_session_grade', 'Recalculate flow session grade'), ('batch_recalculate_flow_session_grade', 'Batch-recalculate flow sesssion grades'), ('reopen_flow_session', 'Reopen flow session'), ('grant_exception', 'Grant exception'), ('view_analytics', 'View analytics'), ('preview_content', 'Preview content'), ('update_content', 'Update content'), ('use_markup_sandbox', 'Use markup sandbox'), ('use_page_sandbox', 'Use page sandbox'), ('test_flow', 'Test flow'), ('edit_events', 'Edit events'), ('query_participation', 'Query participation'), ('edit_participation', 'Edit participation'), ('preapprove_participation', 'Preapprove participation'), ('manage_instant_flow_requests', 'Manage instant flow requests')], db_index=True, max_length=200, verbose_name='Permission'),
+        ),
+        migrations.AlterField(
+            model_name='participationrolepermission',
+            name='permission',
+            field=models.CharField(choices=[('edit_course', 'Edit course'), ('use_admin_interface', 'Use admin interface'), ('impersonate_role', 'Impersonate role'), ('set_fake_time', 'Set fake time'), ('set_pretend_facility', 'Pretend to be in facility'), ('edit_course_permissions', 'Edit course permissions'), ('view_hidden_course_page', 'View hidden course page'), ('view_calendar', 'View calendar'), ('send_instant_message', 'Send instant message'), ('access_files_for', 'Access files for'), ('included_in_grade_statistics', 'Included in grade statistics'), ('edit_exam', 'Edit exam'), ('issue_exam_ticket', 'Issue exam ticket'), ('batch_issue_exam_ticket', 'Batch issue exam ticket'), ('view_participant_masked_profile', "View participants' masked profile only"), ('view_flow_sessions_from_role', 'View flow sessions from role'), ('view_gradebook', 'View gradebook'), ('edit_grading_opportunity', 'Edit grading opportunity'), ('assign_grade', 'Assign grade'), ('view_grader_stats', 'View grader stats'), ('batch_import_grade', 'Batch-import grades'), ('batch_export_grade', 'Batch-export grades'), ('batch_download_submission', 'Batch-download submissions'), ('impose_flow_session_deadline', 'Impose flow session deadline'), ('batch_impose_flow_session_deadline', 'Batch-impose flow session deadline'), ('end_flow_session', 'End flow session'), ('batch_end_flow_session', 'Batch-end flow sessions'), ('regrade_flow_session', 'Regrade flow session'), ('batch_regrade_flow_session', 'Batch-regrade flow sessions'), ('recalculate_flow_session_grade', 'Recalculate flow session grade'), ('batch_recalculate_flow_session_grade', 'Batch-recalculate flow sesssion grades'), ('reopen_flow_session', 'Reopen flow session'), ('grant_exception', 'Grant exception'), ('view_analytics', 'View analytics'), ('preview_content', 'Preview content'), ('update_content', 'Update content'), ('use_markup_sandbox', 'Use markup sandbox'), ('use_page_sandbox', 'Use page sandbox'), ('test_flow', 'Test flow'), ('edit_events', 'Edit events'), ('query_participation', 'Query participation'), ('edit_participation', 'Edit participation'), ('preapprove_participation', 'Preapprove participation'), ('manage_instant_flow_requests', 'Manage instant flow requests')], db_index=True, max_length=200, verbose_name='Permission'),
+        ),
+    ]
diff --git a/course/models.py b/course/models.py
index 6b4c0f32d21fcb0d26d53792fb26ffae2dbc87fc..e6b67a5367a4682311ae4cecc574b8b31e0a7c1c 100644
--- a/course/models.py
+++ b/course/models.py
@@ -40,6 +40,8 @@ from django.dispatch import receiver
 
 from django.conf import settings
 
+from django import forms
+
 from course.constants import (  # noqa
         user_status, USER_STATUS_CHOICES,
         participation_status, PARTICIPATION_STATUS_CHOICES,
@@ -277,6 +279,17 @@ class Event(models.Model):
     shown_in_calendar = models.BooleanField(default=True,
             verbose_name=_('Shown in calendar'))
 
+    # Extra attribute for row-level permission
+
+    creator = models.ForeignKey(settings.AUTH_USER_MODEL, default=1)
+
+    def clean(self):
+        event_edit_staff = [i.user for i in Participation.objects
+                             .filter(course=self.course)
+                            if i.has_permission('edit_events', None)]
+        if (self.creator not in event_edit_staff):
+            raise forms.ValidationError("You cannot add event to this course.")
+
     class Meta:
         verbose_name = _("Event")
         verbose_name_plural = _("Events")
diff --git a/course/rules.py b/course/rules.py
new file mode 100644
index 0000000000000000000000000000000000000000..924cc6c7ad9b0e4039376437d94f890ee9e1712b
--- /dev/null
+++ b/course/rules.py
@@ -0,0 +1,42 @@
+from __future__ import absolute_import
+
+import rules
+from course.models import Participation
+
+# Predicates
+
+
+@rules.predicate
+def has_edit_course_perm(user, course):
+    if not course:
+        return False
+    course_edit_staff = [i.user
+                         for i in Participation.objects.filter(course=course)
+                         if i.has_permission('edit_course', None)]
+    return user in course_edit_staff
+
+
+@rules.predicate
+def has_edit_event_perm(user, event):
+    if not event:
+        return False
+    course = event.course
+    event_edit_staff = [i.user
+                        for i in Participation.objects.filter(course=course)
+                        if i.has_permission('edit_events', None)]
+    return user in event_edit_staff
+# Rules
+
+
+rules.add_rule('change_course', has_edit_course_perm)
+rules.add_rule('delete_course', has_edit_course_perm)
+rules.add_rule('change_event', has_edit_event_perm)
+rules.add_rule('delete_event', has_edit_event_perm)
+# Permissions
+
+rules.add_perm('course.change_course', has_edit_course_perm)
+rules.add_perm('course.delete_course', has_edit_course_perm)
+rules.add_perm('course', rules.always_allow)
+
+rules.add_perm('course.delete_event', has_edit_event_perm)
+rules.add_perm('course.change_event', has_edit_event_perm)
diff --git a/course/views.py b/course/views.py
index 6f1f14cd182bff5af861af60d582f4e66f2734ef..788f1a428a7631b28465ccb91c27280a03d35f7e 100644
--- a/course/views.py
+++ b/course/views.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-from __future__ import division
+from __future__ import division, absolute_import
 
 __copyright__ = "Copyright (C) 2014 Andreas Kloeckner"
 
@@ -1357,10 +1357,10 @@ class EditCourseForm(StyledModelForm):
 
 @course_view
 def edit_course(pctx):
-    if not pctx.has_permission(pperm.edit_course):
-        raise PermissionDenied()
-
     request = pctx.request
+    if (not pctx.has_permission(pperm.edit_course) or
+            not request.user.has_perm('course.change_course', pctx.course)):
+        raise PermissionDenied()
 
     if request.method == 'POST':
         form = EditCourseForm(request.POST, instance=pctx.course)
diff --git a/relate/settings.py b/relate/settings.py
index 207cc6674b16c7b9ff5b247e1ebfdda80e775b53..44f876f83f2e75af6865ec011dc5440cceb88021 100644
--- a/relate/settings.py
+++ b/relate/settings.py
@@ -54,6 +54,7 @@ INSTALLED_APPS = (
 
     "accounts",
     "course",
+    'rules.apps.AutodiscoverRulesConfig'
 )
 
 if local_settings["RELATE_SIGN_IN_BY_SAML2_ENABLED"]:
@@ -87,8 +88,10 @@ AUTHENTICATION_BACKENDS = (
     "course.auth.TokenBackend",
     "course.exam.ExamTicketBackend",
     "django.contrib.auth.backends.ModelBackend",
+    'rules.permissions.ObjectPermissionBackend'
     )
 
+
 if local_settings["RELATE_SIGN_IN_BY_SAML2_ENABLED"]:
     AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + (  # type: ignore
             'course.auth.Saml2Backend',
diff --git a/requirements.txt b/requirements.txt
index b79796bb87dce3975286f8be2ac54be8e45da7ef..51f72729a9862015a331a1fadddde1bdfda908c2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -40,7 +40,7 @@ paramiko
 git+https://github.com/inducer/django-bootstrap3-datetimepicker.git
 
 # For in-class instant messaging
-# dnspython  # Py2
+#dnspython  # Py2
 dnspython3  # Py3
 # Py2 broken was broken in 1.3.1
 git+https://github.com/fritzy/SleekXMPP.git@6e27f28c854ce4ae1d9f0cc8ee407bda8de97d3b
@@ -110,3 +110,6 @@ pytools
 typing
 
 # vim: foldmethod=marker
+
+# row-level permission
+rules