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,131 @@ 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..." )
85100
86- htmlContent , err := loginHTML .ReadFile ("login.html" )
101+ htmlContent := strings .Replace (loginHTMLContent , "RECEIVER_URL_PLACEHOLDER" , receiverURL , - 1 )
102+ ctx .Writer .WriteHeader (200 )
103+ ctx .Writer .Header ().Set ("Content-Type" , "text/html" )
104+ err := writeHtmlContent (ctx , htmlContent )
87105 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 ""
106+ log .Errorf ("failed to write HTML response: %v" , err )
92107 }
93108
109+ return ""
110+ }
111+
112+ func writeHtmlContent (ctx * plugin.GinContext , htmlContent string ) error {
94113 ctx .Writer .WriteHeader (200 )
95114 ctx .Writer .Header ().Set ("Content-Type" , "text/html" )
96- _ , _ = ctx .Writer .Write ([]byte (fmt .Sprintf (string (htmlContent ), receiverURL )))
97-
98- return ctx .Request .Host
115+ _ , err := ctx .Writer .Write ([]byte (htmlContent ))
116+ return err
99117}
100118
101119// TODO get from translator
102120func (g * Connector ) ConfigFields () []plugin.ConfigField {
103121 return []plugin.ConfigField {
104122 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 ),
123+ createTextInput ("server" , "LDAP Server" , "e.g. ldaps:// ldap.example.com:636 " , g .Config .Server , true , false ),
106124 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
108125 createTextInput ("bind_dn" , "Bind DN" , "DN of LDAP bind user" , g .Config .BindDN , true , false ),
109126 createTextInput ("bind_password" , "Bind Password" , "Password for bind DN" , g .Config .BindPassword , true , true ),
110127 createTextInput ("user_attr" , "User Attribute" , "LDAP attribute for username (e.g., uid or sAMAccountName)" , g .Config .UserAttr , true , false ),
128+ createTextInput ("tls_ca_cert_path" , "TLS CA Certificate Path" , "Path to custom CA certificate file (optional)" , g .Config .TLSCACertPath , false , false ),
111129 }
112130}
113131
114132func (g * Connector ) ConfigReceiver (config []byte ) error {
115133 c := & ConnectorConfig {}
116134 if err := json .Unmarshal (config , c ); err != nil {
117- return err
135+ return fmt . Errorf ( "invalid config json: %w" , err )
118136 }
119137 g .Config = c
120138 return nil
121139}
122140
123141func (c * Connector ) ConnectorReceiver (ctx * plugin.GinContext , receiverURL string ) (userInfo plugin.ExternalLoginUserInfo , err error ) {
124- log .Info ("ConnectorReceiver called!" )
125142
126143 username , password , err := extractCredentials (ctx .Request )
127144 if err != nil {
128145 return userInfo , err
129146 }
130147
131- l , err := ldap . DialURL (c .Config .Server )
148+ l , err := dialWithTLS (c .Config .Server , c . Config . TLSCACertPath )
132149 if err != nil {
133150 return userInfo , fmt .Errorf ("failed to connect to LDAP server: %w" , err )
134151 }
135152 defer l .Close ()
136153
137- err = l .Bind (c .Config .BindDN , c .Config .BindPassword )
154+ if err := bindServiceAccount (l , c .Config .BindDN , c .Config .BindPassword ); err != nil {
155+ return userInfo , fmt .Errorf ("service account bind failed: %w" , err )
156+ }
157+
158+ entry , err := searchUser (l , c .Config .BaseDN , c .Config .UserAttr , username )
138159 if err != nil {
139- return userInfo , fmt . Errorf ( "bind failed: %w" , err )
160+ return userInfo , err
140161 }
141162
163+ err = l .Bind (entry .DN , password )
164+ if err != nil {
165+ return userInfo , fmt .Errorf ("invalid username or password" )
166+ }
167+
168+ userInfo , err = extractUserInfo (entry )
169+ if err != nil {
170+ return userInfo , err
171+ }
172+
173+ return userInfo , nil
174+ }
175+
176+ func bindServiceAccount (l * ldap.Conn , bindDN , bindPassword string ) error {
177+ return l .Bind (bindDN , bindPassword )
178+ }
179+
180+ func searchUser (l * ldap.Conn , baseDN , userAttr , username string ) (* ldap.Entry , error ) {
142181 searchRequest := ldap .NewSearchRequest (
143- c . Config . BaseDN ,
182+ baseDN ,
144183 ldap .ScopeWholeSubtree , ldap .NeverDerefAliases , 1 , 0 , false ,
145- fmt .Sprintf ("(%s=%s)" , c . Config . UserAttr , ldap .EscapeFilter (username )),
184+ fmt .Sprintf ("(%s=%s)" , userAttr , ldap .EscapeFilter (username )),
146185 []string {LdapAttributeDn , LdapAttributeUid , LdapAttributeCn , LdapAttributeMail , LdapAttributeDisplayName , LdapAttributeSamAccountName },
147186 nil ,
148187 )
149188
150189 sr , err := l .Search (searchRequest )
151190 if err != nil || len (sr .Entries ) == 0 {
152- return userInfo , fmt .Errorf ("user not found: %w" , err )
153- }
154-
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" )
191+ return nil , fmt .Errorf ("user not found: %w" , err )
160192 }
161193
162- userInfo = extractUserInfo (entry )
163-
164- log .Infof ("userInfo %s" , & userInfo )
165-
166- return userInfo , nil
194+ return sr .Entries [0 ], nil
167195}
168196
169197func extractCredentials (request * http.Request ) (username string , password string , err error ) {
170- queryParams := request .URL .Query ()
198+ err = request .ParseForm ()
199+ if err != nil {
200+ log .Errorf ("failed to parse form: %v" , err )
201+ return "" , "" , err
202+ }
171203
172- username = queryParams . Get ("username" )
173- password = queryParams . Get ("password" )
204+ username = request . FormValue ("username" )
205+ password = request . FormValue ("password" )
174206
175207 if username == "" || password == "" {
176- log .Errorf ("missing username or password" )
208+ log .Errorf ("missing username and/ or password" )
177209 err = fmt .Errorf ("missing username or password" )
178210 }
179211 return
180212}
181213
182- func extractUserInfo (entry * ldap.Entry ) plugin.ExternalLoginUserInfo {
214+ func extractUserInfo (entry * ldap.Entry ) ( plugin.ExternalLoginUserInfo , error ) {
183215
184216 displayName := entry .GetAttributeValue (LdapAttributeDisplayName )
185- log .Infof ("displayName %s" , displayName )
186217
187218 if displayName == "" {
188219 displayName = entry .GetAttributeValue (LdapAttributeCn )
@@ -192,28 +223,24 @@ func extractUserInfo(entry *ldap.Entry) plugin.ExternalLoginUserInfo {
192223 if username == "" {
193224 username = entry .GetAttributeValue (LdapAttributeSamAccountName )
194225 }
195- log .Infof ("username %s" , & username )
196226
197227 externalID := username
198228 if externalID == "" {
199229 externalID = entry .DN // fallback
200230 }
201231
202- /*
203- email is used to login, therefore required.
204- wether the email is correct, is not important for our use case
205- */
232+ //email is used to login, therefore required
206233 email := entry .GetAttributeValue (LdapAttributeMail )
207234 if email == "" {
208- email = username + "@dummymail.xyz"
235+ return nil , fmt . Errorf ( "email is required" )
209236 }
210237
211238 return plugin.ExternalLoginUserInfo {
212239 ExternalID : externalID ,
213240 DisplayName : displayName ,
214241 Username : username ,
215242 Email : email ,
216- }
243+ }, nil
217244}
218245
219246func createTextInput (name , title , desc , value string , require bool , password bool ) plugin.ConfigField {
@@ -235,3 +262,58 @@ func createTextInput(name, title, desc, value string, require bool, password boo
235262 Value : value ,
236263 }
237264}
265+
266+ func createBoolInput (name , title , desc string , value bool , require bool ) plugin.ConfigField {
267+ return plugin.ConfigField {
268+
269+ Name : name ,
270+ Type : plugin .ConfigTypeCheckbox ,
271+ Title : plugin .MakeTranslator (title ),
272+ Description : plugin .MakeTranslator (desc ),
273+ Required : require ,
274+ UIOptions : plugin.ConfigFieldUIOptions {},
275+ Value : value ,
276+ }
277+
278+ }
279+
280+ func dialWithTLS (server string , certPath string ) (* ldap.Conn , error ) {
281+
282+ tlsConfig := & tls.Config {
283+ InsecureSkipVerify : false ,
284+ }
285+
286+ if certPath != "" {
287+ certPool := x509 .NewCertPool ()
288+ certData , err := os .ReadFile (certPath )
289+ if err != nil {
290+ log .Errorf ("failed to read cert file: %v" , err )
291+ return nil , fmt .Errorf ("failed to read LDAP cert: %w" , err )
292+ }
293+
294+ if ! certPool .AppendCertsFromPEM (certData ) {
295+ log .Errorf ("failed to append cert from %s" , certPath )
296+ return nil , fmt .Errorf ("failed to append cert" )
297+ }
298+
299+ tlsConfig .RootCAs = certPool
300+ }
301+
302+ if strings .HasPrefix (server , "ldaps://" ) {
303+ return ldap .DialURL (server , ldap .DialWithTLSConfig (tlsConfig ))
304+ }
305+
306+ conn , err := ldap .DialURL (server )
307+ if err != nil {
308+ log .Errorf ("initial plain connection failed: %v" , err )
309+ return nil , err
310+ }
311+
312+ if err := conn .StartTLS (tlsConfig ); err != nil {
313+ log .Errorf ("startTLS failed: %v" , err )
314+ conn .Close ()
315+ return nil , err
316+ }
317+
318+ return conn , nil
319+ }
0 commit comments