Skip to content

Commit eb5453c

Browse files
author
broccoli
committed
refacorting and ldaps
+ removed logs + using login.html + implemented ldaps: user can set a cert file for private ca
1 parent acd39d1 commit eb5453c

File tree

2 files changed

+147
-64
lines changed

2 files changed

+147
-64
lines changed

connector-ldap/ldap.go

Lines changed: 129 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package ldap
22

33
import (
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

3640
type 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

4650
var _ plugin.Connector = &Connector{}
4751

52+
var loginHTMLContent string
53+
4854
func 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

5469
func (g *Connector) Info() plugin.Info {
@@ -74,115 +89,125 @@ func (g *Connector) ConnectorName() plugin.Translator {
7489

7590
func (g *Connector) ConnectorSlugName() string {
7691
return "ldap"
92+
7793
}
7894

7995
func (g *Connector) ConnectorLogoSVG() string {
8096
return ""
8197
}
8298

8399
func (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
102114
func (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

114126
func (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

123135
func (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

169191
func 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

219240
func 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+
}

connector-ldap/login.html

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
<html>
2-
<head><title>LDAP Login</title></head>
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>LDAP Login</title>
7+
</head>
38
<body>
4-
<h3>LDAP Login</h3>
5-
<form method="get" action="%s">
6-
<label>Username:</label><br/>
7-
<input name="username" type="text"/><br/>
8-
<label>Password:</label><br/>
9-
<input name="password" type="password"/><br/>
10-
<br/>
11-
<input type="submit" value="Login"/>
12-
</form>
9+
<div class="container">
10+
<h3>LDAP Login</h3>
11+
<form method="post" action="RECEIVER_URL_PLACEHOLDER">
12+
<label>Username:</label>
13+
<input name="username" type="text" required/>
14+
<label>Password:</label>
15+
<input name="password" type="password" required/>
16+
<br/>
17+
<input type="submit" value="Login"/>
18+
</form>
19+
</div>
1320
</body>
1421
</html>

0 commit comments

Comments
 (0)