Commits

Luke Plant committed c13cc2b

Made 'attributes' list require all fields to be present

Comments (0)

Files changed (9)

+Version 0.4
+-----------
+
+* Changed 'Anonymizer.attributes' to require every field to be listed.  This is
+  deal with the common security problem when a model is updated, but the
+  Anonymizer is not updated.
+
+  Fields that should not be anonymized should specify the special value "SKIP"
+  as the 'replacer'.
+
+* attributes must now be a list of tuples, not a dictionary.
+
 Version 0.3
 -----------
 

anonymizer/base.py

         If it returns False, the object will not be saved
         """
         attributes = self.get_attributes()
-        if isinstance(attributes, dict):
-            import warnings
-            warnings.warn("The 'attributes' attribute should now be a list of tuples, not a dictionary", PendingDeprecationWarning)
-            items = attributes.items()
-        else:
-            items = attributes
-
-        for attname, replacer in items:
+        for attname, replacer in attributes:
+            if replacer == "SKIP":
+                continue
             self.alter_object_attribute(obj, attname, replacer)
 
     def alter_object_attribute(self, obj, attname, replacer):
         setattr(obj, attname, replacement)
 
     def run(self):
+        self.validate()
         for obj in self.get_query_set().iterator():
             retval = self.alter_object(obj)
             if retval is not False:
-             obj.save()
+                obj.save()
 
+    def validate(self):
+        attributes = self.get_attributes()
+        model_attrs = set(f.attname for f in self.model._meta.fields)
+        given_attrs = set(name for name,replacer in attributes)
+        if model_attrs != given_attrs:
+            msg = ""
+            missing_attrs = model_attrs - given_attrs
+            if missing_attrs:
+                msg += "The following fields are missing: %s. " % ", ".join(missing_attrs)
+                msg += "Add the replacer \"SKIP\" to skip these fields."
+            extra_attrs = given_attrs - model_attrs
+            if extra_attrs:
+                msg += "The following non-existent fields were supplied: %s." % ", ".join(extra_attrs)
+            raise ValueError("The attributes list for %s does not match the complete list of fields for that model. %s" % (self.model.__name__, msg))

anonymizer/introspect.py

 from django.db.models.loading import get_models
 
 field_replacers = {
-    'AutoField': None,
-    'ForeignKey': None,
-    'ManyToManyField': None,
-    'OneToOneField': None,
-    'SlugField': None, # we probably don't want to change slugs
+    'AutoField': '"SKIP"',
+    'ForeignKey': '"SKIP"',
+    'ManyToManyField': '"SKIP"',
+    'OneToOneField': '"SKIP"',
+    'SlugField': '"SKIP"', # we probably don't want to change slugs
     'DateField': '"date"',
     'DateTimeField': '"datetime"',
     'BooleanField': '"bool"',
     return r
 
 attribute_template = "        ('%(attname)s', %(replacer)s),"
-skipped_template   = "         # Skipping field %s"
 class_template = """
 class %(modelname)sAnonymizer(Anonymizer):
 
 
     for f in fields:
         replacer = get_replacer_for_field(f)
-        if replacer is None:
-            attributes.append(skipped_template % f.attname)
-        else:
-            attributes.append(attribute_template % {'attname': f.attname,
-                                                    'replacer': replacer })
+        attributes.append(attribute_template % {'attname': f.attname,
+                                                'replacer': replacer })
     return class_template % {'modelname':model.__name__,
                              'attributes': "\n".join(attributes) }
 

anonymizer/management/commands/anonymize_data.py

                 is_anonymizer = False
 
             if is_anonymizer:
+                v().validate()
                 anonymizers.append(v)
 
         anonymizers.sort(key=lambda c:c.order)

anonymizer/tests/__init__.py

     model = Other
 
     attributes = [
-         # Skipping field id
+        ('id', "SKIP"),
     ]
 
 
     model = EverythingModel
 
     attributes = [
-         # Skipping field id
+        ('id', "SKIP"),
         ('username', "username"),
         ('name', "name"),
         ('email', "email"),
         ('address_city', "city"),
         ('address_post_code', "uk_postcode"),
         ('address', "full_address"),
-         # Skipping field o1_id
+        ('o1_id', "SKIP"),
         ('something', "lorem"),
         ('something_else', "lorem"),
         ('some_varchar', "varchar"),
             model = test_models.EverythingModel
 
             attributes = [
+                ('id', "SKIP"),
+                ('o1_id', "SKIP"),
+                ('icon', "SKIP"),
                 ('username', 'username'),
                 ('name', "name"),
                 ('email', "email"),

docs/anonymizers.rst

       before other fields like name (see :class:`anonymizer.base.DjangoFaker`
       for more details).
 
+      For security in the case of new fields being added to the model, the list
+      must contain all attributes corresponding to fields on the model. To
+      specify that an attribute should not be altered, using the string "SKIP".
+
    .. attribute:: order
 
       Sometimes it is important that some anonymizers are run before others.  By

docs/commands.rst

 in many cases to completely remove anonymizers for models that don't need them.
 
 Currently some fields are deliberately skipped - these include `ForeignKey`
-fields and `ManyToManyField` relations. A comment will be included to show they
-have been skipped.
+fields and `ManyToManyField` relations.
 
 Some fields type are also currently unsupported. The corresponding code
 generated will produce an error on import, to indicate that it must be dealt

docs/overview.rst

 
   * Choose an appropriate 'replacer' from the :mod:`anonymizer.replacers` module.
 
-  * For some fields, you will want to remove them from the list of attributes, so
-    that the values will be unchanged - especially things like denormalised
-    fields.
+  * For some fields, you will want to specify "SKIP" so that the values will be
+    unchanged - especially things like denormalised fields.
+
+    Note that you can't just remove from the list. This is to cope with the
+    problem of new fields being added to the model, but the developer forgetting
+    to add the field to the anonymizer. To prevent that situation, every field
+    must listed in the 'attributes' list.
 
   * Some models may not need any anonymization, and the corresponding anonymizer
     can be deleted.
 
 setup(
     name = "django-anonymizer",
-    version = '0.3',
+    version = '0.4',
     packages = find_packages(),
     include_package_data = True,