package auth import ( "encoding/json" "errors" "fmt" "net/http" "time" uuid "github.com/satori/go.uuid" "imuslab.com/arozos/mod/database" "imuslab.com/arozos/mod/utils" ) /* Account Switch This script handle account switching logic The switchable account pools work like this Let say user A want to switch to user B account A will create a pool with user A and B username inside the pool The pool UUID will be returned to the client, and stored in local storage The client can always switch between A and B as both are in the pool and the client is logged in either A or B's account. */ type SwitchableAccount struct { Username string //Username of the account LastSwitch int64 //Last time this account is accessed } type SwitchableAccountsPool struct { UUID string //UUID of this pool, one pool per browser instance Accounts []*SwitchableAccount //Accounts that is cross switchable in this pool parent *SwitchableAccountPoolManager } type SwitchableAccountPoolManager struct { Database *database.Database ExpireTime int64 //Expire time of the switchable account authAgent *AuthAgent } // Create a new switchable account pool manager func NewSwitchableAccountPoolManager(sysdb *database.Database, parent *AuthAgent) *SwitchableAccountPoolManager { sysdb.NewTable("auth_acswitch") thisManager := SwitchableAccountPoolManager{ Database: sysdb, ExpireTime: 604800, authAgent: parent, } return &thisManager } // Handle switchable account listing for this browser func (m *SwitchableAccountPoolManager) HandleSwitchableAccountListing(w http.ResponseWriter, r *http.Request) { //Get username and pool id currentUsername, err := m.authAgent.GetUserName(w, r) if err != nil { utils.SendErrorResponse(w, err.Error()) return } poolid, err := utils.GetPara(r, "pid") if err != nil { utils.SendErrorResponse(w, "invalid pool id given") return } //Check pool exists targetPool, err := m.GetPoolByID(poolid) if err != nil { utils.SendErrorResponse(w, err.Error()) return } //Check if the user can access this pool if !targetPool.IsAccessibleBy(currentUsername) { utils.SendErrorResponse(w, "permission denied") return } //OK. List all the information about the pool type AccountInfo struct { Username string IsExpired bool } results := []*AccountInfo{} for _, acc := range targetPool.Accounts { results = append(results, &AccountInfo{ Username: acc.Username, IsExpired: (time.Now().Unix() > acc.LastSwitch+m.ExpireTime), }) } js, _ := json.Marshal(results) utils.SendJSONResponse(w, string(js)) } // Logout all the accounts in the pool func (m *SwitchableAccountPoolManager) HandleLogoutAllAccounts(w http.ResponseWriter, r *http.Request) { currentUsername, err := m.authAgent.GetUserName(w, r) if err != nil { utils.SendErrorResponse(w, err.Error()) return } poolid, err := utils.PostPara(r, "pid") if err != nil { utils.SendErrorResponse(w, "invalid pool id given") } //Get the target pool targetpool, err := m.GetPoolByID(poolid) if err != nil { utils.SendErrorResponse(w, err.Error()) return } if !targetpool.IsAccessibleBy(currentUsername) { utils.SendErrorResponse(w, "permission denied") return } //Remove the pool targetpool.Delete() utils.SendOK(w) } // Handle account switching func (m *SwitchableAccountPoolManager) HandleAccountSwitch(w http.ResponseWriter, r *http.Request) { previousUserName, err := m.authAgent.GetUserName(w, r) if err != nil { utils.SendErrorResponse(w, err.Error()) return } poolid, err := utils.PostPara(r, "pid") if err != nil { //No pool is given. Generate a pool for this request poolid = uuid.NewV4().String() newPool := SwitchableAccountsPool{ UUID: poolid, Accounts: []*SwitchableAccount{ { Username: previousUserName, LastSwitch: time.Now().Unix(), }, }, parent: m, } newPool.Save() } //Get switchable pool from manager targetPool, err := m.GetPoolByID(poolid) if err != nil { utils.SendErrorResponse(w, err.Error()) return } //Check if this user can access this pool if !targetPool.IsAccessibleByRequest(w, r) { utils.SendErrorResponse(w, "access request denied: user not belongs to this account pool") return } //OK! Switch the user to alternative account username, err := utils.PostPara(r, "username") if err != nil { utils.SendErrorResponse(w, "invalid or empty username given") return } password, err := utils.PostPara(r, "password") if err != nil { //Password not given. Check for direct switch switchToTargetAlreadySwitchedBefore := targetPool.UserAlreadyInPool(username) if !switchToTargetAlreadySwitchedBefore { utils.SendErrorResponse(w, "account must be added before it can switch without password") return } //Check if the switching is expired lastSwitchTime := targetPool.GetLastSwitchTimeFromUsername(username) if time.Now().Unix() > lastSwitchTime+m.ExpireTime { //Already expired utils.SendErrorResponse(w, "target account session has expired") return } //Not expired. Switch over directly m.authAgent.LoginUserByRequest(w, r, username, true) } else { //Password given. Use Add User Account routine ok, reason := m.authAgent.ValidateUsernameAndPasswordWithReason(username, password) if !ok { utils.SendErrorResponse(w, reason) return } m.authAgent.LoginUserByRequest(w, r, username, true) } //Update the pool account info targetPool.UpdateUserPoolAccountInfo(username) targetPool.Save() js, _ := json.Marshal(poolid) utils.SendJSONResponse(w, string(js)) //Debug print //js, _ = json.MarshalIndent(targetPool, "", " ") //fmt.Println("Switching Pool Updated", string(js)) } func (m *SwitchableAccountPoolManager) GetAllPools() ([]*SwitchableAccountsPool, error) { results := []*SwitchableAccountsPool{} entries, err := m.Database.ListTable("auth_acswitch") if err != nil { return results, err } for _, keypairs := range entries { //thisPoolID := string(keypairs[0]) thisPool := SwitchableAccountsPool{} err = json.Unmarshal(keypairs[1], &thisPool) if err == nil { thisPool.parent = m results = append(results, &thisPool) } } return results, nil } // Get a switchable account pool by its id func (m *SwitchableAccountPoolManager) GetPoolByID(uuid string) (*SwitchableAccountsPool, error) { targetPool := SwitchableAccountsPool{} err := m.authAgent.Database.Read("auth_acswitch", uuid, &targetPool) if err != nil { return nil, errors.New("pool with given uuid not found") } targetPool.parent = m return &targetPool, nil } // Remove user from all switch pool, which should be called when a user is logged out or removed func (p *SwitchableAccountPoolManager) RemoveUserFromAllSwitchableAccountPool(username string) error { allAccountPool, err := p.GetAllPools() if err != nil { return err } for _, accountPool := range allAccountPool { if accountPool.IsAccessibleBy(username) { //aka this user is in the pool accountPool.RemoveUser(username) } } return nil } func (p *SwitchableAccountPoolManager) ExpireUserFromAllSwitchableAccountPool(username string) error { allAccountPool, err := p.GetAllPools() if err != nil { return err } for _, accountPool := range allAccountPool { fmt.Println(allAccountPool) if accountPool.IsAccessibleBy(username) { //aka this user is in the pool accountPool.ExpireUser(username) } } return nil } /* Switachable Account Pool functions */ // Check if the requester can switch within target pool func (p *SwitchableAccountsPool) IsAccessibleByRequest(w http.ResponseWriter, r *http.Request) bool { username, err := p.parent.authAgent.GetUserName(w, r) if err != nil { return false } return p.IsAccessibleBy(username) } // Check if a given username can switch within this pool func (p *SwitchableAccountsPool) IsAccessibleBy(username string) bool { for _, account := range p.Accounts { if account.Username == username { return true } } return false } func (p *SwitchableAccountsPool) UserAlreadyInPool(username string) bool { for _, acc := range p.Accounts { if acc.Username == username { return true } } return false } func (p *SwitchableAccountsPool) UpdateUserLastSwitchTime(username string) bool { for _, acc := range p.Accounts { if acc.Username == username { acc.LastSwitch = time.Now().Unix() } } return false } func (p *SwitchableAccountsPool) GetLastSwitchTimeFromUsername(username string) int64 { for _, acc := range p.Accounts { if acc.Username == username { return acc.LastSwitch } } return 0 } // Everytime switching to a given user in a pool, call this update function to // update contents inside the pool func (p *SwitchableAccountsPool) UpdateUserPoolAccountInfo(username string) { if !p.UserAlreadyInPool(username) { p.Accounts = append(p.Accounts, &SwitchableAccount{ Username: username, LastSwitch: time.Now().Unix(), }) } else { p.UpdateUserLastSwitchTime(username) } } // Expire the session of a user manually func (p *SwitchableAccountsPool) ExpireUser(username string) { for _, acc := range p.Accounts { if acc.Username == username { acc.LastSwitch = 0 } } p.Save() } // Remove a user from the pool func (p *SwitchableAccountsPool) RemoveUser(username string) { newAccountList := []*SwitchableAccount{} for _, acc := range p.Accounts { if acc.Username != username { newAccountList = append(newAccountList, acc) } } p.Accounts = newAccountList p.Save() } // Save changes of this pool to database func (p *SwitchableAccountsPool) Save() { p.parent.Database.Write("auth_acswitch", p.UUID, p) } // Delete this pool from database func (p *SwitchableAccountsPool) Delete() error { return p.parent.Database.Delete("auth_acswitch", p.UUID) }