package handler import ( "crypto/sha256" "encoding/hex" "fmt" "net" "sort" "strings" "github.com/GeertJohan/yubigo" "github.com/glauth/glauth/pkg/config" "github.com/glauth/glauth/pkg/stats" "github.com/nmcclain/ldap" "github.com/op/go-logging" "github.com/pquerna/otp/totp" ) type configHandler struct { log *logging.Logger cfg *config.Config yubikeyAuth *yubigo.YubiAuth } func NewConfigHandler(log *logging.Logger, cfg *config.Config, yubikeyAuth *yubigo.YubiAuth) Handler { handler := configHandler{ log: log, cfg: cfg, yubikeyAuth: yubikeyAuth} return handler } // func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) { bindDN = strings.ToLower(bindDN) baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN) h.log.Debug(fmt.Sprintf("Bind request: bindDN: %s, BaseDN: %s, source: %s", bindDN, h.cfg.Backend.BaseDN, conn.RemoteAddr().String())) stats.Frontend.Add("bind_reqs", 1) // parse the bindDN - ensure that the bindDN ends with the BaseDN if !strings.HasSuffix(bindDN, baseDN) { h.log.Warning(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, h.cfg.Backend.BaseDN)) // h.log.Warning(fmt.Sprintf("Bind Error: BindDN %s not our BaseDN %s", bindDN, baseDN)) return ldap.LDAPResultInvalidCredentials, nil } parts := strings.Split(strings.TrimSuffix(bindDN, baseDN), ",") groupName := "" userName := "" if len(parts) == 1 { userName = strings.TrimPrefix(parts[0], h.cfg.Backend.NameFormat+"=") } else if len(parts) == 2 { userName = strings.TrimPrefix(parts[0], h.cfg.Backend.NameFormat+"=") groupName = strings.TrimPrefix(parts[1], h.cfg.Backend.GroupFormat+"=") } else { h.log.Warning(fmt.Sprintf("Bind Error: BindDN %s should have only one or two parts (has %d)", bindDN, len(parts))) return ldap.LDAPResultInvalidCredentials, nil } // find the user user := config.User{} found := false for _, u := range h.cfg.Users { if u.Name == userName { found = true user = u } } if !found { h.log.Warning(fmt.Sprintf("Bind Error: User %s not found.", userName)) return ldap.LDAPResultInvalidCredentials, nil } // find the group group := config.Group{} found = false for _, g := range h.cfg.Groups { if g.Name == groupName { found = true group = g } } if !found { h.log.Warning(fmt.Sprintf("Bind Error: Group %s not found.", groupName)) return ldap.LDAPResultInvalidCredentials, nil } // validate group membership if user.PrimaryGroup != group.UnixID { h.log.Warning(fmt.Sprintf("Bind Error: User %s primary group is not %s.", userName, groupName)) return ldap.LDAPResultInvalidCredentials, nil } validotp := false if len(user.Yubikey) == 0 && len(user.OTPSecret) == 0 { validotp = true } if len(user.Yubikey) > 0 && h.yubikeyAuth != nil { if len(bindSimplePw) > 44 { otp := bindSimplePw[len(bindSimplePw)-44:] yubikeyid := otp[0:12] bindSimplePw = bindSimplePw[:len(bindSimplePw)-44] if user.Yubikey == yubikeyid { _, ok, _ := h.yubikeyAuth.Verify(otp) if ok { validotp = true } } } } // Store the full bind password provided before possibly modifying // in the otp check // Generate a hash of the provided password hashFull := sha256.New() hashFull.Write([]byte(bindSimplePw)) // Test OTP if exists if len(user.OTPSecret) > 0 && !validotp { if len(bindSimplePw) > 6 { otp := bindSimplePw[len(bindSimplePw)-6:] bindSimplePw = bindSimplePw[:len(bindSimplePw)-6] validotp = totp.Validate(otp, user.OTPSecret) } } // finally, validate user's pw // check app passwords first for index, appPw := range user.PassAppSHA256 { if appPw != hex.EncodeToString(hashFull.Sum(nil)) { h.log.Debug(fmt.Sprintf("Attempted to bind app pw #%d - failure as %s from %s", index, bindDN, conn.RemoteAddr().String())) } else { stats.Frontend.Add("bind_successes", 1) h.log.Debug("Bind success using app pw #%d as %s from %s", index, bindDN, conn.RemoteAddr().String()) return ldap.LDAPResultSuccess, nil } } // then check main password with the hash hash := sha256.New() hash.Write([]byte(bindSimplePw)) // Then ensure the OTP is valid before checking if !validotp { h.log.Warning(fmt.Sprintf("Bind Error: invalid OTP token as %s from %s", bindDN, conn.RemoteAddr().String())) return ldap.LDAPResultInvalidCredentials, nil } // Now, check the hash if user.PassSHA256 != hex.EncodeToString(hash.Sum(nil)) { h.log.Warning(fmt.Sprintf("Bind Error: invalid credentials as %s from %s", bindDN, conn.RemoteAddr().String())) return ldap.LDAPResultInvalidCredentials, nil } stats.Frontend.Add("bind_successes", 1) h.log.Debug(fmt.Sprintf("Bind success as %s from %s", bindDN, conn.RemoteAddr().String())) return ldap.LDAPResultSuccess, nil } // func (h configHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) { bindDN = strings.ToLower(bindDN) baseDN := strings.ToLower("," + h.cfg.Backend.BaseDN) searchBaseDN := strings.ToLower(searchReq.BaseDN) h.log.Debug(fmt.Sprintf("Search request as %s from %s for %s", bindDN, conn.RemoteAddr().String(), searchReq.Filter)) stats.Frontend.Add("search_reqs", 1) // validate the user is authenticated and has appropriate access if len(bindDN) < 1 { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN) } if !strings.HasSuffix(bindDN, baseDN) { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, h.cfg.Backend.BaseDN) } if !strings.HasSuffix(searchBaseDN, h.cfg.Backend.BaseDN) { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: search BaseDN %s is not in our BaseDN %s", searchBaseDN, h.cfg.Backend.BaseDN) } // return all users in the config file - the LDAP library will filter results for us entries := []*ldap.Entry{} filterEntity, err := ldap.GetFilterObjectClass(searchReq.Filter) if err != nil { return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", searchReq.Filter) } switch filterEntity { default: return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter) case "posixgroup": for _, g := range h.cfg.Groups { attrs := []*ldap.EntryAttribute{} attrs = append(attrs, &ldap.EntryAttribute{Name: "cn", Values: []string{g.Name}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "uid", Values: []string{g.Name}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s", g.Name)}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", g.UnixID)}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixGroup"}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "uniqueMember", Values: h.getGroupMembers(g.UnixID)}) attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: h.getGroupMemberIDs(g.UnixID)}) dn := fmt.Sprintf("cn=%s,%s=groups,%s", g.Name, h.cfg.Backend.GroupFormat, h.cfg.Backend.BaseDN) entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) } case "posixaccount", "": for _, u := range h.cfg.Users { attrs := []*ldap.EntryAttribute{} attrs = append(attrs, &ldap.EntryAttribute{Name: "cn", Values: []string{u.Name}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "uid", Values: []string{u.Name}}) if len(u.GivenName) > 0 { attrs = append(attrs, &ldap.EntryAttribute{Name: "givenName", Values: []string{u.GivenName}}) } if len(u.SN) > 0 { attrs = append(attrs, &ldap.EntryAttribute{Name: "sn", Values: []string{u.SN}}) } attrs = append(attrs, &ldap.EntryAttribute{Name: "ou", Values: []string{h.getGroupName(u.PrimaryGroup)}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "uidNumber", Values: []string{fmt.Sprintf("%d", u.UnixID)}}) if u.Disabled { attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}}) } else { attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}}) } if len(u.Mail) > 0 { attrs = append(attrs, &ldap.EntryAttribute{Name: "mail", Values: []string{u.Mail}}) } attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixAccount", "shadowAccount"}}) if len(u.LoginShell) > 0 { attrs = append(attrs, &ldap.EntryAttribute{Name: "loginShell", Values: []string{u.LoginShell}}) } else { attrs = append(attrs, &ldap.EntryAttribute{Name: "loginShell", Values: []string{"/bin/bash"}}) } if len(u.Homedir) > 0 { attrs = append(attrs, &ldap.EntryAttribute{Name: "homeDirectory", Values: []string{u.Homedir}}) } else { attrs = append(attrs, &ldap.EntryAttribute{Name: "homeDirectory", Values: []string{"/home/" + u.Name}}) } attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s", u.Name)}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "gecos", Values: []string{fmt.Sprintf("%s", u.Name)}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", u.PrimaryGroup)}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: h.getGroupDNs(append(u.OtherGroups, u.PrimaryGroup))}) attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowExpire", Values: []string{"-1"}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowFlag", Values: []string{"134538308"}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowInactive", Values: []string{"-1"}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowLastChange", Values: []string{"11000"}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowMax", Values: []string{"99999"}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowMin", Values: []string{"-1"}}) attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowWarning", Values: []string{"7"}}) if len(u.SSHKeys) > 0 { attrs = append(attrs, &ldap.EntryAttribute{Name: h.cfg.Backend.SSHKeyAttr, Values: u.SSHKeys}) } dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.cfg.Backend.NameFormat, u.Name, h.cfg.Backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.cfg.Backend.BaseDN) entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) } } stats.Frontend.Add("search_successes", 1) h.log.Debug(fmt.Sprintf("AP: Search OK: %s", searchReq.Filter)) return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil } // func (h configHandler) Close(boundDn string, conn net.Conn) error { stats.Frontend.Add("closes", 1) return nil } // func (h configHandler) getGroupMembers(gid int) []string { members := make(map[string]bool) for _, u := range h.cfg.Users { if u.PrimaryGroup == gid { dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.cfg.Backend.NameFormat, u.Name, h.cfg.Backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.cfg.Backend.BaseDN) members[dn] = true } else { for _, othergid := range u.OtherGroups { if othergid == gid { dn := fmt.Sprintf("%s=%s,%s=%s,%s", h.cfg.Backend.NameFormat, u.Name, h.cfg.Backend.GroupFormat, h.getGroupName(u.PrimaryGroup), h.cfg.Backend.BaseDN) members[dn] = true } } } } for _, g := range h.cfg.Groups { if gid == g.UnixID { for _, includegroupid := range g.IncludeGroups { if includegroupid != gid { includegroupmembers := h.getGroupMembers(includegroupid) for _, includegroupmember := range includegroupmembers { members[includegroupmember] = true } } } } } m := []string{} for k, _ := range members { m = append(m, k) } sort.Strings(m) return m } // func (h configHandler) getGroupMemberIDs(gid int) []string { members := make(map[string]bool) for _, u := range h.cfg.Users { if u.PrimaryGroup == gid { members[u.Name] = true } else { for _, othergid := range u.OtherGroups { if othergid == gid { members[u.Name] = true } } } } for _, g := range h.cfg.Groups { if gid == g.UnixID { for _, includegroupid := range g.IncludeGroups { if includegroupid == gid { h.log.Warning(fmt.Sprintf("Group: %d - Ignoring myself as included group", includegroupid)) } else { includegroupmemberids := h.getGroupMemberIDs(includegroupid) for _, includegroupmemberid := range includegroupmemberids { members[includegroupmemberid] = true } } } } } m := []string{} for k, _ := range members { m = append(m, k) } sort.Strings(m) return m } // Converts an array of GUIDs into an array of DNs func (h configHandler) getGroupDNs(gids []int) []string { groups := make(map[string]bool) for _, gid := range gids { for _, g := range h.cfg.Groups { if g.UnixID == gid { dn := fmt.Sprintf("cn=%s,%s=groups,%s", g.Name, h.cfg.Backend.GroupFormat, h.cfg.Backend.BaseDN) groups[dn] = true } for _, includegroupid := range g.IncludeGroups { if includegroupid == gid && g.UnixID != gid { includegroupdns := h.getGroupDNs([]int{g.UnixID}) for _, includegroupdn := range includegroupdns { groups[includegroupdn] = true } } } } } g := []string{} for k, _ := range groups { g = append(g, k) } sort.Strings(g) return g } // func (h configHandler) getGroupName(gid int) string { for _, g := range h.cfg.Groups { if g.UnixID == gid { return g.Name } } return "" }