User Tools

Site Tools


Sidebar

cn:ccr:cloud:autenticazione_openstack:autenticazione_aai

Autenticazione AAI in OpenStack

A bari, nel contesto delle attività di PRISMA, stiamo cercando di implementare l'autenticazione basata su AAI.

1. Introduzione

Il presente documento descrive la proposta di integrazione della autenticazione AAI nella dashboard di Openstack.

1.1. Requisiti tecnici

Prima di illustrare i passi necessari all’integrazione è bene specificare i requisiti tecnici necessari.

  1. Installazione di Openstack con i servizi keystone,horizon, ecc… attivi
  2. Installazione djangosaml2 come libreria python per poter comunicare con un idp in SAML
  3. Configurazione djangosaml2
  4. Registrazione dei metadata del serviceprovider su Idp infnAAI

Per le istruzioni relative il punto 1 si rimanda al sito ufficiale di Openstack. (Installazione Devstack)

1.1.1. Installazione djangosaml2

L’autenticazione AAI si espone tramite un Identity Provider SAML, pertanto è necessario integrare in openstack un modulo saml.

Si nota che è stato usato come idp AAI il seguente: https://idp.infn.it/testing/saml2/idp/metadata.php?output=xhtml

Come libreria è stata usata la djangosaml2, scritta in python come openstack.

Sulla macchina su cui è installato openstack, nel nostro caso è openstack-01.ba.infn.it è necessario installare i pre-requisiti di djangosaml2.

  1. apt-get install xmlsec1
  2. easy_install djangosaml2
  3. apt-get install python-django

1.1.2. Modifica file settings.py

E’ necessario integrare il file settings.py (/usr/share/openstack-dashboard/ openstack_dashboard/settings.py) con il seguente.

AUTHENTICATION_BACKENDS = ( 'openstack_auth.backend.KeystoneBackend',) SESSION_EXPIRE_AT_BROWSER_CLOSE = True

from os import path import saml2

BASEDIR = path.dirname(path.abspath(file))

SAML_CONFIG = {

# full path to the xmlsec1 binary programm
'xmlsec_binary': '/usr/bin/xmlsec1',
# your entity id, usually your subdomain plus the url to the metadata view
'entityid': 'http://openstack-01.ba.infn.it/horizon/metadata/',
  "name":"SAML-SP",

# directory with attribute mapping
'attribute_map_dir': '/usr/local/lib/python2.7/dist-packages/djangosaml2-0.10.0-py2.7.egg/djangosaml2/tests/attribute-maps',
'attribute_map':['removeurnprefix','oid2name','name2oid'],
'attribute_name_format':'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',

"service":
{     
   "sp":
      {
       "name" : "SAML-AAI SP",
      "logout_requests_signed": "true",
      'authn_requests_signed': 'true',
      'want_assertions_signed': 'true',
      
      "endpoints":
          {
           "assertion_consumer_service": [("http://openstack-01.ba.infn.it/horizon/acs/",saml2.BINDING_HTTP_POST),                                            
                                          ("http://openstack-01.ba.infn.it/horizon/acs/",saml2.BINDING_HTTP_ARTIFACT),
                                          ("http://openstack-01.ba.infn.it/horizon/acs/",saml2.BINDING_PAOS),                                                                                    
                                          ],
                       
           "single_logout_service": [("http://openstack-01.ba.infn.it/horizon/ls/",saml2.BINDING_HTTP_REDIRECT)                                       
                                     ,("http://openstack-01.ba.infn.it/horizon/ls/",saml2.BINDING_SOAP)
                                     ,("http://openstack-01.ba.infn.it/horizon/ls/",saml2.BINDING_HTTP_POST)
                                     ,("http://openstack-01.ba.infn.it/horizon/ls/",saml2.BINDING_HTTP_ARTIFACT)                                       
                                     ]             
          },
      "required_attributes": ["uid","surname"],
      "optional_attributes": ["surname"],
      
      },
  
     "idp": 
      {         
        "endpoints" :
          {
           "single_sign_on_service" : [("https://idp.infn.it/testing/saml2/idp/SSOService.php",saml2.BINDING_HTTP_REDIRECT)],
            "single_logout_service": [("https://idp.infn.it/testing/saml2/idp/SingleLogoutService.php",saml2.BINDING_HTTP_REDIRECT)                                        
                                      ,("https://idp.infn.it/testing/saml2/idp/SingleLogoutService.php",saml2.BINDING_SOAP)
                                      ,("https://idp.infn.it/testing/saml2/idp/SingleLogoutService.php",saml2.BINDING_HTTP_POST)
                                      ,("https://idp.infn.it/testing/saml2/idp/SingleLogoutService.php",saml2.BINDING_HTTP_ARTIFACT)
                                      ]
            ,"artifact_resolution_service": [("https://idp.infn.it/testing/saml2/idp/ArtifactResolutionService.php",saml2.BINDING_SOAP)]                    
          }, 
      }, 
   },
# where the remote metadata is stored
'metadata': {
           'local': [path.join(BASEDIR, 'configAAI/idp.infn.it-metadata.xml')],
           },
# set to 1 to output debugging information
#'debug': 1,
'debug':False,
# certificate
'key_file':path.join(BASEDIR, 'configAAI/saml.key'),
'cert_file': path.join(BASEDIR, 'configAAI/saml.pem'),

 # you can set multilanguage information here
 'organization': {
     'name': [('Demo aai application', 'it')],
     'display_name': [('Demo login AAI', 'it')],
     'url': [('http://www.infn.it', 'it')],
     },
             
'valid_for': 24,  # how long is our metadata valid
}

SAML_VALID_FOR=0

INSTALLED_APPS = (

  'openstack_dashboard',
  'django.contrib.contenttypes',
  'django.contrib.auth',
  'django.contrib.sessions',
  'django.contrib.messages',
  'django.contrib.staticfiles',
  'django.contrib.humanize',
  'compressor',
  'horizon',
  'openstack_dashboard.dashboards.project',
  'openstack_dashboard.dashboards.admin',
  'openstack_dashboard.dashboards.settings',
  'openstack_auth',
  'django.contrib.admin',
  'djangosaml2',

)   1.1.3. Configurazione cartella configAAI

Sulla macchina openstack-01 nella path: /usr/share/openstack-dashboard/openstack_dashboard/ bisogna creare la cartella che nel nostro esempio abbiamo chiamato configAAI in cui devono esserci i seguenti file:

  • idp.infn.it-metadata.xml
  • saml.key
  • saml.pem

metadata idp, usato idp aai di test presi con il comando: wget -O idp.infn.it-metadata.xml https://idp.infn.it/saml2/idp/metadata.php

Creazione certificato: openssl req -new -x509 -days 3652 -nodes -out saml.crt -keyout saml.key -subj /CN=`hostname -f`/OU=SAML-SP/

Conversione certificato: openssl pkcs12 -export -in saml.crt -inkey saml.key -out saml.p12 openssl pkcs12 -in saml.p12 -nodes -out saml.pem

Nota: i riferimenti di ognuno di essi sono aggiunti nel SAML_CONFIG del settings.py

1.1.4. Configurazione urls.py

Aggiungere nel file urls.py (/usr/share/openstack-dashboard/openstack_dashboard/) nella sezione urlpatterns il seguente url: ,url(r, include('djangosaml2.urls')) Valore originale: urlpatterns = patterns(,

  url(r'^$', 'openstack_dashboard.views.splash', name='splash'),
  url(r'^auth/', include('openstack_auth.urls')),
  url(r'', include(horizon.urls))  )

Valore modificato:

urlpatterns = patterns(, url(r'^$', 'openstack_dashboard.views.splash', name='splash'), url(r'^auth/', include('openstack_auth.urls')), url(r, include(horizon.urls))

  # NUOVO  
  ,url(r'', include('djangosaml2.urls')) )

1.1.5. Registrazione SP su Idp AAI

Mandare in run l’applicazione Openstack e dopo aver verificato al link: 'http://openstack-01.ba.infn.it/horizon/metadata/' i metadata è necessario registrarli all’Idp AAI.

Tramite il form https://idp.infn.it/utils/metadata-send.php aggiungiamo i metadata del SP all'IDP AAI di test.

2. Integrazione

Una volta installati e configurati tutti i pre-requisiti è possibile poter procedere con le fasi di integrazione in openstack.

2.1. Integrazione della Home page con login AAI

Per quanto riguarda la parte grafica è necessario integrare la Home page di OpenStack con il riquadro relativo il login AAI.

Per fare questo è necessario intervenire sulla pagina splash.html (/usr/lip/python2.7/dist-packages/horizon/templates)

Il codice finale è il seguente:

{% load i18n branding %}

<!DOCTYPE html> <html lang="en" xml:lang="en">

<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <title>{% trans "Login" %} - {% site_branding %}</title>
  {% include "_stylesheets.html" %}
</head>
<body id="splash">
  <div class="container">
   	<div class="row large-rounded">
    	{% include 'auth/_loginAAI.html' %}
    	</div>
    	<div class="row large-rounded">
    	{% include 'auth/_login.html' %}
    	</div>      			
  </div>
</body>

</html>

Nella cartella /usr/lip/python2.7/dist-packages/horizon/templates/auth serve creare il file _loginAAI.html il cui contenuto è il seguente:

{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% load url from future %}

{% block modal-header %}{% trans "Log In - INFN AAI" %}{% endblock %} {% block modal_class %}loginAAI {% if hide %}modal hide{% endif %} {% endblock %}

{% block form_action %}{% url 'loginAAI' %}{% endblock %} {% block autocomplete %}horizon_config.password_autocomplete{% endblock %}

{% block modal-body %}

<fieldset>
   Click the box below to login into Openstack with your INFN-AAI credentials
  {% if request.user.is_authenticated and 'next' in request.GET %}
  <div class="control-group clearfix error">
    <span class="help-inline"><p>{% trans "You don't have permissions to access:" %}</p>
      <p><b>{{ request.GET.next }}</b></p>
      <p>{% trans "Login as different user or go back to" %}
      <a href="{% url 'horizon:user_home' %}">{% trans "home page" %}</a></p>
    </span>
  </div>
  {% endif %}
  {% if next %}<input type="hidden" name="{{ redirect_field_name }}" value="{{ next }}" />{% endif %}
  <img align="middle" height="60px" width="80x" alt="Login Infn AAI" src="/static/dashboard/img/logoInfnAAI.png">
</fieldset>

{% endblock %}

{% block modal-footer %}

<button type="submit" class="btn btn-primary pull-right">{% trans "Sign In" %}</button>
<div> Plug-in scritto da <a href="mailto:pasquale.notarangelo@ba.infn.it">Pasquale Notarangelo</a></div>

{% endblock %}

Nota:

  • nella cartella /usr/share/openstack-dashboard/openstack_dashboard/static/dashboard/img/ bisogna aggiungere il logo logoInfnAAI.png
  • nel File css invece serve creare la classe loginAAI e modificare le posizioni della classe login e loginAAI.

Il codice è il seguente:

#splash .login {

background: #fff url(/static/dashboard/img/logo-splash.png) no-repeat center 35px;  
position: absolute;
top: 80px;
left: 65%;
margin: 0 0 0 -195px;
padding-top: 170px;

width: 390px;
border: 1px solid #e1e1e1;
max-height: none;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
-webkit-background-clip: padding-box;
-moz-background-clip: padding-box;
background-clip: padding-box;
form {
  .error {
    width: 100%;
  }
  input {
    width: 350px;
  }
  select {
    width: 360px;
  }
}

}

#splash .loginAAI {

background: #fff url(/static/dashboard/img/logo-splash.png) no-repeat center 35px;
position: absolute;
top: 80px;
left: 30%;  
margin: 0 0 0 -195px;
padding-top: 170px;
 
width: 350px;
border: 1px solid #e1e1e1;
max-height: none;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
-webkit-background-clip: padding-box;
-moz-background-clip: padding-box;
background-clip: padding-box;
form {
  .error {
    width: 100%;
  }
  input {
    width: 350px;
  }
  select {
    width: 360px;
  }
}

}

2.2. Aggiunta dei metodi di login

Fatte queste modifiche la parte puramente grafica è ultimata, ora bisogna collegare l’azione del bottone con le funzioni che redirigono all’Idp e che, tornando da questo con l’autenticazione fatta, chiamino il backEnd Keystone con l’utente loggato che esiste o viene creato in keystone.

2.3. Modifica urls.py

Nel file urls.py (/usr/lib/python2.7/dist-packages/openstack_auth) aggiungere la voce nel urlpattern url(r"^loginAAI/$", "loginAAI", name='loginAAI'),

Valore originale:

urlpatterns = patterns('openstack_auth.views',

  url(r"^login/$", "login", name='login'),
  url(r"^logout/$", 'logout', name='logout'),
  url(r'^switch/(?P<tenant_id>[^/]+)/$', 'switch', name='switch_tenants'),
  url(r'^switch_services_region/(?P<region_name>[^/]+)/$', 'switch_region',
      name='switch_services_region')

)

Valore modificato:

urlpatterns = patterns('openstack_auth.views',

  url(r"^login/$", "login", name='login'),
  
  url(r"^loginAAI/$", "loginAAI", name='loginAAI'),
  
  url(r"^logout/$", 'logout', name='logout'),
  url(r'^switch/(?P<tenant_id>[^/]+)/$', 'switch', name='switch_tenants'),
  url(r'^switch_services_region/(?P<region_name>[^/]+)/$', 'switch_region',
      name='switch_services_region')

)

2.4. Modifica views.py

Nel file views.py (/usr/lib/python2.7/dist-packages/openstack_auth) aggiungere le 4 seguenti funzioni:

• loginAAI • login_AAI • login • logout

Nota: • login e logout vanno a rimpiazzare quelle esistenti. • LoginAAI e login_AAI invece sono nuove • Nel file local_settings.py (/usr/share/openstack-dahboard/openstack_dashboard/local) aggiungere la seguente variabile LOGOUT_REDIRECT_URL='/horizon'

LoginAAI

@sensitive_post_parameters() @csrf_exempt @never_cache def loginAAI(request):

  if request.method == 'POST':
      res=djangosaml2.views.login(request, config_loader_path=None,wayf_template='djangosaml2/wayf.html',authorization_error_template='djangosaml2/auth_error.html')
      return res
  return shortcuts.redirect(settings.LOGIN_REDIRECT_URL)

Login_AAI

def login_AAI(request,username):

  
  try:
      if request.method == 'POST':
          
          """ Istanzio KeystoneClient col Username e Password dell'utente admin """            
          from keystoneclient.v2_0 import client
          insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)       
          usernameAdmin='adminAAI'
          password='adminAAI'
          tenant_name='admin'
          auth_url=settings.OPENSTACK_KEYSTONE_URL
          new_client = client.Client(username=usernameAdmin, password=password,tenant_name=tenant_name, auth_url=auth_url,insecure=insecure)
          url = settings.OPENSTACK_KEYSTONE_URL + "/tokens"
             
          params = {"auth": {"aai_auth": 'AAI',"username":username}}
          headers = {"auth": {"aai_auth": 'AAI',"username":username}}
          
          resp, body = new_client.request(url, 'POST', body=params, headers=headers)
          raw_token=body['access']
          LOG.info('------ raw_token: %s'% raw_token)                     
                     
          from keystoneclient.v2_0.tokens import Token, TokenManager
                  
          dictToken=raw_token.get('token','')
          expires=dictToken.get('expires','')
          idToken=dictToken.get('id','')
             
          unscoped_token_data={"token": {'expires': expires, 'id': idToken}}
          unscoped_token = Token(TokenManager(None),unscoped_token_data,loaded=True)
         
          """ ESTRAGGO I TENANTS DELL'UTENTE LOGGATO """
          clienTenant = client.Client(token=unscoped_token.id,auth_url=settings.OPENSTACK_KEYSTONE_URL,insecure=insecure)
         tenants = clienTenant.tenants.list()
         
          while tenants:
              tenant = tenants.pop()                 
              tenant=tenant.id                                   
              try:
                  client = keystone_client.Client(tenant_id=tenant,token=unscoped_token.id,auth_url=settings.OPENSTACK_KEYSTONE_URL,insecure=insecure)
                  token = client.tokens.authenticate(username=username,token=unscoped_token.id,tenant_id=tenant)
                  break
              except (keystone_exceptions.ClientException,keystone_exceptions.AuthorizationFailure):
                  token = None
                             
          if token is None:
               msg = _("Unable to authenticate to any available projects.")
               raise KeystoneAuthException(msg)
                
          user = create_user_from_token(request,token,client.service_catalog.url_for())
          
          from openstack_auth.utils import check_token_expiration, is_ans1_token
          import hashlib
          if is_ans1_token(unscoped_token.id):
              hashed_token = hashlib.md5(unscoped_token.id).hexdigest()
              unscoped_token._info['token']['id'] = hashed_token
                  
          request.session['unscoped_token'] = unscoped_token.id
          request.user = user
                           
          KEYSTONE_CLIENT_ATTR = "_keystoneclient" 
          # Support client caching to save on auth calls.
          setattr(request, KEYSTONE_CLIENT_ATTR, new_client)
          #LOG.info("OPENSTACK_AUTH.VIEWS Mi preparo a caricare il backEnd")
          from django.contrib.auth import get_backends 
              
          for backend in get_backends():         
              LOG.info('------ BACKEND Estratto - FOR: %s'% backend)
              
              """ Setto il BackEnd all'oggetto user """                                                                
              user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
                                
              auth_login(request, user)
              if request.user.is_authenticated():
                  set_session_from_user(request, request.user)
                  request.session['region_endpoint'] = request.user.endpoint
                  request.session['region_name'] = "Default Region"
              return shortcuts.redirect(settings.LOGIN_REDIRECT_URL)
  except Exception as ecc:
      LOG.error('Login failed for user "%s" - Eccezzione: %s' % (username,ecc))
      return HttpResponseForbidden('Incorrect login data.')

LOGIN

@sensitive_post_parameters() @csrf_exempt @never_cache def login(request):

  if request.method == 'POST':
      username = request.POST.get('username')
      password = request.POST.get('password')
      region = request.POST.get('region')
      tenant = request.POST.get('tenant')
      if not tenant:
          tenant = None
      try:
          user = authenticate(request=request,
                              username=username,
                              password=password,
                              tenant=tenant,
                              auth_url=region)
          
          LOG.warning( 'Login successful for user "%s".' % username )
      except KeystoneAuthException as exc:
          LOG.warning( 'Login failed for user "%s".' % username )
          request.session.flush()
          return HttpResponseForbidden('Incorrect login data.')
      
      auth_login(request, user)
      if request.user.is_authenticated():
          set_session_from_user(request, request.user)
          request.session['region_endpoint'] = request.user.endpoint
          request.session['region_name'] = "Default Region"
      return shortcuts.redirect( settings.LOGIN_REDIRECT_URL)

LOGOUT

def logout(request):

  """ Cancello il file pickle, se Esiste """
  import os
  idFile=str(request.COOKIES.get('csrftoken',''))
  if idFile != '':
      nomeFile='/tmp/data_%s.pkl' % idFile
      if os.path.exists(nomeFile):
          os.remove(nomeFile)    
                  
  msg = 'Logging out user "%(username)s".' % \
      {'username': request.user.username}
  LOG.info(msg)
  if 'token_list' in request.session:
      t = Thread(target=delete_all_tokens,args=(list(request.session['token_list']),))
      t.start()
  """ Securely logs a user out. """
  auth_logout(request)
  return shortcuts.redirect(settings.LOGOUT_REDIRECT_URL)

Nota: in testa al file views.py sostituire/integrare i vecchi import con i seguenti:

import logging from threading import Thread from django import shortcuts from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login as auth_login, logout as auth_logout from django.contrib.auth.decorators import login_required from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt try:

  from django.utils.http import is_safe_url

except ImportError:

  from .utils import is_safe_url

from keystoneclient.v2_0 import client as keystone_client from keystoneclient import exceptions as keystone_exceptions from .forms import Login from .user import set_session_from_user, create_user_from_token from .exceptions import KeystoneAuthException

from django.http import HttpResponse, HttpResponseForbidden LOG = logging.getLogger(name) import djangosaml2 from django.contrib.auth.views import (logout_then_login as django_logout)

2.5. Modifica urls.py

Nel file urls.py (/usr/local/lib/python2.7/dist-packages/djangosaml2-0.10.0-py2.7.egg/djangosaml2) aggiungere il riferimento alla funzione AAI_assertion_consumer_service:

urlpatterns = patterns(

  'djangosaml2.views',
  url(r'^login/$', 'login', name='saml2_login'),
  
  #url(r'^acs/$', 'assertion_consumer_service', name='saml2_acs'),
  url(r'^acs/$', 'AAI_assertion_consumer_service', name='saml2_acs'),
 
  url(r'^logout/$', 'logout', name='saml2_logout'),
  url(r'^ls/$', 'logout_service', name='saml2_ls'),
  url(r'^metadata/$', 'metadata', name='saml2_metadata'),

)

2.6. Modifica views.py

Nel file views.py (/usr/local/lib/python2.7/dist-packages/djangosaml2-0.10.0-py2.7.egg/djangosaml2) aggiungere la funzione AAI_assertion_consumer_service:

@require_POST @csrf_exempt def AAI_assertion_consumer_service(request,config_loader_path=None,attribute_mapping=None,create_unknown_user=None):

  conf = get_config(config_loader_path, request)
   
  if 'SAMLResponse' not in request.POST:
      return HttpResponseBadRequest('Couldn\'t find "SAMLResponse" in POST data.')
   
  post = {'SAMLResponse': request.POST['SAMLResponse']}
  client = Saml2Client(conf, identity_cache=IdentityCache(request.session),logger=logger)
        
  oq_cache = OutstandingQueriesCache(request.session)
  outstanding_queries = oq_cache.outstanding_queries()
   
  # process the authentication response
  response = client.response(post, outstanding_queries)
   
 if response is None:
      logger.error('SAML response is None')
      return HttpResponseBadRequest("SAML response has errors. Please check the logs")

session_id = response.session_id() oq_cache.delete(session_id)

  # authenticate the remote user
  session_info = response.session_info()    

try:

      """ Estraggo lo username AAI dalla variabile Session_info """
      username=''
      d=session_info                      
      ava=d.get('ava','not found')
      if ava != 'not found':
          username=ava['uid'][0]
             
          if username != '':
              logger.info('USERNAME estratto da SessionInfo e pieno: %s' % username)
                 
              username=username+'_AAI'
              logger.info('USERNAME concatenato: %s' % username)
              import openstack_auth.views as ov
              user=ov.login_AAI(request, username)
              return user                 
           else:
              logger.error('username AAI e vuoto')
              return HttpResponseForbidden("Permission denied AAI")
      else:
          logger.error('username AAI e'' vuoto')
          return HttpResponseForbidden("Permission denied AAI")
            
  except Exception,ecc:
      logger.info('***** ECCEZIONE DjangoSaml2. AAI_assertion_consumer_service: %s' % str(ecc))
      return HttpResponseForbidden("Permission denied AAI")

2.7. Modifica Middleware - core.py

Nel file core.py (usr/lib/python2.7/dist-packages/keystone/middleware) è stato aggiunta la classe AAIMiddlewareAuth.

Lo scopo principale per cui è stato implementato un middleware in Keystone è dovuto al vantaggio di aggiungere uno strato disaccoppiato da keystone, ma che lo utilizza come servizio.

In particolare, tramite il json formattato nella request, nello strato viene riconosciuto se si tratta di autenticazione aai o meno. Nel caso il flag sia settato, viene recuperato l’username e settata la variabile REMOTE_USER che indica a keystone che si tratta di autenticazione esterna. Pertanto keystone darà per autenticato l’utente, procedura già avvenuta sull’idp di AAI. In caso contrario la variabile REMOTE_USER non è settata perciò keystone condurrà la procedura di autenticazione solita.

Nel middleware, una volta verificato che si tratta di un utente derivante da autenticazione AAI, viene controllato se esiste o meno su keystone. Questo è necessario poiché come pre-requisito di autenticazione esterna keystone necessita che l’utente esista. Pertanto, se non esiste, viene creato tramite le identity api.

In fase di creazione dell’utente è stato deciso di associarlo ad un tenant di default (tenantAAI) con il ruolo di member.

Opzione alternativa, in fase di studio, sarebbe il mapping delle responsabilità /afferenze estratte da AAI (lette dalla risposta saml dell’idp o interrogando ldap) con i tenant corrispondenti in openstack. Il principale problema è interpretare le appartenenze aai e mapparle correttamente in openstack, valutando anche il ruolo da assegnare all’utente per ogni tenant.

Nota, operazioni preliminari da fare sul repository keystone sono: • creare un utente amministratore con username 'adminAAI', password 'adminAAI' associato al tenant admin. Questo è un utente di servizio con cui il plug-in invoca il middleware. • Creare un tanant: tenantAAI che sarà usato come tenant di default iniziale per gli utenti AAI in fase di loro creazione automatica

  class AAIMiddlewareAuth(wsgi.Middleware):

  
  def __init__(self, *args, **kwargs):
      super(AAIMiddlewareAuth, self).__init__(*args, **kwargs)
  
  def process_request(self, request):
      try:
         """ {'HTTP_AUTH': {'username': 'notarangelo_AAI', 'aai_auth': 'AAI'} ... """
                      """ Estraggo/Cast dal json della Request il flag AAI in aai_auth """ 
          httpAuth=request.environ['HTTP_AUTH']            
          """ Converto in dict la stringa """
          httpAuth=eval(str(httpAuth))
                                                      
          if httpAuth is not None:
              aai_auth=httpAuth.get('aai_auth','not found')
              if aai_auth != 'not found':
                  if aai_auth == 'AAI':
                      """ Estraggo username """
                      username=httpAuth.get('username','not found')
                      if username != 'not found':
                          
                          """ Controllo ulteriore: che finisca con il suffisso _AAI """
                          suffisso=username[-4:]
                          suffisso=suffisso.strip()
                         
                          if suffisso=='_AAI':
                              """ Controllo se l'utente esiste in Keystone, lo creo se non esiste """
                              utenteKeystone=self.createUserAAI(username)
                              if utenteKeystone is not None:
                                  LOG.info('---- SETTO REMOTE_USER')
                                  request.environ['REMOTE_USER'] = username
                              else:
                                  LOG.info('---- NON SETTO REMOTE_USER')
                          else:
                              LOG.info('---- USername non contiene suffisso AAI')                             
                      else:
                          LOG.debug(" http_auth NON contiente username")                        
                  else:
                      LOG.debug(" http_auth NON contiente AAI")                                    
          else:
              LOG.debug("Environment NON contiene http_auth")
 except Exception,ecc:
          LOG.info('***** ECCEZIONE AAI MiddlewareAuth : %s' % str(ecc))

def createUserAAI(self,username):

      LOG.info('Create User AAI')
      utenteKeystone=None
       
      import uuid
      from keystone import exception
      from keystone import identity
        
      identity_api = identity.Manager()
       
      defaultDomain="default"                             
      tenant_AAI_name='tenantAAI'
        
      LOG.info("Controllo che l\'utente: %s  Esista o meno in Keystone"% username)
      try:
          """ Controllo se l'utente estratto da AAI esiste in keystone """
          utenteKeystone=identity_api.get_user_by_name(identity_api,username,defaultDomain)                                                                                    
      except exception.UserNotFound as exc:
          LOG.warning('ECCEZZIONE "%s".' % exc )
          LOG.info('******** Devo creare l\'utente in Keystone, non esiste')                
          try:                
              """ Creo l'utente estratto da AAI in Keystone """
              user_id = uuid.uuid4().hex
              utente = {"id": user_id,"name": username,"enabled": True,"domain_id": defaultDomain}
              utenteKeystone= identity_api.create_user(identity_api,user_id,utente)
               
              LOG.info("**** Creato utente con RandomId: %s - USERNAME: %s" %(user_id, username))
                
              """ Assegno l'utente AAI al tenant di default tenantAAI """                    
              if utenteKeystone is not None:                        
                  """ Recupero id del Tenant AAI """
                  tenant_AAI_id=identity_api.get_project_by_name(identity_api,tenant_AAI_name,defaultDomain)
                  tenant_AAI_id=tenant_AAI_id.get('id','NO')
                  LOG.info('**** Id Tenant AAI: %s' % tenant_AAI_id)
                  """ Assegno l'utente AAI al tenant di default tenantAAI """
                  identity_api.add_user_to_project(identity_api,tenant_AAI_id,user_id)
                  LOG.info("Assegnato user %s al tenant id: %s - tenant name: %s " % (username, tenant_AAI_id,tenant_AAI_name))                    
          except Exception as exc:
              LOG.warning('ECCEZZIONE "%s".' % exc )
              utenteKeystone=None                
      finally:                                 
          LOG.info("Utente estratto da Keystone: %s "% utenteKeystone)
      
      return utenteKeystone

Dopo aver creato la classe è necessario modificare il keystone.conf (etc/keystone)

  • Unordered List ItemAggiunta :

[filter:aai_auth] paste.filter_factory = keystone.middlewareAAIMiddlewareAuth.factory

  • Unordered List ItemModifica public_api aggiungendo my_auth:

[pipeline:public_api] pipeline = access_log sizelimit stats_monitoring url_normalize aai_auth token_auth admin_token_auth xml_body json_body debug ec2_extension user_crud_extension public_service

Proposte e sviluppi futuri

Permettere autenticazione AAI anche da shell

Associare automaticamente l’utente AAI a tenant «derivati» da AAI in openstack

Problemi:

  • estrarre le sigle (da SAML/Ldap) correttamente
  • mappare le sigle in tenant openstack

AAI fornisce una lista di tutti i possibili Tenant (Tenant = VO = Virtual Organizations = sigle di esperimento/gruppo/servizi)

L’admin di Opestack crea i Tenants con uno script, scegliendo quelli che vuole creare e quelli invece che non vuole creare. Inoltre deve essere facile raggruppare diverse sigle in un tenant di ordine superiore. Per esempio per i piccoli esperimenti di gruppo V non si crea un Tenant per esperimento, ma uno complessivo per tutta la CSN5.

AAI deve avere degli attributi specifici per la cloud (differenti da tutto il resto)

  • cloud:i:infn:ba:csn7:biovel:ruolo:admin
  • cloud:s:csn7:biovel:ruolo:admin
  • cloud:i:infn:ba:csn7:biovel:ruolo:simpleuser

Non è automatico che il responsabile scientifico di un esperimento abbia accesso ad Openstack

Possibili attributi specifici Nessun attributo specifico: non viene consentito l’accesso Simpleuser (utente normale): viene autorizzato a fare solo certe cose Admin: può anche svolgere attività di gestione

Al login il sistema:

Caso 1 (utente non esiste in openstack):

  • Viene creato l’utente in openstack (con username letto da aai formattato: username_AAI)
  • Vengono estratti i nomi delle afferenze aai (dal saml/ldap)
  • Vengono estratti, dal file di configurazione, i tenants openstack corrispondenti alle afferenze aai
  • Mapping afferenze aai con i tenant openstack corrispondenti (dal file di configurazione)
  • Associazione utente ai tenants openstack
  • Creazione della storia dell’Utente

Caso 2 (utente esiste in opensack):

  • Vengono estratti i nomi delle afferenze aai (dal saml/ldap)
  • Vengono estratti, dal file di configurazione, i tenants openstack corrispondenti alle afferenze aai
  • Mapping afferenze aai con i tenant openstack corrispondenti (dal file di configurazione)
  • Aggiornamento della storia e Allineamento delle associazioni dell’utente ai relativi tenants openstack (intendiamo gestire quindi nuovi tenants a cui appartiene e rimuoverlo da quelli a cui non afferisce più dinamicamente)

Esempio:

Info lette da AAI per l’utente Rossi_AAI: appartiene a CMS,Alice in aai Appartiene a Finuda,Alice in openstack

Il sistema quindi aggiungerà Rossi_AAI al tenant corrispondente a CMS Lascerà rossi_AAI al tenant corripondente ad Alice Disabiliterà rossi_AAI al tenant corrispondente a Finuda

cn/ccr/cloud/autenticazione_openstack/autenticazione_aai.txt · Last modified: 2014/01/09 22:51 by aiftim@infn.it