Add attribute mapping for user information
authorSimo Sorce <simo@redhat.com>
Thu, 9 Oct 2014 18:44:04 +0000 (14:44 -0400)
committerPatrick Uiterwijk <puiterwijk@redhat.com>
Fri, 24 Oct 2014 16:03:18 +0000 (18:03 +0200)
When user information is retrieved we map any wellknown data to a
standardized set of names.
A ne InfoMapping class takes cares of helping the info modules to
map the data they retrieve so that providers can find it in wellknown
attribute names for further use.
Mapping of attribute names for diplay purposes is also provided.

Signed-off-by: Simo Sorce <simo@redhat.com>
Reviewed-by: Patrick Uiterwijk <puiterwijk@redhat.com>
ipsilon/info/common.py
ipsilon/info/infoldap.py
ipsilon/info/nss.py
ipsilon/login/authldap.py
ipsilon/login/authtest.py
ipsilon/login/common.py
ipsilon/providers/openid/auth.py
ipsilon/providers/openidp.py
ipsilon/providers/saml2/auth.py
templates/openid/consent_form.html

index c4be8fe..92a3ba2 100755 (executable)
@@ -47,6 +47,48 @@ class InfoProviderBase(PluginObject, Log):
         self.debug('Info plugin disabled: %s' % self.name)
 
 
+class InfoMapping(Log):
+
+    def __init__(self):
+        self.standard_attributes = {
+            'fullname': 'Full Name',
+            'nickname': 'Nickname',
+            'surname': 'Last Name',
+            'firstname': 'First Name',
+            'title': 'Title',
+            'dob': 'Date of Birth',
+            'email': 'E-mail Address',
+            'gender': 'Gender',
+            'postcode': 'Postal Code',
+            'street': 'Street Address',
+            'state': 'State or Province',
+            'country': 'Country',
+            'phone': 'Telephone Number',
+            'language': 'Language',
+            'timezone': 'Time Zone',
+        }
+        self.mapping = dict()
+
+    def set_mapping(self, attrs_map):
+        self.mapping = attrs_map
+
+    def display_name(self, name):
+        if name in self.standard_attributes:
+            return self.standard_attributes[name]
+        return name
+
+    def map_attrs(self, attrs):
+        s = dict()
+        e = dict()
+        for a in attrs:
+            if a in self.mapping:
+                s[self.mapping[a]] = attrs[a]
+            else:
+                e[a] = attrs[a]
+
+        return s, e
+
+
 FACILITY = 'info_config'
 
 
index 6d710bd..fb1c121 100755 (executable)
@@ -6,15 +6,33 @@
 
 from ipsilon.info.common import InfoProviderBase
 from ipsilon.info.common import InfoProviderInstaller
+from ipsilon.info.common import InfoMapping
 from ipsilon.util.plugin import PluginObject
 from ipsilon.util.log import Log
 import ldap
 
 
+# TODO: fetch mapping from configuration
+ldap_mapping = {
+    'cn': 'fullname',
+    'commonname': 'fullname',
+    'sn': 'surname',
+    'mail': 'email',
+    'destinationindicator': 'country',
+    'postalcode': 'postcode',
+    'st': 'state',
+    'statetorprovincename': 'state',
+    'streetaddress': 'street',
+    'telephonenumber': 'phone',
+}
+
+
 class InfoProvider(InfoProviderBase, Log):
 
     def __init__(self):
         super(InfoProvider, self).__init__()
+        self.mapper = InfoMapping()
+        self.mapper.set_mapping(ldap_mapping)
         self.name = 'ldap'
         self.description = """
 Info plugin that uses LDAP to retrieve user data. """
@@ -92,24 +110,48 @@ Info plugin that uses LDAP to retrieve user data. """
 
         return conn
 
-    def get_user_data_from_conn(self, conn, dn):
+    def _get_user_data(self, conn, dn):
         result = conn.search_s(dn, ldap.SCOPE_BASE)
         if result is None or result == []:
             raise Exception('User object could not be found!')
         elif len(result) > 1:
             raise Exception('No unique user object could be found!')
-        return result[0][1]
+        data = dict()
+        for name, value in result[0][1].iteritems():
+            if type(value) is list and len(value) == 1:
+                value = value[0]
+            data[name] = value
+        return data
+
+    def _get_user_groups(self, conn, dn, ldapattrs):
+        # TODO: fixme to support RFC2307bis schemas
+        if 'memberuid' in ldapattrs:
+            return ldapattrs['memberuid']
+        else:
+            return []
+
+    def get_user_data_from_conn(self, conn, dn):
+        reply = dict()
+        try:
+            ldapattrs = self._get_user_data(conn, dn)
+            userattrs, extras = self.mapper.map_attrs(ldapattrs)
+            groups = self._get_user_groups(conn, dn, ldapattrs)
+            reply['userdata'] = userattrs
+            reply['groups'] = groups
+            reply['extras'] = {'ldap': extras}
+        except Exception, e:  # pylint: disable=broad-except
+            self.error(e)
+
+        return reply
 
     def get_user_attrs(self, user):
-        userattrs = None
         try:
             conn = self._ldap_bind()
             dn = self.user_dn_tmpl % {'username': user}
-            userattrs = self.get_user_data_from_conn(conn, dn)
+            return self.get_user_data_from_conn(conn, dn)
         except Exception, e:  # pylint: disable=broad-except
             self.error(e)
-
-        return userattrs
+            return {}
 
 
 class Installer(InfoProviderInstaller):
index e9a3a96..4208442 100755 (executable)
@@ -6,27 +6,70 @@
 
 from ipsilon.info.common import InfoProviderBase
 from ipsilon.info.common import InfoProviderInstaller
+from ipsilon.info.common import InfoMapping
 from ipsilon.util.plugin import PluginObject
+import grp
 import pwd
+import os
+
+
+posix_map = {
+    'gecos': 'fullname'
+}
 
 
 class InfoProvider(InfoProviderBase):
 
     def __init__(self):
         super(InfoProvider, self).__init__()
+        self.mapper = InfoMapping()
+        self.mapper.set_mapping(posix_map)
         self.name = 'nss'
 
+    def _get_posix_user(self, user):
+        p = pwd.getpwnam(user)
+        return {'username': p.pw_name, 'uidNumber': p.pw_uid,
+                'gidNumber': p.pw_gid, 'gecos': p.pw_gecos,
+                'homeDirectory': p.pw_dir, 'loginShell': p.pw_shell}
+
+    def _get_posix_groups(self, user, group):
+        groups = set()
+        getgrouplist = getattr(os, 'getgrouplist', None)
+        if getgrouplist:
+            ids = getgrouplist(user, group)
+            for i in ids:
+                try:
+                    g = grp.getgrgid(i)
+                    groups.add(g.gr_name)
+                except KeyError:
+                    pass
+
+        else:
+            g = grp.getgrgid(group)
+            groups.add(g.gr_name)
+
+            allg = grp.getgrall()
+            for g in allg:
+                if user in g.gr_mem:
+                    groups.add(g.gr_name)
+
+        return list(groups)
+
     def get_user_attrs(self, user):
-        userattrs = None
+        reply = dict()
         try:
-            p = pwd.getpwnam(user)
-            userattrs = {'uidNumber': p[2], 'gidNumber': p[3],
-                         'gecos': p[4], 'homeDirectory': p[5],
-                         'loginShell': p[6]}
+            posix_user = self._get_posix_user(user)
+            userattrs, extras = self.mapper.map_attrs(posix_user)
+            groups = self._get_posix_groups(posix_user['username'],
+                                            posix_user['gidNumber'])
+            reply['userdata'] = userattrs
+            reply['groups'] = groups
+            reply['extras'] = {'posix': extras}
+
         except KeyError:
             pass
 
-        return userattrs
+        return reply
 
 
 class Installer(InfoProviderInstaller):
index 0d70479..a41d167 100755 (executable)
@@ -64,7 +64,15 @@ class LDAP(LoginFormBase, Log):
 
         if username and password:
             try:
-                userattrs = self._authenticate(username, password)
+                userdata = self._authenticate(username, password)
+                if userdata:
+                    userattrs = dict()
+                    for d, v in userdata.get('userdata', {}).items():
+                        userattrs[d] = v
+                    if 'groups' in userdata:
+                        userattrs['groups'] = userdata['groups']
+                    if 'extras' in userdata:
+                        userattrs['extras'] = userdata['extras']
                 authed = True
             except Exception, e:  # pylint: disable=broad-except
                 errmsg = "Authentication failed"
index 55b30a4..44492a4 100755 (executable)
@@ -33,8 +33,9 @@ class TestAuth(LoginFormBase):
         if username and password:
             if password == 'ipsilon':
                 cherrypy.log("User %s successfully authenticated." % username)
+                testdata = {'fullname': 'Test User %s' % username}
                 return self.lm.auth_successful(self.trans,
-                                               username, 'password')
+                                               username, 'password', testdata)
             else:
                 cherrypy.log("User %s failed authentication." % username)
                 error = "Authentication failed"
index 2fee357..6231997 100755 (executable)
@@ -49,9 +49,17 @@ class LoginManagerBase(PluginObject, Log):
         if self.info:
             userattrs = self.info.get_user_attrs(username)
             if userdata:
-                userdata.update(userattrs or {})
+                userdata.update(userattrs.get('userdata', {}))
             else:
-                userdata = userattrs
+                userdata = userattrs.get('userdata', {})
+
+            # merge groups and extras from login plugin and info plugin
+            userdata['groups'] = list(set(userdata.get('groups', []) +
+                                          userattrs.get('groups', [])))
+
+            userdata['extras'] = userdata.get('extras', {})
+            userdata['extras'].update(userattrs.get('extras', {}))
+
             self.debug("User %s attributes: %s" % (username, repr(userdata)))
 
         if auth_type:
index abf19ae..868daf1 100755 (executable)
@@ -162,17 +162,16 @@ class AuthenticateRequest(ProviderPageBase):
                     'openid_request': json.dumps(kwargs)}
             self.trans.store(data)
 
-            # Add extension data to this list of dictionaries
-            ad = [
-                {
-                    "Trust Root": request.trust_root,
-                },
-            ]
+            # Add extension data to this dictionary
+            ad = {
+                "Trust Root": request.trust_root,
+            }
             userattrs = us.get_user_attrs()
             for n, e in self.cfg.extensions.items():
                 data = e.get_display_data(request, userattrs)
                 self.debug('%s returned %s' % (n, repr(data)))
-                ad.append(data)
+                for key, value in data.items():
+                    ad[self.cfg.mapping.display_name(key)] = value
 
             context = {
                 "title": 'Consent',
index 2e41050..a3e1b63 100755 (executable)
@@ -9,6 +9,7 @@ from ipsilon.providers.common import FACILITY
 from ipsilon.providers.openid.auth import OpenID
 from ipsilon.providers.openid.extensions.common import LoadExtensions
 from ipsilon.util.plugin import PluginObject
+from ipsilon.info.common import InfoMapping
 
 from openid.server.server import Server
 # TODO: Move this to the database
@@ -19,6 +20,7 @@ class IdpProvider(ProviderBase):
 
     def __init__(self):
         super(IdpProvider, self).__init__('openid', 'openid')
+        self.mapping = InfoMapping()
         self.page = None
         self.server = None
         self.basepath = None
index cbfeaaa..87f4ac8 100755 (executable)
@@ -210,18 +210,33 @@ class AuthenticateRequest(ProviderPageBase):
         if not attrstat.attribute:
             attrstat.attribute = ()
 
-        attributes = us.get_user_attrs()
+        attributes = dict()
+        userattrs = us.get_user_attrs()
+        for key, value in userattrs.get('userdata', {}).iteritems():
+            if type(value) is str:
+                attributes[key] = value
+        if 'groups' in userattrs:
+            attributes['group'] = userattrs['groups']
+        for _, info in userattrs.get('extras', {}).iteritems():
+            for key, value in info.items():
+                attributes[key] = value
+
         for key in attributes:
-            attr = lasso.Saml2Attribute()
-            attr.name = key
-            attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
-            value = str(attributes[key]).encode('utf-8')
-            node = lasso.MiscTextNode.newWithString(value)
-            node.textChild = True
-            attrvalue = lasso.Saml2AttributeValue()
-            attrvalue.any = [node]
-            attr.attributeValue = [attrvalue]
-            attrstat.attribute = attrstat.attribute + (attr,)
+            values = attributes[key]
+            if type(values) is not list:
+                values = [values]
+            for value in values:
+                attr = lasso.Saml2Attribute()
+                attr.name = key
+                attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
+                value = str(value).encode('utf-8')
+                self.debug('value %s' % value)
+                node = lasso.MiscTextNode.newWithString(value)
+                node.textChild = True
+                attrvalue = lasso.Saml2AttributeValue()
+                attrvalue.any = [node]
+                attr.attributeValue = [attrvalue]
+                attrstat.attribute = attrstat.attribute + (attr,)
 
         self.debug('Assertion: %s' % login.assertion.dump())
 
index 8c3813e..e8ead1d 100644 (file)
   <form class="form-horizontal" role="form" id="consent_form" action="{{ action }}" method="post" enctype="application/x-www-form-urlencoded">
     <input type="hidden" name="ipsilon_transaction_id" id="ipsilon_transaction_id" value="{{ ipsilon_transaction_id }}">
     <div class="alert alert-danger">
-{%- for items in authz_details %}
-  {%- for item in items|dictsort %}
+{%- for item in authz_details|dictsort %}
         <div class="form-group">
             <div class="col-sm-10 col-md-10">{{ item[0] }}:</div>
             <div class="col-sm-10 col-md-10">{{ item[1] }}</div>
         </div>
-    {%- endfor %}
 {%- endfor %}
     </div>
     <div class="form-group">