jamf / NetSUS

NetBoot and Software Update Server
https://jamfnation.jamfsoftware.com/viewProduct.html?id=180
304 stars 68 forks source link

LDAP login for webadmin #99

Open jelockwood opened 6 years ago

jelockwood commented 6 years ago

Contrary to https://github.com/jamf/NetSUS/pull/49 I do not believe the current NetSUS 4.2.1 has LDAP login capabilities. What it does purport to have is Active Directory login capabilities.

AD is of course based on LDAP but is by no means sufficiently similar to make it straight forward to use with a none AD but LDAP based directory server.

I would therefore re-open the request that NetSUS add LDAP logins for webadmin.

Note: I have LDAP logins working for JAMF JSS.

If there is a particular PHP file for NetSUS that could be modified to do a more generic LDAP login I am willing to look at this. For example I have done so for MunkiReports-PHP and other projects.

duncan-mccracken commented 6 years ago

Addressed in latest release

jelockwood commented 6 years ago

@duncan-mccracken Hi, you say above that LDAP admin logins for the webadmin interface is addressed in NetSUS 5.0 but I have just installed it and the configuration for adding LDAP user accounts is it seems unchanged. In fact it still clearly describes itself not as an LDAP user accounts but Active Directory user account creation.

Via the NetSUS web admin user interface I still cannot successfully configure it to connect to an LDAP server as opposed to an Active Directory server. I also cannot define the different filter needed to check membership of an LDAP group.

I am therefore puzzled how you can say this is addressed in NetSUS 5.0

Is there a different undocumented method of configuring it?

screen shot 2018-10-01 at 10 40 27

duncan-mccracken commented 6 years ago

Sorry my bad, I was confusing this issue with another which was related to Active Directory LDAP. The implementation of the LDAP sign-in back-end is specific to AD, as a few LDAP environment variables are needed, which is why it it is referred to as AD. Straight-up OpenLDAP or LDAP isn't yet supported, and would require an additional login option and class framework to be created with a myriad of other options/settings required. Currently with the amount of time that would be required, I don't have this on the roadmap for implementation. Apologies.

jelockwood commented 6 years ago

Would it be appropriate to re-open this issue until it is addressed?

By the way, issue https://github.com/jamf/NetSUS/pull/49 which is also closed does seem to imply some one else submitted a modification to add LDAP support but clearly this is not part of the main code still.

duncan-mccracken commented 6 years ago

I'll re-open it and have a look at #49 (which pre-dates my involvement), see what I can do and then make a decision from there.

duncan-mccracken commented 6 years ago

Looking over #49, it also appears to be in the context of Active Directory LDAP. In order to do a successful implementation, straight LDAP authentication would require some for of mapping UI for users to the appropriate attributes, which would be a substantial amount of work. I'll have to weigh up the options, as it may be a better idea to use pam-auth at a system level and leverage the os-level auth for the webadmin UI, but once again, that is a fair amount of work. I'll have to shelve this one until next week as I'm quite committed this week.

jelockwood commented 6 years ago

I would agree it is not a top priority. However as it currently stands in the absence of our being able to use Active Directory and because it does not support plain LDAP we have to share the single webadmin login account and password amongst multiple IT staff since it is also not possible to define additional admin logins.

Hence for our organisations security policies it is regarded as being a currently unavoidable breach of policy. (Not supposed to use shared accounts.)

For what it's worth I run other open source projects which use PHP e.g. MunkiReports-PHP and this uses a PHP module which can support both AD and LDAP depending on how you configure it. If I remember correctly the module it uses is this one - https://github.com/Adldap2/Adldap2

duncan-mccracken commented 6 years ago

Thanks, I'll look into that. Also being able to add additional webadmin accounts is actually on my list of things to also investigate.

jelockwood commented 5 years ago

@duncan-mccracken Gentle reminder about this issue. :)

james-tipler commented 5 years ago

This was annoying me too, so I've patched it. It's pretty rough and ready so I'm not sure it warrants a pull request, but this may be of help to someone.

--- accounts.orig.php   2019-10-16 15:12:39.141102508 +0100
+++ accounts.php    2019-10-16 15:18:54.739950233 +0100
@@ -81,6 +81,7 @@
 }
 $ldap_domain = $conf->getSetting("ldapdomain");
 $ldap_base = $conf->getSetting("ldapbase");
+$ldap_user_base = $conf->getSetting("ldapuserbase");
 $ldap_admins = $conf->getAdmins();

 // System Users
@@ -377,11 +378,13 @@
                    var ldaphost = document.getElementById('ldaphost');
                    var ldapport = document.getElementById('ldapport');
                    var ldapbase = document.getElementById('ldapbase');
+                   var ldapuserbase = document.getElementById('ldapuserbase');
                    hideWarning(ldapport);
                    if (ldapdomain.value == '' && ldaphost.value == '' && ldapbase.value == '') {
                        ajaxPost('ajax.php', 'ldapdomain=');
                        ajaxPost('ajax.php', 'ldapserver=');
                        ajaxPost('ajax.php', 'ldapbase=');
+                       ajaxPost('ajax.php', 'ldapuserbase=');
                        $('#ldapstatus').text('Not Configured');
                        $('#addldapgroup').prop('disabled', true);
                        ldap_server = "";
@@ -389,6 +392,7 @@
                        ajaxPost('ajax.php', 'ldapdomain='+ldapdomain.value);
                        ajaxPost('ajax.php', 'ldapserver='+ldapscheme.value+'://'+ldaphost.value+':'+ldapport.value);
                        ajaxPost('ajax.php', 'ldapbase='+ldapbase.value);
+                       ajaxPost('ajax.php', 'ldapuserbase='+ldapuserbase.value);
                        $('#ldapstatus').text($('#ldapdomain').val());
                        $('#addldapgroup').prop('disabled', false);
                        ldap_server = ldapscheme.value+'://'+ldaphost.value+':'+ldapport.value;
@@ -774,6 +778,10 @@
                                        <div class="form-group has-feedback">
                                            <input type="text" name="ldapbase" id="ldapbase" class="form-control input-sm" placeholder="[Required]" value="<?php echo $ldap_base; ?>" onFocus="validLdap();" onKeyUp="validLdap();" onBlur="validLdap();"/>
                                        </div>
+                                       <h5 id="ldapuserdn_label"><strong>User Search Base</strong> <small>Distinguished name of the LDAP search base containing user objects.</small></h5>
+                                       <div class="form-group has-feedback">
+                                           <input type="text" name="ldapuserbase" id="ldapuserbase" class="form-control input-sm" placeholder="[Not Required for AD. Required for OpenLDAP]" value="<?php echo $ldap_user_base; ?>" onFocus="validLdap();" onKeyUp="validLdap();" onBlur="validLdap();"/>
+                                       </div>
                                    </div>
                                    <div class="modal-footer">
                                        <button type="button" class="btn btn-default btn-sm pull-left" data-dismiss="modal">Cancel</button>
--- ajax.orig.php   2019-10-16 15:23:44.518720469 +0100
+++ ajax.php    2019-10-16 15:36:48.122895661 +0100
@@ -62,5 +62,13 @@
        }
    }

+   if (isset($_POST['ldapuserbase'])) {
+       if ($_POST['ldapuserbase'] == "") {
+           $conf->deleteSetting("ldapuserbase");
+       } else {
+           $conf->setSetting("ldapuserbase", $_POST['ldapuserbase']);
+       }
+   }
+
 }
-?>
\ No newline at end of file
+?>
--- index.orig.php  2019-10-10 14:06:32.467594347 +0100
+++ index.php   2019-10-16 15:41:47.401186664 +0100
@@ -34,7 +34,8 @@
        }
    }

-   if ($_POST['loginwith'] == 'adlogin') {
+   // This is all the same for both generic LDAP and AD
+   if ($_POST['loginwith'] == 'ldaplogin' || $_POST['loginwith'] == 'adlogin' ) {

        define(LDAP_OPT_DIAGNOSTIC_MESSAGE, 0x0032);
        $type="adlogin";
@@ -61,24 +62,34 @@
        ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
        ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);

+   }
+
+   if ($_POST['loginwith'] == 'ldaplogin' ) {
+
+       // Get the base that contains user objects so we can build a DN with the username.
+       $ldap_user_base = $conf->getSetting("ldapuserbase");
+
+       // Make this a valid DN
+       $userdn = "uid=$username,$ldap_user_base";
+
        // verify user and password
-       if($bind = @ldap_bind($ldap, $username.$domain, $password)) {
+       if($bind = ldap_bind($ldap, $userdn, $password)) {
            // valid
            // check presence in groups
-           $filter = "(sAMAccountName=".$username.")";
-           $attr = array("memberof");
-           $result = ldap_search($ldap, $ldap_dn, $filter, $attr);
+           $filter = "(memberUid=$username)";
+           $attr = array("memberUid");

+           $result = ldap_search($ldap, $ldap_dn, $filter, $attr);
            $entries = ldap_get_entries($ldap, $result);
            ldap_unbind($ldap);

            // check groups
            $access = 0;
-           foreach ($entries[0]['memberof'] as $grps) {
+           foreach ($entries as $grps) {
                // check group membership
                foreach ($admin_grps as $key => $value) {
                    // is admin, break loop
-                   if(strpos($grps, $value['cn'])) { $isAuth = TRUE; break 2; }
+                   if(strpos($grps['dn'], $value['cn'])) { $isAuth = TRUE; break 2; }
                }
            }
            if (!$isAuth) {
@@ -94,6 +105,42 @@
            }
        }
    }
+
+   if ($_POST['loginwith'] == 'adlogin' ) {
+
+                // verify user and password
+                if($bind = @ldap_bind($ldap, $username.$domain, $password)) {
+                        // valid
+                        // check presence in groups
+                        $filter = "(sAMAccountName=".$username.")";
+                        $attr = array("memberof");
+                        $result = ldap_search($ldap, $ldap_dn, $filter, $attr);
+
+                        $entries = ldap_get_entries($ldap, $result);
+                        ldap_unbind($ldap);
+
+                        // check groups
+                        $access = 0;
+                        foreach ($entries[0]['memberof'] as $grps) {
+
+                                foreach ($admin_grps as $key => $value) {
+                                        // is admin, break loop
+                                        if(strpos($grps, $value['cn'])) { $isAuth = TRUE; break 2; }
+                                }
+                        }
+                        if (!$isAuth) {
+                                $loginerror = "Access Denied for ".$username;
+                        }
+                } else {
+                        if (ldap_get_option($handle, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
+                                // get error msg
+                                $loginerror = $extended_error;
+                        } else {
+                                // invalid user or password
+                                $loginerror = "Invalid Username or Password";
+           }
+       }
+   }
 }

 if ($isAuth) {
@@ -241,11 +288,15 @@
                            <div class="radio radio-inline radio-primary">
                                <input type="radio" id="suslogin" name="loginwith" value="suslogin" <?php echo ($type == "suslogin" ? "checked" : ""); ?>>
                                <label for="suslogin">Built-In Account</label>
-                           </div>
+                           </div></br>
                            <div class="radio radio-inline radio-primary">
                                <input type="radio" id="adlogin" name="loginwith" value="adlogin" <?php echo ($type == "adlogin" ? "checked" : ""); ?> <?php echo ($ldap_enabled ? "" : "disabled"); ?>>
                                <label for="adlogin">Active Directory</label>
                            </div>
+                           <div class="radio radio-inline radio-primary">
+                               <input type="radio" id="ldaplogin" name="loginwith" value="ldaplogin" <?php echo ($type == "adlogin" ? "checked" : ""); ?> <?php echo ($ldap_enabled ? "" : "disabled"); ?>>
+                               <label for="ldaplogin">LDAP</label>
+                           </div>
                        </div>
                        <div class="username">
                            <input type="text" name="username" id="username" class="form-control input-sm" placeholder="[Username]" />
duncan-mccracken commented 5 years ago

Hi @james-tipler ,

I'm going to do a maintenance release next week, can you submit a pull request for this. I would like to review and include it, and a PR will make that process easier for me.

Thanks,

~Duncan

james-tipler commented 5 years ago

@duncan-mccracken

142

This really is the absolute least I could do to get it working in the most basic case - it's sufficient for my needs here, but if there's enough interest I can add the relevant config options for configuring things like group search filters, memberUid vs member, actual documentation on how you should use this etc.

jelockwood commented 5 years ago

@james-tipler Thanks for your efforts adding LDAP support. I would certainly be interested in being able to use an LDAP group to allow access so that my entire IT team can be simply given access.

james-tipler commented 5 years ago

@jelockwood The patch I submitted does have group support - but in a very primitive way. It assumes that the groups in question are of a type that uses "memberUid" as the membership attribute, and contains uid's not DNs. It gets group information from the same fields as the "active directory groups" defined in the ui. It also assumes that the groups can be found inside the search base, so it's not exactly sophisticated.