Skip to content

Commit 6a1d40a

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 6a1d40a

File tree

2 files changed

+202
-57
lines changed

2 files changed

+202
-57
lines changed

connector-ldap/ldap.go

Lines changed: 122 additions & 46 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,106 +89,114 @@ 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
140155
}
141156

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+
return userInfo, nil
165+
}
166+
167+
func bindServiceAccount(l *ldap.Conn, bindDN, bindPassword string) error {
168+
return l.Bind(bindDN, bindPassword)
169+
}
170+
171+
func searchUser(l *ldap.Conn, baseDN, userAttr, username string) (*ldap.Entry, error) {
142172
searchRequest := ldap.NewSearchRequest(
143-
c.Config.BaseDN,
173+
baseDN,
144174
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
145-
fmt.Sprintf("(%s=%s)", c.Config.UserAttr, ldap.EscapeFilter(username)),
175+
fmt.Sprintf("(%s=%s)", userAttr, ldap.EscapeFilter(username)),
146176
[]string{LdapAttributeDn, LdapAttributeUid, LdapAttributeCn, LdapAttributeMail, LdapAttributeDisplayName, LdapAttributeSamAccountName},
147177
nil,
148178
)
149179

150180
sr, err := l.Search(searchRequest)
151181
if err != nil || len(sr.Entries) == 0 {
152-
return userInfo, fmt.Errorf("user not found: %w", err)
182+
return nil, fmt.Errorf("user not found: %w", err)
153183
}
154184

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
185+
return sr.Entries[0], nil
167186
}
168187

169188
func extractCredentials(request *http.Request) (username string, password string, err error) {
170-
queryParams := request.URL.Query()
189+
err = request.ParseForm()
190+
if err != nil {
191+
log.Errorf("failed to parse form: %v", err)
192+
return "", "", err
193+
}
171194

172-
username = queryParams.Get("username")
173-
password = queryParams.Get("password")
195+
username = request.FormValue("username")
196+
password = request.FormValue("password")
174197

175198
if username == "" || password == "" {
176-
log.Errorf("missing username or password")
199+
log.Errorf("missing username and/or password")
177200
err = fmt.Errorf("missing username or password")
178201
}
179202
return
@@ -182,7 +205,6 @@ func extractCredentials(request *http.Request) (username string, password string
182205
func extractUserInfo(entry *ldap.Entry) plugin.ExternalLoginUserInfo {
183206

184207
displayName := entry.GetAttributeValue(LdapAttributeDisplayName)
185-
log.Infof("displayName %s", displayName)
186208

187209
if displayName == "" {
188210
displayName = entry.GetAttributeValue(LdapAttributeCn)
@@ -192,7 +214,6 @@ func extractUserInfo(entry *ldap.Entry) plugin.ExternalLoginUserInfo {
192214
if username == "" {
193215
username = entry.GetAttributeValue(LdapAttributeSamAccountName)
194216
}
195-
log.Infof("username %s", &username)
196217

197218
externalID := username
198219
if externalID == "" {
@@ -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: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,83 @@
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+
<style>
8+
body {
9+
font-family: Arial, sans-serif;
10+
background-color: #f8f9fa;
11+
display: flex;
12+
justify-content: center;
13+
align-items: center;
14+
height: 100vh;
15+
margin: 0;
16+
text-align: center;
17+
}
18+
19+
h3 {
20+
color: #31171f;
21+
margin-bottom: 20px;
22+
}
23+
24+
.container {
25+
display: inline-block;
26+
text-align: center;
27+
}
28+
29+
form {
30+
background-color: #ffffff;
31+
padding: 20px;
32+
border-radius: 8px;
33+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
34+
width: 300px;
35+
text-align: left;
36+
}
37+
38+
label {
39+
color: #495057;
40+
margin-bottom: 5px;
41+
display: block;
42+
}
43+
44+
input[type="text"],
45+
input[type="password"] {
46+
width: calc(100% - 20px);
47+
color: #31171f;
48+
padding: 10px;
49+
margin-bottom: 15px;
50+
border: 1px solid #31171f;
51+
border-radius: 5px;
52+
}
53+
54+
input[type="submit"] {
55+
background-color: #ea0029;
56+
color: white;
57+
border: none;
58+
padding: 10px;
59+
border-radius: 5px;
60+
cursor: pointer;
61+
font-size: 16px;
62+
width: 100%;
63+
}
64+
65+
input[type="submit"]:hover {
66+
background-color: #31171f;
67+
}
68+
</style>
69+
</head>
370
<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>
71+
<div class="container">
72+
<h3>LDAP Login</h3>
73+
<form method="post" action="RECEIVER_URL_PLACEHOLDER">
74+
<label>Username:</label>
75+
<input name="username" type="text" required/>
76+
<label>Password:</label>
77+
<input name="password" type="password" required/>
78+
<br/>
79+
<input type="submit" value="Login"/>
80+
</form>
81+
</div>
1382
</body>
1483
</html>

0 commit comments

Comments
 (0)