package ldap import ( "encoding/json" "log" "net/http" "regexp" "strconv" "github.com/go-ldap/ldap" auth "imuslab.com/arozos/mod/auth" "imuslab.com/arozos/mod/auth/ldap/ldapreader" "imuslab.com/arozos/mod/auth/oauth2/syncdb" reg "imuslab.com/arozos/mod/auth/register" "imuslab.com/arozos/mod/common" db "imuslab.com/arozos/mod/database" permission "imuslab.com/arozos/mod/permission" "imuslab.com/arozos/mod/time/nightly" "imuslab.com/arozos/mod/user" ) type ldapHandler struct { ag *auth.AuthAgent ldapreader *ldapreader.LdapReader reg *reg.RegisterHandler coredb *db.Database permissionHandler *permission.PermissionHandler userHandler *user.UserHandler iconSystem string syncdb *syncdb.SyncDB nightlyManager *nightly.TaskManager } type Config struct { Enabled bool `json:"enabled"` BindUsername string `json:"bind_username"` BindPassword string `json:"bind_password"` FQDN string `json:"fqdn"` BaseDN string `json:"base_dn"` } type UserAccount struct { Username string `json:"username"` Group []string `json:"group"` EquivGroup []string `json:"equiv_group"` } //syncorizeUserReturnInterface not designed to be used outside type syncorizeUserReturnInterface struct { Userinfo []UserAccount `json:"userinfo"` Length int `json:"length"` Error string `json:"error"` } //NewLdapHandler xxx func NewLdapHandler(authAgent *auth.AuthAgent, register *reg.RegisterHandler, coreDb *db.Database, permissionHandler *permission.PermissionHandler, userHandler *user.UserHandler, nightlyManager *nightly.TaskManager, iconSystem string) *ldapHandler { //ldap handler init log.Println("Starting LDAP client...") err := coreDb.NewTable("ldap") if err != nil { log.Println("Failed to create LDAP database. Terminating.") panic(err) } //key value to be used for LDAP authentication BindUsername := readSingleConfig("BindUsername", coreDb) BindPassword := readSingleConfig("BindPassword", coreDb) FQDN := readSingleConfig("FQDN", coreDb) BaseDN := readSingleConfig("BaseDN", coreDb) LDAPHandler := ldapHandler{ ag: authAgent, ldapreader: ldapreader.NewLDAPReader(BindUsername, BindPassword, FQDN, BaseDN), reg: register, coredb: coreDb, permissionHandler: permissionHandler, userHandler: userHandler, iconSystem: iconSystem, syncdb: syncdb.NewSyncDB(), nightlyManager: nightlyManager, } nightlyManager.RegisterNightlyTask(LDAPHandler.NightlySync) return &LDAPHandler } func (ldap *ldapHandler) ReadConfig(w http.ResponseWriter, r *http.Request) { //basic components enabled, err := strconv.ParseBool(ldap.readSingleConfig("enabled")) if err != nil { common.SendTextResponse(w, "Invalid config value [key=enabled].") return } //get the LDAP config from db BindUsername := ldap.readSingleConfig("BindUsername") BindPassword := ldap.readSingleConfig("BindPassword") FQDN := ldap.readSingleConfig("FQDN") BaseDN := ldap.readSingleConfig("BaseDN") //marshall it and return config, err := json.Marshal(Config{ Enabled: enabled, BindUsername: BindUsername, BindPassword: BindPassword, FQDN: FQDN, BaseDN: BaseDN, }) if err != nil { empty, err := json.Marshal(Config{}) if err != nil { common.SendErrorResponse(w, "Error while marshalling config") } common.SendJSONResponse(w, string(empty)) } common.SendJSONResponse(w, string(config)) } func (ldap *ldapHandler) WriteConfig(w http.ResponseWriter, r *http.Request) { //receive the parameter enabled, err := common.Mv(r, "enabled", true) if err != nil { common.SendErrorResponse(w, "enabled field can't be empty") return } //allow empty fields if enabled is false showError := true if enabled != "true" { showError = false } //four fields to store the LDAP authentication information BindUsername, err := common.Mv(r, "bind_username", true) if err != nil { if showError { common.SendErrorResponse(w, "bind_username field can't be empty") return } } BindPassword, err := common.Mv(r, "bind_password", true) if err != nil { if showError { common.SendErrorResponse(w, "bind_password field can't be empty") return } } FQDN, err := common.Mv(r, "fqdn", true) if err != nil { if showError { common.SendErrorResponse(w, "fqdn field can't be empty") return } } BaseDN, err := common.Mv(r, "base_dn", true) if err != nil { if showError { common.SendErrorResponse(w, "base_dn field can't be empty") return } } //write the data back to db ldap.coredb.Write("ldap", "enabled", enabled) ldap.coredb.Write("ldap", "BindUsername", BindUsername) ldap.coredb.Write("ldap", "BindPassword", BindPassword) ldap.coredb.Write("ldap", "FQDN", FQDN) ldap.coredb.Write("ldap", "BaseDN", BaseDN) //update the new authencation infromation ldap.ldapreader = ldapreader.NewLDAPReader(BindUsername, BindPassword, FQDN, BaseDN) //return ok common.SendOK(w) } //@para limit: -1 means unlimited func (ldap *ldapHandler) getAllUser(limit int) ([]UserAccount, error) { //read the user account from ldap, if limit is -1 then it will read all USERS var accounts []UserAccount result, err := ldap.ldapreader.GetAllUser() if err != nil { return []UserAccount{}, err } //loop through the result for i, v := range result { account := ldap.convertGroup(v) accounts = append(accounts, account) if i+1 > limit && limit != -1 { break } } //check if the return struct is empty, if yes then insert empty if len(accounts) > 0 { return accounts[1:], nil } else { return []UserAccount{}, nil } } func (ldap *ldapHandler) convertGroup(ldapUser *ldap.Entry) UserAccount { //check the group belongs var Group []string var EquivGroup []string regexSyntax := regexp.MustCompile("cn=([^,]+),") for _, v := range ldapUser.GetAttributeValues("memberOf") { groups := regexSyntax.FindStringSubmatch(v) if len(groups) > 0 { //check if the LDAP group is already exists in ArOZOS system if ldap.permissionHandler.GroupExists(groups[1]) { EquivGroup = append(EquivGroup, groups[1]) } //LDAP list Group = append(Group, groups[1]) } } if len(EquivGroup) < 1 { if !ldap.permissionHandler.GroupExists(ldap.reg.GetDefaultUserGroup()) { //create new user group named default, prventing user don't have a group ldap.permissionHandler.NewPermissionGroup("default", false, 15<<30, []string{}, "Desktop") ldap.reg.SetDefaultUserGroup("default") } EquivGroup = append(EquivGroup, ldap.reg.GetDefaultUserGroup()) } account := UserAccount{ Username: ldapUser.GetAttributeValue("cn"), Group: Group, EquivGroup: EquivGroup, } return account } func (ldap *ldapHandler) TestConnection(w http.ResponseWriter, r *http.Request) { //marshall it and return the connection status userList, err := ldap.getAllUser(10) if err != nil { errMessage, err := json.Marshal(syncorizeUserReturnInterface{Error: err.Error()}) if err != nil { common.SendErrorResponse(w, "{\"error\":\"Error while marshalling information\"}") return } common.SendJSONResponse(w, string(errMessage)) return } returnJSON := syncorizeUserReturnInterface{Userinfo: userList, Length: len(userList), Error: ""} accountJSON, err := json.Marshal(returnJSON) if err != nil { errMessage, err := json.Marshal(syncorizeUserReturnInterface{Error: err.Error()}) if err != nil { common.SendErrorResponse(w, "{\"error\":\"Error while marshalling information\"}") return } common.SendJSONResponse(w, string(errMessage)) return } common.SendJSONResponse(w, string(accountJSON)) } func (ldap *ldapHandler) checkCurrUserAdmin(w http.ResponseWriter, r *http.Request) bool { //check current user is admin and new update will remove it or not currentLoggedInUser, err := ldap.userHandler.GetUserInfoFromRequest(w, r) if err != nil { common.SendErrorResponse(w, "Error while getting user info") return false } ldapCurrUserInfo, err := ldap.ldapreader.GetUser(currentLoggedInUser.Username) if err != nil { common.SendErrorResponse(w, "Error while getting user info from LDAP") return false } isAdmin := false //get the croups out from LDAP group list regexSyntax := regexp.MustCompile("cn=([^,]+),") for _, v := range ldapCurrUserInfo.GetAttributeValues("memberOf") { //loop through all memberOf's array groups := regexSyntax.FindStringSubmatch(v) //if after regex there is still groups exists if len(groups) > 0 { //check if the LDAP group is already exists in ArOZOS system if ldap.permissionHandler.GroupExists(groups[1]) { if ldap.permissionHandler.GetPermissionGroupByName(groups[1]).IsAdmin { isAdmin = true } } } } return isAdmin } func (ldap *ldapHandler) SynchronizeUser(w http.ResponseWriter, r *http.Request) { //check if suer is admin before executing the command //if user is admin then check if user will lost him/her's admin access consistencyCheck := ldap.checkCurrUserAdmin(w, r) if !consistencyCheck { common.SendErrorResponse(w, "You will no longer become the admin after synchronizing, synchronize terminated") return } err := ldap.SynchronizeUserFromLDAP() if err != nil { common.SendErrorResponse(w, err.Error()) return } common.SendOK(w) } func (ldap *ldapHandler) NightlySync() { err := ldap.SynchronizeUserFromLDAP() log.Println(err) } func (ldap *ldapHandler) SynchronizeUserFromLDAP() error { //check if suer is admin before executing the command //if user is admin then check if user will lost him/her's admin access ldapUsersList, err := ldap.getAllUser(-1) if err != nil { return err } for _, ldapUser := range ldapUsersList { //check if user exist in system if ldap.ag.UserExists(ldapUser.Username) { //if exists, then check if the user group is the same with ldap's setting //Get the permission groups by their ids userinfo, err := ldap.userHandler.GetUserInfoFromUsername(ldapUser.Username) if err != nil { return err } newPermissionGroups := ldap.permissionHandler.GetPermissionGroupByNameList(ldapUser.EquivGroup) //Set the user's permission to these groups userinfo.SetUserPermissionGroup(newPermissionGroups) } } return nil } //LOGIN related function //functions basically same as arozos's original function func (ldap *ldapHandler) HandleLoginPage(w http.ResponseWriter, r *http.Request) { checkLDAPenabled := ldap.readSingleConfig("enabled") if checkLDAPenabled == "false" { common.SendTextResponse(w, "LDAP not enabled.") return } //load the template from file and inject necessary variables red, _ := common.Mv(r, "redirect", false) //Append the redirection addr into the template imgsrc := "./web/" + ldap.iconSystem if !common.FileExists(imgsrc) { imgsrc = "./web/img/public/auth_icon.png" } imageBase64, _ := common.LoadImageAsBase64(imgsrc) parsedPage, err := common.Templateload("web/login.system", map[string]interface{}{ "redirection_addr": red, "usercount": strconv.Itoa(ldap.ag.GetUserCounts()), "service_logo": imageBase64, "login_addr": "system/auth/ldap/login", }) if err != nil { panic("Error. Unable to parse login page. Is web directory data exists?") } w.Header().Add("Content-Type", "text/html; charset=UTF-8") w.Write([]byte(parsedPage)) } func (ldap *ldapHandler) HandleNewPasswordPage(w http.ResponseWriter, r *http.Request) { checkLDAPenabled := ldap.readSingleConfig("enabled") if checkLDAPenabled == "false" { common.SendTextResponse(w, "LDAP not enabled.") return } //get the parameter from the request acc, err := common.Mv(r, "username", false) if err != nil { common.SendErrorResponse(w, err.Error()) return } displayname, err := common.Mv(r, "displayname", false) if err != nil { common.SendErrorResponse(w, err.Error()) return } key, err := common.Mv(r, "authkey", false) if err != nil { common.SendErrorResponse(w, err.Error()) return } //init the web interface imgsrc := "./web/" + ldap.iconSystem if !common.FileExists(imgsrc) { imgsrc = "./web/img/public/auth_icon.png" } imageBase64, _ := common.LoadImageAsBase64(imgsrc) template, err := common.Templateload("system/ldap/newPasswordTemplate.html", map[string]interface{}{ "vendor_logo": imageBase64, "username": acc, "display_name": displayname, "key": key, }) if err != nil { log.Fatal(err) } w.Header().Set("Content-Type", "text/html; charset=UTF-8") w.Write([]byte(template)) } func (ldap *ldapHandler) HandleLogin(w http.ResponseWriter, r *http.Request) { checkLDAPenabled := ldap.readSingleConfig("enabled") if checkLDAPenabled == "false" { common.SendTextResponse(w, "LDAP not enabled.") return } //Get username from request using POST mode username, err := common.Mv(r, "username", true) if err != nil { //Username not defined log.Println("[System Auth] Someone trying to login with username: " + username) //Write to log ldap.ag.Logger.LogAuth(r, false) common.SendErrorResponse(w, "Username not defined or empty.") return } //Get password from request using POST mode password, err := common.Mv(r, "password", true) if err != nil { //Password not defined ldap.ag.Logger.LogAuth(r, false) common.SendErrorResponse(w, "Password not defined or empty.") return } //Get rememberme settings rememberme := false rmbme, _ := common.Mv(r, "rmbme", true) if rmbme == "true" { rememberme = true } //Check the database and see if this user is in the database passwordCorrect, err := ldap.ldapreader.Authenticate(username, password) if err != nil { ldap.ag.Logger.LogAuth(r, false) common.SendErrorResponse(w, "Unable to connect to LDAP server") log.Println("LDAP Authentication error, " + err.Error()) return } //The database contain this user information. Check its password if it is correct if passwordCorrect { //Password correct //if user not exist then redirect to create pwd screen if !ldap.ag.UserExists(username) { authkey := ldap.syncdb.Store(username) common.SendJSONResponse(w, "{\"redirect\":\"system/auth/ldap/newPassword?username="+username+"&displayname="+username+"&authkey="+authkey+"\"}") } else { // Set user as authenticated ldap.ag.LoginUserByRequest(w, r, username, rememberme) //Print the login message to console log.Println(username + " logged in.") ldap.ag.Logger.LogAuth(r, true) common.SendOK(w) } } else { //Password incorrect log.Println(username + " has entered an invalid username or password") common.SendErrorResponse(w, "Invalid username or password") ldap.ag.Logger.LogAuth(r, false) return } } func (ldap *ldapHandler) HandleSetPassword(w http.ResponseWriter, r *http.Request) { checkLDAPenabled := ldap.readSingleConfig("enabled") if checkLDAPenabled == "false" { common.SendTextResponse(w, "LDAP not enabled.") return } //get paramters from request username, err := common.Mv(r, "username", true) if err != nil { common.SendErrorResponse(w, err.Error()) return } password, err := common.Mv(r, "password", true) if err != nil { common.SendErrorResponse(w, err.Error()) return } authkey, err := common.Mv(r, "authkey", true) if err != nil { common.SendErrorResponse(w, err.Error()) return } //check if the input key matches the database's username isValid := ldap.syncdb.Read(authkey) == username ldap.syncdb.Delete(authkey) // remove the key, aka key is one time use only //if db data match the username, proceed if isValid { //if not exists if !ldap.ag.UserExists(username) { //get the user from ldap server ldapUser, err := ldap.ldapreader.GetUser(username) if err != nil { common.SendErrorResponse(w, err.Error()) return } //convert the ldap usergroup to arozos usergroup convertedInfo := ldap.convertGroup(ldapUser) //create user account and login ldap.ag.CreateUserAccount(username, password, convertedInfo.EquivGroup) ldap.ag.Logger.LogAuth(r, true) ldap.ag.LoginUserByRequest(w, r, username, false) common.SendOK(w) return } else { //if exist then return error common.SendErrorResponse(w, "User exists, please contact the system administrator if you believe this is an error.") return } } else { common.SendErrorResponse(w, "Improper key detected") log.Println(r.RemoteAddr + " attempted to use invaild key to create new user but failed") return } } //CheckOAuth check if oauth is enabled func (ldap *ldapHandler) HandleCheckLDAP(w http.ResponseWriter, r *http.Request) { enabledB := false enabled := ldap.readSingleConfig("enabled") if enabled == "true" { enabledB = true } type returnFormat struct { Enabled bool `json:"enabled"` } json, err := json.Marshal(returnFormat{Enabled: enabledB}) if err != nil { common.SendErrorResponse(w, "Error occurred while marshalling JSON response") } common.SendJSONResponse(w, string(json)) }