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
-