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 <dylan_v...@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 r...@apache.org.