libadclient - Active Directory client for c++, Python and Golang.
Table of Contents
This simple C++/Python/Golang classes can be used to manipulate Active Directory from c++, Python and Golang programs.
Full list of supported methods can be found in:
- adclient.h (for c++)
- adclient.py (for Python)
- adclient_wrapper.go (for Golang)
Requirements:
- OpenLDAP or SunLDAP
- SASL (DIGEST-MD5, GSSAPI)
- KRB5
Note: you must have scons installed
git clone https://github.com/paleg/libadclient.git
cd libadclient
scons install (to build/install c++ library)
python setup.py install (to build/install Python library)Note: step 4 depends on step 3, if your want to upgrade Python module, you should upgrade c++ library first.
If you are getting errors in Python while importing module:
ImportError: dlopen(/Library/Python/2.7/site-packages/_adclient.so, 2): Library not loaded: libadclient.dylib
Referenced from: /Library/Python/2.7/site-packages/_adclient.so
Reason: unsafe use of relative rpath libadclient.dylib in /Library/Python/2.7/site-packages/_adclient.so with restricted binarythat is because System Integrity Protection. You can fix it with:
sudo install_name_tool -change libadclient.dylib /usr/local/lib/libadclient.dylib /Library/Python/2.7/site-packages/_adclient.sogo get github.com/paleg/libadclientYou must have swig >= 3.0.6 and Golang >= 1.4 to compile library. However, Golang >= 1.5.1 is recommended. When building Golang library only openldap >= 2.2 are supported.
Note: this library is not safe for concurrent use. If you need to use library from concurrently executing goroutines, the calls must be mediated by some kind of synchronization mechanism (channels or mutexes).
- boolean
adConnParams.securedandadConnParams.use_gssapichooses login authentication mode:SASL DIGEST-MD5auth (default). It requires properly configured DNS (both direct and reverse) and SPN records (see issue 1 for details).adConnParams.secured = trueadConnParams.use_gssapi = false
SASL GSSAPIauth (adConnParams.use_gssapimust be set totrue). It requires properly configured kerberos library (krb5.conf) withdefault_keytab_nameset to keytab with computer account.msktutilcan be used for this purpose, see Squid Active Directory Integration for example.adConnParams.secured = trueadConnParams.use_gssapi = true
simpleauth (clear text username and password). It does not require proper DNS and SPN setup, but with simple auth AD will refuse to do some actions (e.g. change passwords). For some configurations (2008 domain?), to get simple auth to work, binddn must contain domain suffix i.e.adConnParams.binddn = "user@domain.local").adConnParams.secured = false
- choosing Domain Controller to connect can be performed:
- automatically with domain DNS name (
adConnParams.domain) with optional AD site name (adConnParams.site):- ldap uries for
domainwill be detected via DNS SRV query (_ldap._tcp.SITE._sites.DOMAIN.LOCAL/_ldap._tcp.DOMAIN.LOCAL) - if
adConnParams.search_baseis empty - it will be constructed automatically from domain name (DC=DOMAIN,DC=LOCAL).
- ldap uries for
- manually with vector of ldap uries (
adConnParams.uries):- ldap uries must be prefixed with
adclient::ldap_prefix adConnParams.search_basemust be set explicitly.
- ldap uries must be prefixed with
- automatically with domain DNS name (
LDAP_OPT_NETWORK_TIMEOUTandLDAP_OPT_TIMEOUTcan be set withadConnParams.nettimeout.LDAP_OPT_TIMELIMITcan be set withadConnParams.timelimit.- after successfull binding following methods can be used to check connection properties:
binded_uri()- to get connected server ldap URIsearch_base()- to get current search baselogin_method()- to get method used for login (GSSAPI,DIGEST-MD5,SIMPLE)
Some object attributes (e.g. objectSid) are stored in Active Directory as binary values, so some functions (e.g. getObjectAttribute(user, "objectSid")) can return binary data (which can include NULL character as well as any unprintable characters). Usually it is not a problem as in c++, Python and Golang string type could hold any values, but:
- calling code should be ready to handle such values
- in Python3, binary data will be returned as
bytes, notunicodestrings.
decodeSID(sid)(c++)adclient.decodeSID(sid)(Python)adclient.DecodeSID(sid)(golang)
Can be used to convert objectSid from binary representation to string value
if sid, err := adclient.GetObjectAttribute(user, "objectSid"); err == nil {
fmt.Printf("objectSid encoded is %q\n", sid[0])
fmt.Printf("objectSid decoded is %v\n", adclient.DecodeSID(sid[0]))
}will return
objectSid encoded is "\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00;\x10\x8a\xceui\xab\xc3\xfao\x93\x81\xbe\xa2\x00\x00"
objectSid decoded is S-1-5-21-3465154619-3282790773-2173923322-41662int2ip(string value) / ip2int(string ip)(c++)adclient.int2ip(string value)(Python)adclient.Int2ip(value string) / adclient.Ip2int(ip string)(golang)
IP addresses (e.g. msRADIUSFramedIPAddress) are stored in object attributes as integer, these functions can be used to convert IP to integer and vise versa.
ip := "192.168.1.100"
ipInt := adclient.Ip2int(ip)
fmt.Printf("%s to int: %v\n", ip, ipInt)
ipStr := adclient.Int2ip(strconv.Itoa(ipInt))
fmt.Printf("%v to string: %s\n", ipInt, ipStr)will return
192.168.1.100 to int: -1062731420
-1062731420 to string: 192.168.1.100adclient::get_ldap_servers(domain, site)(c++)adclient.get_ldap_servers(domain, site)(Python)adclient.Ldap_servers(domain, site)(golang)
Can be used to get information about LDAP servers for domain/site from DNS. site parameter could be empty to get servers from domain level.
adclient::domain2dn(domain)(c++)adclient.domain2dn(domain)(Python)adclient.Domain2dn(domain)(golang)
Can be used to perform simple convertion from DOMAIN.COM to DC=DOMAIN,DC=COM.
#include "adclient.h"
#include <iostream>
#include <vector>
#include <map>
#include <string>
using namespace std;
int main() {
adConnParams params;
// login with a domain name
params.domain = "DOMAIN.LOCAL";
// choose DC from SITE (optional, could be ommited)
params.site = "SITE";
// or login with a list of ldap uries
// params.uries.push_back(adclient::ldap_prefix + "Server1");
// params.uries.push_back(adclient::ldap_prefix + "Server2");
// params.search_base = "dc=DOMAIN,dc=LOCAL";
params.binddn = "user";
params.bindpw = "password";
// simple auth mode
// params.secured = false;
// GSSAPI auth, only domain and use_gssapi required
// params.domain = "DOMAIN.LOCAL";
// params.site = "SITE";
// params.use_gssapi = true;
adclient ad;
try {
ad.login(params);
}
catch(ADBindException& ex) {
cout << "ADBindLogin: " << ex.msg << endl;
return 1;
}
try {
ad.groupRemoveUser("Group", "User");
vector <string> user_groups = ad.getUserGroups("User");
vector <string> users_in_group = ad.getUsersInGroup("Groups");
vector <string> user_lastlogon = ad.getObjectAttribute("User", "lastLogon");
map <string, vector<string> > user_attrs = ad.getObjectAttributes("User");
vector <string> users = ad.getUsers();
string dn = ad.getObjectDN("User");
bool result = ad.checkUserPassword("User", "Password");
bool disabled = ad.ifUserDisabled("User");
bool locked = ad.ifUserLocked("User");
}
catch (const ADOperationalException& ex) {
cout << "ADOperationalException: " << ex.msg << endl;
}
catch (const ADSearchException& ex) {
cout << "ADSearchException: " << ex.msg << endl;
}
return 0;
}import adclient
params = adclient.ADConnParams()
# login with a domain name
params.domain = "DOMAIN.LOCAL"
# choose DC from SITE (optional, could be ommited)
params.site = "SITE"
# or login with a list of ldap uries
# params.uries = [adclient.LdapPrefix+"Server1", adclient.LdapPrefix+"Server2"]
# params.search_base = "dc=DOMAIN,dc=LOCAL";
params.binddn = "user";
params.bindpw = "password";
# simple auth mode
# params.secured = False;
# GSSAPI auth, only domain and use_gssapi required
# params.domain = "DOMAIN.LOCAL";
# params.site = "SITE";
# params.use_gssapi = True;
ad = adclient.ADClient()
try:
ad.login(params)
except ADBindError as ex:
print("failed to connect to Active Directory: {}".format(ex))
exit(1)
try:
dn = ad.getObjectDN("User");
except adclient.ADSearchError as ex:
code = ad.get_error_num()
if code == adclient.AD_OBJECT_NOT_FOUND:
print("no such user")
else:
print("unknown search error")
except ADOperationalError:
print("unknown operational error")package main
import (
"github.com/paleg/libadclient"
"fmt"
)
func main() {
adclient.New()
defer adclient.Delete()
params := adclient.DefaultADConnParams()
// login with a domain name
params.Domain = "DOMAIN.LOCAL"
// choose DC from SITE (optional, could be ommited)
params.Site = "SITE"
// or login with a list of ldap uries
// params.Uries = append(params.Uries, adclient.LdapPrefix()+"Server1", adclient.LdapPrefix()+"Server2")
// params.Search_base = "dc=DOMAIN,dc=LOCAL";
params.Binddn = "user";
params.Bindpw = "password";
// simple auth mode
// params.Secured = false;
// enable GSSAPI auth
// params.UseGSSAPI = true
params.Timelimit = 60
params.Nettimeout = 60
if err := adclient.Login(params); err != nil {
fmt.Printf("Failed to AD login: %v\n", err)
return
}
group := "Domain Admins"
if users, err := adclient.GetUsersInGroup(group, true); err != nil {
fmt.Printf("Failed to get users in '%v': %v\n", group, err)
} else {
fmt.Printf("Users in '%v':\n", group)
for _, user := range users {
fmt.Printf("\t%v\n", user)
}
}
}