/* ldap.cc: Helper functions for ldap access to Active Directory. Copyright 2014 Red Hat, Inc. This file is part of Cygwin. This software is a copyrighted work licensed under the terms of the Cygwin license. Please consult the file "CYGWIN_LICENSE" for details. */ #include "winsup.h" #include "ldap.h" #include "cygerrno.h" #include "security.h" #include "path.h" #include "fhandler.h" #include "dtable.h" #include "cygheap.h" #include "registry.h" #include "pinfo.h" #include "lm.h" #include "dsgetdc.h" #include "tls_pbuf.h" static LDAP_TIMEVAL tv = { 3, 0 }; static PWCHAR rootdse_attr[] = { (PWCHAR) L"defaultNamingContext", (PWCHAR) L"supportedCapabilities", NULL }; static PWCHAR user_attr[] = { (PWCHAR) L"uid", (PWCHAR) L"primaryGroupID", (PWCHAR) L"gecos", (PWCHAR) L"unixHomeDirectory", (PWCHAR) L"loginShell", (PWCHAR) L"uidNumber", NULL }; static PWCHAR group_attr[] = { (PWCHAR) L"cn", (PWCHAR) L"gidNumber", NULL }; PWCHAR tdom_attr[] = { (PWCHAR) L"trustPosixOffset", NULL }; PWCHAR sid_attr[] = { (PWCHAR) L"objectSid", NULL }; PWCHAR rfc2307_uid_attr[] = { (PWCHAR) L"uid", NULL }; PWCHAR rfc2307_gid_attr[] = { (PWCHAR) L"cn", NULL }; DWORD WINAPI rediscover_thread (LPVOID domain) { PDOMAIN_CONTROLLER_INFOW pdci; DWORD ret = DsGetDcNameW (NULL, (PWCHAR) domain, NULL, NULL, DS_FORCE_REDISCOVERY | DS_ONLY_LDAP_NEEDED, &pdci); if (ret == ERROR_SUCCESS) NetApiBufferFree (pdci); else debug_printf ("DsGetDcNameW(%W) failed with error %u", domain, ret); return 0; } bool cyg_ldap::connect_ssl (PCWSTR domain) { ULONG ret, timelimit = 3; /* secs */ if (!(lh = ldap_sslinitW ((PWCHAR) domain, LDAP_SSL_PORT, 1))) { debug_printf ("ldap_init(%W) error 0x%02x", domain, LdapGetLastError ()); return false; } if ((ret = ldap_set_option (lh, LDAP_OPT_TIMELIMIT, &timelimit)) != LDAP_SUCCESS) debug_printf ("ldap_set_option(LDAP_OPT_TIMELIMIT) error 0x%02x", ret); if ((ret = ldap_bind_s (lh, NULL, NULL, LDAP_AUTH_NEGOTIATE)) != LDAP_SUCCESS) { debug_printf ("ldap_bind(%W) 0x%02x", domain, ret); ldap_unbind (lh); lh = NULL; return false; } return true; } bool cyg_ldap::connect_non_ssl (PCWSTR domain) { ULONG ret, timelimit = 3; /* secs */ if (!(lh = ldap_initW ((PWCHAR) domain, LDAP_PORT))) { debug_printf ("ldap_init(%W) error 0x%02x", domain, LdapGetLastError ()); return false; } if ((ret = ldap_set_option (lh, LDAP_OPT_SIGN, LDAP_OPT_ON)) != LDAP_SUCCESS) debug_printf ("ldap_set_option(LDAP_OPT_SIGN) error 0x%02x", ret); if ((ret = ldap_set_option (lh, LDAP_OPT_ENCRYPT, LDAP_OPT_ON)) != LDAP_SUCCESS) debug_printf ("ldap_set_option(LDAP_OPT_ENCRYPT) error 0x%02x", ret); if ((ret = ldap_set_option (lh, LDAP_OPT_TIMELIMIT, &timelimit)) != LDAP_SUCCESS) debug_printf ("ldap_set_option(LDAP_OPT_TIMELIMIT) error 0x%02x", ret); if ((ret = ldap_bind_s (lh, NULL, NULL, LDAP_AUTH_NEGOTIATE)) != LDAP_SUCCESS) { debug_printf ("ldap_bind(%W) 0x%02x", domain, ret); ldap_unbind (lh); lh = NULL; return false; } return true; } bool cyg_ldap::open (PCWSTR domain) { LARGE_INTEGER start, stop; static LARGE_INTEGER last_rediscover; ULONG ret; close (); GetSystemTimeAsFileTime ((LPFILETIME) &start); /* FIXME? connect_ssl can take ages even when failing, so we're trying to do everything the non-SSL (but still encrypted) way. */ if (/*!connect_ssl (NULL) && */ !connect_non_ssl (domain)) return false; /* For some obscure reason, there's a chance that the ldap_bind_s call takes a long time, if the current primary DC is... well, burping or something. If so, we rediscover in the background which usually switches to the next fastest DC. */ GetSystemTimeAsFileTime ((LPFILETIME) &stop); if ((stop.QuadPart - start.QuadPart) >= 3000000LL /* 0.3s */ && (stop.QuadPart - last_rediscover.QuadPart) >= 30000000LL) /* 3s */ { debug_printf ("ldap_bind_s is laming. Try to rediscover."); HANDLE thr = CreateThread (&sec_none_nih, 4 * PTHREAD_STACK_MIN, rediscover_thread, (LPVOID) domain, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL); if (!thr) debug_printf ("Couldn't start rediscover thread."); else { last_rediscover = stop; CloseHandle (thr); } } if ((ret = ldap_search_stW (lh, NULL, LDAP_SCOPE_BASE, (PWCHAR) L"(objectclass=*)", rootdse_attr, 0, &tv, &msg)) != LDAP_SUCCESS) { debug_printf ("ldap_search(%W, ROOTDSE) error 0x%02x", domain, ret); goto err; } if (!(entry = ldap_first_entry (lh, msg))) { debug_printf ("No ROOTDSE entry for %W", domain); goto err; } if (!(val = ldap_get_valuesW (lh, entry, rootdse_attr[0]))) { debug_printf ("No ROOTDSE value for %W", domain); goto err; } if (!(rootdse = wcsdup (val[0]))) { debug_printf ("wcsdup(%W, ROOTDSE) %d", domain, get_errno ()); goto err; } ldap_value_freeW (val); if ((val = ldap_get_valuesW (lh, entry, rootdse_attr[1]))) { for (ULONG idx = 0; idx < ldap_count_valuesW (val); ++idx) if (!wcscmp (val[idx], LDAP_CAP_ACTIVE_DIRECTORY_OID_W)) { isAD = true; break; } } ldap_value_freeW (val); val = NULL; ldap_memfreeW ((PWCHAR) msg); msg = entry = NULL; return true; err: close (); return false; } void cyg_ldap::close () { if (msg_id != (ULONG) -1) ldap_abandon (lh, msg_id); if (lh) ldap_unbind (lh); if (msg) ldap_memfreeW ((PWCHAR) msg); if (val) ldap_value_freeW (val); if (rootdse) free (rootdse); lh = NULL; msg = entry = NULL; val = NULL; rootdse = NULL; msg_id = (ULONG) -1; } bool cyg_ldap::fetch_ad_account (PSID sid, bool group) { WCHAR filter[140], *f; LONG len = (LONG) RtlLengthSid (sid); PBYTE s = (PBYTE) sid; static WCHAR hex_wchars[] = L"0123456789abcdef"; ULONG ret; if (msg) { ldap_memfreeW ((PWCHAR) msg); msg = entry = NULL; } if (val) { ldap_value_freeW (val); val = NULL; } f = wcpcpy (filter, L"(objectSid="); while (len-- > 0) { *f++ = L'\\'; *f++ = hex_wchars[*s >> 4]; *f++ = hex_wchars[*s++ & 0xf]; } wcpcpy (f, L")"); attr = group ? group_attr : user_attr; if ((ret = ldap_search_stW (lh, rootdse, LDAP_SCOPE_SUBTREE, filter, attr, 0, &tv, &msg)) != LDAP_SUCCESS) { debug_printf ("ldap_search_stW(%W,%W) error 0x%02x", rootdse, filter, ret); return false; } if (!(entry = ldap_first_entry (lh, msg))) { debug_printf ("No entry for %W in rootdse %W", filter, rootdse); return false; } return true; } bool cyg_ldap::enumerate_ad_accounts (PCWSTR domain, bool group) { tmp_pathbuf tp; PCWSTR filter; PWCHAR dse; if (msg) { ldap_memfreeW ((PWCHAR) msg); msg = entry = NULL; } if (val) { ldap_value_freeW (val); val = NULL; } if (!group) filter = L"(&(objectClass=User)" "(objectCategory=Person)" /* 512 == ADS_UF_NORMAL_ACCOUNT */ "(userAccountControl:" LDAP_MATCHING_RULE_BIT_AND ":=512)" "(objectSid=*))"; else if (!domain) filter = L"(&(objectClass=Group)" "(objectSid=*))"; else filter = L"(&(objectClass=Group)" /* 1 == ACCOUNT_GROUP */ "(!(groupType:" LDAP_MATCHING_RULE_BIT_AND ":=1))" "(objectSid=*))"; if (!domain) dse = rootdse; else { /* create rootdse from domain name. */ dse = tp.w_get (); PCWSTR ps, pe; PWCHAR d; d = dse; for (ps = domain; (pe = wcschr (ps, L'.')); ps = pe + 1) { if (d > dse) d = wcpcpy (d, L","); d = wcpncpy (wcpcpy (d, L"DC="), ps, pe - ps); } if (d > dse) d = wcpcpy (d, L","); d = wcpcpy (wcpcpy (d, L"DC="), ps); } msg_id = ldap_searchW (lh, dse, LDAP_SCOPE_SUBTREE, (PWCHAR) filter, sid_attr, 0); if (msg_id == (ULONG) -1) { debug_printf ("ldap_searchW(%W,%W) error 0x%02x", dse, filter, LdapGetLastError ()); return false; } return true; } bool cyg_ldap::next_account (cygsid &sid) { ULONG ret; PLDAP_BERVAL *bval; if (msg) { ldap_memfreeW ((PWCHAR) msg); msg = entry = NULL; } if (val) { ldap_value_freeW (val); val = NULL; } ret = ldap_result (lh, msg_id, LDAP_MSG_ONE, &tv, &msg); if (ret == 0) { debug_printf ("ldap_result() timeout!"); return false; } if (ret == (ULONG) -1) { debug_printf ("ldap_result() error 0x%02x", LdapGetLastError ()); return false; } if ((entry = ldap_first_entry (lh, msg)) && (bval = ldap_get_values_lenW (lh, entry, sid_attr[0]))) { sid = (PSID) bval[0]->bv_val; ldap_value_free_len (bval); return true; } return false; } uint32_t cyg_ldap::fetch_posix_offset_for_domain (PCWSTR domain) { WCHAR filter[300]; ULONG ret; if (msg) { ldap_memfreeW ((PWCHAR) msg); msg = entry = NULL; } if (val) { ldap_value_freeW (val); val = NULL; } /* If domain name has no dot, it's a Netbios name. In that case, filter by flatName rather than by name. */ __small_swprintf (filter, L"(&(objectClass=trustedDomain)(%W=%W))", wcschr (domain, L'.') ? L"name" : L"flatName", domain); if ((ret = ldap_search_stW (lh, rootdse, LDAP_SCOPE_SUBTREE, filter, attr = tdom_attr, 0, &tv, &msg)) != LDAP_SUCCESS) { debug_printf ("ldap_search_stW(%W,%W) error 0x%02x", rootdse, filter, ret); return 0; } if (!(entry = ldap_first_entry (lh, msg))) { debug_printf ("No entry for %W in rootdse %W", filter, rootdse); return 0; } return get_num_attribute (0); } PWCHAR cyg_ldap::get_string_attribute (int idx) { if (val) ldap_value_freeW (val); val = ldap_get_valuesW (lh, entry, attr[idx]); if (val) return val[0]; return NULL; } uint32_t cyg_ldap::get_num_attribute (int idx) { PWCHAR ret = get_string_attribute (idx); if (ret) return (uint32_t) wcstoul (ret, NULL, 10); return (uint32_t) -1; } bool cyg_ldap::fetch_unix_sid_from_ad (uint32_t id, cygsid &sid, bool group) { WCHAR filter[48]; ULONG ret; PLDAP_BERVAL *bval; if (msg) { ldap_memfreeW ((PWCHAR) msg); msg = entry = NULL; } if (group) __small_swprintf (filter, L"(&(objectClass=Group)(gidNumber=%u))", id); else __small_swprintf (filter, L"(&(objectClass=User)(uidNumber=%u))", id); if ((ret = ldap_search_stW (lh, rootdse, LDAP_SCOPE_SUBTREE, filter, sid_attr, 0, &tv, &msg)) != LDAP_SUCCESS) { debug_printf ("ldap_search_stW(%W,%W) error 0x%02x", rootdse, filter, ret); return false; } if ((entry = ldap_first_entry (lh, msg)) && (bval = ldap_get_values_lenW (lh, entry, sid_attr[0]))) { sid = (PSID) bval[0]->bv_val; ldap_value_free_len (bval); return true; } return false; } PWCHAR cyg_ldap::fetch_unix_name_from_rfc2307 (uint32_t id, bool group) { WCHAR filter[52]; ULONG ret; if (msg) { ldap_memfreeW ((PWCHAR) msg); msg = entry = NULL; } if (val) { ldap_value_freeW (val); val = NULL; } attr = group ? rfc2307_gid_attr : rfc2307_uid_attr; if (group) __small_swprintf (filter, L"(&(objectClass=posixGroup)(gidNumber=%u))", id); else __small_swprintf (filter, L"(&(objectClass=posixAccount)(uidNumber=%u))", id); if ((ret = ldap_search_stW (lh, rootdse, LDAP_SCOPE_SUBTREE, filter, attr, 0, &tv, &msg)) != LDAP_SUCCESS) { debug_printf ("ldap_search_stW(%W,%W) error 0x%02x", rootdse, filter, ret); return NULL; } if (!(entry = ldap_first_entry (lh, msg))) { debug_printf ("No entry for %W in rootdse %W", filter, rootdse); return NULL; } return get_string_attribute (0); } uid_t cyg_ldap::remap_uid (uid_t uid) { cygsid user (NO_SID); PWCHAR name; struct passwd *pw; if (isAD) { if (fetch_unix_sid_from_ad (uid, user, false) && user != NO_SID && (pw = internal_getpwsid (user))) return pw->pw_uid; } else if ((name = fetch_unix_name_from_rfc2307 (uid, false))) { char *mbname = NULL; sys_wcstombs_alloc (&mbname, HEAP_NOTHEAP, name); if ((pw = internal_getpwnam (mbname))) return pw->pw_uid; } return ILLEGAL_UID; } gid_t cyg_ldap::remap_gid (gid_t gid) { cygsid group (NO_SID); PWCHAR name; struct group *gr; if (isAD) { if (fetch_unix_sid_from_ad (gid, group, true) && group != NO_SID && (gr = internal_getgrsid (group))) return gr->gr_gid; } else if ((name = fetch_unix_name_from_rfc2307 (gid, true))) { char *mbname = NULL; sys_wcstombs_alloc (&mbname, HEAP_NOTHEAP, name); if ((gr = internal_getgrnam (mbname))) return gr->gr_gid; } return ILLEGAL_GID; }