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
 -----------
 
         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"),
       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
 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
 
   * 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,