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")
 $zt="Error"
 Status = 0
 Password="" {
Status= $SYSTEM.Status.Error($$$InvalidUsernameOrPassword)
Error
 }
 $$$WindowsLDAPServer{
AdminDN=$Get(^OSUMCLDAP("User"))
AdminPW=$Get(^OSUMCLDAP("Pass"))
 }
 $$$ISWINDOWS,$$$UseSecureConnection{
 LD=##Class(%SYS.LDAP).Init($$$LDAPServer)
 LD=0 {
 Status=##Class(%SYS.LDAP).GetLastError()
 Status="Init error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
 Error
  }
 Status=##Class(%SYS.LDAP).SetOption(LD,$$$LDAPOPTXTLSCACERTFILE,$$$UnixCertificateFile)
 i Status'=$$$LDAPSUCCESS{
 Status ="SetOption error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
 Error

  Status=##class(%SYS.LDAP).StartTLSs(LD)
  Status'=$$$LDAPSUCCESS{
  Status="ldap_setoption(Certificate) - "_##class(%SYS.LDAP).Err2String(Status)
  Error
  }
 }

 Status=##Class(%SYS.LDAP).SimpleBinds(LD,AdminDN,AdminPW)
 Status'=$$$LDAPSUCCESS 
  {
Status = "ldap_Simple_Bind(AdminDN) - "_##Class(%SYS.LDAP).Err2String(Status)
#;w !,Status
Error
  }
  $$$WindowsLDAPServer {
  s Filter=$$$WindowsFilter_"="_Username
 }
 $$$WindowsLDAPServer {
AttributeList=$$$WindowsAttributeList
#;AttributeList
 
 $$$WindowsLDAPServer {
BaseDN=$$$WindowsBaseDN
#;BaseDN
 
  SearchScope=$$$LDAPSCOPESUBTREE
  Timeout=30
  SizeLimit=1
 Status=##Class(%SYS.LDAP).SearchExts(LD,BaseDN,SearchScope,Filter,AttributeList,0,"","",Timeout,"",.SearchResult)
 Status'=$$$LDAPSUCCESS {
Status=$$$XLDAPFILTERERROR {
Status="1,User "_Username_" does not exist"
!,Status
else {
Status=Status_",ldap_Search_Ext - "_##Class(%SYS.LDAP).Err2String(Status)
}
Error
 }
 NumEntries=##Class(%SYS.LDAP).CountEntries(LD,SearchResult)
 NumEntries=-1 {
 Status=##Class(%SYS.LDAP).GetError(LD)
 Status=Status_",ldap_Count_Entries - "_##Class(%SYS.LDAP).Err2String(Status)
 Error
 }
 NumEntries=0 {
Status="1,User "_Username_" does not exist"
  Error
 }
 NumEntries>1 {
Status="1,LDAP Filter is not unique"
  Error
 }
 CurrentEntry=##Class(%SYS.LDAP).FirstEntry(LD,SearchResult)
 CurrentEntry=0 {
Status=##Class(%SYS.LDAP).GetError(LD)
Status="ldap_FirstEntry - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 }
 DN=##Class(%SYS.LDAP).GetDN(LD,CurrentEntry)
 Password="" {
Status="1,ldap_Simple_Bind("_DN_") - password cannot be null"
Error
 }
 Status=##Class(%SYS.LDAP).SimpleBinds(LD,DN,Password)
 Status'=$$$LDAPSUCCESS {
Status=Status_",ldap_Simple_Bind("_DN_") - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 }
 Attribute=##Class(%SYS.LDAP).FirstAttribute(LD,CurrentEntry,.Ptr)
 while (Attribute'="") {
   Values=##Class(%SYS.LDAP).GetValuesLen(LD,CurrentEntry,Attribute)
   #;Values:"_Values
   Properties("Attributes",Attribute)=Values
  Attribute=##Class(%SYS.LDAP).NextAttribute(LD,CurrentEntry,.Ptr)
 }
 Properties("Username")=Username
 Properties("FullName")=$li(Properties("Attributes","displayName"))
 Properties("Attributes","displayName")
 Properties("Comment")=$li(Properties("Attributes","department"))
 Properties("Attributes","department")
 Properties("EmailAddress")=$li(Properties("Attributes","mail"))
 Properties("Attributes","mail")
 $d(SearchResult) ##Class(%SYS.LDAP).MsgFree(SearchResult)
 GroupFilter="(&(objectClass=group)(member:1.2.840.113556.1.4.1941:="_DN_"))"
 GroupAttributes=""
 Status=##Class(%SYS.LDAP).SearchExts(LD,BaseDN,$$$LDAPSCOPESUBTREE,GroupFilter,GroupAttributes,0,"","",10,0,.GroupSearchResult)
 Status'=$$$LDAPSUCCESS {
!,"SearchExts error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 }
 GroupNumEntries=##Class(%SYS.LDAP).CountEntries(LD,GroupSearchResult)
 GroupNumEntries=-1 {
Status=##Class(%SYS.LDAP).GetError(LD)
Status=##Class(%SYS.LDAP).Err2String(Status)
Error
 }
 !
 GroupNumEntries=0 {
!,"No nested groups for "_Username_" found"
Done
 }
 GroupNumEntries>0 {
//w !,"Found "_GroupNumEntries_" nested groups for user "_Username
 }
 GroupCurrentEntry=##Class(%SYS.LDAP).FirstEntry(LD,GroupSearchResult)
 GroupCurrentEntry=0 {
Status=##Class(%SYS.LDAP).GetError(LD)
!,"FirstEntry error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 }
 Groups=""
 While (GroupCurrentEntry'=0) {
GroupDN=##Class(%SYS.LDAP).GetDN(LD,GroupCurrentEntry)
GroupDN="" {
Status=##Class(%SYS.LDAP).GetError(LD)
!,"GetDN Group error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
Error
}
CN=$p(GroupDN,",",1)
AD=$p(CN,"=",2)
AD=$zcvt(AD,"L")
exists=''$d(^|"%SYS"|SYS("Security","RolesD",AD))
exists{
Properties("Roles") = AD
}
  GroupCurrentEntry=##Class(%SYS.LDAP).NextEntry(LD,GroupCurrentEntry)
 }
Done
 //i $d(SearchResult) d ##Class(%SYS.LDAP).MsgFree(SearchResult)
 $d(GroupSearchResult) ##Class(%SYS.LDAP).MsgFree(GroupSearchResult)
#;Close the connection and free the LDAP in memory structures.
 +$d(LD) ##Class(%SYS.LDAP).UnBinds(LD)
 #;w !,"SystemOK "_$SYSTEM.Status.OK()
 $SYSTEM.Status.OK()
Error $zt=""
 $d(SearchResult) ##Class(%SYS.LDAP).MsgFree(SearchResult)
 $d(GroupSearchResult) ##Class(%SYS.LDAP).MsgFree(GroupSearchResult)
 +$d(LD) Status=##class(%SYS.LDAP).UnBinds(LD)
 $ze'=""{
 #;w !,"ERROR:"_$SYSTEM.Status.Error($$$CacheError,$ze)
 $SYSTEM.Status.Error($$$CacheError,$ze)
 else{  
  #;w !,"ERROR:"_$SYSTEM.Status.Error($$$GeneralError,"LDAP error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status))
  $SYSTEM.Status.Error($$$GeneralError,"LDAP error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status))
 }
}
 

Thanks

Scott Roth

The Ohio State University Wexner Medical Center

  • + 1
  • 0
  • 234
  • 12
  • 2

Answers

Analysing your code I  found @ line 24

  $$$ISWINDOWS,$$$UseSecureConnection{

ending @ line 42   that contains 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")
$zt="Error"
 
 Status = 0

 Password="" {
Status= $SYSTEM.Status.Error($$$InvalidUsernameOrPassword)
Error
 }
 $$$WindowsLDAPServer{
AdminDN=$Get(^OSUMCLDAP("User"))
AdminPW=$Get(^OSUMCLDAP("Pass"))
 }

 #;The following line sets up the internal LDAP structures.
 $$$ISWINDOWS,$$$UseSecureConnection {
LD=##Class(%SYS.LDAP).Init($$$LDAPServer,636)
 else {
LD=##Class(%SYS.LDAP).Init($$$LDAPServer)
 }
 LD=0 {
Status=##Class(%SYS.LDAP).GetLastError()
Status="Init error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 }
 Status=##Class(%SYS.LDAP).SetOption(LD,$$$LDAPOPTXTLSCACERTFILE,$$$UnixCertificateFile)
 Status'=$$$LDAPSUCCESS{
Status ="SetOption error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 
 Status=##class(%SYS.LDAP).StartTLSs(LD)
 Status'=$$$LDAPSUCCESS{
Status=Status_",ldap_setoption(Certificate) - "_##class(%SYS.LDAP).Err2String(Status)
Error
  }
 
 Status=##Class(%SYS.LDAP).SimpleBinds(LD,AdminDN,AdminPW)
 Status'=$$$LDAPSUCCESS 
  {
Status = Status_", ldap_Simple_Bind(AdminDN) - "_##Class(%SYS.LDAP).Err2String(Status)
#;w !,Status
Error
  }

 $$$WindowsLDAPServer {
Filter=$$$WindowsFilter_"="_Username
 }
 $$$WindowsLDAPServer {
AttributeList=$$$WindowsAttributeList

 
 $$$WindowsLDAPServer {
BaseDN=$$$WindowsBaseDN

 
 
 SearchScope=$$$LDAPSCOPESUBTREE
 Timeout=30
 
 SizeLimit=1
 
 Status=##Class(%SYS.LDAP).SearchExts(LD,BaseDN,SearchScope,Filter,AttributeList,0,"","",Timeout,"",.SearchResult)
 Status'=$$$LDAPSUCCESS {

Status=$$$XLDAPFILTERERROR {
Status="1,User "_Username_" does not exist"
!,Status
else {
Status=Status_",ldap_Search_Ext - "_##Class(%SYS.LDAP).Err2String(Status)
}
Error
 }

 NumEntries=##Class(%SYS.LDAP).CountEntries(LD,SearchResult)
 NumEntries=-1 {
 Status=##Class(%SYS.LDAP).GetError(LD)
 Status=Status_",ldap_Count_Entries - "_##Class(%SYS.LDAP).Err2String(Status)
 Error
 }
NumEntries=0 {
Status="1,User "_Username_" does not exist"
  Error
 }
 
 NumEntries>1 {
Status="1,LDAP Filter is not unique"
  Error
 }
 
 CurrentEntry=##Class(%SYS.LDAP).FirstEntry(LD,SearchResult)
 CurrentEntry=0 {
Status=##Class(%SYS.LDAP).GetError(LD)
Status=Status_",ldap_FirstEntry - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 }
 
 DN=##Class(%SYS.LDAP).GetDN(LD,CurrentEntry
 Password="" {
Status="1,ldap_Simple_Bind("_DN_") - password cannot be null"
Error
 }
 
 Status=##Class(%SYS.LDAP).SimpleBinds(LD,DN,Password)
 Status'=$$$LDAPSUCCESS {
Status=Status_",ldap_Simple_Bind("_DN_") - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 }

 Attribute=##Class(%SYS.LDAP).FirstAttribute(LD,CurrentEntry,.Ptr)

 while (Attribute'="") {
   Values=##Class(%SYS.LDAP).GetValuesLen(LD,CurrentEntry,Attribute)
   #;Values:"_Values
   Properties("Attributes",Attribute)=Values
  Attribute=##Class(%SYS.LDAP).NextAttribute(LD,CurrentEntry,.Ptr)
 }
 Properties("Username")=Username
 Properties("FullName")=$li(Properties("Attributes","displayName"))
 Properties("Attributes","displayName")
 Properties("Comment")=$li(Properties("Attributes","department"))
 Properties("Attributes","department")
 Properties("EmailAddress")=$li(Properties("Attributes","mail"))
 Properties("Attributes","mail")
 
 $d(SearchResult) ##Class(%SYS.LDAP).MsgFree(SearchResult)
 
 GroupFilter="(&(objectClass=group)(member:1.2.840.113556.1.4.1941:="_DN_"))"
 GroupAttributes=""
 Status=##Class(%SYS.LDAP).SearchExts(LD,BaseDN,$$$LDAPSCOPESUBTREE,GroupFilter,GroupAttributes,0,"","",10,0,.GroupSearchResult)
 #;GroupSearch Status: "_Status
 Status'=$$$LDAPSUCCESS {
!,"SearchExts error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 }
 GroupNumEntries=##Class(%SYS.LDAP).CountEntries(LD,GroupSearchResult)
 GroupNumEntries=-1 {
Status=##Class(%SYS.LDAP).GetError(LD)
Status=##Class(%SYS.LDAP).Err2String(Status)
Error
 }
!
 GroupNumEntries=0 {
!,"No nested groups for "_Username_" found"
Done
 }
 GroupNumEntries>0 {
 }
 
 GroupCurrentEntry=##Class(%SYS.LDAP).FirstEntry(LD,GroupSearchResult)
 GroupCurrentEntry=0 {
Status=##Class(%SYS.LDAP).GetError(LD)
!,"FirstEntry error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
Error
 }
 Groups=""
 While (GroupCurrentEntry'=0) {
GroupDN=##Class(%SYS.LDAP).GetDN(LD,GroupCurrentEntry)
GroupDN="" {
Status=##Class(%SYS.LDAP).GetError(LD)
!,"GetDN Group error: "_Status_" - "_##Class(%SYS.LDAP).Err2String(Status)
Error
}
CN=$p(GroupDN,",",1)
AD=$p(CN,"=",2)
AD=$zcvt(AD,"L")
exists=''$d(^|"%SYS"|SYS("Security","RolesD",AD))
exists{
Properties("Roles") = AD

}


  GroupCurrentEntry=##Class(%SYS.LDAP).NextEntry(LD,GroupCurrentEntry)
 }
Done
+$d(LD) ##Class(%SYS.LDAP).UnBinds(LD)
 #;w !,"SystemOK "_$SYSTEM.Status.OK()
 $SYSTEM.Status.OK()
Error $zt=""
 $d(SearchResult) ##Class(%SYS.LDAP).MsgFree(SearchResult)
 $d(GroupSearchResult) ##Class(%SYS.LDAP).MsgFree(GroupSearchResult)
 +$d(LD) Status=##class(%SYS.LDAP).UnBinds(LD)
 $ze'=""{
 $SYSTEM.Status.Error($$$CacheError,$ze)
 else{  
  $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.
 

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")

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:

ZBREAK /TRACE:ALL:"/your/file/location/trace.log"

To trace when particular lines are executed

ZBREAK /TRACE:ON:"/your/file/location/trace.log"
Error^ZAUTHENTICATE:"T"
+42^ZAUTHENTICATE:"T"

To format the log file to remove blank lines for better readability.

sed  '/^$/d' /your/file/location/trace.log

I was able to track down the error to

Status=##class(%SYS.LDAP).StartTLSs(LD)
 Status'=$$$LDAPSUCCESS{
Status=Status_",ldap_setoption(Certificate) - "_##class(%SYS.LDAP).Err2String(Status)
Status,!
Error
  }

-1,ldap_setoption(Certificate) - Can't contact LDAP server

I was able to track down the error to

Status=##class(%SYS.LDAP).StartTLSs(LD)
 Status'=$$$LDAPSUCCESS{
Status=Status_",ldap_setoption(Certificate) - "_##class(%SYS.LDAP).Err2String(Status)
Status,!
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 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/...."?

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 1 if LDAP SimpleBind successful.
/// Return 0 if LDAP SimpleBind unsuccessful
ClassMethod Connect(ByRef Username As %String, ByRef Password As %String) As %Boolean {
   set userContext = "uid="_Username_",ou=aixuser,cn=aixsecdb,cn=aixdata,o=mycoolcompany_aix"
   set ldapConnection = ##class(%SYS.LDAP).Init("ldapserver1.mycoolcompany.com",51000)
   set status =##class(%SYS.LDAP).SimpleBinds(ldapConnection,userContext,Password)
   if (status=$$$LDAPSUCCESS)
   {
      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

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.

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.

It could be sufficient to  set ID=""
at the beginning of your code to avoid the <UNDEFINED> later down.