11package ldap
22
33import (
4+ "crypto/tls"
5+ "crypto/x509"
46 "embed"
57 "encoding/json"
68 "fmt"
79 "net/http"
10+ "os"
11+ "strings"
812
913 "github.com/DanielAuerX/answer-plugins/connector-ldap/i18n"
1014 "github.com/segmentfault/pacman/log"
@@ -34,21 +38,32 @@ type Connector struct {
3438}
3539
3640type ConnectorConfig struct {
37- Name string `json:"name"`
38- Server string `json:"server"`
39- BaseDN string `json:"base_dn"`
40- BindPrefix string `json:"bind_prefix"` // e.g., uid=
41- BindDN string `json:"bind_dn"` // service account DN
42- BindPassword string `json:"bind_password"` // service account password
43- UserAttr string `json:"user_attr"` // e.g., uid, sAMAccountName
41+ Name string `json:"name"`
42+ Server string `json:"server"`
43+ BaseDN string `json:"base_dn"`
44+ BindDN string `json:"bind_dn"`
45+ BindPassword string `json:"bind_password"`
46+ UserAttr string `json:"user_attr"`
47+ TLSCACertPath string `json:"tls_ca_cert_path"`
4448}
4549
4650var _ plugin.Connector = & Connector {}
4751
52+ var loginHTMLContent string
53+
4854func init () {
4955 plugin .Register (& Connector {
5056 Config : & ConnectorConfig {},
5157 })
58+
59+ htmlContent , err := loginHTML .ReadFile ("login.html" )
60+ if err != nil {
61+ log .Errorf ("failed to read embedded html file: %v" , err )
62+ }
63+ loginHTMLContent = string (htmlContent )
64+ if "" == loginHTMLContent {
65+ log .Error ("html file is empty" )
66+ }
5267}
5368
5469func (g * Connector ) Info () plugin.Info {
@@ -74,115 +89,125 @@ func (g *Connector) ConnectorName() plugin.Translator {
7489
7590func (g * Connector ) ConnectorSlugName () string {
7691 return "ldap"
92+
7793}
7894
7995func (g * Connector ) ConnectorLogoSVG () string {
8096 return ""
8197}
8298
8399func (g * Connector ) ConnectorSender (ctx * plugin.GinContext , receiverURL string ) string {
84- log .Info ("LDAP connector ConnectorSender..." )
85-
86- htmlContent , err := loginHTML .ReadFile ("login.html" )
87- if err != nil {
88- log .Errorf ("failed to read embedded html file: %v" , err )
89- ctx .Writer .WriteHeader (500 )
90- ctx .Writer .Write ([]byte ("Internal Server Error" ))
91- return ""
92- }
93100
94101 ctx .Writer .WriteHeader (200 )
95102 ctx .Writer .Header ().Set ("Content-Type" , "text/html" )
96- _ , _ = ctx .Writer .Write ([]byte (fmt .Sprintf (string (htmlContent ), receiverURL )))
97103
98- return ctx .Request .Host
104+ htmlContent := strings .Replace (loginHTMLContent , "RECEIVER_URL_PLACEHOLDER" , receiverURL , - 1 )
105+ _ , err := ctx .Writer .Write ([]byte (htmlContent ))
106+ if err != nil {
107+ log .Errorf ("failed to write HTML response: %v" , err )
108+ }
109+
110+ return ""
99111}
100112
101113// TODO get from translator
102114func (g * Connector ) ConfigFields () []plugin.ConfigField {
103115 return []plugin.ConfigField {
104116 createTextInput ("name" , "LDAP" , "LDAP connector name" , g .Config .Name , true , false ),
105- createTextInput ("server" , "LDAP Server" , "e.g. ldap.example.com:389 " , g .Config .Server , true , false ),
117+ createTextInput ("server" , "LDAP Server" , "e.g. ldaps:// ldap.example.com:636 " , g .Config .Server , true , false ),
106118 createTextInput ("base_dn" , "Base DN" , "e.g. dc=example,dc=com" , g .Config .BaseDN , true , false ),
107- createTextInput ("bind_prefix" , "Bind Prefix" , "e.g. CN= or uid=" , g .Config .BindPrefix , false , false ), //TODO NOT USED YET
108119 createTextInput ("bind_dn" , "Bind DN" , "DN of LDAP bind user" , g .Config .BindDN , true , false ),
109120 createTextInput ("bind_password" , "Bind Password" , "Password for bind DN" , g .Config .BindPassword , true , true ),
110121 createTextInput ("user_attr" , "User Attribute" , "LDAP attribute for username (e.g., uid or sAMAccountName)" , g .Config .UserAttr , true , false ),
122+ createTextInput ("tls_ca_cert_path" , "TLS CA Certificate Path" , "Path to custom CA certificate file (optional)" , g .Config .TLSCACertPath , false , false ),
111123 }
112124}
113125
114126func (g * Connector ) ConfigReceiver (config []byte ) error {
115127 c := & ConnectorConfig {}
116128 if err := json .Unmarshal (config , c ); err != nil {
117- return err
129+ return fmt . Errorf ( "invalid config json: %w" , err )
118130 }
119131 g .Config = c
120132 return nil
121133}
122134
123135func (c * Connector ) ConnectorReceiver (ctx * plugin.GinContext , receiverURL string ) (userInfo plugin.ExternalLoginUserInfo , err error ) {
124- log .Info ("ConnectorReceiver called!" )
125136
126137 username , password , err := extractCredentials (ctx .Request )
127138 if err != nil {
128139 return userInfo , err
129140 }
130141
131- l , err := ldap . DialURL (c .Config .Server )
142+ l , err := dialWithTLS (c .Config .Server , c . Config . TLSCACertPath )
132143 if err != nil {
133144 return userInfo , fmt .Errorf ("failed to connect to LDAP server: %w" , err )
134145 }
135146 defer l .Close ()
136147
137- err = l .Bind (c .Config .BindDN , c .Config .BindPassword )
148+ if err := bindServiceAccount (l , c .Config .BindDN , c .Config .BindPassword ); err != nil {
149+ return userInfo , fmt .Errorf ("service account bind failed: %w" , err )
150+ }
151+
152+ entry , err := searchUser (l , c .Config .BaseDN , c .Config .UserAttr , username )
138153 if err != nil {
139- return userInfo , fmt .Errorf ("bind failed: %w" , err )
154+ return userInfo , err
155+ }
156+
157+ err = l .Bind (entry .DN , password )
158+ if err != nil {
159+ return userInfo , fmt .Errorf ("invalid username or password" )
160+ }
161+
162+ userInfo , err = extractUserInfo (entry )
163+ if err != nil {
164+ return userInfo , err
140165 }
141166
167+ return userInfo , nil
168+ }
169+
170+ func bindServiceAccount (l * ldap.Conn , bindDN , bindPassword string ) error {
171+ return l .Bind (bindDN , bindPassword )
172+ }
173+
174+ func searchUser (l * ldap.Conn , baseDN , userAttr , username string ) (* ldap.Entry , error ) {
142175 searchRequest := ldap .NewSearchRequest (
143- c . Config . BaseDN ,
176+ baseDN ,
144177 ldap .ScopeWholeSubtree , ldap .NeverDerefAliases , 1 , 0 , false ,
145- fmt .Sprintf ("(%s=%s)" , c . Config . UserAttr , ldap .EscapeFilter (username )),
178+ fmt .Sprintf ("(%s=%s)" , userAttr , ldap .EscapeFilter (username )),
146179 []string {LdapAttributeDn , LdapAttributeUid , LdapAttributeCn , LdapAttributeMail , LdapAttributeDisplayName , LdapAttributeSamAccountName },
147180 nil ,
148181 )
149182
150183 sr , err := l .Search (searchRequest )
151184 if err != nil || len (sr .Entries ) == 0 {
152- return userInfo , fmt .Errorf ("user not found: %w" , err )
185+ return nil , fmt .Errorf ("user not found: %w" , err )
153186 }
154187
155- entry := sr .Entries [0 ]
156-
157- err = l .Bind (entry .DN , password )
158- if err != nil {
159- return userInfo , fmt .Errorf ("invalid username or password" )
160- }
161-
162- userInfo = extractUserInfo (entry )
163-
164- log .Infof ("userInfo %s" , & userInfo )
165-
166- return userInfo , nil
188+ return sr .Entries [0 ], nil
167189}
168190
169191func extractCredentials (request * http.Request ) (username string , password string , err error ) {
170- queryParams := request .URL .Query ()
192+ err = request .ParseForm ()
193+ if err != nil {
194+ log .Errorf ("failed to parse form: %v" , err )
195+ return "" , "" , err
196+ }
171197
172- username = queryParams . Get ("username" )
173- password = queryParams . Get ("password" )
198+ username = request . FormValue ("username" )
199+ password = request . FormValue ("password" )
174200
175201 if username == "" || password == "" {
176- log .Errorf ("missing username or password" )
202+ log .Errorf ("missing username and/ or password" )
177203 err = fmt .Errorf ("missing username or password" )
178204 }
179205 return
180206}
181207
182- func extractUserInfo (entry * ldap.Entry ) plugin.ExternalLoginUserInfo {
208+ func extractUserInfo (entry * ldap.Entry ) ( plugin.ExternalLoginUserInfo , error ) {
183209
184210 displayName := entry .GetAttributeValue (LdapAttributeDisplayName )
185- log .Infof ("displayName %s" , displayName )
186211
187212 if displayName == "" {
188213 displayName = entry .GetAttributeValue (LdapAttributeCn )
@@ -192,28 +217,24 @@ func extractUserInfo(entry *ldap.Entry) plugin.ExternalLoginUserInfo {
192217 if username == "" {
193218 username = entry .GetAttributeValue (LdapAttributeSamAccountName )
194219 }
195- log .Infof ("username %s" , & username )
196220
197221 externalID := username
198222 if externalID == "" {
199223 externalID = entry .DN // fallback
200224 }
201225
202- /*
203- email is used to login, therefore required.
204- wether the email is correct, is not important for our use case
205- */
226+ //email is used to login, therefore required
206227 email := entry .GetAttributeValue (LdapAttributeMail )
207228 if email == "" {
208- email = username + "@dummymail.xyz"
229+ return nil , fmt . Errorf ( "email is required" )
209230 }
210231
211232 return plugin.ExternalLoginUserInfo {
212233 ExternalID : externalID ,
213234 DisplayName : displayName ,
214235 Username : username ,
215236 Email : email ,
216- }
237+ }, nil
217238}
218239
219240func createTextInput (name , title , desc , value string , require bool , password bool ) plugin.ConfigField {
@@ -235,3 +256,58 @@ func createTextInput(name, title, desc, value string, require bool, password boo
235256 Value : value ,
236257 }
237258}
259+
260+ func createBoolInput (name , title , desc string , value bool , require bool ) plugin.ConfigField {
261+ return plugin.ConfigField {
262+
263+ Name : name ,
264+ Type : plugin .ConfigTypeCheckbox ,
265+ Title : plugin .MakeTranslator (title ),
266+ Description : plugin .MakeTranslator (desc ),
267+ Required : require ,
268+ UIOptions : plugin.ConfigFieldUIOptions {},
269+ Value : value ,
270+ }
271+
272+ }
273+
274+ func dialWithTLS (server string , certPath string ) (* ldap.Conn , error ) {
275+
276+ tlsConfig := & tls.Config {
277+ InsecureSkipVerify : false ,
278+ }
279+
280+ if certPath != "" {
281+ certPool := x509 .NewCertPool ()
282+ certData , err := os .ReadFile (certPath )
283+ if err != nil {
284+ log .Errorf ("Failed to read cert file: %v" , err )
285+ return nil , fmt .Errorf ("failed to read LDAP cert: %w" , err )
286+ }
287+
288+ if ! certPool .AppendCertsFromPEM (certData ) {
289+ log .Errorf ("Failed to append cert from %s" , certPath )
290+ return nil , fmt .Errorf ("failed to append cert" )
291+ }
292+
293+ tlsConfig .RootCAs = certPool
294+ }
295+
296+ if strings .HasPrefix (server , "ldaps://" ) {
297+ return ldap .DialURL (server , ldap .DialWithTLSConfig (tlsConfig ))
298+ }
299+
300+ conn , err := ldap .DialURL (server )
301+ if err != nil {
302+ log .Errorf ("Initial plain connection failed: %v" , err )
303+ return nil , err
304+ }
305+
306+ if err := conn .StartTLS (tlsConfig ); err != nil {
307+ log .Errorf ("StartTLS failed: %v" , err )
308+ conn .Close ()
309+ return nil , err
310+ }
311+
312+ return conn , nil
313+ }
0 commit comments