add support for fine-grained field permissions for user model
authorScott Baker <smbaker@gmail.com>
Fri, 3 Oct 2014 07:32:37 +0000 (00:32 -0700)
committerScott Baker <smbaker@gmail.com>
Fri, 3 Oct 2014 07:32:37 +0000 (00:32 -0700)
planetstack/core/admin.py
planetstack/core/models/__init__.py
planetstack/core/models/plcorebase.py
planetstack/core/models/user.py

index 94e8453..f4d6f8f 100644 (file)
@@ -8,7 +8,7 @@ from django import forms
 from django.utils.safestring import mark_safe
 from django.contrib.auth.admin import UserAdmin
 from django.contrib.admin.widgets import FilteredSelectMultiple
 from django.utils.safestring import mark_safe
 from django.contrib.auth.admin import UserAdmin
 from django.contrib.admin.widgets import FilteredSelectMultiple
-from django.contrib.auth.forms import ReadOnlyPasswordHashField
+from django.contrib.auth.forms import ReadOnlyPasswordHashField, AdminPasswordChangeForm
 from django.contrib.auth.signals import user_logged_in
 from django.utils import timezone
 from django.contrib.contenttypes import generic
 from django.contrib.auth.signals import user_logged_in
 from django.utils import timezone
 from django.contrib.contenttypes import generic
@@ -16,6 +16,14 @@ from suit.widgets import LinkedSelect
 from django.core.exceptions import PermissionDenied
 from django.core.urlresolvers import reverse, NoReverseMatch
 
 from django.core.exceptions import PermissionDenied
 from django.core.urlresolvers import reverse, NoReverseMatch
 
+# this block of stuff is needed for UserAdmin
+from django.db import transaction
+from django.utils.decorators import method_decorator
+from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.debug import sensitive_post_parameters
+csrf_protect_m = method_decorator(csrf_protect)
+sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
+
 import django_evolution
 
 def backend_icon(obj): # backend_status, enacted, updated):
 import django_evolution
 
 def backend_icon(obj): # backend_status, enacted, updated):
@@ -43,7 +51,8 @@ class PlainTextWidget(forms.HiddenInput):
             value = ''
         return mark_safe(str(value) + super(PlainTextWidget, self).render(name, value, attrs))
 
             value = ''
         return mark_safe(str(value) + super(PlainTextWidget, self).render(name, value, attrs))
 
-class ReadOnlyAwareAdmin(admin.ModelAdmin):
+class PermissionCheckingAdmin(admin.ModelAdmin):
+    # call save_by_user and delete_by_user instead of save and delete
 
     def has_add_permission(self, request, obj=None):
         return (not self.__user_is_readonly(request))
 
     def has_add_permission(self, request, obj=None):
         return (not self.__user_is_readonly(request))
@@ -53,13 +62,42 @@ class ReadOnlyAwareAdmin(admin.ModelAdmin):
 
     def save_model(self, request, obj, form, change):
         if self.__user_is_readonly(request):
 
     def save_model(self, request, obj, form, change):
         if self.__user_is_readonly(request):
+            # this 'if' might be redundant if save_by_user is implemented right
             raise PermissionDenied
             raise PermissionDenied
-            #pass
-        else:
-            return super(ReadOnlyAwareAdmin, self).save_model(request, obj, form, change)
+
+        obj.caller = request.user
+        # update openstack connection to use this site/tenant
+        obj.save_by_user(request.user)
+
+    def delete_model(self, request, obj):
+        obj.delete_by_user(request.user)
+
+    def save_formset(self, request, form, formset, change):
+        instances = formset.save(commit=False)
+        for instance in instances:
+            instance.save_by_user(request.user)
+
+        # BUG in django 1.7? Objects are not deleted by formset.save if
+        # commit is False. So let's delete them ourselves.
+        #
+        # code from forms/models.py save_existing_objects()
+        try:
+            forms_to_delete = formset.deleted_forms\r
+        except AttributeError:\r
+            forms_to_delete = []
+        if formset.initial_forms:
+            for form in formset.initial_forms:
+                obj = form.instance
+                if form in forms_to_delete:
+                    if obj.pk is None:
+                        continue
+                    formset.deleted_objects.append(obj)
+                    obj.delete()
+
+        formset.save_m2m()
 
     def get_actions(self,request):
 
     def get_actions(self,request):
-        actions = super(ReadOnlyAwareAdmin,self).get_actions(request)
+        actions = super(PermissionCheckingAdmin,self).get_actions(request)
 
         if self.__user_is_readonly(request):
             if 'delete_selected' in actions:
 
         if self.__user_is_readonly(request):
             if 'delete_selected' in actions:
@@ -85,13 +123,13 @@ class ReadOnlyAwareAdmin(admin.ModelAdmin):
                 self.inlines = self.inlines_save
 
         try:
                 self.inlines = self.inlines_save
 
         try:
-            return super(ReadOnlyAwareAdmin, self).change_view(request, object_id, extra_context=extra_context)
+            return super(PermissionCheckingAdmin, self).change_view(request, object_id, extra_context=extra_context)
         except PermissionDenied:
             pass
         if request.method == 'POST':
             raise PermissionDenied
         request.readonly = True
         except PermissionDenied:
             pass
         if request.method == 'POST':
             raise PermissionDenied
         request.readonly = True
-        return super(ReadOnlyAwareAdmin, self).change_view(request, object_id, extra_context=extra_context)
+        return super(PermissionCheckingAdmin, self).change_view(request, object_id, extra_context=extra_context)
 
     def __user_is_readonly(self, request):
         return request.user.isReadOnlyUser()
 
     def __user_is_readonly(self, request):
         return request.user.isReadOnlyUser()
@@ -103,6 +141,11 @@ class ReadOnlyAwareAdmin(admin.ModelAdmin):
         return mark_safe(backend_icon(obj))
     backend_status_icon.short_description = ""
 
         return mark_safe(backend_icon(obj))
     backend_status_icon.short_description = ""
 
+class ReadOnlyAwareAdmin(PermissionCheckingAdmin):
+    pass
+
+class PlanetStackBaseAdmin(ReadOnlyAwareAdmin):
+    save_on_top = False
 
 class SingletonAdmin (ReadOnlyAwareAdmin):
     def has_add_permission(self, request):
 
 class SingletonAdmin (ReadOnlyAwareAdmin):
     def has_add_permission(self, request):
@@ -115,7 +158,6 @@ class SingletonAdmin (ReadOnlyAwareAdmin):
         else:
             return True
 
         else:
             return True
 
-
 class PlStackTabularInline(admin.TabularInline):
     def __init__(self, *args, **kwargs):
         super(PlStackTabularInline, self).__init__(*args, **kwargs)
 class PlStackTabularInline(admin.TabularInline):
     def __init__(self, *args, **kwargs):
         super(PlStackTabularInline, self).__init__(*args, **kwargs)
@@ -404,41 +446,6 @@ class ImageDeploymentsInline(PlStackTabularInline):
     fields = ['backend_status_icon', 'image', 'deployment', 'glance_image_id']
     readonly_fields = ['backend_status_icon', 'glance_image_id']
 
     fields = ['backend_status_icon', 'image', 'deployment', 'glance_image_id']
     readonly_fields = ['backend_status_icon', 'glance_image_id']
 
-class PlanetStackBaseAdmin(ReadOnlyAwareAdmin):
-    save_on_top = False
-
-    def save_model(self, request, obj, form, change):
-        obj.caller = request.user
-        # update openstack connection to use this site/tenant
-        obj.save_by_user(request.user)
-
-    def delete_model(self, request, obj):
-        obj.delete_by_user(request.user)
-
-    def save_formset(self, request, form, formset, change):
-        instances = formset.save(commit=False)
-        for instance in instances:
-            instance.save_by_user(request.user)
-
-        # BUG in django 1.7? Objects are not deleted by formset.save if
-        # commit is False. So let's delete them ourselves.
-        #
-        # code from forms/models.py save_existing_objects()
-        try:
-            forms_to_delete = formset.deleted_forms\r
-        except AttributeError:\r
-            forms_to_delete = []
-        if formset.initial_forms:
-            for form in formset.initial_forms:
-                obj = form.instance
-                if form in forms_to_delete:
-                    if obj.pk is None:
-                        continue
-                    formset.deleted_objects.append(obj)
-                    obj.delete()
-
-        formset.save_m2m()
-
 class SliceRoleAdmin(PlanetStackBaseAdmin):
     model = SliceRole
     pass
 class SliceRoleAdmin(PlanetStackBaseAdmin):
     model = SliceRole
     pass
@@ -1004,13 +1011,17 @@ class UserDashboardViewInline(PlStackTabularInline):
     suit_classes = 'suit-tab suit-tab-dashboards'
     fields = ['user', 'dashboardView', 'order']
 
     suit_classes = 'suit-tab suit-tab-dashboards'
     fields = ['user', 'dashboardView', 'order']
 
-class UserAdmin(UserAdmin):
+class UserAdmin(PlanetStackBaseAdmin):
     class Meta:
         app_label = "core"
 
     class Meta:
         app_label = "core"
 
+    add_form_template = 'admin/auth/user/add_form.html'
+    change_user_password_template = None
+
     # The forms to add and change user instances
     form = UserChangeForm
     add_form = UserCreationForm
     # The forms to add and change user instances
     form = UserChangeForm
     add_form = UserCreationForm
+    change_password_form = AdminPasswordChangeForm
 
     # The fields to be used in displaying the User model.
     # These override the definitions on the base UserAdmin
 
     # The fields to be used in displaying the User model.
     # These override the definitions on the base UserAdmin
@@ -1019,7 +1030,7 @@ class UserAdmin(UserAdmin):
     list_filter = ('site',)
     inlines = [SlicePrivilegeInline,SitePrivilegeInline,DeploymentPrivilegeInline,UserDashboardViewInline]
 
     list_filter = ('site',)
     inlines = [SlicePrivilegeInline,SitePrivilegeInline,DeploymentPrivilegeInline,UserDashboardViewInline]
 
-    fieldListLoginDetails = ['email','site','password','is_active','is_readonly','is_admin','public_key']
+    fieldListLoginDetails = ['backend_status_text', 'email','site','password','is_active','is_readonly','is_admin','public_key']
     fieldListContactInfo = ['firstname','lastname','phone','timezone']
 
     fieldsets = (
     fieldListContactInfo = ['firstname','lastname','phone','timezone']
 
     fieldsets = (
@@ -1054,61 +1065,135 @@ class UserAdmin(UserAdmin):
 
         return super(UserAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
 
 
         return super(UserAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
 
-    def has_add_permission(self, request, obj=None):
-        return (not self.__user_is_readonly(request))
-
-    def has_delete_permission(self, request, obj=None):
-        return (not self.__user_is_readonly(request))
-
-    def get_actions(self,request):
-        actions = super(UserAdmin,self).get_actions(request)
-
-        if self.__user_is_readonly(request):
-            if 'delete_selected' in actions:
-                del actions['delete_selected']
-
-        return actions
-
-    def change_view(self,request,object_id, extra_context=None):
-
-        if self.__user_is_readonly(request):
-            if not hasattr(self, "readonly_save"):
-                # save the original readonly fields\r
-                self.readonly_save = self.readonly_fields\r
-                self.inlines_save = self.inlines
-            if hasattr(self, "user_readonly_fields"):
-                self.readonly_fields=self.user_readonly_fields
-            if hasattr(self, "user_readonly_inlines"):
-                self.inlines = self.user_readonly_inlines
-        else:
-            if hasattr(self, "readonly_save"):\r
-                # restore the original readonly fields\r
-                self.readonly_fields = self.readonly_save\r
-                self.inlines = self.inlines_save
-
-        try:
-            return super(UserAdmin, self).change_view(request, object_id, extra_context=extra_context)
-        except PermissionDenied:
-            pass
-        if request.method == 'POST':
-            raise PermissionDenied
-        request.readonly = True
-        return super(UserAdmin, self).change_view(request, object_id, extra_context=extra_context)
-
-    def __user_is_readonly(self, request):
-        #groups = [x.name for x in request.user.groups.all() ]
-        #return "readonly" in groups
-        return request.user.isReadOnlyUser()
-
     def queryset(self, request):
         return User.select_by_user(request.user)
 
     def queryset(self, request):
         return User.select_by_user(request.user)
 
-    def backend_status_text(self, obj):
-        return mark_safe(backend_text(obj))
+    # ------------------------------------------------------------------------
+    # stuff copied from ModelAdmin.UserAdmin
+    # ------------------------------------------------------------------------
+    def get_fieldsets(self, request, obj=None):
+        if not obj:\r
+            return self.add_fieldsets\r
+        return super(UserAdmin, self).get_fieldsets(request, obj)
+
+    def get_form(self, request, obj=None, **kwargs):
+        """\r
+        Use special form during user creation\r
+        """\r
+        defaults = {}\r
+        if obj is None:\r
+            defaults['form'] = self.add_form\r
+        defaults.update(kwargs)\r
+        return super(UserAdmin, self).get_form(request, obj, **defaults)\r
+\r
+    def get_urls(self):\r
+        from django.conf.urls import patterns\r
+        return patterns('',\r
+            (r'^(\d+)/password/$',\r
+             self.admin_site.admin_view(self.user_change_password))\r
+        ) + super(UserAdmin, self).get_urls()\r
+\r
+    def lookup_allowed(self, lookup, value):\r
+        # See #20078: we don't want to allow any lookups involving passwords.\r
+        if lookup.startswith('password'):\r
+            return False\r
+        return super(UserAdmin, self).lookup_allowed(lookup, value)\r
+\r
+    @sensitive_post_parameters_m\r
+    @csrf_protect_m\r
+    @transaction.atomic\r
+    def add_view(self, request, form_url='', extra_context=None):\r
+        # It's an error for a user to have add permission but NOT change\r
+        # permission for users. If we allowed such users to add users, they\r
+        # could create superusers, which would mean they would essentially have\r
+        # the permission to change users. To avoid the problem entirely, we\r
+        # disallow users from adding users if they don't have change\r
+        # permission.\r
+        if not self.has_change_permission(request):\r
+            if self.has_add_permission(request) and settings.DEBUG:\r
+                # Raise Http404 in debug mode so that the user gets a helpful\r
+                # error message.\r
+                raise Http404(\r
+                    'Your user does not have the "Change user" permission. In '\r
+                    'order to add users, Django requires that your user '\r
+                    'account have both the "Add user" and "Change user" '\r
+                    'permissions set.')\r
+            raise PermissionDenied\r
+        if extra_context is None:\r
+            extra_context = {}\r
+        username_field = self.model._meta.get_field(self.model.USERNAME_FIELD)\r
+        defaults = {\r
+            'auto_populated_fields': (),\r
+            'username_help_text': username_field.help_text,\r
+        }\r
+        extra_context.update(defaults)\r
+        return super(UserAdmin, self).add_view(request, form_url,\r
+                                               extra_context)\r
+\r
+    @sensitive_post_parameters_m\r
+    def user_change_password(self, request, id, form_url=''):\r
+        if not self.has_change_permission(request):\r
+            raise PermissionDenied\r
+        user = get_object_or_404(self.get_queryset(request), pk=id)\r
+        if request.method == 'POST':\r
+            form = self.change_password_form(user, request.POST)\r
+            if form.is_valid():\r
+                form.save()\r
+                change_message = self.construct_change_message(request, form, None)\r
+                self.log_change(request, user, change_message)\r
+                msg = ugettext('Password changed successfully.')\r
+                messages.success(request, msg)\r
+                update_session_auth_hash(request, form.user)\r
+                return HttpResponseRedirect('..')\r
+        else:\r
+            form = self.change_password_form(user)\r
+\r
+        fieldsets = [(None, {'fields': list(form.base_fields)})]\r
+        adminForm = admin.helpers.AdminForm(form, fieldsets, {})\r
+\r
+        context = {\r
+            'title': _('Change password: %s') % escape(user.get_username()),\r
+            'adminForm': adminForm,\r
+            'form_url': form_url,\r
+            'form': form,\r
+            'is_popup': (IS_POPUP_VAR in request.POST or\r
+                         IS_POPUP_VAR in request.GET),\r
+            'add': True,\r
+            'change': False,\r
+            'has_delete_permission': False,\r
+            'has_change_permission': True,\r
+            'has_absolute_url': False,\r
+            'opts': self.model._meta,\r
+            'original': user,\r
+            'save_as': False,\r
+            'show_save': True,\r
+        }\r
+        context.update(admin.site.each_context())\r
+        return TemplateResponse(request,\r
+            self.change_user_password_template or\r
+            'admin/auth/user/change_password.html',\r
+            context, current_app=self.admin_site.name)\r
+\r
+    def response_add(self, request, obj, post_url_continue=None):\r
+        """\r
+        Determines the HttpResponse for the add_view stage. It mostly defers to\r
+        its superclass implementation but is customized because the User model\r
+        has a slightly different workflow.\r
+        """\r
+        # We should allow further modification of the user just added i.e. the\r
+        # 'Save' button should behave like the 'Save and continue editing'\r
+        # button except in two scenarios:\r
+        # * The user has pressed the 'Save and add another' button\r
+        # * We are adding a user in a popup\r
+        if '_addanother' not in request.POST and IS_POPUP_VAR not in request.POST:\r
+            request.POST['_continue'] = 1\r
+        return super(UserAdmin, self).response_add(request, obj,\r
+                                                   post_url_continue)
+
+    # ------------------------------------------------------------------------
+    # end stuff copied from ModelAdmin.UserAdmin
+    # ------------------------------------------------------------------------
 
 
-    def backend_status_icon(self, obj):
-        return mark_safe(backend_icon(obj))
-    backend_status_icon.short_description = ""
 
 class DashboardViewAdmin(PlanetStackBaseAdmin):
     fieldsets = [('Dashboard View Details',
 
 class DashboardViewAdmin(PlanetStackBaseAdmin):
     fieldsets = [('Dashboard View Details',
index f3991dd..2070e16 100644 (file)
@@ -1,4 +1,4 @@
-from .plcorebase import PlCoreBase,PlCoreBaseManager,PlCoreBaseDeletionManager
+from .plcorebase import PlCoreBase,PlCoreBaseManager,PlCoreBaseDeletionManager,DiffModelMixIn
 from .project import Project
 from .singletonmodel import SingletonModel
 from .service import Service
 from .project import Project
 from .singletonmodel import SingletonModel
 from .service import Service
index b9692c6..51049a4 100644 (file)
@@ -48,7 +48,43 @@ class PlCoreBaseManager(models.Manager):
     def get_query_set(self):
         return self.get_queryset()
 
     def get_query_set(self):
         return self.get_queryset()
 
-class PlCoreBase(models.Model):
+class DiffModelMixIn:
+    # Provides useful methods for computing which objects in a model have
+    # changed. Make sure to do self._initial = self._dict in the __init__
+    # method.
+
+    # This is broken out of PlCoreBase into a Mixin so the User model can
+    # also make use of it.
+
+    @property
+    def _dict(self):
+        return model_to_dict(self, fields=[field.name for field in
+                             self._meta.fields])
+
+    @property
+    def diff(self):
+        d1 = self._initial
+        d2 = self._dict
+        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
+        return dict(diffs)
+
+    @property
+    def has_changed(self):
+        return bool(self.diff)
+
+    @property
+    def changed_fields(self):
+        return self.diff.keys()
+
+    @property
+    def has_field_changed(self, field_name):
+        return field_name in self.diff.keys()
+
+    def get_field_diff(self, field_name):
+        return self.diff.get(field_name, None)
+
+
+class PlCoreBase(models.Model, DiffModelMixIn):
     objects = PlCoreBaseManager()
     deleted_objects = PlCoreBaseDeletionManager()
 
     objects = PlCoreBaseManager()
     deleted_objects = PlCoreBaseDeletionManager()
 
@@ -69,27 +105,9 @@ class PlCoreBase(models.Model):
 
     def __init__(self, *args, **kwargs):
         super(PlCoreBase, self).__init__(*args, **kwargs)
 
     def __init__(self, *args, **kwargs):
         super(PlCoreBase, self).__init__(*args, **kwargs)
-        self.__initial = self._dict
+        self._initial = self._dict # for DiffModelMixIn
         self.silent = False
 
         self.silent = False
 
-    @property
-    def diff(self):
-        d1 = self.__initial
-        d2 = self._dict
-        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
-        return dict(diffs)
-
-    @property
-    def has_changed(self):
-        return bool(self.diff)
-
-    @property
-    def changed_fields(self):
-        return self.diff.keys()
-
-    def get_field_diff(self, field_name):
-        return self.diff.get(field_name, None)
-
     def can_update(self, user):
         if user.is_readonly:
             return False
     def can_update(self, user):
         if user.is_readonly:
             return False
@@ -97,6 +115,11 @@ class PlCoreBase(models.Model):
             return True
         return False
 
             return True
         return False
 
+    def can_update_field(self, user, fieldName):
+        # Give us the opportunity to implement fine-grained permission checking.
+        # Default to True, and let can_update() permit or deny the whole object.
+        return True
+
     def delete(self, *args, **kwds):
         # so we have something to give the observer
         purge = kwds.get('purge',False)
     def delete(self, *args, **kwds):
         # so we have something to give the observer
         purge = kwds.get('purge',False)
@@ -131,6 +154,11 @@ class PlCoreBase(models.Model):
     def save_by_user(self, user, *args, **kwds):
         if not self.can_update(user):
             raise PermissionDenied("You do not have permission to update %s objects" % self.__class__.__name__)
     def save_by_user(self, user, *args, **kwds):
         if not self.can_update(user):
             raise PermissionDenied("You do not have permission to update %s objects" % self.__class__.__name__)
+
+        for fieldName in self.changed_fields:
+            if not self.can_update_field(user, fieldName):
+                raise PermissionDenied("You do not have permission to update field %s in object %s" % (fieldName, self.__class__.__name__))
+
         self.save(*args, **kwds)
 
     def delete_by_user(self, user, *args, **kwds):
         self.save(*args, **kwds)
 
     def delete_by_user(self, user, *args, **kwds):
@@ -138,10 +166,6 @@ class PlCoreBase(models.Model):
             raise PermissionDenied("You do not have permission to delete %s objects" % self.__class__.__name__)
         self.delete(*args, **kwds)
 
             raise PermissionDenied("You do not have permission to delete %s objects" % self.__class__.__name__)
         self.delete(*args, **kwds)
 
-    @property
-    def _dict(self):
-        return model_to_dict(self, fields=[field.name for field in
-                             self._meta.fields])
 
 
 
 
 
 
index 9a62e34..9b54da9 100644 (file)
@@ -3,7 +3,7 @@ import datetime
 from collections import defaultdict
 from django.db import models
 from django.db.models import F, Q
 from collections import defaultdict
 from django.db import models
 from django.db.models import F, Q
-from core.models import PlCoreBase,Site, DashboardView
+from core.models import PlCoreBase,Site, DashboardView, DiffModelMixIn
 from core.models.site import Deployment
 from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
 from timezones.fields import TimeZoneField
 from core.models.site import Deployment
 from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
 from timezones.fields import TimeZoneField
@@ -11,6 +11,7 @@ from operator import itemgetter, attrgetter
 from django.core.mail import EmailMultiAlternatives
 from core.middleware import get_request
 import model_policy
 from django.core.mail import EmailMultiAlternatives
 from core.middleware import get_request
 import model_policy
+from django.core.exceptions import PermissionDenied
 
 # Create your models here.
 class UserManager(BaseUserManager):
 
 # Create your models here.
 class UserManager(BaseUserManager):
@@ -55,7 +56,7 @@ class DeletedUserManager(UserManager):
     def get_query_set(self):
         return self.get_queryset()
 
     def get_query_set(self):
         return self.get_queryset()
 
-class User(AbstractBaseUser):
+class User(AbstractBaseUser, DiffModelMixIn):
 
     class Meta:
         app_label = "core"
 
     class Meta:
         app_label = "core"
@@ -99,6 +100,10 @@ class User(AbstractBaseUser):
     USERNAME_FIELD = 'email'
     REQUIRED_FIELDS = ['firstname', 'lastname']
 
     USERNAME_FIELD = 'email'
     REQUIRED_FIELDS = ['firstname', 'lastname']
 
+    def __init__(self, *args, **kwargs):
+        super(User, self).__init__(*args, **kwargs)
+        self._initial = self._dict # for DiffModelMixIn
+
     def isReadOnlyUser(self):
         return self.is_readonly
 
     def isReadOnlyUser(self):
         return self.is_readonly
 
@@ -182,6 +187,8 @@ class User(AbstractBaseUser):
         self.username = self.email
         super(User, self).save(*args, **kwds)
 
         self.username = self.email
         super(User, self).save(*args, **kwds)
 
+        self._initial = self._dict
+
     def send_temporary_password(self):
         password = User.objects.make_random_password()
         self.set_password(password)\r
     def send_temporary_password(self):
         password = User.objects.make_random_password()
         self.set_password(password)\r
@@ -193,6 +200,41 @@ class User(AbstractBaseUser):
         msg.attach_alternative(html_content, "text/html")\r
         msg.send()
 
         msg.attach_alternative(html_content, "text/html")\r
         msg.send()
 
+    def can_update_field(self, user, fieldName):
+        from core.models import SitePrivilege
+        if (user.is_admin):
+            # admin can update anything
+            return True
+
+        # fields that a site PI can update
+        if fieldName in ["is_active", "is_readonly"]:
+            site_privs = SitePrivilege.objects.filter(user=user, site=self.site)
+            for site_priv in site_privs:
+                if site_priv.role.role == 'pi':
+                    return True
+
+        # fields that a user cannot update in his/her own record
+        if fieldName in ["is_admin", "is_active", "site", "is_staff", "is_readonly"]:
+            return False
+
+        return True
+
+    def can_update(self, user):
+        from core.models import SitePrivilege
+        if user.is_readonly:
+            return False
+        if user.is_admin:
+            return True
+        if (user.id == self.id):
+            return True
+        # site pis can update
+        site_privs = SitePrivilege.objects.filter(user=user, site=self.site)
+        for site_priv in site_privs:
+            if site_priv.role.role == 'pi':
+                return True
+
+        return False
+
     @staticmethod
     def select_by_user(user):
         if user.is_admin:
     @staticmethod
     def select_by_user(user):
         if user.is_admin:
@@ -208,6 +250,21 @@ class User(AbstractBaseUser):
             qs = User.objects.filter(Q(site__in=sites) | Q(id__in=user_ids))
         return qs
 
             qs = User.objects.filter(Q(site__in=sites) | Q(id__in=user_ids))
         return qs
 
+    def save_by_user(self, user, *args, **kwds):
+        if not self.can_update(user):
+            raise PermissionDenied("You do not have permission to update %s objects" % self.__class__.__name__)
+
+        for fieldName in self.changed_fields:
+            if not self.can_update_field(user, fieldName):
+                raise PermissionDenied("You do not have permission to update field %s in object %s" % (fieldName, self.__class__.__name__))
+
+        self.save(*args, **kwds)
+
+    def delete_by_user(self, user, *args, **kwds):
+        if not self.can_update(user):
+            raise PermissionDenied("You do not have permission to delete %s objects" % self.__class__.__name__)
+        self.delete(*args, **kwds)
+
 class UserDashboardView(PlCoreBase):
      user = models.ForeignKey(User, related_name="dashboardViews")
      dashboardView = models.ForeignKey(DashboardView, related_name="dashboardViews")
 class UserDashboardView(PlCoreBase):
      user = models.ForeignKey(User, related_name="dashboardViews")
      dashboardView = models.ForeignKey(DashboardView, related_name="dashboardViews")