You've already forked docker-ldap-change-password-web-ui
Initial version.
This commit is contained in:
130
app/ldap.go
Normal file
130
app/ldap.go
Normal file
@ -0,0 +1,130 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
// SecurityProtocol protocol type
|
||||
type SecurityProtocol int
|
||||
|
||||
// Note: new type must be added at the end of list to maintain compatibility.
|
||||
const (
|
||||
SecurityProtocolUnencrypted SecurityProtocol = iota
|
||||
SecurityProtocolLDAPS
|
||||
SecurityProtocolStartTLS
|
||||
)
|
||||
|
||||
// LDAPClient Basic LDAP authentication service
|
||||
type LDAPClient struct {
|
||||
Name string // canonical name (ie. corporate.ad)
|
||||
Host string // LDAP host
|
||||
Port int // port number
|
||||
SecurityProtocol SecurityProtocol
|
||||
SkipVerify bool
|
||||
UserBase string // Base search path for users
|
||||
UserDN string // Template for the DN of the user for simple auth
|
||||
Enabled bool // if this LDAPClient is disabled
|
||||
}
|
||||
|
||||
func bindUser(l *ldap.Conn, userDN, passwd string) error {
|
||||
log.Printf("\nBinding with userDN: %s", userDN)
|
||||
err := l.Bind(userDN, passwd)
|
||||
if err != nil {
|
||||
log.Printf("\nLDAP auth. failed for %s, reason: %v", userDN, err)
|
||||
return err
|
||||
}
|
||||
log.Printf("\nBound successfully with userDN: %s", userDN)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ls *LDAPClient) sanitizedUserDN(username string) (string, bool) {
|
||||
// See http://tools.ietf.org/search/rfc4514: "special characters"
|
||||
badCharacters := "\x00()*\\,='\"#+;<>"
|
||||
if strings.ContainsAny(username, badCharacters) {
|
||||
log.Printf("\n'%s' contains invalid DN characters. Aborting.", username)
|
||||
return "", false
|
||||
}
|
||||
|
||||
return fmt.Sprintf(ls.UserDN, username), true
|
||||
}
|
||||
|
||||
func dial(ls *LDAPClient) (*ldap.Conn, error) {
|
||||
log.Printf("\nDialing LDAP with security protocol (%v) without verifying: %v", ls.SecurityProtocol, ls.SkipVerify)
|
||||
|
||||
tlsCfg := &tls.Config{
|
||||
ServerName: ls.Host,
|
||||
InsecureSkipVerify: ls.SkipVerify,
|
||||
}
|
||||
if ls.SecurityProtocol == SecurityProtocolLDAPS {
|
||||
return ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port), tlsCfg)
|
||||
}
|
||||
|
||||
conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ls.Host, ls.Port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Dial: %v", err)
|
||||
}
|
||||
|
||||
if ls.SecurityProtocol == SecurityProtocolStartTLS {
|
||||
if err = conn.StartTLS(tlsCfg); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("StartTLS: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// ModifyPassword : modify user's password
|
||||
func (ls *LDAPClient) ModifyPassword(name, passwd, newPassword string) error {
|
||||
if len(passwd) == 0 {
|
||||
return fmt.Errorf("Auth. failed for %s, password cannot be empty", name)
|
||||
}
|
||||
l, err := dial(ls)
|
||||
if err != nil {
|
||||
ls.Enabled = false
|
||||
return fmt.Errorf("LDAP Connect error, %s:%v", ls.Host, err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
var userDN string
|
||||
log.Printf("\nLDAP will bind directly via UserDN template: %s", ls.UserDN)
|
||||
|
||||
var ok bool
|
||||
userDN, ok = ls.sanitizedUserDN(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("Error sanitizing name %s", name)
|
||||
}
|
||||
bindUser(l, userDN, passwd)
|
||||
|
||||
log.Printf("\nLDAP will execute password change on: %s", userDN)
|
||||
req := ldap.NewPasswordModifyRequest(userDN, passwd, newPassword)
|
||||
_, err = l.PasswordModify(req)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewLDAPClient : Creates new LDAPClient capable of binding and changing passwords
|
||||
func NewLDAPClient() *LDAPClient {
|
||||
|
||||
securityProtocol := SecurityProtocolUnencrypted
|
||||
if envBool("LPW_ENCRYPTED", true) {
|
||||
securityProtocol = SecurityProtocolLDAPS
|
||||
if envBool("LPW_START_TLS", false) {
|
||||
securityProtocol = SecurityProtocolStartTLS
|
||||
}
|
||||
}
|
||||
|
||||
return &LDAPClient{
|
||||
Host: envStr("LPW_HOST", ""),
|
||||
Port: envInt("LPW_PORT", 636), // 389
|
||||
SecurityProtocol: securityProtocol,
|
||||
SkipVerify: envBool("LPW_SSL_SKIP_VERIFY", false),
|
||||
UserDN: envStr("LPW_USER_DN", "uid=%s,ou=people,dc=example,dc=org"),
|
||||
UserBase: envStr("LPW_USER_BASE", "ou=people,dc=example,dc=org"),
|
||||
}
|
||||
}
|
42
app/util.go
Normal file
42
app/util.go
Normal file
@ -0,0 +1,42 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func getTitle() string {
|
||||
return envStr("LPW_TITLE", "Change your password on example.org")
|
||||
}
|
||||
|
||||
func envStr(key, defaultValue string) string {
|
||||
val := os.Getenv(key)
|
||||
if val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func envInt(key string, defaultValue int) int {
|
||||
val := os.Getenv(key)
|
||||
if val != "" {
|
||||
i, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return i
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func envBool(key string, defaultValue bool) bool {
|
||||
val := os.Getenv(key)
|
||||
if val != "" {
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return b
|
||||
}
|
||||
return defaultValue
|
||||
}
|
113
app/web.go
Normal file
113
app/web.go
Normal file
@ -0,0 +1,113 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"html/template"
|
||||
|
||||
"regexp"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type route struct {
|
||||
pattern *regexp.Regexp
|
||||
verb string
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
type RegexpHandler struct {
|
||||
routes []*route
|
||||
}
|
||||
|
||||
func (h *RegexpHandler) Handler(pattern *regexp.Regexp, verb string, handler http.Handler) {
|
||||
h.routes = append(h.routes, &route{pattern, verb, handler})
|
||||
}
|
||||
|
||||
func (h *RegexpHandler) HandleFunc(r string, v string, handler func(http.ResponseWriter, *http.Request)) {
|
||||
re := regexp.MustCompile(r)
|
||||
h.routes = append(h.routes, &route{re, v, http.HandlerFunc(handler)})
|
||||
}
|
||||
|
||||
func (h *RegexpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
for _, route := range h.routes {
|
||||
if route.pattern.MatchString(r.URL.Path) && route.verb == r.Method {
|
||||
route.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
Title string
|
||||
Username string
|
||||
Alerts map[string]string
|
||||
}
|
||||
|
||||
// ServeAssets : Serves the static assets
|
||||
func ServeAssets(w http.ResponseWriter, req *http.Request) {
|
||||
http.ServeFile(w, req, path.Join("static", req.URL.Path[1:]))
|
||||
}
|
||||
|
||||
// ServeIndex : Serves index page on GET request
|
||||
func ServeIndex(w http.ResponseWriter, req *http.Request) {
|
||||
p := &pageData{Title: getTitle()}
|
||||
t, e := template.ParseFiles(path.Join("templates", "index.html"))
|
||||
if e != nil {
|
||||
log.Printf("Error parsing file %v\n", e)
|
||||
} else {
|
||||
t.Execute(w, p)
|
||||
}
|
||||
}
|
||||
|
||||
// ChangePassword : Serves index page on POST request - executes the change
|
||||
func ChangePassword(w http.ResponseWriter, req *http.Request) {
|
||||
req.ParseForm()
|
||||
un := ""
|
||||
username := req.Form["username"]
|
||||
oldPassword := req.Form["old-password"]
|
||||
newPassword := req.Form["new-password"]
|
||||
confirmPassword := req.Form["confirm-password"]
|
||||
|
||||
alerts := map[string]string{}
|
||||
|
||||
if len(username) < 1 || username[0] == "" {
|
||||
alerts["error"] = "Username not specified.<br/>"
|
||||
} else {
|
||||
un = username[0]
|
||||
}
|
||||
if len(oldPassword) < 1 || oldPassword[0] == "" {
|
||||
alerts["error"] = alerts["error"] + "Old password not specified.<br/>"
|
||||
}
|
||||
if len(newPassword) < 1 || newPassword[0] == "" {
|
||||
alerts["error"] = alerts["error"] + "New password not specified.<br/>"
|
||||
}
|
||||
if len(confirmPassword) < 1 || confirmPassword[0] == "" {
|
||||
alerts["error"] = alerts["error"] + "Confirmation password not specified.<br/>"
|
||||
}
|
||||
|
||||
if len(confirmPassword) >= 1 && len(newPassword) >= 1 && strings.Compare(newPassword[0], confirmPassword[0]) != 0 {
|
||||
alerts["error"] = alerts["error"] + "New and confirmation passwords does not match.<br/>"
|
||||
}
|
||||
if len(alerts) == 0 {
|
||||
client := NewLDAPClient()
|
||||
if err := client.ModifyPassword(un, oldPassword[0], newPassword[0]); err != nil {
|
||||
alerts["error"] = fmt.Sprintf("%v", err)
|
||||
} else {
|
||||
alerts["success"] = "Password successfuly changed"
|
||||
}
|
||||
}
|
||||
|
||||
p := &pageData{Title: getTitle(), Alerts: alerts, Username: un}
|
||||
|
||||
t, e := template.ParseFiles(path.Join("templates", "index.html"))
|
||||
if e != nil {
|
||||
log.Printf("Error parsing file %v\n", e)
|
||||
} else {
|
||||
t.Execute(w, p)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user