You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ro...@apache.org on 2018/05/17 22:41:20 UTC

[incubator-trafficcontrol] 01/19: implement login flow in go for local user and ldap

This is an automated email from the ASF dual-hosted git repository.

rob pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-trafficcontrol.git

commit b7c3c2e8158ca45f862980d4c59525e76e43cf27
Author: Dylan Volz <Dy...@comcast.com>
AuthorDate: Tue Mar 13 13:37:28 2018 -0600

    implement login flow in go for local user and ldap
---
 traffic_ops/traffic_ops_golang/auth/ldap.go        |  64 ++++++++++++
 traffic_ops/traffic_ops_golang/auth/login.go       | 112 +++++++++++++++++++++
 traffic_ops/traffic_ops_golang/config/config.go    |  40 +++++++-
 traffic_ops/traffic_ops_golang/routes.go           |   3 +
 .../traffic_ops_golang/traffic_ops_golang.go       |   3 +-
 5 files changed, 220 insertions(+), 2 deletions(-)

diff --git a/traffic_ops/traffic_ops_golang/auth/ldap.go b/traffic_ops/traffic_ops_golang/auth/ldap.go
new file mode 100644
index 0000000..1903505
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/auth/ldap.go
@@ -0,0 +1,64 @@
+package auth
+
+import (
+	"crypto/tls"
+	"errors"
+	"fmt"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/config"
+
+	"gopkg.in/ldap.v2"
+)
+
+func LookupUserDN(username string, cfg *config.ConfigLDAP) (string, bool, error) {
+	l, err := ldap.DialTLS("tcp", cfg.Host, &tls.Config{InsecureSkipVerify: true})
+	if err != nil {
+		log.Errorln("error dialing tls")
+		return "", false, err
+	}
+	defer l.Close()
+	// Bind with admin user
+	err = l.Bind(cfg.AdminDN, cfg.AdminPass)
+	if err != nil {
+		log.Errorln("error binding admin user")
+		return "", false, err
+	}
+
+	// Search for the given username
+	searchRequest := ldap.NewSearchRequest(
+		cfg.SearchBase,
+		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+		fmt.Sprintf("(&(objectCategory=person)(objectClass=user)(sAMAccountName=%s))", username),
+		[]string{"dn"},
+		nil,
+	)
+
+	sr, err := l.Search(searchRequest)
+	if err != nil {
+		log.Errorln("error issuing search:")
+		return "", false, err
+	}
+
+	if len(sr.Entries) != 1 {
+		return "", false, errors.New("User does not exist or too many entries returned")
+	}
+	userDN := sr.Entries[0].DN
+	return userDN, true, nil
+}
+
+func AuthenticateUserDN(userDN string, password string, cfg *config.ConfigLDAP) (bool, error) {
+	l, err := ldap.DialTLS("tcp", cfg.Host, &tls.Config{InsecureSkipVerify: true})
+	if err != nil {
+		log.Errorln("error dialing tls")
+		return false, err
+	}
+	defer l.Close()
+
+	// Bind as the user to verify their password
+	err = l.Bind(userDN, password)
+	if err != nil {
+		return false, err
+	}
+	return true, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/auth/login.go b/traffic_ops/traffic_ops_golang/auth/login.go
new file mode 100644
index 0000000..e55a6d8
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/auth/login.go
@@ -0,0 +1,112 @@
+package auth
+
+import (
+	"crypto/sha1"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/jmoiron/sqlx"
+
+	"time"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tocookie"
+	"github.com/pkg/errors"
+)
+
+type passwordForm struct {
+	Username string `json:"u"`
+	Password string `json:"p"`
+}
+
+func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		handleErrs := tc.GetHandleErrorsFunc(w, r)
+		defer r.Body.Close()
+		form := passwordForm{}
+		if err := json.NewDecoder(r.Body).Decode(&form); err != nil {
+			handleErrs(http.StatusBadRequest, err)
+			return
+		}
+		authenticated, err := checkLocalUser(form, db)
+		if err != nil {
+			log.Errorf("error checking local user: %s\n", err.Error())
+		}
+		var ldapErr error
+		if !authenticated {
+			if cfg.LDAPEnabled {
+				authenticated, ldapErr = checkLDAPUser(form, cfg.ConfigLDAP)
+				if ldapErr != nil {
+					log.Errorf("error checking ldap user: %s\n", ldapErr.Error())
+				}
+			}
+		}
+		resp := struct {
+			tc.Alerts
+		}{}
+		if authenticated {
+			expiry := time.Now().Add(time.Hour * 6)
+			cookie := tocookie.New(form.Username, expiry, cfg.Secrets[0])
+			httpCookie := http.Cookie{Name: "mojolicious", Value: cookie, Path: "/", Expires: expiry, HttpOnly: true}
+			http.SetCookie(w, &httpCookie)
+			resp = struct {
+				tc.Alerts
+			}{tc.CreateAlerts(tc.SuccessLevel, "Successfully logged in.")}
+
+		} else {
+			resp = struct {
+				tc.Alerts
+			}{tc.CreateAlerts(tc.ErrorLevel, "Invalid username or password.")}
+		}
+		respBts, err := json.Marshal(resp)
+		if err != nil {
+			handleErrs(http.StatusInternalServerError, err)
+			return
+		}
+
+		w.Header().Set(tc.ContentType, tc.ApplicationJson)
+		fmt.Fprintf(w, "%s", respBts)
+	}
+}
+
+func checkLocalUser(form passwordForm, db *sqlx.DB) (bool, error) {
+	var hashedPassword string
+	err := db.Get(&hashedPassword, "SELECT local_passwd FROM tm_user WHERE username=$1", form.Username)
+	if err != nil {
+		return false, err
+	}
+	err = VerifyPassword(form.Password, hashedPassword)
+	if err != nil {
+		if hashedPassword == sha1Hex(form.Password) {
+			return true, nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+func sha1Hex(s string) string {
+	// SHA1 hash
+	hash := sha1.New()
+	hash.Write([]byte(s))
+	hashBytes := hash.Sum(nil)
+
+	// Hexadecimal conversion
+	hexSha1 := hex.EncodeToString(hashBytes)
+	return hexSha1
+}
+
+func checkLDAPUser(form passwordForm, cfg *config.ConfigLDAP) (bool, error) {
+	userDN, valid, err := LookupUserDN(form.Username, cfg)
+	if err != nil {
+		return false, err
+	}
+	if valid {
+		return AuthenticateUserDN(userDN, form.Password, cfg)
+	}
+	return false, errors.New("User not found in LDAP")
+}
diff --git a/traffic_ops/traffic_ops_golang/config/config.go b/traffic_ops/traffic_ops_golang/config/config.go
index f6d0f10..4864845 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -42,6 +42,8 @@ type Config struct {
 	// NOTE: don't care about any other fields for now..
 	RiakAuthOptions *riak.AuthOptions
 	RiakEnabled     bool
+	ConfigLDAP      *ConfigLDAP
+	LDAPEnabled     bool
 	Version         string
 }
 
@@ -84,6 +86,13 @@ type ConfigDatabase struct {
 	SSL         bool   `json:"ssl"`
 }
 
+type ConfigLDAP struct {
+	AdminPass  string `json:"admin_pass"`
+	SearchBase string `json:"search_base"`
+	AdminDN    string `json:"admin_dn"`
+	Host       string `json:"host"`
+}
+
 // ErrorLog - critical messages
 func (c Config) ErrorLog() log.LogLocation {
 	return log.LogLocation(c.LogLocationError)
@@ -108,7 +117,7 @@ func (c Config) EventLog() log.LogLocation {
 }
 
 // LoadConfig - reads the config file into the Config struct
-func LoadConfig(cdnConfPath string, dbConfPath string, riakConfPath string, appVersion string) (Config, error) {
+func LoadConfig(cdnConfPath string, dbConfPath string, riakConfPath string, ldapConfPath string, appVersion string) (Config, error) {
 	// load json from cdn.conf
 	confBytes, err := ioutil.ReadFile(cdnConfPath)
 	if err != nil {
@@ -142,6 +151,16 @@ func LoadConfig(cdnConfPath string, dbConfPath string, riakConfPath string, appV
 		}
 	}
 
+	if ldapConfPath != "" {
+		cfg.LDAPEnabled, cfg.ConfigLDAP, err = GetLDAPConfig(ldapConfPath)
+		if err != nil {
+			cfg.LDAPEnabled = false // probably unnecessary
+			return cfg, fmt.Errorf("parsing ldap config '%s': %v", ldapConfPath, err)
+		}
+	} else {
+		cfg.LDAPEnabled = false
+	}
+
 	return cfg, err
 }
 
@@ -226,3 +245,22 @@ func ParseConfig(cfg Config) (Config, error) {
 
 	return cfg, nil
 }
+
+func GetLDAPConfig(LDAPConfPath string) (bool, *ConfigLDAP, error) {
+	LDAPConfBytes, err := ioutil.ReadFile(LDAPConfPath)
+	if err != nil {
+
+		return false, nil, fmt.Errorf("reading LDAP conf '%v': %v", LDAPConfPath, err)
+	}
+	LDAPconf, err := getLDAPConf(string(LDAPConfBytes))
+	if err != nil {
+		return false, LDAPconf, fmt.Errorf("parsing LDAP conf '%v': %v", LDAPConfBytes, err)
+	}
+	return true, LDAPconf, nil
+}
+
+func getLDAPConf(s string) (*ConfigLDAP, error) {
+	ldapConf := ConfigLDAP{}
+	err := json.Unmarshal([]byte(s), &ldapConf)
+	return &ldapConf, err
+}
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index 3437de1..cba2190 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -119,6 +119,9 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		//HWInfo
 		{1.1, http.MethodGet, `hwinfo-wip/?(\.json)?$`, hwinfo.HWInfoHandler(d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
 
+		//Login
+		{1.2, http.MethodPost, `user/login/?$`, auth.LoginHandler(d.DB, d.Config), 0, NoAuth, nil}, {1.3, http.MethodPost, `user/login/?$`, auth.LoginHandler(d.DB, d.Config), 0, NoAuth, nil},
+
 		//Parameter: CRUD
 		{1.1, http.MethodGet, `parameters/?(\.json)?$`, api.ReadHandler(parameter.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
 		{1.1, http.MethodGet, `parameters/{id}$`, api.ReadHandler(parameter.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
diff --git a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
index 9de1055..462a58a 100644
--- a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
+++ b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
@@ -48,6 +48,7 @@ func main() {
 	configFileName := flag.String("cfg", "", "The config file path")
 	dbConfigFileName := flag.String("dbcfg", "", "The db config file path")
 	riakConfigFileName := flag.String("riakcfg", "", "The riak config file path")
+	ldapConfigFileName := flag.String("ldapcfg", "", "the ldap config file path")
 	flag.Parse()
 
 	if *showVersion {
@@ -63,7 +64,7 @@ func main() {
 	var err error
 	var errorToLog error
 
-	if cfg, err = config.LoadConfig(*configFileName, *dbConfigFileName, *riakConfigFileName, version); err != nil {
+	if cfg, err = config.LoadConfig(*configFileName, *dbConfigFileName, *riakConfigFileName, *ldapConfigFileName, version); err != nil {
 		if !strings.Contains(err.Error(), "riak conf") {
 			fmt.Println("Error loading config: " + err.Error())
 			return

-- 
To stop receiving notification emails like this one, please contact
rob@apache.org.