src/server/daemon/ldap_auth.c

Wed, 27 Nov 2024 23:00:07 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Wed, 27 Nov 2024 23:00:07 +0100
changeset 563
6ca97c99173e
parent 490
d218607f5a7e
permissions
-rw-r--r--

add TODO to use a future ucx feature

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2013 Olaf Wintermann. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. 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 HOLDER 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.
 */

#ifdef __gnu_linux__
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>

#include <cx/utils.h>
#include <cx/hash_map.h>
#include <cx/printf.h>

#include "../util/util.h"

#include "ldap_auth.h"
#include "ldap_resource.h"

static cxstring ws_ldap_default_uid_attr[] = {
    CX_STR("uid")
};

static cxstring ws_ldap_default_member_attr[] = {
    CX_STR("member"),
    CX_STR("uniqueMember")
};

static LDAPConfig ws_ldap_default_config = {
    NULL, // resource
    NULL, // basedn
    NULL, // binddn
    NULL, // bindpw
    "(&(objectclass=inetorgperson)(|(cn=%s)(uid=%s)))", // userSearchFilter
    ws_ldap_default_uid_attr, // uidAttributes
    1, // numUidAttributes
    "(&(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames))(cn=%s))", // groupSearchFilter
    ws_ldap_default_member_attr, // memberAttributes
    2, // numMemberAttributes
    WS_LDAP_GROUP_MEMBER_DN, // groupMemberType
    TRUE, // enableGroups
    FALSE // userNameIsDN
};

// TODO: AD
static cxstring ws_ad_default_uid_attr[] = {
    CX_STR("uid")
};

static cxstring ws_ad_default_member_attr[] = {
    CX_STR("member"),
    CX_STR("uniqueMember")
};

static LDAPConfig ws_ldap_ad_config = {
    NULL, // resource
    NULL, // basedn
    NULL, // binddn
    NULL, // bindpw
    "(&(objectclass=inetorgperson)(|(cn=%s)(uid=%s)))", // userSearchFilter
    ws_ad_default_uid_attr, // uidAttributes
    1, // numUidAttributes
    "", // groupSearchFilter
    ws_ad_default_member_attr, // memberAttributes
    2, // numMemberAttributes
    WS_LDAP_GROUP_MEMBER_DN, // groupMemberType
    TRUE, // enableGroups
    FALSE // userNameIsDN
};

static cxstring ws_posix_default_uid_attr[] = {
    CX_STR("uid")
};

static cxstring ws_posix_default_member_attr[] = {
    CX_STR("memberUid")
};

static LDAPConfig ws_ldap_posix_config = {
    NULL, // resource
    NULL, // basedn
    NULL, // binddn
    NULL, // bindpw
    "(&(objectclass=posixAccount)(uid=%s))", // userSearchFilter
    ws_posix_default_uid_attr, // uidAttributes
    1, // numUidAttributes
    "(&(objectclass=posixGroup)(cn=%s))", // groupSearchFilter
    ws_posix_default_member_attr, // memberAttributes
    1, // numMemberAttributes
    WS_LDAP_GROUP_MEMBER_UID, // groupMemberType
    TRUE, // enableGroups
    FALSE // userNameIsDN
};

AuthDB* create_ldap_authdb(ServerConfiguration *cfg, const char *name, ConfigNode *node) {
    LDAPAuthDB *authdb = cxMalloc(cfg->a, sizeof(LDAPAuthDB));
    if(!authdb) {
        return NULL;
    }
    authdb->authdb.name = pool_strdup(cfg->pool, name);
    if(!authdb->authdb.name) {
        return NULL;
    }
    authdb->authdb.get_user = ldap_get_user;
    authdb->authdb.use_cache = 0; // TODO: enable caching when cache actually works
    
    // initialize default ldap config
    cxstring dirtype = serverconfig_object_directive_value(node, cx_str("DirectoryType"));
    LDAPConfig *default_config;
    if(!dirtype.ptr) {
        default_config = &ws_ldap_default_config;
    } else if(!cx_strcmp(dirtype, cx_str("ldap"))) {
        default_config = &ws_ldap_default_config;
    } else if(!cx_strcmp(dirtype, cx_str("posix"))) {
        default_config = &ws_ldap_posix_config;
    } else if(!cx_strcmp(dirtype, cx_str("ad"))) {
        default_config = &ws_ldap_ad_config;
    } else {
        log_ereport(LOG_FAILURE, "cannot create ldap authdb %s: unknown directory type %s", name, dirtype.ptr);
    }
    memcpy(&authdb->config, default_config, sizeof(LDAPConfig));
    
    // custom config
    cxstring resource = serverconfig_object_directive_value(node, cx_str("Resource"));
    cxstring basedn = serverconfig_object_directive_value(node, cx_str("Basedn"));
    cxstring binddn = serverconfig_object_directive_value(node, cx_str("Binddn"));
    cxstring bindpw = serverconfig_object_directive_value(node, cx_str("Bindpw"));
    cxstring userSearchFilter = serverconfig_object_directive_value(node, cx_str("UserSearchFilter"));
    cxstring uidAttributes = serverconfig_object_directive_value(node, cx_str("UidAttributes")); 
    cxstring groupSearchFilter = serverconfig_object_directive_value(node, cx_str("GroupSearchFilter"));
    cxstring memberAttributes = serverconfig_object_directive_value(node, cx_str("MemberAttributes"));
    cxstring memberType = serverconfig_object_directive_value(node, cx_str("MemberType")); 
    cxstring enableGroups = serverconfig_object_directive_value(node, cx_str("EnableGroups"));	 
    cxstring userNameIsDn = serverconfig_object_directive_value(node, cx_str("UserNameIsDn"));
    
    if(!resource.ptr) {
        // implicitly create a resource pool for this authdb
        cxmutstr respool_name = cx_asprintf_a(cfg->a, "_authdb_%s", name);
        if(!respool_name.ptr) {
            return NULL;
        }
        log_ereport(
                LOG_INFORM,
                "ldap authdb %s: no resource specified: create resource pool %s",
                name,
                respool_name.ptr);
        if(resourcepool_new(cfg, cx_str("ldap"), cx_strcast(respool_name), node)) {
            log_ereport(
                    LOG_FAILURE,
                    "ldap authdb %s: cannot create ldap resource pool",
                    name);
            return NULL;
        }
        authdb->config.resource = respool_name.ptr;
    } else {
        authdb->config.resource = cx_strdup_a(cfg->a, resource).ptr;
        if(!authdb->config.resource) return NULL;
    }
    
    if(!basedn.ptr) {
        log_ereport(LOG_FAILURE, "ldap authdb %s: basedn is required", name);
        return NULL;
    }
    authdb->config.basedn = cx_strdup_a(cfg->a, basedn).ptr;
    if(!authdb->config.basedn) return NULL;
    
    // optional config
    if(binddn.ptr) {
        if(!bindpw.ptr) {
            log_ereport(LOG_FAILURE, "ldap authdb %s: binddn specified, but no bindpw", name);
            return NULL;
        }
        
        authdb->config.binddn = cx_strdup_a(cfg->a, binddn).ptr;
        authdb->config.bindpw = cx_strdup_a(cfg->a, bindpw).ptr;
        
        if(!authdb->config.binddn || !authdb->config.bindpw) {
            return NULL;
        }
    }

    
    if(userSearchFilter.ptr) {
        authdb->config.userSearchFilter = cx_strdup_a(cfg->a, userSearchFilter).ptr;
    }
    if(uidAttributes.ptr) {
        cxmutstr uidAttributesCopy = cx_strdup_a(cfg->a, uidAttributes);
        if(uidAttributesCopy.ptr) {
            authdb->config.numUidAttributes = cx_strsplit_a(
                    cfg->a,
                    cx_strcast(uidAttributesCopy),
                    cx_str(","),
                    1024,
                    &authdb->config.uidAttributes);
        }
    }
    if(groupSearchFilter.ptr) {
        authdb->config.groupSearchFilter = groupSearchFilter.ptr;
    }
    if(memberAttributes.ptr) {
        cxmutstr memberAttributesCopy = cx_strdup_a(cfg->a, memberAttributes);
        if(memberAttributesCopy.ptr) {
            authdb->config.numMemberAttributes = cx_strsplit_a(
                    cfg->a,
                    cx_strcast(memberAttributesCopy),
                    cx_str(","),
                    1024,
                    &authdb->config.memberAttributes);
        }
    }
    if(memberType.ptr) {
        if(!cx_strcmp(memberType, cx_str("dn"))) {
            authdb->config.groupMemberType = WS_LDAP_GROUP_MEMBER_DN;
        } else if(!cx_strcmp(memberType, cx_str("uid"))) {
            authdb->config.groupMemberType = WS_LDAP_GROUP_MEMBER_UID;
        } else {
            log_ereport(LOG_FAILURE, "ldap authdb %s: unknown MemberType %s", name, memberType.ptr);
            return NULL;
        }
    }
    if(enableGroups.ptr) {
        authdb->config.enableGroups = util_getboolean_s(enableGroups, FALSE);
    }
    if(userNameIsDn.ptr) {
        authdb->config.userNameIsDN = util_getboolean_s(userNameIsDn, FALSE);
    }
    
    
    // initialize group cache
    authdb->groups.first = NULL;
    authdb->groups.last = NULL;
    authdb->groups.map = cxHashMapCreate(cfg->a, CX_STORE_POINTERS, 32);
    if(!authdb->groups.map) {
        return NULL;
    }
    
    log_ereport(LOG_INFORM, "create authdb name=%s type=ldap resource=%s", name, resource.ptr);

    return (AuthDB*) authdb;
}

LDAP* get_ldap_session(Session *sn, Request *rq, LDAPAuthDB *authdb) {
    ResourceData *res = resourcepool_lookup(sn, rq, authdb->config.resource, 0);
    if(!res) {
        log_ereport(LOG_FAILURE, "AuthDB %s: cannot get resource %s", authdb->authdb.name, authdb->config.resource);
        return NULL;
    }
    
    LDAP *ldap = res->data;
    
    if(authdb->config.binddn) {
        struct berval *server_cred;
        int r = ws_ldap_bind(ldap, authdb->config.binddn, authdb->config.bindpw, &server_cred);
        if(r != LDAP_SUCCESS) {
            log_ereport(LOG_FAILURE, "AuthDB %s: bind to %s failed: %s", authdb->config.binddn, ldap_err2string(r));
            resourcepool_free(sn, rq, res);
            return NULL;
        }
    }
    
    return ldap;
}

static LDAPUser* ldap_msg_to_user(
        Session *sn,
        Request *rq,
        LDAPAuthDB *authdb,
        LDAP *ldap,
        LDAPMessage *msg)
{
    CxAllocator *a = pool_allocator(sn->pool);
    
    LDAPUser *user = pool_malloc(sn->pool, sizeof(LDAPUser));
    if(!user) {
        return NULL;
    }
    
    // get dn
    char *ldap_dn = ldap_get_dn(ldap, msg);
    if(!ldap_dn) {
        return NULL;
    }
    char *dn = pool_strdup(sn->pool, ldap_dn);
    ldap_memfree(ldap_dn);
    if(!dn) {
        return NULL;
    }
    
    // get uid
    char *uid = NULL;

    // values of configured UidAttributes
    size_t numUidAttributes = authdb->config.numUidAttributes;
    cxmutstr *uid_values = pool_calloc(sn->pool, authdb->config.numUidAttributes, sizeof(cxmutstr));
    if(!uid_values) {
        return NULL;
    }


    BerElement *ber = NULL;
    char *attribute = ldap_first_attribute(ldap, msg, &ber);
    while(attribute) {
        cxstring attr = cx_str(attribute);
        for(int i=0;i<numUidAttributes;i++) {
            // check if the attribute is one of the uid attributes
            if(!uid_values[i].ptr && !cx_strcmp(attr, authdb->config.uidAttributes[i])) {
                // copy value to uid_values
                struct berval **values = ldap_get_values_len(ldap, msg, attribute);
                if(values) {
                    int count = ldap_count_values_len(values);
                    if(count > 0) {
                        cxstring attr_val = cx_strn(values[0]->bv_val, values[0]->bv_len);
                        uid_values[i] = cx_strdup_a(a, attr_val);
                    } else {
                        log_ereport(LOG_FAILURE, "ldap user: dn: %s   attribute %s: no values", dn, attribute);
                    }
                    ldap_value_free_len(values);
                }
            }
        }
        
        if(uid_values[0].ptr) {
            // if we found a value for the first attribute, we can use that
            break;
        }

        ldap_memfree(attribute);
        attribute = ldap_next_attribute(ldap, msg, ber); 
    }
    if(ber) {
        ber_free(ber, 0);
    }
        

    
    // use first value as uid
    for(int i=0;i<numUidAttributes;i++) {
        if(uid_values[i].ptr) {
            if(!uid) {
                uid = uid_values[i].ptr;
            } else {
                cxFree(a, uid_values[i].ptr);
            }
        }
    }
    pool_free(sn->pool, uid_values);
    
   // get user name
    char *username;
    if(authdb->config.userNameIsDN) {
        username = dn;
    }  else {
        username = uid;
    }
    
    if(!username) {
        return NULL;
    }
    
    user->authdb = authdb;
    user->user.verify_password = ldap_user_verify_password;
    user->user.check_group = ldap_user_check_group;
    user->user.free = ldap_user_free;
    user->user.name = username;
    user->sn = sn;
    user->rq = rq;

    // TODO: get uid/gid from ldap
    user->user.uid = -1;
    user->user.gid = -1;

    user->ldap = ldap;
    user->userdn = dn;
    user->uid_attr = uid;
    
    return user;
}

User* ldap_get_user(AuthDB *db, Session *sn, Request *rq, const char *username) {
    LDAPAuthDB *authdb = (LDAPAuthDB*) db;
    LDAPConfig *config = &authdb->config;
    CxAllocator *a = pool_allocator(sn->pool);

    LDAP *ld = get_ldap_session(sn, rq, authdb);
    if (ld == NULL) {
        return NULL;
    }

    // get the user dn
    cxstring userSearch = cx_str(config->userSearchFilter);
    cxmutstr filter = cx_strreplace_a(a, userSearch, cx_str("%s"), cx_str(username));
    if(!filter.ptr) {
        return NULL;
    }

    log_ereport(LOG_DEBUG, "ldap_get_user: filter: %s", filter.ptr);
    
    LDAPMessage *result;
    struct timeval timeout;
    timeout.tv_sec = 8; // TODO: add config parameter for timeout
    timeout.tv_usec = 0;
    int r = ldap_search_ext_s(
            ld,
            config->basedn,
            LDAP_SCOPE_SUBTREE,
            filter.ptr,
            NULL,
            0,
            NULL,        // server controls
            NULL,        // client controls
            &timeout,
            2,           // size limit
            &result);
    cxFree(a, filter.ptr);
    if(r != LDAP_SUCCESS) {
        if(result) {
            ldap_msgfree(result);
        }
        log_ereport(LOG_FAILURE, "ldap_get_user: search failed: %s", ldap_err2string(r));
        return NULL;
    }
    if(!result) {
        // not sure if this can happen
        log_ereport(LOG_FAILURE, "ldap_get_user: search failed: no result");
        return NULL;
    }

    LDAPMessage *msg = ldap_first_entry(ld, result);
    LDAPUser *user = NULL;
    if(msg) {
        if(ldap_count_entries(ld, msg) > 1) {
            log_ereport(LOG_FAILURE, "ldap_get_user: more than one search result");
        } else {
            user = ldap_msg_to_user(sn, rq, authdb, ld, msg);
        }
    }
    ldap_msgfree(result);

    return (User*)user;
}


static int is_member_attribute(LDAPAuthDB *auth, const char *attribute) {
    LDAPConfig *config = &auth->config;
    cxstring attr = cx_str(attribute);
    for(int i=0;i<config->numMemberAttributes;i++) {
        if(!cx_strcmp(config->memberAttributes[i], attr)) {
            return 1;
        }
    }
    return 0;
}

static int group_add_member(LDAPGroup *group, LDAP *ldap, LDAPMessage *msg, char *attribute) {
    struct berval **values = ldap_get_values_len(ldap, msg, attribute);
    int ret = 0;
    if(values) {
        int count = ldap_count_values_len(values);
        for(int i=0;i<count;i++) {
            cxstring memberValue = cx_strn(values[i]->bv_val, values[i]->bv_len);
            CxHashKey key = cx_hash_key(memberValue.ptr, memberValue.length);
            char *g_member = cxMapGet(group->members, key);
            if(!g_member) {
                cxmutstr member = cx_strdup_a(group->members->allocator, memberValue);
                if(!member.ptr) {
                    ret = 1;
                    break;
                }
                if(cxMapPut(group->members, key, member.ptr)) {
                    ret = 1;
                    break;
                }
            }
        }
        ldap_value_free_len(values);
    }
    return ret;
}

static LDAPGroup* ldap_msg_to_group(
        Session *sn,
        Request *rq,
        LDAPAuthDB *authdb,
        LDAP *ldap,
        LDAPMessage *msg,
        const char *group_name)
{
    CxAllocator *a = pool_allocator(sn->pool);
    
    LDAPGroup *group = pool_malloc(sn->pool, sizeof(LDAPGroup));
    if(!group) {
        return NULL;
    }
    group->members = cxHashMapCreate(a, CX_STORE_POINTERS, 32);
    if(!group->members) {
        pool_free(sn->pool, group);
        return NULL;
    }
    group->name = pool_strdup(sn->pool, group_name);
    
    BerElement *ber = NULL;
    char *attribute = ldap_first_attribute(ldap, msg, &ber);
    while(attribute) {
        if(is_member_attribute(authdb, attribute)) {
            if(group_add_member(group, ldap, msg, attribute)) {
                // OOM
                ldap_memfree(attribute);
                // free at least some memory
                cxMapDestroy(group->members);
                pool_free(sn->pool, group);
                group = NULL;
                break;
            }
        }
        
        ldap_memfree(attribute);
        attribute = ldap_next_attribute(ldap, msg, ber); 
    }
    if(ber) {
        ber_free(ber, 0);
    }
    
    return group;
}

LDAPGroup* ldap_get_group(Session *sn, Request *rq, LDAPAuthDB *authdb, const char *group) {
    LDAPConfig *config = &authdb->config;
    CxAllocator *a = pool_allocator(sn->pool);
    
    LDAP *ld = get_ldap_session(sn, rq, authdb);
    if (ld == NULL) {
        return NULL;
    }
    
    // if userNameIsDN is true, group will be the full group dn and we
    // don't need to search with a filter, to get the entry
    char *filterStr;
    const char *basedn;
    int scope;
    if(config->userNameIsDN) {
        filterStr = NULL;
        basedn = group;
        scope = LDAP_SCOPE_BASE;
    } else {
        cxstring groupSearch = cx_str(config->groupSearchFilter);
        cxmutstr filter = cx_strreplace_a(a, groupSearch, cx_str("%s"), cx_str(group));
        if(!filter.ptr) {
            return NULL;
        }
        filterStr = filter.ptr;
        basedn = config->basedn;
        scope = LDAP_SCOPE_SUBTREE;
    }
    
    log_ereport(LOG_DEBUG, "ldap_get_group: basedn: %s filter: %s", basedn, filterStr);

    LDAPMessage *result;
    struct timeval timeout;
    timeout.tv_sec = 8;
    timeout.tv_usec = 0;
    int r = ldap_search_ext_s(
            ld,
            basedn,
            scope,
            filterStr,
            NULL,
            0,
            NULL,        // server controls
            NULL,        // client controls
            &timeout,
            2,           // size limit
            &result);
    if(filterStr) {
        cxFree(a, filterStr);
    }
    
    if (r != LDAP_SUCCESS) {
        if(result) {
            ldap_msgfree(result);
        }
        log_ereport(LOG_FAILURE, "ldap_get_group %s: search failed: %s", group, ldap_err2string(r));
        return NULL;
    }
    
    LDAPMessage *msg = ldap_first_entry(ld, result);
    LDAPGroup *wsgroup = NULL;
    if(msg) {
        if(ldap_count_entries(ld, msg) > 1) {
            log_ereport(LOG_FAILURE, "ldap_get_user: more than one search result");
        } else {
            wsgroup = ldap_msg_to_group(sn, rq, authdb, ld, msg, group);
        }
    }
    ldap_msgfree(result);
    
    return wsgroup;
}

int ldap_user_verify_password(User *u, const char *password) {
    LDAPUser *user = (LDAPUser*)u;
    
    struct berval cred;
    cred.bv_val = (char*)password;
    cred.bv_len = strlen(password);
    struct berval *server_cred;
    int r = ldap_sasl_bind_s(
            user->ldap,
            user->userdn,
            LDAP_SASL_SIMPLE,
            &cred,
            NULL,
            NULL,
            &server_cred);
    if(r == LDAP_SUCCESS) {
        log_ereport(LOG_VERBOSE, "ldap user %s password ok", user->userdn);
        return 1;
    } else {
        log_ereport(LOG_VERBOSE, "ldap user %s password not ok", user->userdn);
        return 0;
    }
}

int ldap_user_check_group(User *u, const char *group_str) {
    LDAPUser *user = (LDAPUser*)u;
    LDAPAuthDB *authdb = user->authdb;
    if(!authdb->config.enableGroups) {
        log_ereport(
                LOG_DEBUG,
                "ldap_user_check_group: authdb %s: groups disabled",
                authdb->authdb.name);
        return 0;
    }
    
    int ret = 0;
    LDAPGroup *group = ldap_get_group(user->sn, user->rq, authdb, group_str);
    if(group) {
        const char *usr = authdb->config.groupMemberType == WS_LDAP_GROUP_MEMBER_DN ? user->userdn : user->uid_attr;
        char *member = cxMapGet(group->members, cx_hash_key_str(usr));
        if(member) {
            ret = 1;
        }
    }
    
    return ret;
}

void ldap_user_free(User *u) {
    LDAPUser *user = (LDAPUser*)u;
    pool_free(user->sn->pool, user->userdn);
    pool_free(user->sn->pool, user->uid_attr);
    pool_free(user->sn->pool, user);
}

mercurial