CORD-844 add session token authentication

Change-Id: Ic43c8794c5bfdd2c82cae1ec717826a84c34a212
diff --git a/xos/grpc/apihelper.py b/xos/grpc/apihelper.py
index bf128c2..b757ae7 100644
--- a/xos/grpc/apihelper.py
+++ b/xos/grpc/apihelper.py
@@ -7,6 +7,10 @@
 from core.models import *
 from xos.exceptions import *
 
+from importlib import import_module
+from django.conf import settings
+SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
+
 class XOSAPIHelperMixin(object):
     def getProtoClass(self, djangoClass):
         pClass = getattr(xos_pb2, djangoClass.__name__)
@@ -146,9 +150,18 @@
                     (username, password) = auth.split(":")
                     user = django_authenticate(username=username, password=password)
                     if not user:
-                        raise Exception("failed to authenticate %s:%s" % (username, password))
+                        raise XOSPermissionDenied("failed to authenticate %s:%s" % (username, password))
                     print "authenticated %s:%s as %s" % (username, password, 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
+
 
         return None
 
diff --git a/xos/grpc/grpc_client.py b/xos/grpc/grpc_client.py
index 8defd42..cd561c7 100644
--- a/xos/grpc/grpc_client.py
+++ b/xos/grpc/grpc_client.py
@@ -2,7 +2,8 @@
 import grpc
 from protos.common_pb2 import *
 from protos.xos_pb2 import *
-from protos import xos_pb2_grpc, modeldefs_pb2_grpc
+from protos.utility_pb2 import *
+from protos import xos_pb2_grpc, modeldefs_pb2_grpc, utility_pb2_grpc
 from google.protobuf.empty_pb2 import Empty
 from grpc import metadata_call_credentials, ChannelCredentials, composite_channel_credentials, ssl_channel_credentials
 
@@ -18,6 +19,14 @@
         metadata = (('Authorization', basic_auth),)
         callback(metadata, None)
 
+class SessionIdCallCredentials(grpc.AuthMetadataPlugin):
+  """Metadata wrapper for raw access token credentials."""
+  def __init__(self, sessionid):
+        self._sessionid = sessionid
+  def __call__(self, context, callback):
+        metadata = (('x-xossession', self._sessionid),)
+        callback(metadata, None)
+
 class XOSClient(object):
     def __init__(self, hostname, port):
         self.hostname = hostname
@@ -29,19 +38,24 @@
         self.channel = grpc.insecure_channel("%s:%d" % (self.hostname, self.port))
         self.stub = xos_pb2_grpc.xosStub(self.channel)
         self.modeldefs = modeldefs_pb2_grpc.modeldefsStub(self.channel)
+        self.utility = utility_pb2_grpc.utilityStub(self.channel)
 
 class SecureClient(XOSClient):
-    def __init__(self, hostname, port=50051, cacert=SERVER_CA, username=None, password=None):
+    def __init__(self, hostname, port=50051, cacert=SERVER_CA, username=None, password=None, sessionid=None):
         super(SecureClient,self).__init__(hostname, port)
 
         server_ca = open(cacert,"r").read()
-        call_creds = metadata_call_credentials(UsernamePasswordCallCredentials(username, password))
+        if (sessionid):
+            call_creds = metadata_call_credentials(SessionIdCallCredentials(sessionid))
+        else:
+            call_creds = metadata_call_credentials(UsernamePasswordCallCredentials(username, password))
         chan_creds = ssl_channel_credentials(server_ca)
         chan_creds = composite_channel_credentials(chan_creds, call_creds)
 
         self.channel = grpc.secure_channel("%s:%d" % (self.hostname, self.port), chan_creds)
         self.stub = xos_pb2_grpc.xosStub(self.channel)
         self.modeldefs = modeldefs_pb2_grpc.modeldefsStub(self.channel)
+        self.utility = utility_pb2_grpc.utilityStub(self.channel)
 
 def main():  # self-test
     client = InsecureClient("xos-core.cord.lab")
diff --git a/xos/grpc/grpc_server.py b/xos/grpc/grpc_server.py
index d231bfc..8ffa8c3 100644
--- a/xos/grpc/grpc_server.py
+++ b/xos/grpc/grpc_server.py
@@ -29,9 +29,10 @@
 sys.path.append('/opt/xos')
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
 
-from protos import xos_pb2, schema_pb2, modeldefs_pb2
+from protos import xos_pb2, schema_pb2, modeldefs_pb2, utility_pb2
 from xos_grpc_api import XosService
 from xos_modeldefs_api import ModelDefsService
+from xos_utility_api import UtilityService
 from google.protobuf.empty_pb2 import Empty
 
 from xos.logger import Logger, logging
@@ -118,6 +119,7 @@
             (schema_pb2.add_SchemaServiceServicer_to_server, SchemaService),
             (xos_pb2.add_xosServicer_to_server, XosService),
             (modeldefs_pb2.add_modeldefsServicer_to_server, ModelDefsService),
+            (utility_pb2.add_utilityServicer_to_server, UtilityService),
         ):
             service = service_class(self.thread_pool)
             self.register(activator_func, service)
diff --git a/xos/grpc/protos/utility.proto b/xos/grpc/protos/utility.proto
new file mode 100644
index 0000000..36b5ed0
--- /dev/null
+++ b/xos/grpc/protos/utility.proto
@@ -0,0 +1,33 @@
+syntax = "proto3";
+
+package xos;
+
+import "google/protobuf/empty.proto";
+import "google/api/annotations.proto";
+import "common.proto";
+
+message LoginRequest {
+    string username = 1;
+    string password = 2;
+};
+
+message LoginResponse {
+    string sessionid = 1;
+};
+
+service utility {
+
+  rpc Login(LoginRequest) returns (LoginResponse) {
+        option (google.api.http) = {
+            post: "/xosapi/v1/utility/login"
+            body: "*"
+        };
+  }
+
+  rpc Logout(google.protobuf.Empty) returns (google.protobuf.Empty) {
+        option (google.api.http) = {
+            post: "/xosapi/v1/utility/logout"
+            body: "*"
+        };
+  }
+};
diff --git a/xos/grpc/xos_utility_api.py b/xos/grpc/xos_utility_api.py
new file mode 100644
index 0000000..eb580ac
--- /dev/null
+++ b/xos/grpc/xos_utility_api.py
@@ -0,0 +1,51 @@
+import base64
+import time
+from protos import utility_pb2
+from google.protobuf.empty_pb2 import Empty
+
+from importlib import import_module
+from django.conf import settings

+SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
+
+from django.contrib.auth import authenticate as django_authenticate
+import django.apps
+from core.models import *
+from xos.exceptions import *
+from apihelper import XOSAPIHelperMixin
+
+class UtilityService(utility_pb2.utilityServicer, XOSAPIHelperMixin):
+    def __init__(self, thread_pool):
+        self.thread_pool = thread_pool
+
+    def stop(self):
+        pass
+
+    def Login(self, request, context):
+        if not request.username:
+            raise XOSPermissionDenied("No username")
+
+        u=django_authenticate(username=request.username, password=request.password)
+        if not u:
+            raise XOSPermissionDenied("Failed to authenticate user %s" % request.username)
+
+        session = SessionStore()
+        auth = {"username": request.username, "password": request.password}
+        session["auth"] = auth
+        session['_auth_user_id'] = u.pk
+        session['_auth_user_backend'] = u.backend
+        session.save()
+
+        response = utility_pb2.LoginResponse()
+        response.sessionid = session.session_key
+
+        return response
+
+    def Logout(self, request, context):
+        for (k, v) in context.invocation_metadata():
+            if (k.lower()=="x-xossession"):
+                s = SessionStore(session_key=v)
+                if "_auth_user_id" in s:
+                    del s["_auth_user_id"]
+                    s.save()
+        return Empty()
+
diff --git a/xos/tools/apigen/grpc_list_test.template.py b/xos/tools/apigen/grpc_list_test.template.py
index fd22503..9b7fffb 100644
--- a/xos/tools/apigen/grpc_list_test.template.py
+++ b/xos/tools/apigen/grpc_list_test.template.py
@@ -12,7 +12,21 @@
 c=grpc_client.SecureClient("xos-core.cord.lab", username="padmin@vicci.org", password="letmein")
 
 {% for object in generator.all() %}
-print "testing secure List{{ object.camel() }}...",
+print "testing basic secure List{{ object.camel() }}...",
+c.stub.List{{ object.camel() }}(Empty())
+print "Okay"
+{%- endfor %}
+
+# now try to login
+c=grpc_client.InsecureClient("xos-core.cord.lab")
+lr=grpc_client.LoginRequest()
+lr.username="padmin@vicci.org"
+lr.password="letmein"
+session=c.utility.Login(lr)
+
+c=grpc_client.SecureClient("xos-core.cord.lab", sessionid=session.sessionid)
+{% for object in generator.all() %}
+print "testing session secure List{{ object.camel() }}...",
 c.stub.List{{ object.camel() }}(Empty())
 print "Okay"
 {%- endfor %}