Adding TLS to ZAUTHENTICATE
I wrote a ZAUTHENTICATE.mac a couple of months back, and found recently that it is creating coredumps on almost a nightly basis. I think I have figured out this problem to be not clearing out my MsgSearch after I am doing 2 of them within the code.
1. Get User Attibutes from AD
2. Get User Groups From AD
So while I am trying to cleanup the code I thought it would be a good time to add a Certificate and TLS to the mix since I should of been using that all along. However I keep running into issues
Error message: Cache error: <UNDEFINED>ZAUTHENTICATE+104^ZAUTHENTICATE *LD
its not displaying the error code it should be from the ZAUTHENTICATE in the Audit Database. How do I get it to tell me where it is actually stopping in the ZAUTHENTICATE code? Or can someone look at the code below and see what I might be doing wrong?
ZAUTHENTICATE(ServiceName,Namespace,Username,Password,Credentials,Properties) PUBLIC {
#include %occErrors
#include %sySecurity
#include %sySite
#include %syLDAP
#define LDAPServer $Get(^OSUMCLDAP("Server"))
#define WindowsLDAPServer 1
#define WindowsCacheClient 0
#define UseSecureConnection 1
#define UnixCertificateFile $Get(^OSUMCLDAP("LDAPKey"))_"certnew.pem"
#define WindowsBaseDN "dc="_$Get(^OSUMCLDAP("Domain"))_",dc=edu"
#define WindowsFilter "sAMAccountname"
#define WindowsAttributeList $lb("displayName","department","mail")
s $zt="Error"
s Status = 0
i Password="" {
s Status= $SYSTEM.Status.Error($$$InvalidUsernameOrPassword)
g Error
}
i $$$WindowsLDAPServer{
s AdminDN=$Get(^OSUMCLDAP("User"))
s AdminPW=$Get(^OSUMCLDAP("Pass"))
}
i $$$ISWINDOWS,$$$UseSecureConnection{
s LD=##Class(%SYS.LDAP).Init($$$LDAPServer)
i LD=0 {
s Status=##Class(%SYS.LDAP).GetLastError()
s Status="Init error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s Status=##Class(%SYS.LDAP).SetOption(LD,$$$LDAPOPTXTLSCACERTFILE,$$$UnixCertificateFile)
i Status'=$$$LDAPSUCCESS{
s Status ="SetOption error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s Status=##class(%SYS.LDAP).StartTLSs(LD)
i Status'=$$$LDAPSUCCESS{
s Status="ldap_setoption(Certificate) - "_##class(%SYS.LDAP).Err2String(Status)
g Error
}
}
s Status=##Class(%SYS.LDAP).SimpleBinds(LD,AdminDN,AdminPW)
i Status'=$$$LDAPSUCCESS
{
s Status = "ldap_Simple_Bind(AdminDN) - "_##Class(%SYS.LDAP).Err2String(Status)
#;w !,Status
g Error
}
i $$$WindowsLDAPServer {
s Filter=$$$WindowsFilter_"="_Username
}
i $$$WindowsLDAPServer {
s AttributeList=$$$WindowsAttributeList
#;AttributeList
}
i $$$WindowsLDAPServer {
s BaseDN=$$$WindowsBaseDN
#;BaseDN
}
s SearchScope=$$$LDAPSCOPESUBTREE
s Timeout=30
s SizeLimit=1
s Status=##Class(%SYS.LDAP).SearchExts(LD,BaseDN,SearchScope,Filter,AttributeList,0,"","",Timeout,"",.SearchResult)
i Status'=$$$LDAPSUCCESS {
i Status=$$$XLDAPFILTERERROR {
s Status="1,User "_Username_" does not exist"
w !,Status
} else {
s Status=Status_",ldap_Search_Ext - "_##Class(%SYS.LDAP).Err2String(Status)
}
g Error
}
s NumEntries=##Class(%SYS.LDAP).CountEntries(LD,SearchResult)
i NumEntries=-1 {
s Status=##Class(%SYS.LDAP).GetError(LD)
s Status=Status_",ldap_Count_Entries - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
i NumEntries=0 {
s Status="1,User "_Username_" does not exist"
g Error
}
i NumEntries>1 {
s Status="1,LDAP Filter is not unique"
g Error
}
s CurrentEntry=##Class(%SYS.LDAP).FirstEntry(LD,SearchResult)
i CurrentEntry=0 {
s Status=##Class(%SYS.LDAP).GetError(LD)
s Status="ldap_FirstEntry - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s DN=##Class(%SYS.LDAP).GetDN(LD,CurrentEntry)
i Password="" {
s Status="1,ldap_Simple_Bind("_DN_") - password cannot be null"
g Error
}
s Status=##Class(%SYS.LDAP).SimpleBinds(LD,DN,Password)
i Status'=$$$LDAPSUCCESS {
s Status=Status_",ldap_Simple_Bind("_DN_") - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s Attribute=##Class(%SYS.LDAP).FirstAttribute(LD,CurrentEntry,.Ptr)
while (Attribute'="") {
s Values=##Class(%SYS.LDAP).GetValuesLen(LD,CurrentEntry,Attribute)
#;Values:"_Values
s Properties("Attributes",Attribute)=Values
s Attribute=##Class(%SYS.LDAP).NextAttribute(LD,CurrentEntry,.Ptr)
}
s Properties("Username")=Username
s Properties("FullName")=$li(Properties("Attributes","displayName"))
k Properties("Attributes","displayName")
s Properties("Comment")=$li(Properties("Attributes","department"))
k Properties("Attributes","department")
s Properties("EmailAddress")=$li(Properties("Attributes","mail"))
k Properties("Attributes","mail")
i $d(SearchResult) d ##Class(%SYS.LDAP).MsgFree(SearchResult)
s GroupFilter="(&(objectClass=group)(member:1.2.840.113556.1.4.1941:="_DN_"))"
s GroupAttributes=""
s Status=##Class(%SYS.LDAP).SearchExts(LD,BaseDN,$$$LDAPSCOPESUBTREE,GroupFilter,GroupAttributes,0,"","",10,0,.GroupSearchResult)
i Status'=$$$LDAPSUCCESS {
w !,"SearchExts error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s GroupNumEntries=##Class(%SYS.LDAP).CountEntries(LD,GroupSearchResult)
i GroupNumEntries=-1 {
s Status=##Class(%SYS.LDAP).GetError(LD)
s Status=##Class(%SYS.LDAP).Err2String(Status)
g Error
}
w !
i GroupNumEntries=0 {
w !,"No nested groups for "_Username_" found"
g Done
}
i GroupNumEntries>0 {
//w !,"Found "_GroupNumEntries_" nested groups for user "_Username
}
s GroupCurrentEntry=##Class(%SYS.LDAP).FirstEntry(LD,GroupSearchResult)
i GroupCurrentEntry=0 {
s Status=##Class(%SYS.LDAP).GetError(LD)
w !,"FirstEntry error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s Groups=""
While (GroupCurrentEntry'=0) {
s GroupDN=##Class(%SYS.LDAP).GetDN(LD,GroupCurrentEntry)
i GroupDN="" {
s Status=##Class(%SYS.LDAP).GetError(LD)
w !,"GetDN Group error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s CN=$p(GroupDN,",",1)
s AD=$p(CN,"=",2)
s AD=$zcvt(AD,"L")
s exists=''$d(^|"%SYS"|SYS("Security","RolesD",AD))
i exists{
s Properties("Roles") = AD
}
s GroupCurrentEntry=##Class(%SYS.LDAP).NextEntry(LD,GroupCurrentEntry)
}
Done
//i $d(SearchResult) d ##Class(%SYS.LDAP).MsgFree(SearchResult)
i $d(GroupSearchResult) d ##Class(%SYS.LDAP).MsgFree(GroupSearchResult)
#;Close the connection and free the LDAP in memory structures.
i +$d(LD) d ##Class(%SYS.LDAP).UnBinds(LD)
#;w !,"SystemOK "_$SYSTEM.Status.OK()
q $SYSTEM.Status.OK()
Error s $zt=""
i $d(SearchResult) d ##Class(%SYS.LDAP).MsgFree(SearchResult)
i $d(GroupSearchResult) d ##Class(%SYS.LDAP).MsgFree(GroupSearchResult)
i +$d(LD) s Status=##class(%SYS.LDAP).UnBinds(LD)
i $ze'=""{
#;w !,"ERROR:"_$SYSTEM.Status.Error($$$CacheError,$ze)
q $SYSTEM.Status.Error($$$CacheError,$ze)
} else{
#;w !,"ERROR:"_$SYSTEM.Status.Error($$$GeneralError,"LDAP error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status))
q $SYSTEM.Status.Error($$$GeneralError,"LDAP error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status))
}
}
Thanks
Scott Roth
The Ohio State University Wexner Medical Center
Comments
Analysing your code I found @ line 24
i $$$ISWINDOWS,$$$UseSecureConnection{
ending @ line 42 that contains s LD=##Class(%SYS.LDAP).Init($$$LDAPServer)
but i didn't see an ELSE if the first IF fails.
Then LD is undefined.
I'm a little bit surprised how it could go up to line 104
Thanks.
I am now to the point where I am getting Error message: LDAP error: 0 - Success. How can a Success be a Failure?
ZAUTHENTICATE(ServiceName,Namespace,Username,Password,Credentials,Properties) PUBLIC {
#include %occErrors
#include %sySecurity
#include %sySite
#include %syLDAP
#define LDAPServer $Get(^OSUMCLDAP("Server"))
#define WindowsLDAPServer 1
#define WindowsCacheClient 0
#define UseSecureConnection 1
#define UnixCertificateFile $Get(^OSUMCLDAP("LDAPKey"))_"certnew.pem"
#define WindowsBaseDN "dc="_$Get(^OSUMCLDAP("Domain"))_",dc=edu"
#define WindowsFilter "sAMAccountname"
#define WindowsAttributeList $lb("displayName","department","mail")
s $zt="Error"
s Status = 0
i Password="" {
s Status= $SYSTEM.Status.Error($$$InvalidUsernameOrPassword)
g Error
}
i $$$WindowsLDAPServer{
s AdminDN=$Get(^OSUMCLDAP("User"))
s AdminPW=$Get(^OSUMCLDAP("Pass"))
}
#;The following line sets up the internal LDAP structures.
i $$$ISWINDOWS,$$$UseSecureConnection {
s LD=##Class(%SYS.LDAP).Init($$$LDAPServer,636)
} else {
s LD=##Class(%SYS.LDAP).Init($$$LDAPServer)
}
i LD=0 {
s Status=##Class(%SYS.LDAP).GetLastError()
s Status="Init error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s Status=##Class(%SYS.LDAP).SetOption(LD,$$$LDAPOPTXTLSCACERTFILE,$$$UnixCertificateFile)
i Status'=$$$LDAPSUCCESS{
s Status ="SetOption error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s Status=##class(%SYS.LDAP).StartTLSs(LD)
i Status'=$$$LDAPSUCCESS{
s Status=Status_",ldap_setoption(Certificate) - "_##class(%SYS.LDAP).Err2String(Status)
g Error
}
s Status=##Class(%SYS.LDAP).SimpleBinds(LD,AdminDN,AdminPW)
i Status'=$$$LDAPSUCCESS
{
s Status = Status_", ldap_Simple_Bind(AdminDN) - "_##Class(%SYS.LDAP).Err2String(Status)
#;w !,Status
g Error
}
i $$$WindowsLDAPServer {
s Filter=$$$WindowsFilter_"="_Username
}
i $$$WindowsLDAPServer {
s AttributeList=$$$WindowsAttributeList
}
i $$$WindowsLDAPServer {
s BaseDN=$$$WindowsBaseDN
}
s SearchScope=$$$LDAPSCOPESUBTREE
s Timeout=30
s SizeLimit=1
s Status=##Class(%SYS.LDAP).SearchExts(LD,BaseDN,SearchScope,Filter,AttributeList,0,"","",Timeout,"",.SearchResult)
i Status'=$$$LDAPSUCCESS {
i Status=$$$XLDAPFILTERERROR {
s Status="1,User "_Username_" does not exist"
w !,Status
} else {
s Status=Status_",ldap_Search_Ext - "_##Class(%SYS.LDAP).Err2String(Status)
}
g Error
}
s NumEntries=##Class(%SYS.LDAP).CountEntries(LD,SearchResult)
i NumEntries=-1 {
s Status=##Class(%SYS.LDAP).GetError(LD)
s Status=Status_",ldap_Count_Entries - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
i NumEntries=0 {
s Status="1,User "_Username_" does not exist"
g Error
}
i NumEntries>1 {
s Status="1,LDAP Filter is not unique"
g Error
}
s CurrentEntry=##Class(%SYS.LDAP).FirstEntry(LD,SearchResult)
i CurrentEntry=0 {
s Status=##Class(%SYS.LDAP).GetError(LD)
s Status=Status_",ldap_FirstEntry - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s DN=##Class(%SYS.LDAP).GetDN(LD,CurrentEntry)
i Password="" {
s Status="1,ldap_Simple_Bind("_DN_") - password cannot be null"
g Error
}
s Status=##Class(%SYS.LDAP).SimpleBinds(LD,DN,Password)
i Status'=$$$LDAPSUCCESS {
s Status=Status_",ldap_Simple_Bind("_DN_") - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s Attribute=##Class(%SYS.LDAP).FirstAttribute(LD,CurrentEntry,.Ptr)
while (Attribute'="") {
s Values=##Class(%SYS.LDAP).GetValuesLen(LD,CurrentEntry,Attribute)
#;Values:"_Values
s Properties("Attributes",Attribute)=Values
s Attribute=##Class(%SYS.LDAP).NextAttribute(LD,CurrentEntry,.Ptr)
}
s Properties("Username")=Username
s Properties("FullName")=$li(Properties("Attributes","displayName"))
k Properties("Attributes","displayName")
s Properties("Comment")=$li(Properties("Attributes","department"))
k Properties("Attributes","department")
s Properties("EmailAddress")=$li(Properties("Attributes","mail"))
k Properties("Attributes","mail")
i $d(SearchResult) d ##Class(%SYS.LDAP).MsgFree(SearchResult)
s GroupFilter="(&(objectClass=group)(member:1.2.840.113556.1.4.1941:="_DN_"))"
s GroupAttributes=""
s Status=##Class(%SYS.LDAP).SearchExts(LD,BaseDN,$$$LDAPSCOPESUBTREE,GroupFilter,GroupAttributes,0,"","",10,0,.GroupSearchResult)
#;GroupSearch Status: "_Status
i Status'=$$$LDAPSUCCESS {
w !,"SearchExts error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s GroupNumEntries=##Class(%SYS.LDAP).CountEntries(LD,GroupSearchResult)
i GroupNumEntries=-1 {
s Status=##Class(%SYS.LDAP).GetError(LD)
s Status=##Class(%SYS.LDAP).Err2String(Status)
g Error
}
w !
i GroupNumEntries=0 {
w !,"No nested groups for "_Username_" found"
g Done
}
i GroupNumEntries>0 {
}
s GroupCurrentEntry=##Class(%SYS.LDAP).FirstEntry(LD,GroupSearchResult)
i GroupCurrentEntry=0 {
s Status=##Class(%SYS.LDAP).GetError(LD)
w !,"FirstEntry error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s Groups=""
While (GroupCurrentEntry'=0) {
s GroupDN=##Class(%SYS.LDAP).GetDN(LD,GroupCurrentEntry)
i GroupDN="" {
s Status=##Class(%SYS.LDAP).GetError(LD)
w !,"GetDN Group error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
g Error
}
s CN=$p(GroupDN,",",1)
s AD=$p(CN,"=",2)
s AD=$zcvt(AD,"L")
s exists=''$d(^|"%SYS"|SYS("Security","RolesD",AD))
i exists{
s Properties("Roles") = AD
}
s GroupCurrentEntry=##Class(%SYS.LDAP).NextEntry(LD,GroupCurrentEntry)
}
Done
i +$d(LD) d ##Class(%SYS.LDAP).UnBinds(LD)
#;w !,"SystemOK "_$SYSTEM.Status.OK()
q $SYSTEM.Status.OK()
Error s $zt=""
i $d(SearchResult) d ##Class(%SYS.LDAP).MsgFree(SearchResult)
i $d(GroupSearchResult) d ##Class(%SYS.LDAP).MsgFree(GroupSearchResult)
i +$d(LD) s Status=##class(%SYS.LDAP).UnBinds(LD)
i $ze'=""{
q $SYSTEM.Status.Error($$$CacheError,$ze)
} else{
q $SYSTEM.Status.Error($$$GeneralError,"LDAP error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status))
}
}
Hi Scott,
It's indeed surprising.
But digging into docs tells me>>>> it's not an ERROR code but a RESULT code and 0 = Success. (like SQL)
more LDAP Result Codes
Err2String is defintely a misleading naming.
That's the part I am unsure of, because it is not logging to the Audit Database. How can I run this and put breakpoints in to verify where it might be getting stuck?
My preferred approach would be to use the ZBREAK utility but you could also potentially use ^%SYS.MONLBL. You can use ZBREAK for setting breakpoints or watchpoints or tracing line-by-line execution. You could even set it at the beginning of your ZAUTHENTICATE routine itself rather than a shell session. There is a bug with this utility regarding the use of round-brackets for setting a group of variables to a value, which I have documented here and with WRC.
To trace every line of execution:
To trace when particular lines are executed
Error^ZAUTHENTICATE:"T"
+42^ZAUTHENTICATE:"T"
To format the log file to remove blank lines for better readability.
I was able to track down the error to
s Status=##class(%SYS.LDAP).StartTLSs(LD)
i Status'=$$$LDAPSUCCESS{
s Status=Status_",ldap_setoption(Certificate) - "_##class(%SYS.LDAP).Err2String(Status)
w Status,!
g Error
}
-1,ldap_setoption(Certificate) - Can't contact LDAP server
Minus one (-1) is commonly used for a null reference error. Verify your certificate is in the PEM format, verify it exists in the relative path defined in your global and refer to the documentation in the %SYS.LDAP.StartTLSs and %SYS.LDAP.SetOption methods. You might want to test passing in a raw string value pointing to the certificate file, just to rule out any directory parsing errors.
I was able to track down the error to
s Status=##class(%SYS.LDAP).StartTLSs(LD)
i Status'=$$$LDAPSUCCESS{
s Status=Status_",ldap_setoption(Certificate) - "_##class(%SYS.LDAP).Err2String(Status)
w Status,!
g Error
}
-1,ldap_setoption(Certificate) - Can't contact LDAP server
You would probably get a syntax error if LD resolved to an empty string (""). I would put a trace/watchpoint on LD or alternatively log its contents to a global to verify it is not null. You might also want to check the definition #define LDAPServer $Get(^OSUMCLDAP("Server")) to see if it is correct and verify whether it should or should not contain a port number.
Here's another sample SimpleBinds() operation using server ldapserver1.mycoolcompany.com port 51000
/// Return 0 if LDAP SimpleBind unsuccessful
set ldapConnection = ##class(%SYS.LDAP).Init("ldapserver1.mycoolcompany.com",51000)
set status =##class(%SYS.LDAP).SimpleBinds(ldapConnection,userContext,Password)
{
write !, "LDAP SimpleBind Successful!"
return 1
}
else
{
write !,"LDAP SimpleBind failed!"
write !,"Error code : ",status
write !,"Error message : ",##Class(%SYS.LDAP).Err2String(status)
return 0
}
The Init() method documentation has some good troubleshooting tips under 'Error Codes' for connecting using SSL/TLS
Yes I was. That would explain the syntax issue. I have verified that the SimpleBinds without StartTLSs works fine. I have added some additional print statements to see where the issue might lie. The code gets past the Init, and SetOption, but then dies on the StartTLSs.
LD=1
SetOption=Success
-11,ldap_StartTLSs(Certificate) - Connect error
I went ahead and opened a ticket up with WRC to see if they could help. Thanks everyone.
Are you running that command at the programmer prompt, by any chance? It's got a macro defined, and therefore won't work at the command prompt unless you change the macro.
I think it has something to with Unix/Linux status 0 meaning the command executed successfully. Caché ObjectScript error status 0 usually means some error has occurred. $$$LDAPSUCCESS from %syLDAP.inc is defined as 0. The LDAP Error message: LDAP error: 0 - Success is partially hard-coded. Can you verify where your code is getting into the Error label?
#define LDAPSUCCESS $zhex("00")
I am still struggling to get this to work. If I go through %SYS and manually try to start a TLS connection in AIX to my LDAP server I am getting...
s Status=##Class(%SYS.LDAP).SetOption(LD,$$$LDAPOPTXTLSCACERTFILE,"/ensemble/TestClin/mgr/LDAPKeyStore/OSUWMC_CA.pem") - Error <SYNTAX>
Does the certificate need to be in a certain directory for this to work as the examples suggested " /usr/share/ssl/certs/...."?
It could be sufficient to set ID=""
at the beginning of your code to avoid the <UNDEFINED> later down.