Initial version.

This commit is contained in:
Nick Penkov
2018-01-26 12:38:19 +01:00
parent 29be16c897
commit 173d059c82
10 changed files with 522 additions and 0 deletions

130
app/ldap.go Normal file
View 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
View 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
View 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)
}
}