features for voms authentication
authornasia <nassia.a@gmail.com>
Mon, 16 Dec 2013 13:49:30 +0000 (15:49 +0200)
committernasia <nassia.a@gmail.com>
Mon, 16 Dec 2013 13:49:30 +0000 (15:49 +0200)
14 files changed:
docs/index.rst
setup.py
snfOCCI/APIserver.py
snfOCCI/__init__.py
snfOCCI/__init__.pyc [new file with mode: 0644]
snfOCCI/config.py
snfOCCI/config.pyc [new file with mode: 0644]
snfOCCI/httpd/snf_voms-paste.ini [new file with mode: 0644]
snfOCCI/httpd/snf_voms.py [new file with mode: 0644]
snfOCCI/httpd/snf_voms_auth-paste.ini [new file with mode: 0644]
snfOCCI/httpd/snf_voms_auth.py [new file with mode: 0644]
snfOCCI/registry.py
snfOCCI/snf_voms/__init__.py [new file with mode: 0644]
snfOCCI/snf_voms/voms_helper.py [new file with mode: 0644]

index 708c02b..f8ea59f 100644 (file)
@@ -107,7 +107,7 @@ Installation
 
 First, you need to install the required dependencies which can be found here:
 
-* `pyssf <https://code.grnet.gr/attachments/download/1182/pyssf-0.4.5.tar>`_
+* `pyssf <https://pypi.python.org/pypi/pyssf>`_
 * `kamaki <https://code.grnet.gr/projects/kamaki>`_  
 
 Then you can install **snf-occi** API translation server by cloning our latest source code:
index 149e8a0..9666e17 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -6,9 +6,9 @@ setup(
     description='OCCI to Openstack/Cyclades API bridge',
     url='http://code.grnet.gr/projects/snf-occi',
     license='BSD',
-    packages = ['snfOCCI'],
-    entry_points = {
-        'console_scripts' : ['snf-occi = snfOCCI.APIserver:main']
-        }
-
-    )
+    packages = ['snfOCCI','snfOCCI.snf_voms','snfOCCI.httpd','snfOCCI.snfServer'],
+    entry_points = ''' 
+        [paste.app_factory]
+        snf_occi_app = snfOCCI:main
+        ''',   
+    )
\ No newline at end of file
index 056839e..bb76720 100644 (file)
@@ -1,43 +1,92 @@
 #!/usr/bin/env python
 
+import sys
+from optparse import OptionParser, OptionValueError
+import string
+import sqlite3
+import eventlet
+from eventlet import wsgi
+import os
+import json
+import uuid
+
 from snfOCCI.registry import snfRegistry
 from snfOCCI.compute import ComputeBackend
+from snfOCCI.config import SERVER_CONFIG, KAMAKI_CONFIG, VOMS_CONFIG
+import snf_voms
 from snfOCCI.network import NetworkBackend, IpNetworkBackend, IpNetworkInterfaceBackend, NetworkInterfaceBackend
 
-from snfOCCI.config import SERVER_CONFIG, KAMAKI_CONFIG
 
 from kamaki.clients.compute import ComputeClient
 from kamaki.clients.cyclades import CycladesClient
+from kamaki.clients import astakos
 from kamaki.clients import ClientError
+from kamaki.cli import config as kamaki_config
 
 from occi.core_model import Mixin, Resource
-from occi.backend import MixinBackend, KindBackend
+from occi.backend import MixinBackend
 from occi.extensions.infrastructure import COMPUTE, START, STOP, SUSPEND, RESTART, RESOURCE_TEMPLATE, OS_TEMPLATE, NETWORK, IPNETWORK, NETWORKINTERFACE,IPNETWORKINTERFACE 
-from occi.wsgi import Application
+from occi import wsgi
 from occi.exceptions import HTTPError
 from occi import core_model
 
 from wsgiref.simple_server import make_server
 from wsgiref.validate import validator
-import uuid
+from webob import Request
+from pprint import pprint
+
 
-class MyAPP(Application):
+class MyAPP(wsgi.Application):
     '''
     An OCCI WSGI application.
     '''
 
+    def __init__(self):
+        """
+        Initialization of the WSGI OCCI application for synnefo
+        """
+        global ENABLE_VOMS, VOMS_DB
+        ENABLE_VOMS = VOMS_CONFIG['enable_voms']
+        super(MyAPP,self).__init__(registry=snfRegistry())
+        self._register_backends()
+        VALIDATOR_APP = validator(self)
+         
+        
+    def _register_backends(self):
+        COMPUTE_BACKEND = ComputeBackend()
+        NETWORK_BACKEND = NetworkBackend() 
+        NETWORKINTERFACE_BACKEND = NetworkInterfaceBackend()
+        IPNETWORK_BACKEND = IpNetworkBackend()
+        IPNETWORKINTERFACE_BACKEND = IpNetworkInterfaceBackend()
+    
+        self.register_backend(COMPUTE, COMPUTE_BACKEND)
+        self.register_backend(START, COMPUTE_BACKEND)
+        self.register_backend(STOP, COMPUTE_BACKEND)
+        self.register_backend(RESTART, COMPUTE_BACKEND)
+        self.register_backend(SUSPEND, COMPUTE_BACKEND)
+        self.register_backend(RESOURCE_TEMPLATE, MixinBackend())
+        self.register_backend(OS_TEMPLATE, MixinBackend())
+       
+        # Network related backends
+        self.register_backend(NETWORK, NETWORK_BACKEND)
+        self.register_backend(IPNETWORK, IPNETWORK_BACKEND)
+        self.register_backend(NETWORKINTERFACE,NETWORKINTERFACE_BACKEND)
+        self.register_backend(IPNETWORKINTERFACE, IPNETWORKINTERFACE_BACKEND)
+     
+        
     def refresh_images(self, snf, client):
-
-        images = snf.list_images()
-        for image in images:
-            IMAGE_ATTRIBUTES = {'occi.core.id': str(image['id'])}
-            IMAGE = Mixin("http://schemas.ogf.org/occi/infrastructure#", str(image['name']), [OS_TEMPLATE], attributes = IMAGE_ATTRIBUTES)
-            self.register_backend(IMAGE, MixinBackend())
-
+        try:
+            images = snf.list_images()
+            for image in images:
+                    IMAGE_ATTRIBUTES = {'occi.core.id': str(image['id'])}
+                    IMAGE = Mixin("http://schemas.ogf.org/occi/os_tpl#", occify_terms(str(image['name'])), [OS_TEMPLATE],title='IMAGE' ,attributes = IMAGE_ATTRIBUTES)
+                    self.register_backend(IMAGE, MixinBackend())
+        except:
+            raise HTTPError(404, "Unauthorized access")
+      
     def refresh_flavors(self, snf, client):
         
         flavors = snf.list_flavors()
-        print "Retrieving details for each image id"
         for flavor in flavors:
             details = snf.get_flavor_details(flavor['id'])
             FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
@@ -45,24 +94,23 @@ class MyAPP(Application):
                                  'occi.compute.memory': str(details['ram']),
                                  'occi.storage.size': str(details['disk']),
                                  }
-            FLAVOR = Mixin("http://schemas.ogf.org/occi/infrastructure#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
+            FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
             self.register_backend(FLAVOR, MixinBackend())
             
+            
     def refresh_flavors_norecursive(self, snf, client):
         flavors = snf.list_flavors(True)
         print "Retrieving details for each image id"
         for flavor in flavors:
-            # details = snf.get_flavor_details(flavor['id'])
             FLAVOR_ATTRIBUTES = {'occi.core.id': flavor['id'],
                                  'occi.compute.cores': str(flavor['vcpus']),
                                  'occi.compute.memory': str(flavor['ram']),
                                  'occi.storage.size': str(flavor['disk']),
                                  }
              
-            FLAVOR = Mixin("http://schemas.ogf.org/occi/infrastructure#", str(flavor['name']), [RESOURCE_TEMPLATE], attributes = FLAVOR_ATTRIBUTES)
+            FLAVOR = Mixin("http://schemas.ogf.org/occi/resource_tpl#", occify_terms(str(flavor['name'])), [RESOURCE_TEMPLATE], title='FLAVOR',attributes = FLAVOR_ATTRIBUTES)
             self.register_backend(FLAVOR, MixinBackend())
             
-       
     def refresh_network_instances(self,client):
         networks =client.networks_get(command = 'detail')
         network_details = networks.json['networks']
@@ -90,7 +138,7 @@ class MyAPP(Application):
                 self.registry.add_resource(netID, snf_net, None)       
             
         
-            
+    
     def refresh_compute_instances(self, snf, client):
         '''Syncing registry with cyclades resources'''
         
@@ -102,8 +150,7 @@ class MyAPP(Application):
         resources = self.registry.resources
         occi_keys = resources.keys()
         
-        print resources.keys()
-        
+        print occi_keys
         for serverID in occi_keys:
             if '/compute/' in serverID and resources[serverID].attributes['occi.compute.hostname'] == "":
                 self.registry.delete_resource(serverID, None)
@@ -113,20 +160,22 @@ class MyAPP(Application):
             
         #Compute instances in synnefo not available in registry
         diff = [x for x in snf_keys if '/compute/'+x not in occi_keys]
+        
         for key in diff:
 
             details = snf.get_server_details(int(key))
             flavor = snf.get_flavor_details(details['flavor']['id'])
             
             try:
-                print "line 65:Finished getting image details for VM with ID" + str(details['flavor']['id'])
+                print "line 65:Finished getting image details for VM "+key+" with ID" + str(details['flavor']['id'])
                 image = snf.get_image_details(details['image']['id'])
                 
                 for i in self.registry.backends:
-                    if i.term == str(image['name']):
+                    if i.term ==  occify_terms(str(image['name'])):
                         rel_image = i
-                    if i.term == str(flavor['name']):
+                    if i.term ==  occify_terms(str(flavor['name'])):
                         rel_flavor = i
+
                         
                 resource = Resource(key, COMPUTE, [rel_flavor, rel_image])
                 resource.actions = [START]
@@ -138,9 +187,7 @@ class MyAPP(Application):
                 resource.attributes['occi.core.title'] = str(details['name'])
                 networkIDs = details['addresses'].keys()
                 if len(networkIDs)>0: 
-                    #resource.attributes['occi.compute.hostname'] = SERVER_CONFIG['hostname'] % {'id':int(key)}
                     resource.attributes['occi.compute.hostname'] =  str(details['addresses'][networkIDs[0]][0]['addr'])
-                    #resource.attributes['occi.networkinterface.address'] = str(details['addresses'][networkIDs[0]][0]['addr'])
                 else:
                     resource.attributes['occi.compute.hostname'] = ""
                     
@@ -166,7 +213,7 @@ class MyAPP(Application):
                                           'occi.networkinterface.allocation' : allocheme,
                                           'occi.networking.interface': str(item['id']),
                                           'occi.networkinterface.mac' : str(item['mac_address']),
-                                          'occi.networkinterface.ip4' : ip4address,
+                                          'occi.networkinterface.address' : ip4address,
                                           'occi.networkinterface.ip6' :  ip6address                      
                                       }
                     elif  len(details['addresses'][netKey])>0:
@@ -174,7 +221,7 @@ class MyAPP(Application):
                                           'occi.networkinterface.allocation' : allocheme,
                                           'occi.networking.interface': '',
                                           'occi.networkinterface.mac' : '',
-                                          'occi.networkinterface.ip4' : ip4address,
+                                          'occi.networkinterface.address' : ip4address,
                                           'occi.networkinterface.ip6' :  ip6address                      
                                       }
     
@@ -183,7 +230,7 @@ class MyAPP(Application):
                                           'occi.networkinterface.allocation' : '',
                                           'occi.networking.interface': '',
                                           'occi.networkinterface.mac' : '',
-                                          'occi.networkinterface.ip4' :'',
+                                          'occi.networkinterface.address' :'',
                                           'occi.networkinterface.ip6' : '' }
                                       
                     resource.links.append(NET_LINK)
@@ -192,7 +239,7 @@ class MyAPP(Application):
                 
             except ClientError as ce:
                 if ce.status == 404:
-                    print('Image not found, sorry!!!')
+                    print('Image not found (probably older version')
                     continue
                 else:
                     raise ce
@@ -203,50 +250,145 @@ class MyAPP(Application):
             if '/network/' not in key:
                 self.registry.delete_resource(key, None)
 
-    
+
     def __call__(self, environ, response):
         
+        # Enable VOMS Authorization
+        print "snf-occi application has been called!"
+        
+        req = Request(environ) 
+        auth_endpoint = 'snf-auth uri=\'https://'+SERVER_CONFIG['hostname']+':5000/main\''
+        
+        if not req.environ.has_key('HTTP_X_AUTH_TOKEN'):
+              
+                print "Error: An authentication token has not been provided!"
+                status = '401 Not Authorized'
+                headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]        
+                response(status,headers)               
+                return [str(response)]
+   
+   
+        if ENABLE_VOMS:
+                
+            if req.environ.has_key('HTTP_X_AUTH_TOKEN'):
+               
+                environ['HTTP_AUTH_TOKEN']= req.environ['HTTP_X_AUTH_TOKEN']
+                compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
+                cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
+
+                try:
+                    #Up-to-date flavors and images
+                    self.refresh_images(compClient,cyclClient)           
+                    self.refresh_flavors_norecursive(compClient,cyclClient)
+                    self.refresh_network_instances(cyclClient)
+                    self.refresh_compute_instances(compClient,cyclClient)
+                    # token will be represented in self.extras
+                    return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
+                except HTTPError:
+                    print "Exception from unauthorized access!"
+                    status = '401 Not Authorized'
+                    headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
+                    response(status,headers)
+                    return [str(response)]
+
+            else:
+                
+                #raise HTTPError(404, "Unauthorized access")
+                status = '401 Not Authorized'
+                headers = [('Content-Type', 'text/html'),('Www-Authenticate',auth_endpoint)]
+                response(status,headers)
+                return [str(response)]
+
+        else:  
             compClient = ComputeClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
             cyclClient = CycladesClient(KAMAKI_CONFIG['compute_url'], environ['HTTP_AUTH_TOKEN'])
 
             #Up-to-date flavors and images
-            print "@refresh_images"
+           
             self.refresh_images(compClient,cyclClient)
-            print "@refresh_flavors"
+            
             self.refresh_flavors_norecursive(compClient,cyclClient)
             self.refresh_network_instances(cyclClient)
-            print "@refresh_compute_instances"
             self.refresh_compute_instances(compClient,cyclClient)
-           
+            
             # token will be represented in self.extras
             return self._call_occi(environ, response, security = None, token = environ['HTTP_AUTH_TOKEN'], snf = compClient, client = cyclClient)
 
+def application(env, start_response):
+    
+    print "snf-occi will execute voms authentication"
+    t =snf_voms.VomsAuthN()       
+    (user_dn, user_vo, user_fqans) = t.process_request(env)
+    print (user_dn, user_vo, user_fqans)
+      
+    env['HTTP_AUTH_TOKEN'] = get_user_token(user_dn)
+   
+    # Get user authentication details
+    astakosClient = astakos.AstakosClient(KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'])
+    user_details = astakosClient.authenticate()
+    
+    response = {'access': {'token':{'issued_at':'','expires': user_details['access']['token']['expires'] , 'id':env['HTTP_AUTH_TOKEN']},
+                           'serviceCatalog': [],
+                           'user':{'username': user_dn,'roles_links':user_details['access']['user']['roles_links'],'id': user_details['access']['user']['id'], 'roles':[], 'name':user_dn },
+                           'metadata': {'is_admin': 0, 'roles': user_details['access']['user']['roles']}}}        
+           
+   
+    status = '200 OK'
+    headers = [('Content-Type', 'application/json')]        
+    start_response(status,headers)
 
-def main():
+    body = json.dumps(response)
+    print body
+    return [body]
 
-    APP = MyAPP(registry = snfRegistry())
-    COMPUTE_BACKEND = ComputeBackend()
-    NETWORK_BACKEND = NetworkBackend() 
-    NETWORKINTERFACE_BACKEND = NetworkInterfaceBackend()
-    IPNETWORK_BACKEND = IpNetworkBackend()
-    IPNETWORKINTERFACE_BACKEND = IpNetworkInterfaceBackend()
-      
-    APP.register_backend(COMPUTE, COMPUTE_BACKEND)
-    APP.register_backend(START, COMPUTE_BACKEND)
-    APP.register_backend(STOP, COMPUTE_BACKEND)
-    APP.register_backend(RESTART, COMPUTE_BACKEND)
-    APP.register_backend(SUSPEND, COMPUTE_BACKEND)
-    APP.register_backend(RESOURCE_TEMPLATE, MixinBackend())
-    APP.register_backend(OS_TEMPLATE, MixinBackend())
+
+def app_factory(global_config, **local_config):
+    """This function wraps our simple WSGI app so it
+    can be used with paste.deploy"""
+    return application
+
+def tenant_application(env, start_response):
+    
+    print "snf-occi will return tenant information"
+    if env.has_key('SSL_CLIENT_S_DN_ENV'):
+        print env['SSL_CLIENT_S_DN_ENV'], env['SSL_CLIENT_CERT_ENV']    
  
-    # Network related backends
-    APP.register_backend(NETWORK, NETWORK_BACKEND)
-    APP.register_backend(IPNETWORK, IPNETWORK_BACKEND)
-    APP.register_backend(NETWORKINTERFACE,NETWORKINTERFACE_BACKEND)
-    APP.register_backend(IPNETWORKINTERFACE, IPNETWORKINTERFACE_BACKEND)
-     
-    VALIDATOR_APP = validator(APP)
-  
-    HTTPD = make_server('', SERVER_CONFIG['port'], VALIDATOR_APP)
-    HTTPD.serve_forever()
+    req = Request(env) 
+    if req.environ.has_key('HTTP_X_AUTH_TOKEN'):
+            env['HTTP_AUTH_TOKEN']= req.environ['HTTP_X_AUTH_TOKEN']
+    else:
+            raise HTTPError(404, "Unauthorized access") 
+    # Get user authentication details
+    print "@ refresh_user authentication details"
+    astakosClient = astakos.AstakosClient(KAMAKI_CONFIG['astakos_url'], env['HTTP_AUTH_TOKEN'])
+    user_details = astakosClient.authenticate()
+   
+    response = {'tenants_links': [], 'tenants':[{'description':'Instances of EGI Federated Clouds TF','enabled': True, 'id':user_details['access']['user']['id'],'name':'EGI_FCTF'}]}           
+    status = '200 OK'
+    headers = [('Content-Type', 'application/json')]        
+    start_response(status,headers)
+
+    body = json.dumps(response)
+    print body
+    return [body]
+
+
+def tenant_app_factory(global_config, **local_config):
+    """This function wraps our simple WSGI app so it
+    can be used with paste.deploy"""
+    return tenant_application
+
+
     
+def occify_terms(term_name):
+    '''
+    Occifies a term_name so that it is compliant with GFD 185.
+    '''
+    term = term_name.strip().replace(' ', '_').replace('.', '-').lower()
+    term=term.replace('(','_').replace(')','_').replace('@','_').replace('+','-_')
+    return term
+
+def get_user_token(user_dn):
+        config = kamaki_config.Config()
+        return config.get_cloud("default", "token")
index e69de29..7823cbb 100644 (file)
@@ -0,0 +1,30 @@
+
+"""
+This it the entry point for paste deploy .
+
+Paste config file needs to point to egg:<package name>:<entrypoint name>:
+
+use = egg:snfOCCI#sample_app
+
+sample_app entry point is defined in setup.py:
+
+entry_points='''
+[paste.app_factory]
+sample_app = snf_voms:main
+''',
+
+which point to this function call (<module name>:function).
+"""
+
+# W0613:unused args
+# pylint: disable=W0613
+
+from snfOCCI import APIserver
+
+
+#noinspection PyUnusedLocal
+def main(global_config, **settings):
+    """
+This is the entry point for paste into the OCCI OS world.
+"""
+    return APIserver.MyAPP()
\ No newline at end of file
diff --git a/snfOCCI/__init__.pyc b/snfOCCI/__init__.pyc
new file mode 100644 (file)
index 0000000..a0d34ad
Binary files /dev/null and b/snfOCCI/__init__.pyc differ
index 4dba0b1..48afc10 100644 (file)
@@ -1,11 +1,19 @@
 SERVER_CONFIG = {
     'port': 8888,
-    'hostname': '',
-    'compute_arch': ''
+    'hostname': '$vm_hostname$',
+    'compute_arch': 'x86'
     }
 
 KAMAKI_CONFIG = {
-    'compute_url': ''
+    'compute_url': 'https://cyclades.okeanos.grnet.gr/compute/v2.0/',
+    'astakos_url': 'https://accounts.okeanos.grnet.gr/identity/v2.0/'
 }
         
-    
+VOMS_CONFIG = {
+    'enable_voms' : 'True',           
+    'voms_policy' : '/etc/snf/voms.json',
+    'vomsdir_path' : '/etc/grid-security/vomsdir/',
+    'ca_path': '/etc/grid-security/certificates/',
+    'cert_dir' : '/etc/ssl/certs/',
+    'key_dir' : '/etc/ssl/private/'               
+}
\ No newline at end of file
diff --git a/snfOCCI/config.pyc b/snfOCCI/config.pyc
new file mode 100644 (file)
index 0000000..500cdaa
Binary files /dev/null and b/snfOCCI/config.pyc differ
diff --git a/snfOCCI/httpd/snf_voms-paste.ini b/snfOCCI/httpd/snf_voms-paste.ini
new file mode 100644 (file)
index 0000000..06737ab
--- /dev/null
@@ -0,0 +1,16 @@
+# snf_voms authentication PasteDeploy configuration file
+
+[composite:main]
+use = egg:Paste#urlmap
+/:snf_occiapp
+/v2.0/tokens:authapp
+/v2.0/tenants:tenantapp
+
+[app:snf_occiapp]
+use = egg:snf-occi#snf_occi_app
+
+[app:authapp]
+paste.app_factory = snfOCCI.APIserver:app_factory
+
+[app:tenantapp]
+paste.app_factory = snfOCCI.APIserver:tenant_app_factory
\ No newline at end of file
diff --git a/snfOCCI/httpd/snf_voms.py b/snfOCCI/httpd/snf_voms.py
new file mode 100644 (file)
index 0000000..78d1901
--- /dev/null
@@ -0,0 +1,12 @@
+import os
+
+from paste import deploy
+
+import logging 
+
+LOG = logging.getLogger(__name__)
+
+# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
+# The following is a reference to Python Paste Deploy documentation
+# http://pythonpaste.org/deploy/
+application = deploy.loadapp('config:/home/synnefo/snf_voms-paste.ini')
diff --git a/snfOCCI/httpd/snf_voms_auth-paste.ini b/snfOCCI/httpd/snf_voms_auth-paste.ini
new file mode 100644 (file)
index 0000000..1b853f5
--- /dev/null
@@ -0,0 +1,12 @@
+# snf_voms authentication PasteDeploy configuration file
+
+[composite:main]
+use = egg:Paste#urlmap
+/v2.0/tokens:authapp
+/v2.0/tenants:tenantapp
+
+[app:authapp]
+paste.app_factory = snfOCCI.APIserver:app_factory
+
+[app:tenantapp]
+paste.app_factory = snfOCCI.APIserver:tenant_app_factory
\ No newline at end of file
diff --git a/snfOCCI/httpd/snf_voms_auth.py b/snfOCCI/httpd/snf_voms_auth.py
new file mode 100644 (file)
index 0000000..4210f73
--- /dev/null
@@ -0,0 +1,12 @@
+import os
+
+from paste import deploy
+
+import logging 
+
+LOG = logging.getLogger(__name__)
+
+# NOTE(ldbragst): 'application' is required in this context by WSGI spec.
+# The following is a reference to Python Paste Deploy documentation
+# http://pythonpaste.org/deploy/
+application = deploy.loadapp('config:/home/synnefo/snf_voms_auth-paste.ini')
\ No newline at end of file
index 9bacb97..e6d09d2 100644 (file)
@@ -2,6 +2,8 @@ from kamaki.clients.compute import ComputeClient
 from kamaki.clients.cyclades import CycladesClient
 from kamaki.cli.config  import Config
 
+from snfOCCI.config import SERVER_CONFIG
+
 from occi import registry
 from occi.core_model import Mixin
 from occi.backend import MixinBackend
@@ -15,3 +17,7 @@ class snfRegistry(registry.NonePersistentRegistry):
         resource.identifier = key
 
         super(snfRegistry, self).add_resource(key, resource, extras)
+
+    def set_hostname(self, hostname):
+        hostname = "https://" + SERVER_CONFIG['hostname'] + ":" + str(SERVER_CONFIG['port']) 
+        super(snfRegistry, self).set_hostname(hostname)
\ No newline at end of file
diff --git a/snfOCCI/snf_voms/__init__.py b/snfOCCI/snf_voms/__init__.py
new file mode 100644 (file)
index 0000000..e8c124a
--- /dev/null
@@ -0,0 +1,218 @@
+# Copyright 2012 Spanish National Research Council
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+import M2Crypto
+import ast
+
+from logging import getLogger
+import voms_helper
+from kamaki.clients import ClientError
+
+LOG =  getLogger(__name__)
+
+# Environment variable used to pass the request context
+CONTEXT_ENV = 'snf.context'
+
+
+SSL_CLIENT_S_DN_ENV = "SSL_CLIENT_S_DN"
+SSL_CLIENT_CERT_ENV = "SSL_CLIENT_CERT"
+SSL_CLIENT_CERT_CHAIN_ENV_PREFIX = "SSL_CLIENT_CERT_CHAIN_"
+
+"""Global variables that contain VOMS related paths
+"""
+VOMS_POLICY = "/etc/snf/voms.json"
+VOMSDIR_PATH = "/etc/grid-security/vomsdir/"
+CA_PATH = "/etc/grid-security/certificates/"
+VOMSAPI_LIB = "/usr/lib/libvomsapi.so.1"
+PARAMS_ENV = 'snf_voms.params'
+
+class VomsAuthN():
+    """Filter that checks for the SSL data in the reqest.
+
+    Sets 'ssl' in the context as a dictionary containing this data.
+    """
+    
+    def __init__(self, *args, **kwargs):
+        
+       
+        # VOMS stuff
+        try:
+            self.voms_json = json.loads(
+                open(VOMS_POLICY).read())
+        except ValueError:
+            raise ClientError(
+                'Bad Formatted VOMS json',
+                details='The VOMS json data was not corectly formatted in file %s' % VOMS_POLICY)
+        except:
+            raise ClientError(
+                              'No loading of VOMS json file',
+                details='The VOMS json file located in %s was not loaded' % VOMS_POLICY)
+        
+        self._no_verify = False
+
+        #super(VomsAuthN, self).__init__(*args, **kwargs)
+
+    @staticmethod
+    def _get_cert_chain(ssl_info):
+        """Return certificate and chain from the ssl info in M2Crypto format"""
+
+        cert = M2Crypto.X509.load_cert_string(ssl_info.get("cert", ""))
+        chain = M2Crypto.X509.X509_Stack()
+        for c in ssl_info.get("chain", []):
+            aux = M2Crypto.X509.load_cert_string(c)
+            chain.push(aux)
+        return cert, chain
+
+    def _get_voms_info(self, ssl_info):
+        """Extract voms info from ssl_info and return dict with it."""
+
+        try:
+            cert, chain = self._get_cert_chain(ssl_info)
+        except M2Crypto.X509.X509Error:
+            raise ClientError(
+                              'SSL data not verified',
+                              details=CONTEXT_ENV)
+       
+        with voms_helper.VOMS(VOMSDIR_PATH,
+                              CA_PATH, VOMSAPI_LIB) as v:
+            if self._no_verify:
+                v.set_no_verify()
+               
+            voms_data = v.retrieve(cert, chain)
+            
+            if not voms_data:
+                raise VomsError(v.error.value)
+
+            d = {}
+            for attr in ('user', 'userca', 'server', 'serverca',
+                         'voname',  'uri', 'version', 'serial',
+                         ('not_before', 'date1'), ('not_after', 'date2')):
+                if isinstance(attr, basestring):
+                    d[attr] = getattr(voms_data, attr)
+                else:
+                    d[attr[0]] = getattr(voms_data, attr[1])
+
+            d["fqans"] = []
+            for fqan in iter(voms_data.fqan):
+                if fqan is None:
+                    break
+                d["fqans"].append(fqan)
+
+        return d
+
+    @staticmethod
+    def _split_fqan(fqan):
+        """
+        gets a fqan and returns a tuple containing
+        (vo/groups, role, capability)
+        """
+        l = fqan.split("/")
+        capability = l.pop().split("=")[-1]
+        role = l.pop().split("=")[-1]
+        vogroup = "/".join(l)
+        return (vogroup, role, capability)
+    
+    def _process_environ(self, environ):
+        
+        LOG.warning("Getting the environment parameters...")
+        # the environment variable CONTENT_LENGTH may be empty or missing
+        try:
+            request_body_size = int(environ.get('CONTENT_LENGTH', 0))
+        except (ValueError):
+            request_body_size = 0
+            raise ClientError(
+                'Not auth method provided',
+                details='The request body is empty, while it should contain the authentication method')
+            
+        request_body = environ['wsgi.input'].read(request_body_size)
+        
+        print request_body
+        
+        request_body = request_body.replace("true","\"true\"")
+        request_body = request_body.replace('"','\'' )  
+        
+        params_parsed = ast.literal_eval(request_body)
+        
+        
+        params = {}
+        for k, v in params_parsed.iteritems():
+            if k in ('self', 'context'):
+                continue
+            if k.startswith('_'):
+                continue
+            params[k] = v
+            
+        
+        environ[PARAMS_ENV] = params
+        print environ[PARAMS_ENV]
+
+    def is_applicable(self, environ):
+        """Check if the request is applicable for this handler or not"""
+        print "Checking if the request is applicable for this handler or not..."
+        self._process_environ(environ)
+        params = environ.get(PARAMS_ENV, {})
+        auth = params.get("auth", {})
+        if "voms" in auth:
+            if "true" in auth["voms"]:
+                return True
+            else:
+                raise ClientError(
+                'Error in json',
+                details='Error in JSON, voms must be set to true')
+            
+        return False
+
+
+    def authenticate(self,ssl_data):
+        
+        try:
+            voms_info = self._get_voms_info(ssl_data)
+        except VomsError as e:
+            raise e
+        user_dn = voms_info["user"]
+        user_vo = voms_info["voname"]
+        user_fqans = voms_info["fqans"] 
+        
+        return user_dn, user_vo, user_fqans 
+
+          
+    def process_request(self, environ):
+        
+        print "Inside process_Request at last!!!!"
+        if not self.is_applicable(environ):
+            return self.application
+
+        ssl_dict = {
+            "dn": environ.get(SSL_CLIENT_S_DN_ENV, None),
+            "cert": environ.get(SSL_CLIENT_CERT_ENV, None),
+            "chain": [],
+        }
+        for k, v in environ.iteritems():
+            if k.startswith(SSL_CLIENT_CERT_CHAIN_ENV_PREFIX):
+                ssl_dict["chain"].append(v)
+
+        voms_info = self._get_voms_info(ssl_dict)
+
+        params  = environ[PARAMS_ENV]
+        
+        tenant_from_req = params["auth"].get("tenantName", None)
+        
+        print voms_info, tenant_from_req
+        user_dn = voms_info["user"]
+        user_vo = voms_info["voname"]
+        user_fqans = voms_info["fqans"] 
+        environ['REMOTE_USER'] = user_dn
+        
+        return user_dn, user_vo, user_fqans
diff --git a/snfOCCI/snf_voms/voms_helper.py b/snfOCCI/snf_voms/voms_helper.py
new file mode 100644 (file)
index 0000000..c84beb9
--- /dev/null
@@ -0,0 +1,104 @@
+# Copyright 2012 Spanish National Research Council
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import ctypes
+
+#import M2Crypto
+
+
+class _voms(ctypes.Structure):
+    _fields_ = [
+        ("siglen", ctypes.c_int32),
+        ("signature", ctypes.c_char_p),
+        ("user", ctypes.c_char_p),
+        ("userca", ctypes.c_char_p),
+        ("server", ctypes.c_char_p),
+        ("serverca", ctypes.c_char_p),
+        ("voname", ctypes.c_char_p),
+        ("uri", ctypes.c_char_p),
+        ("date1", ctypes.c_char_p),
+        ("date2", ctypes.c_char_p),
+        ("type", ctypes.c_int32),
+        ("std", ctypes.c_void_p),
+        ("custom", ctypes.c_char_p),
+        ("datalen", ctypes.c_int32),
+        ("version", ctypes.c_int32),
+        ("fqan", ctypes.POINTER(ctypes.c_char_p)),
+        ("serial", ctypes.c_char_p),
+        ("ac", ctypes.c_void_p),
+        ("holder", ctypes.c_void_p),
+    ]
+
+
+class _vomsdata(ctypes.Structure):
+    _fields_ = [
+        ("cdir", ctypes.c_char_p),
+        ("vdir", ctypes.c_char_p),
+        ("data", ctypes.POINTER(ctypes.POINTER(_voms))),
+        ("workvo", ctypes.c_char_p),
+        ("extra_data", ctypes.c_char_p),
+        ("volen", ctypes.c_int32),
+        ("extralen", ctypes.c_int32),
+        ("real", ctypes.c_void_p),
+    ]
+
+
+class VOMS(object):
+    """Context Manager for VOMS handling"""
+
+    def __init__(self, vomsdir_path, ca_path, vomsapi_lib):
+        self.VOMSApi = ctypes.CDLL(vomsapi_lib)
+        self.VOMSApi.VOMS_Init.restype = ctypes.POINTER(_vomsdata)
+
+        self.VOMSDIR = vomsdir_path
+        self.CADIR = ca_path
+
+        self.vd = None
+
+    def __enter__(self):
+        self.vd = self.VOMSApi.VOMS_Init(self.VOMSDIR, self.CADIR).contents
+        return self
+
+    def set_no_verify(self):
+        """Skip verification of AC.
+
+        This method skips the AC signature verification, this it should
+        only be used for debugging and tests.
+        """
+
+        error = ctypes.c_int32(0)
+        self.VOMSApi.VOMS_SetVerificationType(0x040,
+                                              ctypes.byref(self.vd),
+                                              ctypes.byref(error))
+
+    def retrieve(self, cert, chain):
+        """Retrieve VOMS credentials from a certificate and chain."""
+
+        self.error = ctypes.c_int32(0)
+
+        cert_ptr = ctypes.cast(long(cert._ptr()), ctypes.c_void_p)
+        chain_ptr = ctypes.cast(long(chain._ptr()), ctypes.c_void_p)
+
+        res = self.VOMSApi.VOMS_Retrieve(cert_ptr,
+                                         chain_ptr,
+                                         0,
+                                         ctypes.byref(self.vd),
+                                         ctypes.byref(self.error))
+        if res == 0:
+            return None
+        else:
+            return self.vd.data.contents.contents
+
+    def __exit__(self, type, value, tb):
+        self.VOMSApi.VOMS_Destroy(ctypes.byref(self.vd))
\ No newline at end of file