Skip to content

Commit 9812413

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 9812413

File tree

2 files changed

+151
-62
lines changed

2 files changed

+151
-62
lines changed

connector-ldap/ldap.go

Lines changed: 133 additions & 51 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,131 @@ 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...")
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
102120
func (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

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

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

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

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

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)