CORD-1723: Security errors if user not logged in

Change-Id: I01c37123cbc9dfbf716e72029bf5149d0b291079
(cherry picked from commit c56fd913e9ccd1b6d532dae45f0d6dd389e3982e)
diff --git a/xos/coreapi/apihelper.py b/xos/coreapi/apihelper.py
index 05f7ec7..f8ea699 100644
--- a/xos/coreapi/apihelper.py
+++ b/xos/coreapi/apihelper.py
@@ -26,26 +26,43 @@
 
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.auth import authenticate as django_authenticate
-from django.db.models import F,Q
+from django.db.models import F, Q
 from core.models import *
 from xos.exceptions import *
 
 from importlib import import_module
 from django.conf import settings
 
+
 class XOSDefaultSecurityContext(object):
     grant_access = True
     write_access = True
     read_access = True
 
+
+xos_anonymous_site = Site(
+    name='XOS Anonymous Site',
+    enabled=True,
+    hosts_nodes=False,
+    hosts_users=True,
+    login_base='xos',
+    abbreviated_name='xos-anonymous')
+
+xos_anonymous_user = User(
+    username='XOS Anonymous User',
+    email='xos@example.com',
+    is_admin=False,
+    site=xos_anonymous_site)
+
 SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
 
+
 def translate_exceptions(function):
     """ this decorator translates XOS exceptions to grpc status codes """
     def wrapper(*args, **kwargs):
         try:
             return function(*args, **kwargs)
-        except Exception, e:
+        except Exception as e:
             if "context" in kwargs:
                 context = kwargs["context"]
             else:
@@ -56,11 +73,11 @@
             elif hasattr(e, 'detail'):
                 context.set_details(e.detail)
 
-            if (type(e) == XOSPermissionDenied):
+            if (isinstance(e, XOSPermissionDenied)):
                 context.set_code(grpc.StatusCode.PERMISSION_DENIED)
-            elif (type(e) == XOSValidationError):
+            elif (isinstance(e, XOSValidationError)):
                 context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
-            elif (type(e) == XOSNotAuthenticated):
+            elif (isinstance(e, XOSNotAuthenticated)):
                 context.set_code(grpc.StatusCode.UNAUTHENTICATED)
             raise
     return wrapper
@@ -68,21 +85,24 @@
 
 bench_tStart = time.time()
 bench_ops = 0
+
+
 def benchmark(function):
     """ this decorator will report gRPC benchmark statistics every 10 seconds """
     def wrapper(*args, **kwargs):
         global bench_tStart
         global bench_ops
         result = function(*args, **kwargs)
-        bench_ops = bench_ops+1
+        bench_ops = bench_ops + 1
         elap = time.time() - bench_tStart
         if (elap >= 10):
-            print "performance %d" % (bench_ops/elap)
-            bench_ops=0
+            print "performance %d" % (bench_ops / elap)
+            bench_ops = 0
             bench_tStart = time.time()
         return result
     return wrapper
 
+
 class CachedAuthenticator(object):
     """ Django Authentication is very slow (~ 10 ops/second), so cache
         authentication results and reuse them.
@@ -91,7 +111,8 @@
     def __init__(self):
         self.cached_creds = {}
         self.timeout = 10          # keep cache entries around for 10s
-        self.lock = threading.Lock()   # lock to keep multiple callers from trimming at the same time
+        # lock to keep multiple callers from trimming at the same time
+        self.lock = threading.Lock()
 
     def authenticate(self, username, password):
         self.trim()
@@ -101,14 +122,18 @@
         if cred:
             user = User.objects.filter(id=cred["user_id"])
             if user:
-               user = user[0]
-               #print "cached authenticated %s:%s as %s" % (username, password, user)
-               return user
+                user = user[0]
+                # print "cached authenticated %s:%s as %s" % (username,
+                # password, user)
+                return user
 
         user = django_authenticate(username=username, password=password)
         if user:
-            #print "django authenticated %s:%s as %s" % (username, password, user)
-            self.cached_creds[key] = {"timeout": time.time() + self.timeout, "user_id": user.id}
+            # print "django authenticated %s:%s as %s" % (username, password,
+            # user)
+            self.cached_creds[key] = {
+                "timeout": time.time() + self.timeout,
+                "user_id": user.id}
 
         return user
 
@@ -120,8 +145,10 @@
                 del self.cached_creds[k]
         self.lock.release()
 
+
 cached_authenticator = CachedAuthenticator()
 
+
 class XOSAPIHelperMixin(object):
     def __init__(self):
         import django.apps
@@ -151,9 +178,10 @@
         if not x:
             return 0
         else:
-            utc=pytz.utc
-            return (x-datetime.datetime(1970,1,1,tzinfo=utc)).total_seconds()
-            #return time.mktime(x.timetuple())
+            utc = pytz.utc
+            return (x - datetime.datetime(1970, 1,
+                                          1, tzinfo=utc)).total_seconds()
+            # return time.mktime(x.timetuple())
 
     def convertForeignKey(self, x):
         if not x:
@@ -164,11 +192,12 @@
     def objToProto(self, obj):
         p_obj = self.getProtoClass(obj.__class__)()
         for field in obj._meta.fields:
-            if getattr(obj, field.name) == None:
+            if getattr(obj, field.name) is None:
                 continue
 
             ftype = field.get_internal_type()
-            if (ftype == "CharField") or (ftype == "TextField") or (ftype == "SlugField"):
+            if (ftype == "CharField") or (
+                    ftype == "TextField") or (ftype == "SlugField"):
                 setattr(p_obj, field.name, str(getattr(obj, field.name)))
             elif (ftype == "BooleanField"):
                 setattr(p_obj, field.name, getattr(obj, field.name))
@@ -177,9 +206,21 @@
             elif (ftype == "IntegerField") or (ftype == "PositiveIntegerField") or (ftype == "BigIntegerField"):
                 setattr(p_obj, field.name, int(getattr(obj, field.name)))
             elif (ftype == "ForeignKey"):
-                setattr(p_obj, field.name+"_id", self.convertForeignKey(getattr(obj, field.name)))
+                setattr(
+                    p_obj,
+                    field.name + "_id",
+                    self.convertForeignKey(
+                        getattr(
+                            obj,
+                            field.name)))
             elif (ftype == "DateTimeField"):
-                setattr(p_obj, field.name, self.convertDateTime(getattr(obj, field.name)))
+                setattr(
+                    p_obj,
+                    field.name,
+                    self.convertDateTime(
+                        getattr(
+                            obj,
+                            field.name)))
             elif (ftype == "FloatField"):
                 setattr(p_obj, field.name, float(getattr(obj, field.name)))
             elif (ftype == "GenericIPAddressField"):
@@ -193,7 +234,7 @@
                 continue
             try:
                 rel_objs = getattr(obj, related_name)
-            except Exception, e:
+            except Exception as e:
                 # django makes catching this exception unnecessarily difficult
                 if type(e).__name__ == "RelatedObjectDoesNotExist":
                     # OneToOneField throws this if relation does not exist
@@ -206,40 +247,44 @@
                 continue
 
             for rel_obj in rel_objs.all():
-                if not hasattr(p_obj,related_name+"_ids"):
+                if not hasattr(p_obj, related_name + "_ids"):
                     continue
-                getattr(p_obj,related_name+"_ids").append(rel_obj.id)
+                getattr(p_obj, related_name + "_ids").append(rel_obj.id)
 
         # Generate a list of class names for the object. This includes its
         # ancestors. Anything that is a descendant of XOSBase or User
         # counts.
 
         bases = inspect.getmro(obj.__class__)
-        bases = [x for x in bases if issubclass(x, XOSBase) or issubclass(x, User)]
-        p_obj.class_names = ",".join( [x.__name__ for x in bases] )
+        bases = [
+            x for x in bases if issubclass(
+                x, XOSBase) or issubclass(
+                x, User)]
+        p_obj.class_names = ",".join([x.__name__ for x in bases])
 
         p_obj.self_content_type_id = obj.get_content_type_key()
 
         return p_obj
 
     def protoToArgs(self, djangoClass, message):
-        args={}
-        fmap={}
-        fset={}
+        args = {}
+        fmap = {}
+        fset = {}
         for field in djangoClass._meta.fields:
             fmap[field.name] = field
             if field.get_internal_type() == "ForeignKey":
-               # foreign key can be represented as an id
-               fmap[field.name + "_id"] = field
+                # foreign key can be represented as an id
+                fmap[field.name + "_id"] = field
 
         for (fieldDesc, val) in message.ListFields():
             name = fieldDesc.name
             if name in fmap:
-                if (name=="id"):
+                if (name == "id"):
                     # don't let anyone set the id
                     continue
                 ftype = fmap[name].get_internal_type()
-                if (ftype == "CharField") or (ftype == "TextField") or (ftype == "SlugField"):
+                if (ftype == "CharField") or (
+                        ftype == "TextField") or (ftype == "SlugField"):
                     args[name] = val
                 elif (ftype == "BooleanField"):
                     args[name] = val
@@ -248,13 +293,14 @@
                 elif (ftype == "IntegerField") or (ftype == "PositiveIntegerField") or (ftype == "BigIntegerField"):
                     args[name] = val
                 elif (ftype == "ForeignKey"):
-                    if val==0: # assume object id 0 means None
+                    if val == 0:  # assume object id 0 means None
                         args[name] = None
                     else:
-                        args[name] = val # field name already has "_id" at the end
+                        # field name already has "_id" at the end
+                        args[name] = val
                 elif (ftype == "DateTimeField"):
                     utc = pytz.utc
-                    args[name] = datetime.datetime.fromtimestamp(val,tz=utc)
+                    args[name] = datetime.datetime.fromtimestamp(val, tz=utc)
                 elif (ftype == "FloatField"):
                     args[name] = val
                 elif (ftype == "GenericIPAddressField"):
@@ -268,8 +314,8 @@
         p_objs = self.getPluralProtoClass(djangoClass)()
 
         for obj in objs:
-           new_obj = p_objs.items.add()
-           new_obj.CopyFrom(self.objToProto(obj))
+            new_obj = p_objs.items.add()
+            new_obj.CopyFrom(self.objToProto(obj))
 
         return p_objs
 
@@ -289,14 +335,17 @@
 
     def xos_security_gate(self, obj, user, **access_types):
         sec_ctx = XOSDefaultSecurityContext()
+        if not user:
+            user = xos_anonymous_user
+
         sec_ctx.user = user
 
-        for k,v in access_types.items():
+        for k, v in access_types.items():
             setattr(sec_ctx, k, v)
 
         obj_ctx = obj
 
-        verdict, policy_name = obj.can_access(ctx = sec_ctx)
+        verdict, policy_name = obj.can_access(ctx=sec_ctx)
 
         # FIXME: This is the central point of enforcement for security policies
         #        Implement Auditing here.
@@ -304,22 +353,29 @@
 
         if not verdict:
             #    logging.critical( ... )
-            if object_id:
-                object_descriptor = 'object %d'%object_id
+            if obj.id:
+                object_descriptor = 'object %d' % obj.id
             else:
                 object_descriptor = 'new object'
 
-            raise XOSPermissionDenied("User %(user_email)s cannot access %(django_class_name)s %(descriptor)s due to policy %(policy_name)s"%{'user_email':user.email, 'django_class_name':obj.__class__.__name__, 'policy_name': policy_name, 'descriptor': object_descriptor})
+            raise XOSPermissionDenied(
+                "User %(user_email)s cannot access %(django_class_name)s %(descriptor)s due to policy %(policy_name)s" % {
+                    'user_email': user.email,
+                    'django_class_name': obj.__class__.__name__,
+                    'policy_name': policy_name,
+                    'descriptor': object_descriptor})
 
     def xos_security_check(self, obj, user, **access_types):
         sec_ctx = XOSDefaultSecurityContext()
+        if not user:
+            user = xos_anonymous_user
         sec_ctx.user = user
-        for k,v in access_types.items():
+        for k, v in access_types.items():
             setattr(sec_ctx, k, v)
 
         obj_ctx = obj
 
-        verdict, _ = obj.can_access(ctx = sec_ctx)
+        verdict, _ = obj.can_access(ctx=sec_ctx)
         return verdict
 
     def get(self, djangoClass, user, id):
@@ -346,28 +402,28 @@
         self.xos_security_gate(obj, user, write_access=True)
 
         args = self.protoToArgs(djangoClass, message)
-        for (k,v) in args.iteritems():
+        for (k, v) in args.iteritems():
             setattr(obj, k, v)
 
-        save_kwargs={}
+        save_kwargs = {}
         for (k, v) in context.invocation_metadata():
-            if k=="update_fields":
+            if k == "update_fields":
                 save_kwargs["update_fields"] = v.split(",")
-            elif k=="caller_kind":
+            elif k == "caller_kind":
                 save_kwargs["caller_kind"] = v
-            elif k=="always_update_timestamp":
+            elif k == "always_update_timestamp":
                 save_kwargs["always_update_timestamp"] = True
 
         obj.save(**save_kwargs)
         return self.objToProto(obj)
 
     def delete(self, djangoClass, user, id):
-      obj = djangoClass.objects.get(id=id)
+        obj = djangoClass.objects.get(id=id)
 
-      self.xos_security_gate(obj, user, write_access=True)
+        self.xos_security_gate(obj, user, write_access=True)
 
-      obj.delete()
-      return Empty()
+        obj.delete()
+        return Empty()
 
     def query_element_to_q(self, element):
         value = element.sValue
@@ -398,7 +454,9 @@
 
     def list(self, djangoClass, user):
         queryset = djangoClass.objects.all()
-        filtered_queryset = (elt for elt in queryset if self.xos_security_check(elt, user, read_access=True))
+        filtered_queryset = (
+            elt for elt in queryset if self.xos_security_check(
+                elt, user, read_access=True))
 
         # FIXME: Implement auditing here
         # logging.info("User requested x objects, y objects were filtered out by policy z")
@@ -415,12 +473,14 @@
                     query = self.query_element_to_q(element)
             queryset = djangoClass.objects.filter(query)
         elif request.kind == request.SYNCHRONIZER_DIRTY_OBJECTS:
-            query = (Q(enacted__lt=F('updated')) | Q(enacted=None)) & Q(lazy_blocked=False) &Q(no_sync=False)
+            query = (Q(enacted__lt=F('updated')) | Q(enacted=None)) & Q(
+                lazy_blocked=False) & Q(no_sync=False)
             queryset = djangoClass.objects.filter(query)
         elif request.kind == request.SYNCHRONIZER_DELETED_OBJECTS:
             queryset = djangoClass.deleted_objects.all()
         elif request.kind == request.SYNCHRONIZER_DIRTY_POLICIES:
-            query = (Q(policed__lt=F('updated')) | Q(policed=None)) & Q(no_policy=False)
+            query = (Q(policed__lt=F('updated')) | Q(
+                policed=None)) & Q(no_policy=False)
             queryset = djangoClass.objects.filter(query)
         elif request.kind == request.SYNCHRONIZER_DELETED_POLICIES:
             query = Q(policed__lt=F('updated')) | Q(policed=None)
@@ -428,7 +488,9 @@
         elif request.kind == request.ALL:
             queryset = djangoClass.objects.all()
 
-        filtered_queryset = (elt for elt in queryset if self.xos_security_check(elt, user, read_access=True))
+        filtered_queryset = (
+            elt for elt in queryset if self.xos_security_check(
+                elt, user, read_access=True))
 
         # FIXME: Implement auditing here
         # logging.info("User requested x objects, y objects were filtered out by policy z")
@@ -437,26 +499,29 @@
 
     def authenticate(self, context, required=False):
         for (k, v) in context.invocation_metadata():
-            if (k.lower()=="authorization"):
-                (method, auth) = v.split(" ",1)
+            if (k.lower() == "authorization"):
+                (method, auth) = v.split(" ", 1)
                 if (method.lower() == "basic"):
                     auth = base64.b64decode(auth)
                     (username, password) = auth.split(":")
-                    user = cached_authenticator.authenticate(username=username, password=password)
+                    user = cached_authenticator.authenticate(
+                        username=username, password=password)
                     if not user:
-                        raise XOSPermissionDenied("failed to authenticate %s:%s" % (username, password))
+                        raise XOSPermissionDenied(
+                            "failed to authenticate %s:%s" %
+                            (username, password))
                     return user
-            elif (k.lower()=="x-xossession"):
-                 s = SessionStore(session_key=v)
-                 id = s.get("_auth_user_id", None)
-                 if not id:
-                     raise XOSPermissionDenied("failed to authenticate token %s" % v)
-                 user = User.objects.get(id=id)
-                 print "authenticated sessionid %s as %s" % (v, user)
-                 return user
+            elif (k.lower() == "x-xossession"):
+                s = SessionStore(session_key=v)
+                id = s.get("_auth_user_id", None)
+                if not id:
+                    raise XOSPermissionDenied(
+                        "failed to authenticate token %s" % v)
+                user = User.objects.get(id=id)
+                print "authenticated sessionid %s as %s" % (v, user)
+                return user
 
         if required:
             raise XOSPermissionDenied("This API requires authentication")
 
         return None
-