瀏覽代碼

Merge branch 'ldap-2022Feb19' of tmp/arozos into master

LGTM
TC 3 年之前
父節點
當前提交
026ae12450

+ 4 - 0
go.mod

@@ -13,6 +13,9 @@ require (
 	github.com/frankban/quicktest v1.10.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.1.0
 	github.com/go-git/go-git/v5 v5.2.0
+	github.com/go-ldap/ldap v3.0.3+incompatible // indirect
+	github.com/go-ldap/ldap/v3 v3.4.2 // indirect
+	github.com/google/uuid v1.3.0 // indirect
 	github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96 // indirect
 	github.com/gorilla/sessions v1.2.0
 	github.com/gorilla/websocket v1.4.2
@@ -41,6 +44,7 @@ require (
 	golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 // indirect
+	gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
 	gopkg.in/sourcemap.v1 v1.0.5 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )

+ 13 - 0
go.sum

@@ -39,6 +39,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
+github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@@ -146,6 +148,8 @@ github.com/gabriel-vasile/mimetype v1.1.0/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pm
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
 github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
+github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
 github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
 github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
@@ -161,6 +165,10 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo=
 github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
+github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk=
+github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
+github.com/go-ldap/ldap/v3 v3.4.2 h1:zFZKcXKLqZpFMrMQGHeHWKXbDTdNCmhGY9AK41zPh+8=
+github.com/go-ldap/ldap/v3 v3.4.2/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
@@ -244,6 +252,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -544,6 +554,7 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
@@ -906,6 +917,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
 google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 43 - 0
ldap.go

@@ -0,0 +1,43 @@
+package main
+
+import (
+	"net/http"
+
+	ldap "imuslab.com/arozos/mod/auth/ldap"
+	prout "imuslab.com/arozos/mod/prouter"
+)
+
+func ldapInit() {
+	//ldap
+	ldapHandler := ldap.NewLdapHandler(authAgent, registerHandler, sysdb, permissionHandler, userHandler, nightlyManager, iconSystem)
+
+	//add a entry to the system settings
+	adminRouter := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName:  "System Setting",
+		AdminOnly:   true,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			errorHandlePermissionDenied(w, r)
+		},
+	})
+	registerSetting(settingModule{
+		Name:         "LDAP<sup>BETA</sup>",
+		Desc:         "Allows external account access to system",
+		IconPath:     "SystemAO/advance/img/small_icon.png",
+		Group:        "Security",
+		StartDir:     "SystemAO/advance/ldap.html",
+		RequireAdmin: true,
+	})
+
+	adminRouter.HandleFunc("/system/auth/ldap/config/read", ldapHandler.ReadConfig)
+	adminRouter.HandleFunc("/system/auth/ldap/config/write", ldapHandler.WriteConfig)
+	adminRouter.HandleFunc("/system/auth/ldap/config/testConnection", ldapHandler.TestConnection)
+	adminRouter.HandleFunc("/system/auth/ldap/config/syncorizeUser", ldapHandler.SynchronizeUser)
+
+	//login interface and login handler
+	http.HandleFunc("/system/auth/ldap/login", ldapHandler.HandleLogin)
+	http.HandleFunc("/system/auth/ldap/setPassword", ldapHandler.HandleSetPassword)
+	http.HandleFunc("/system/auth/ldap/newPassword", ldapHandler.HandleNewPasswordPage)
+	http.HandleFunc("/ldapLogin.system", ldapHandler.HandleLoginPage)
+	http.HandleFunc("/system/auth/ldap/checkldap", ldapHandler.HandleCheckLDAP)
+}

+ 2 - 1
main.router.go

@@ -37,10 +37,11 @@ func mrouter(h http.Handler) http.Handler {
 				imgsrc = "./web/img/public/auth_icon.png"
 			}
 			imageBase64, _ := LoadImageAsBase64(imgsrc)
-			parsedPage, err := template_load("web/login.system", map[string]interface{}{
+			parsedPage, err := common.Templateload("web/login.system", map[string]interface{}{
 				"redirection_addr": red,
 				"usercount":        strconv.Itoa(authAgent.GetUserCounts()),
 				"service_logo":     imageBase64,
+				"login_addr":       "system/auth/login",
 			})
 			if err != nil {
 				panic("Error. Unable to parse login page. Is web directory data exists?")

+ 21 - 0
mod/auth/ldap/common.go

@@ -0,0 +1,21 @@
+package ldap
+
+import db "imuslab.com/arozos/mod/database"
+
+func readSingleConfig(key string, coredb *db.Database) string {
+	var value string
+	err := coredb.Read("ldap", key, &value)
+	if err != nil {
+		value = ""
+	}
+	return value
+}
+
+func (ldap *ldapHandler) readSingleConfig(key string) string {
+	var value string
+	err := ldap.coredb.Read("ldap", key, &value)
+	if err != nil {
+		value = ""
+	}
+	return value
+}

+ 170 - 0
mod/auth/ldap/ldap.go

@@ -0,0 +1,170 @@
+package ldap
+
+import (
+	"log"
+	"regexp"
+
+	"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"
+	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"`
+	TotalLength int           `json:"total_length"`
+	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
+}
+
+//@para limit: -1 means unlimited
+func (ldap *ldapHandler) getAllUser(limit int) ([]UserAccount, int, 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{}, 0, 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:], len(result), nil
+	} else {
+		return []UserAccount{}, 0, 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) NightlySync() {
+	err := ldap.SynchronizeUserFromLDAP()
+	if err != nil {
+		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
+}

+ 102 - 0
mod/auth/ldap/ldapreader/reader.go

@@ -0,0 +1,102 @@
+package ldapreader
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/go-ldap/ldap"
+)
+
+type LdapReader struct {
+	username string
+	password string
+	server   string
+	basedn   string
+}
+
+//NewOauthHandler xxx
+func NewLDAPReader(username string, password string, server string, basedn string) *LdapReader {
+
+	LDAPHandler := LdapReader{
+		username: username,
+		password: password,
+		server:   server,
+		basedn:   basedn,
+	}
+
+	return &LDAPHandler
+}
+
+func (handler *LdapReader) GetUser(username string) (*ldap.Entry, error) {
+	returnVal, err := handler.retrieveInformation("uid="+username+","+handler.basedn, "(objectClass=*)", ldap.ScopeBaseObject, handler.username, handler.password)
+	if err != nil {
+		return nil, err
+	}
+	if len(returnVal) == 0 {
+		return nil, fmt.Errorf("nothing found for user %s", username)
+	}
+	return returnVal[0], nil
+}
+
+func (handler *LdapReader) GetAllUser() ([]*ldap.Entry, error) {
+	return handler.retrieveInformation(handler.basedn, "(objectClass=*)", ldap.ScopeWholeSubtree, handler.username, handler.password)
+}
+
+func (handler *LdapReader) Authenticate(username string, password string) (bool, error) {
+	userInformation, err := handler.retrieveInformation("uid="+username+","+handler.basedn, "(objectClass=person)", ldap.ScopeBaseObject, "uid="+username+","+handler.basedn, password)
+	if err != nil {
+		if strings.Contains(err.Error(), "LDAP Result Code 32") {
+			return false, nil
+		}
+		if strings.Contains(err.Error(), "LDAP Result Code 53") {
+			return false, nil
+		}
+		if strings.Contains(err.Error(), "Couldn't fetch search entries") {
+			return false, nil
+		}
+		return false, err
+	}
+	if len(userInformation) > 0 {
+		if userInformation[0].GetAttributeValue("cn") == username {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+func (handler *LdapReader) retrieveInformation(dn string, filter string, typeOfSearch int, username string, password string) ([]*ldap.Entry, error) {
+	ldapURL, err := ldap.DialURL(fmt.Sprintf("ldap://%s:389", handler.server))
+	if err != nil {
+		return nil, err
+	}
+	defer ldapURL.Close()
+
+	ldapURL.Bind(username, password)
+	searchReq := ldap.NewSearchRequest(
+		dn,
+		typeOfSearch,
+		ldap.NeverDerefAliases,
+		0,
+		0,
+		false,
+		filter,
+		[]string{"uid", "memberOf", "cn", "sAMAccountName"},
+		//[]string{},
+		nil,
+	)
+	result, err := ldapURL.Search(searchReq)
+	/*
+		if err == nil {
+			result.PrettyPrint(4)
+		}
+	*/
+	if err != nil {
+		return nil, fmt.Errorf("Search Error: %s", err)
+	}
+
+	if len(result.Entries) > 0 {
+		return result.Entries, nil
+	} else {
+		return nil, fmt.Errorf("Couldn't fetch search entries")
+	}
+}

+ 79 - 0
mod/auth/ldap/syncdb/syncdb.go

@@ -0,0 +1,79 @@
+package syncdb
+
+import (
+	"fmt"
+	"sync"
+	"time"
+
+	uuid "github.com/satori/go.uuid"
+)
+
+type SyncDB struct {
+	db *sync.Map //HERE ALSO CHANGED, USE POINTER INSTEAD OF A COPY OF THE ORIGINAL SYNCNAMP
+}
+
+type dbStructure struct {
+	timestamp time.Time
+	value     string
+}
+
+func NewSyncDB() *SyncDB {
+	//Create a new SyncMap for this SyncDB Object
+	newDB := sync.Map{}
+	//Put the newly craeted syncmap into the db object
+	newSyncDB := SyncDB{db: &newDB} //!!! USE POINTER HERE INSTEAD OF THE SYNC MAP ITSELF
+	//Return the pointer of the new SyncDB object
+	newSyncDB.AutoCleaning()
+	return &newSyncDB
+}
+
+func (p SyncDB) AutoCleaning() {
+	//create the routine for auto clean trash
+	go func() {
+		for {
+			<-time.After(5 * 60 * time.Second) //no rush, clean every five minute
+			p.db.Range(func(key, value interface{}) bool {
+				if time.Now().Sub(value.(dbStructure).timestamp).Minutes() >= 30 {
+					p.db.Delete(key)
+				}
+				return true
+			})
+		}
+	}()
+}
+
+func (p SyncDB) Store(value string) string {
+	uid := uuid.NewV4().String()
+	NewField := dbStructure{
+		timestamp: time.Now(),
+		value:     value,
+	}
+	p.db.Store(uid, NewField)
+	return uid
+}
+
+func (p SyncDB) Read(uuid string) string {
+	value, ok := p.db.Load(uuid)
+	if !ok {
+		return ""
+	} else {
+		return value.(dbStructure).value
+	}
+}
+
+func (p SyncDB) Delete(uuid string) {
+	p.db.Delete(uuid)
+}
+
+func (p SyncDB) ToString() {
+	p.db.Range(func(key, value interface{}) bool {
+		fmt.Print(key)
+		fmt.Print(" : ")
+		fmt.Println(value.(dbStructure).value)
+		fmt.Print(" @ ")
+		//fmt.Print(value.(dbStructure).timestamp)
+		fmt.Print(time.Now().Sub(value.(dbStructure).timestamp).Seconds())
+		fmt.Print("\n")
+		return true
+	})
+}

+ 174 - 0
mod/auth/ldap/web_admin.go

@@ -0,0 +1,174 @@
+package ldap
+
+import (
+	"encoding/json"
+	"net/http"
+	"regexp"
+	"strconv"
+
+	"imuslab.com/arozos/mod/auth/ldap/ldapreader"
+	"imuslab.com/arozos/mod/common"
+)
+
+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)
+}
+
+func (ldap *ldapHandler) TestConnection(w http.ResponseWriter, r *http.Request) {
+	//marshall it and return the connection status
+	userList, totalLength, 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), TotalLength: totalLength, 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)
+}

+ 220 - 0
mod/auth/ldap/web_login.go

@@ -0,0 +1,220 @@
+package ldap
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+	"strconv"
+
+	"imuslab.com/arozos/mod/common"
+)
+
+//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
+	}
+}
+
+//HandleCheckLDAP check if ldap 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))
+}

+ 7 - 6
template.go → mod/common/template.go

@@ -1,8 +1,9 @@
-package main
+package common
 
 import (
-	"github.com/valyala/fasttemplate"
 	"io/ioutil"
+
+	"github.com/valyala/fasttemplate"
 )
 
 /*
@@ -11,12 +12,12 @@ import (
 	This is the main system core module that perform function similar to what PHP did.
 	To replace part of the content of any file, use {{paramter}} to replace it.
 
-	
+
 */
 
-func template_load(filename string, replacement map[string]interface{}) (string, error){
+func Templateload(filename string, replacement map[string]interface{}) (string, error) {
 	content, err := ioutil.ReadFile(filename)
-	if (err != nil){
+	if err != nil {
 		return "", nil
 	}
 	t := fasttemplate.New(string(content), "{{", "}}")
@@ -24,7 +25,7 @@ func template_load(filename string, replacement map[string]interface{}) (string,
 	return string(s), nil
 }
 
-func template_apply(templateString string, replacement map[string]interface{}) string{
+func TemplateApply(templateString string, replacement map[string]interface{}) string {
 	t := fasttemplate.New(templateString, "{{", "}}")
 	s := t.ExecuteString(replacement)
 	return string(s)

+ 1 - 0
startup.go

@@ -88,6 +88,7 @@ func RunStartup() {
 	mediaServer_init()
 	security_init()
 	backup_init()
+	ldapInit() //LDAP system init
 
 	//Start High Level Services that requires full arozos architectures
 	FTPServerInit() //Start FTP Server Endpoints

+ 37 - 36
system.resetpw.go

@@ -1,11 +1,12 @@
 package main
 
 import (
-	"net/http"
-	"log"
 	"errors"
+	"log"
+	"net/http"
 
 	auth "imuslab.com/arozos/mod/auth"
+	"imuslab.com/arozos/mod/common"
 )
 
 /*
@@ -14,20 +15,20 @@ import (
 	This module exists to serve the password restart page with security check
 */
 
-func system_resetpw_init(){
-	http.HandleFunc("/system/reset/validateResetKey", system_resetpw_validateResetKeyHandler);
-	http.HandleFunc("/system/reset/confirmPasswordReset", system_resetpw_confirmReset);
+func system_resetpw_init() {
+	http.HandleFunc("/system/reset/validateResetKey", system_resetpw_validateResetKeyHandler)
+	http.HandleFunc("/system/reset/confirmPasswordReset", system_resetpw_confirmReset)
 }
 
 //Validate if the ysername and rkey is valid
-func system_resetpw_validateResetKeyHandler(w http.ResponseWriter, r *http.Request){
+func system_resetpw_validateResetKeyHandler(w http.ResponseWriter, r *http.Request) {
 	username, err := mv(r, "username", true)
-	if err != nil{
+	if err != nil {
 		sendErrorResponse(w, "Invalid username or key")
 		return
 	}
 	rkey, err := mv(r, "rkey", true)
-	if err != nil{
+	if err != nil {
 		sendErrorResponse(w, "Invalid username or key")
 		return
 	}
@@ -39,7 +40,7 @@ func system_resetpw_validateResetKeyHandler(w http.ResponseWriter, r *http.Reque
 
 	//Check if the pair is valid
 	err = system_resetpw_validateResetKey(username, rkey)
-	if err != nil{
+	if err != nil {
 		sendErrorResponse(w, err.Error())
 		return
 	}
@@ -48,68 +49,68 @@ func system_resetpw_validateResetKeyHandler(w http.ResponseWriter, r *http.Reque
 
 }
 
-func system_resetpw_confirmReset(w http.ResponseWriter, r *http.Request){
+func system_resetpw_confirmReset(w http.ResponseWriter, r *http.Request) {
 	username, _ := mv(r, "username", true)
 	rkey, _ := mv(r, "rkey", true)
 	newpw, _ := mv(r, "pw", true)
-	if (username == "" || rkey == "" || newpw == ""){
+	if username == "" || rkey == "" || newpw == "" {
 		sendErrorResponse(w, "Internal Server Error")
 		return
 	}
 
 	//Check user exists
-	if !authAgent.UserExists(username){
+	if !authAgent.UserExists(username) {
 		sendErrorResponse(w, "Username not exists")
 		return
 	}
 
 	//Validate rkey
 	err := system_resetpw_validateResetKey(username, rkey)
-	if err != nil{
+	if err != nil {
 		sendErrorResponse(w, err.Error())
 		return
 	}
 
 	//OK to procced
 	newHashedPassword := auth.Hash(newpw)
-	err = sysdb.Write("auth", "passhash/" + username, newHashedPassword)
-	if err != nil{
+	err = sysdb.Write("auth", "passhash/"+username, newHashedPassword)
+	if err != nil {
 		sendErrorResponse(w, err.Error())
 		return
 	}
 
-	sendOK(w);
+	sendOK(w)
 
 }
 
-func system_resetpw_validateResetKey(username string, key string) error{
+func system_resetpw_validateResetKey(username string, key string) error {
 	//Get current password from db
 	passwordInDB := ""
-	err := sysdb.Read("auth", "passhash/" + username, &passwordInDB)
-	if err != nil{
+	err := sysdb.Read("auth", "passhash/"+username, &passwordInDB)
+	if err != nil {
 		return err
 	}
 
 	//Get hashed user key
 	hashedKey := auth.Hash(key)
-	if (passwordInDB != hashedKey){
+	if passwordInDB != hashedKey {
 		return errors.New("Invalid Password Reset Key")
 	}
 
 	return nil
 }
 
-func system_resetpw_handlePasswordReset(w http.ResponseWriter, r *http.Request){
+func system_resetpw_handlePasswordReset(w http.ResponseWriter, r *http.Request) {
 	//Check if the user click on this link with reset password key string. If not, ask the user to input one
 	acc, err := mv(r, "acc", false)
 	if err != nil || acc == "" {
-		system_resetpw_serveIdEnterInterface(w,r);
+		system_resetpw_serveIdEnterInterface(w, r)
 		return
 	}
 
 	resetkey, err := mv(r, "rkey", false)
 	if err != nil || resetkey == "" {
-		system_resetpw_serveIdEnterInterface(w,r);
+		system_resetpw_serveIdEnterInterface(w, r)
 		return
 	}
 
@@ -122,28 +123,28 @@ func system_resetpw_handlePasswordReset(w http.ResponseWriter, r *http.Request){
 
 	//OK. Create the New Password Entering UI
 	imageBase64, _ := LoadImageAsBase64("./web/" + iconVendor)
-	template, err := template_load("system/reset/resetPasswordTemplate.html",map[string]interface{}{
+	template, err := common.Templateload("system/reset/resetPasswordTemplate.html", map[string]interface{}{
 		"vendor_logo": imageBase64,
-		"host_name": *host_name,
-		"username": acc,
-		"rkey": resetkey,
-	});
-	if err != nil{
-		log.Fatal(err);
+		"host_name":   *host_name,
+		"username":    acc,
+		"rkey":        resetkey,
+	})
+	if err != nil {
+		log.Fatal(err)
 	}
 	w.Header().Set("Content-Type", "text/html; charset=UTF-8")
 	w.Write([]byte(template))
 }
 
-func system_resetpw_serveIdEnterInterface(w http.ResponseWriter, r *http.Request){
+func system_resetpw_serveIdEnterInterface(w http.ResponseWriter, r *http.Request) {
 	//Reset Key or Username not found, Serve entering interface
 	imageBase64, _ := LoadImageAsBase64("./web/" + iconVendor)
-	template, err := template_load("system/reset/resetCodeTemplate.html",map[string]interface{}{
+	template, err := common.Templateload("system/reset/resetCodeTemplate.html", map[string]interface{}{
 		"vendor_logo": imageBase64,
-		"host_name": *host_name,
-	});
-	if err != nil{
-		log.Fatal(err);
+		"host_name":   *host_name,
+	})
+	if err != nil {
+		log.Fatal(err)
 	}
 	w.Header().Set("Content-Type", "text/html; charset=UTF-8")
 	w.Write([]byte(template))

+ 99 - 0
system/ldap/newPasswordTemplate.html

@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <title>New Password</title>
+    <link rel="stylesheet" href="../../../script/semantic/semantic.css">
+    <link rel="stylesheet" href="../../../script/ao.css">
+    <script type="application/javascript" src="../../../script/jquery.min.js"></script>
+    <script type="application/javascript" src="../../../script/semantic/semantic.js"></script>
+    <style>
+
+    </style>
+</head>
+
+<body>
+    <br><br><br>
+    <div class="ui container" align="center">
+        <div class="ui basic segment" style="max-width:400px;" align="left">
+            <div class="imageRight" align="center">
+                <img class="ui small image" src="data:image/png;base64, {{vendor_logo}}"></img>
+            </div>
+            <div class="ui divider"></div>
+            <div class="ui text container">
+                <p>Hi {{display_name}}, Please set your local password.</p>
+            </div>
+            <div class="ui divider"></div>
+            <form class="ui form" onsubmit="handleFormSubmit(event, this);">
+                <div class="ui divider"></div>
+                <div class="field">
+                    <label>New Password</label>
+                    <input id="npw" type="password" name="newpw" placeholder="New Password">
+                </div>
+                <div class="field">
+                    <label>Confirm New Password</label>
+                    <input id="cpw" type="password" name="confirmnewpw" placeholder="Confirm New Password">
+                </div>
+                <button id="submitbtn" class="ui green button" type="submit">Submit</button>
+            </form>
+            <div id="errmsg" class="ui red inverted segment" style="display:none;">
+                <i class="remove icon"></i> <span id="errtext">Internal Server Error</span>
+            </div>
+            <br>
+            <p>Back to <a href="../../../ldapLogin.system">Login</a></p>
+        </div>
+    </div>
+
+    <script>
+        var username = "{{username}}";
+        var key = "{{key}}";
+
+        function handleFormSubmit(evt, obj) {
+            evt.preventDefault();
+            var newpw = obj.newpw.value;
+            var cpw = obj.confirmnewpw.value;
+            $("#npw").parent().removeClass("error");
+            $("#cpw").parent().removeClass("error");
+
+            if (newpw != cpw) {
+                showErrorMessage("Confirm password does not match.")
+                $("#cpw").parent().addClass("error");
+                return
+            }
+
+            if (newpw == "" || cpw == "") {
+                showErrorMessage("Password cannot be empty")
+                $("#npw").parent().addClass("error");
+            }
+
+            //Should be OK now. Submit the form for reset password
+            $.ajax({
+                url: "../../../system/auth/ldap/setPassword",
+                method: "POST",
+                data: {
+                    username: username,
+                    password: newpw,
+                    authkey: key,
+                },
+                success: function(data) {
+                    if (data.error !== undefined) {
+                        showErrorMessage(data.error);
+                    } else {
+                        //OK
+                        window.location.href = "../../../";
+                    }
+                }
+            })
+
+        }
+
+        function showErrorMessage(msg) {
+            $("#errtext").text(msg);
+            $("#errmsg").stop().finish().slideDown("fast");
+        }
+    </script>
+</body>
+
+</html>

+ 215 - 0
web/SystemAO/advance/ldap.html

@@ -0,0 +1,215 @@
+<html>
+
+<head>
+    <title>LDAP Login</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
+    <link rel="stylesheet" href="../../script/semantic/semantic.css">
+    <script type="application/javascript" src="../../script/jquery.min.js"></script>
+    <script type="application/javascript" src="../../script/clipboard.min.js"></script>
+    <script type="application/javascript" src="../../script/semantic/semantic.js"></script>
+    <style>
+        /* Tooltip container */
+        
+        .tooltip {
+            position: relative;
+            display: inline-block;
+            border-bottom: 1px dotted black;
+            /* If you want dots under the hoverable text */
+        }
+        /* Tooltip text */
+        
+        .tooltip .tooltiptext {
+            visibility: hidden;
+            width: 120px;
+            background-color: #555;
+            color: #fff;
+            text-align: center;
+            padding: 5px 0;
+            border-radius: 6px;
+            /* Position the tooltip text */
+            position: absolute;
+            z-index: 1;
+            bottom: 125%;
+            left: 50%;
+            margin-left: -60px;
+            /* Fade in tooltip */
+            opacity: 0;
+            transition: opacity 0.3s;
+        }
+        /* Tooltip arrow */
+        
+        .tooltip .tooltiptext::after {
+            content: "";
+            position: absolute;
+            top: 100%;
+            left: 50%;
+            margin-left: -5px;
+            border-width: 5px;
+            border-style: solid;
+            border-color: #555 transparent transparent transparent;
+        }
+    </style>
+</head>
+
+<body>
+    <div class="ui container">
+        <div class="ui basic segment">
+            <div class="ui header">
+                <i class="key icon"></i>
+                <div class="content">
+                    LDAP Access
+                    <div class="sub header">Allow external account to access ArozOS with LDAP</div>
+                </div>
+            </div>
+        </div>
+        <div class="ui divider"></div>
+        <div class="ui green inverted segment" style="display:none;" id="updateSet">
+            <h5 class="ui header">
+                <i class="checkmark icon"></i>
+                <div class="content">
+                    Settings Updated
+                </div>
+            </h5>
+        </div>
+        <div class="ui form">
+            <div class="field">
+                <div class="ui toggle checkbox">
+                    <input type="checkbox" id="enable" name="public">
+                    <label>Enable LDAP</label>
+                </div>
+            </div>
+            <div class="field">
+                <label>Bind Username</label>
+                <div class="ui fluid input">
+                    <input type="text" id="bind_username" placeholder="root">
+                </div>
+            </div>
+            <div class="field">
+                <label>Bind Password</label>
+                <div class="ui fluid input">
+                    <input type="password" id="bind_password" placeholder="p@ssw0rd">
+                </div>
+            </div>
+            <div class="field">
+                <label>FQDN</label>
+                <div class="ui fluid input">
+                    <input type="text" id="fqdn" placeholder="10.0.0.1">
+                </div>
+            </div>
+            <div class="field">
+                <label>Base DN</label>
+                <div class="ui fluid input">
+                    <input type="text" id="base_dn" placeholder="cn=users,dc=ldap">
+                </div>
+            </div>
+            <button id="ntb" onclick="update();" class="ui green button" type="submit">Update</button>
+            <button id="test_btn" onclick="test();" class="ui button" type="submit">Test Connection</button>
+        </div>
+        <div class="ui divider"></div>
+        <div id="testConnection" style="display: none">
+            <table class="ui celled table">
+                <thead>
+                    <tr>
+                        <th>Username</th>
+                        <th>Group belongs to</th>
+                        <th>Equivalence user group in arozos</th>
+                    </tr>
+                </thead>
+                <tbody id="information">
+                </tbody>
+            </table>
+            <button id="sync_btn" onclick="syncorize();" class="ui button" type="submit">Syncorize User</button>
+        </div>
+        <br><br>
+    </div>
+
+
+    <script>
+        $(document).ready(function() {
+            read();
+        });
+
+        function read() {
+            $.getJSON("../../system/auth/ldap/config/read", function(data) {
+                if (data.enabled) {
+                    $("#enable").parent().checkbox("check")
+                }
+                if (data.autoredirect) {
+                    $("#autoredirect").parent().checkbox("check")
+                }
+                $("#bind_username").val(data.bind_username);
+                $("#bind_password").val(data.bind_password);
+                $("#fqdn").val(data.fqdn);
+                $("#base_dn").val(data.base_dn);
+            });
+        }
+
+        function update() {
+            $.post("../../system/auth/ldap/config/write", {
+                    enabled: $("#enable").parent().checkbox("is checked"),
+                    bind_username: $("#bind_username").val(),
+                    bind_password: $("#bind_password").val(),
+                    fqdn: $("#fqdn").val(),
+                    base_dn: $("#base_dn").val(),
+                })
+                .done(function(data) {
+                    if (data.error != undefined) {
+                        alert(data.error);
+                    } else {
+                        //OK!
+                        $("#updateSet").stop().finish().slideDown("fast").delay(3000).slideUp('fast');
+                    }
+                });
+        }
+
+        function test() {
+            $("#test_btn").text("Testing...");
+            $.get("../../system/auth/ldap/config/testConnection")
+                .done(function(data) {
+                    if (data.error != undefined) {
+                        if (data.error != "") {
+                            alert(data.error);
+                        }
+                    }
+                    if (data.userinfo == null) {
+                        alert("No entries was found");
+                    }
+                    //OK!
+                    $("#information").html("");
+                    $(data.userinfo).each(function(index, element) {
+                        $("#information").append(`
+                            <tr>
+                                <td data-label="username">` + element.username + `</td>
+                                <td data-label="ldap_group">` + element.group + `</td>
+                                <td data-label="equiv_group">` + element.equiv_group + `</td>
+                            </tr>
+                        `);
+                    });
+                    $("#information").append(`
+                            <tr>
+                                <td data-label="length" colspan="3">Showing ` + data.length + ` of ` + data.total_length + ` entries</td>
+                            </tr>
+                    `);
+                    $("#testConnection").show("fast");
+                    $("#test_btn").text("Test connection");
+                });
+        }
+
+        function syncorize() {
+            $("#sync_btn").text("Syncorizing...");
+            $.get("../../system/auth/ldap/config/syncorizeUser")
+                .done(function(data) {
+                    if (data.error != undefined) {
+                        alert(data.error);
+                    } else {
+                        //OK!
+                        $("#updateSet").stop().finish().slideDown("fast").delay(3000).slideUp('fast');
+                    }
+                    $("#sync_btn").text("Syncorize User");
+                });
+        }
+    </script>
+</body>
+
+</html>

+ 19 - 2
web/login.system

@@ -103,6 +103,9 @@
                 <div class="oauthonly" style="display:none;">
                     <a class="ts fluid small button oauthbtn subthemecolor" href="system/auth/oauth/login">Sign In via OAuth 2.0</a><br>
                 </div>
+                <div class="ldaponly" style="display:none;">
+                    <a class="ts fluid small button oauthbtn subthemecolor" href="ldapLogin.system">Sign In via LDAP</a><br>
+                </div>
                 <br>
                 <div class="ts fluid input textbox">
                     <input id="username" type="text" placeholder="Username">
@@ -135,6 +138,7 @@
         
     <script>
         var redirectionAddress = "{{redirection_addr}}";
+        var loginAddress = "{{login_addr}}";
         var systemUserCount = "{{usercount}}" - 0; //Magic way to convert string to int :)
         var autoRedirectTimer;
         var isMobile = false; //initiate as false
@@ -215,10 +219,20 @@
                         $(".ts.borderless.basic.segment").after('<div id="autoRedirectSegment" class="ts borderless basic segment"><p><i class="key icon"></i>Redirecting to organization sign-in page in 5 seconds...</p><br><a style="cursor: pointer;" onclick="stopAutoRedirect()">Cancel</a></div>');
                         autoRedirectTimer = setTimeout(function(){
                             window.location.href = "system/auth/oauth/login?redirect=" + redirectionAddress;
-                        }, 5000);
+                        }, 3000);
                     }
                 }
             });
+
+            //LDAP related code, check if system is open for ext login
+            $.getJSON("system/auth/ldap/checkldap",function(data){
+                if (data.enabled == true && window.location.pathname.toLowerCase() != "/ldaplogin.system"){
+                    $(".ldaponly").show();
+                }else{
+                    $(".ldaponly").hide();
+                }
+            });
+
             if(get('redirect') != undefined){
                 $(".section.signin").attr("href","system/auth/oauth/login?redirect=" + redirectionAddress);
             }
@@ -254,11 +268,14 @@
             var magic = $("#magic").val();
             var rmbme = document.getElementById("rmbme").checked;
             $("input").addClass('disabled');
-            $.post("system/auth/login", {"username": username, "password": magic, "rmbme": rmbme}).done(function(data){
+            $.post(loginAddress, {"username": username, "password": magic, "rmbme": rmbme}).done(function(data){
                 if (data.error !== undefined){
                     //Something went wrong during the login
                     $("#errmsg").text(data.error);
                     $("#errmsg").parent().slideDown('fast').delay(5000).slideUp('fast');
+                }else if(data.redirect !== undefined){
+                    //LDAP Related Code
+                    window.location.href = data.redirect;
                 }else{
                     //Login succeed
                     if (redirectionAddress == ""){