Attached is a pam_python module that can be used to perform FreeIPA HBAC 
authorization in conjunction with pam_python.so 
(http://ace-host.stuart.id.au/russell/files/pam_python/)

I have been working on this for a while as an alternative to sssd on systems 
that cannot support the sssd installation.  There is no caching provided by 
this code, and is intended as a proof of concept or interim fix on a small 
scale.

I have been craving a more formal c code approach to this general method, but 
am not adept in the c language.  If anyone is feeling savoy, assistance in 
creating a more formal pam module would be very appreciated!

#!/usr/bin/env python
#
#   pam_pyauth.py (Python LDAP RBAC)
#
#   Requires Python 2.4 or Greater
#
#   Copyright (c) 2010 Jr Aquino
#   
#   All rights reserved.
#   
#   Redistribution and use in source and binary forms, with or without
#   modification, are permitted (subject to the limitations in the
#   disclaimer below) provided that the following conditions are met:
#   
#    * Redistributions of source code must retain the above copyright
#      notice, this list of conditions and the following disclaimer.
#   
#    * Redistributions in binary form must reproduce the above copyright
#      notice, this list of conditions and the following disclaimer in the
#      documentation and/or other materials provided with the
#      distribution.
#
#   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT
#   HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
#   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
#   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
#   DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
#   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
#   BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
#   WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
#   OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
#   IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import re
import os
import socket
import syslog
import ldap

class LDAP(object):
    "This class is used for defining ldap.conf values and searching ldap"
    
    def __init__(self):
        # Initial Setup
        
        # Read in ldap.conf
        conf = open('/etc/ldap.conf', 'r').readlines()
        
        # Setup base variables
        self.binddn = None
        self.bindpw = None
        self.baseDN = None
        self.ugroupDN = None
        self.pwgroupDN = None
        self.sysgroupDN = None
        self.hostgroupDN = None
        self.ignore_users = []
        self.ldap_servers = []

        # Regex Definitions
        uri_check = uri_check = re.compile(r'uri ((ldap|ldaps)://(.*))')
        binddn_check = re.compile(r'binddn (.*)')
        bindpw_check = re.compile(r'bindpw (.*)')
        basedn_check = re.compile(r'base (.*)')
        ignore_check = re.compile(r'nss_initgroups_ignoreusers (.*)')
        ugroup_check = re.compile(r'nss_base_group (.*)')
        pwgroup_check = re.compile(r'nss_base_passwd (.*)')
        sysgroup_check = re.compile(r'nss_base_systemgroup (.*)')
        ldaphostgroup_check = re.compile(r'nss_base_hostgroup (.*)')
        rolegroup_check = re.compile(r'nss_base_rolegroup (.*)')
        ignore_users = []
        ldap_servers = []
        # Anonymously bind if no auth data present
        self.binddn = ''
        self.bindpw = ''
        
        # Parse ldap.conf
        for line in conf:
            binddn_match = binddn_check.search(line)
            bindpw_match = bindpw_check.search(line)
            basedn_match = basedn_check.search(line)
            uri_match = uri_check.search(line)
            ignore_match = ignore_check.search(line)
            ugroup_match = ugroup_check.search(line)
            pwgroup_match = pwgroup_check.search(line)
            sysgroup_match = sysgroup_check.search(line)
            hostgroup_match = ldaphostgroup_check.search(line)
            rolegroup_match = rolegroup_check.search(line)
            if binddn_match:
                self.binddn = binddn_match.group(1)
            if bindpw_match:
                self.bindpw = bindpw_match.group(1)
            if basedn_match:
                self.baseDN = basedn_match.group(1)
            if uri_match:
                self.ldap_servers = uri_match.group(1).split()
            if ignore_match:
                self.ignore_users = ignore_match.group(1).split(',')
            if ugroup_match:
                self.ugroupDN = ugroup_match.group(1)
            if pwgroup_match:
                self.pwgroupDN = pwgroup_match.group(1)
            if sysgroup_match:
                self.sysgroupDN = sysgroup_match.group(1)
            if hostgroup_match:
                self.hostgroupDN = hostgroup_match.group(1)
            if rolegroup_match:
                self.rolegroupDN = rolegroup_match.group(1)

    def Search(self, searchBase, attributes, searchFilter):
        "Search LDAP for Entries"

        # Search LDAP Servers
        for ldap_server in self.ldap_servers:
            results = []
            try:
                l = ldap.initialize(ldap_server)
                ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT,
                                ldap.OPT_X_TLS_ALLOW)
                l.protocol_version = ldap.VERSION3
                l.simple_bind_s(self.binddn, self.bindpw)
            except ldap.SERVER_DOWN:
                continue
    
            searchScope = ldap.SCOPE_SUBTREE
            try:
                ldap_result_id = l.search(searchBase, searchScope,
                                            searchFilter, attributes)
                result_set = []
                while 1:
                    result_type, result_data = l.result(ldap_result_id, 0)
                    if (result_data == []):
                        break
                    else:
                        if result_type == ldap.RES_SEARCH_ENTRY:
                            result_set.append(result_data)
                for item in result_set:
                    for value in item:
                        for line in value:
                            if type(line) == str:
                                dn = line
                            elif type(line) == dict:
                                data = line
                        data['dn'] = [dn]
                        search_results = (dn, data)
                        results.append(data)
                results = tuple(results)
                return results
            except ldap.LDAPError, e:
                print e
                raise
            l.unbind_s()
            return

def logOutput(message):
    "Send output to syslog"

    authpriv_notice = 86
    syslog.syslog(authpriv_notice, message)
    return
    
def pam_sm_acct_mgmt(pamh, flags, argv):
    """Check LDAP for Authorization
    PAM Python calls this method directly
    /etc/pam.d/system-auth:
    account     required      /lib/security/pam_python.so pam_pyauth.py
    """
    
    # Initialize Class
    ldaplib = LDAP()

    # Initalize Syslog
    syslog.openlog('pam_pyauth')
    
    # Setup base variables
    hostname = socket.gethostname()
    usergroups = []
    hostgroups = []
    ignore_users = []
    message = pamh.PAM_PERM_DENIED
    status = 'Failure'
    logmsg = 'Unknown Failure'
    # Enumerate Username
    try:
        user = pamh.get_user(None)
    except pamh.exception, e:
        return e.pam_result
    if user == None:
        return pamh.PAM_USER_UNKNOWN
    elif user in ldaplib.ignore_users:
        return pamh.PAM_SUCCESS
    else:
        # Search filter for posix group membership
        ugrp_search = '(member=uid=%s,%s)' % (user, ldaplib.pwgroupDN)

        # Search filter for hostgroup membership        
        hgrp_search = '(member=fqdn=%s,%s)' % (hostname, ldaplib.sysgroupDN)

        # Perform the usergroup search and return list
        ldapusergroups = ldaplib.Search(ldaplib.ugroupDN, ['cn'], ugrp_search)

        # Perform the hostgroup search and return list
        ldaphostgroups = ldaplib.Search(ldaplib.hostgroupDN, ['cn'], hgrp_search)

        # Attempt to find nested usergroups
        if ldapusergroups:
            for ugrp in ldapusergroups:
                # Add the usergroups to the list
                usergroup = ugrp['dn'][0]
                usergroups.append(usergroup)
                # Search for nested usergroups
                nestsearch = ('member=%s' % (usergroup))
                nestedldapgroups = ldaplib.Search(ldaplib.ugroupDN, ['cn'],
                                                   nestsearch)
                #Add the nested usergroups to the list
                if nestedldapgroups:
                   for nestedldapgroup in nestedldapgroups:
                       nestedgroup = nestedldapgroup['dn'][0]
                       usergroups.append(nestedgroup)
        # Attempt to find hostgroups
        if ldaphostgroups:
            for hostgroup in ldaphostgroups:
                hostgroup = hostgroup['dn'][0]
                hostgroups.append(hostgroup)
                # Search for nested hostgroups
                nestsearch = ('member=%s' % (hostgroup))
                nestedldapgroups = ldaplib.Search(ldaplib.hostgroupDN, ['cn'],
                                                   nestsearch)
                #Add the nested hostgroups to the list
                if nestedldapgroups:
                   for nestedldapgroup in nestedldapgroups:
                       nestedgroup = nestedldapgroup['dn'][0]
                       hostgroups.append(nestedgroup)

        # Finally: Match usergroup and hostgroup in role object
        if usergroups and hostgroups:
            for usergroup in usergroups:
                role_search = 'memberUser=%s' % usergroup
                role_list = ldaplib.Search(ldaplib.rolegroupDN, None,
                                            role_search)
                if role_list:
                    for role in role_list:
                        role_name = role['cn'][0]
                        accessRuleType = role['accessRuleType'][0]
                        ipaEnabledFlag = role['ipaEnabledFlag'][0]
                        role_host_groups = role['memberHost']
                        if (accessRuleType == 'allow' and
                            ipaEnabledFlag == 'TRUE'):
                            
                            for hostgroup in role_host_groups:
                                if hostgroup in hostgroups:
                                    message = pamh.PAM_SUCCESS
                                    status = 'Successful'
                                    logmsg = ('Authorization %s: '
                                                '(User=%s Role=%s Usergroup=%s '
                                                'Hostgroup=%s)' % 
                                                (status, user, role_name,
                                                usergroup, hostgroup))
                            
                                    break
                        else:
                            logmsg = ('Authorization %s: '
                            '(User=%s, Usergroups=%s, Hostgroups=%s, '
                            'accessRuleType=%s, ipaEnabledFlag=%s)' % 
                            (status, user, str(usergroups), str(hostgroups),
                            accessRuleType, ipaEnabledFlag))
                
                elif logmsg == 'Unknown Failure':
                    logmsg = ('Authorization %s: No Role (User=%s'
                        ' Usergroups=%s Hostgroups=%s)' %
                        (status, user, str(usergroups), str(hostgroups)))

        else:
            logmsg = ('Authorization %s: (User=%s Usergroups=%s '
                        'Hostgroups=%s)' % 
                        (status, user, str(usergroups), str(hostgroups)))

    logOutput(logmsg)
    return message
    
if __name__ == '__main__':

    print ('This is a pam module and is designed to be called from the '
            'pam_python.so library.\n'
            '(http://ace-host.stuart.id.au/russell/files/pam_python/)')

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Jr Aquino, GCIH | Information Security Specialist
Citrix Online | 7408 Hollister Avenue | Goleta, CA 93117
T:  +1 805.690.3478
jr.aqu...@citrixonline.com
http://www.citrixonline.com

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to