view pkg/auth/store.go @ 904:e4b72a199258

New default bottleneck colors Mainly to make the stroke color one actually selectable in the ui. In addition the pink does better match the collors used on the ECDIS layer.
author Sascha Wilde <wilde@intevation.de>
date Tue, 02 Oct 2018 13:34:59 +0200
parents be8b79109679
children a244b18cb916
line wrap: on
line source

package auth

import (
	"bytes"
	"errors"
	"log"
	"time"

	bolt "github.com/etcd-io/bbolt"
)

var ErrNoSuchToken = errors.New("No such token")

// Sessions is the global connection pool.
var Sessions *SessionStore

type SessionStore struct {
	storage  *bolt.DB
	sessions map[string]*Session
	cmds     chan func()
}

var sessionsBucket = []byte("sessions")

func NewSessionStore(filename string) (*SessionStore, error) {

	ss := &SessionStore{
		sessions: make(map[string]*Session),
		cmds:     make(chan func()),
	}
	if err := ss.openStorage(filename); err != nil {
		return nil, err
	}
	go ss.run()
	return ss, nil
}

// openStorage opens a storage file.
func (ss *SessionStore) openStorage(filename string) error {

	// No file, nothing to restore/persist.
	if filename == "" {
		return nil
	}

	db, err := bolt.Open(filename, 0600, nil)
	if err != nil {
		return err
	}

	err = db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists(sessionsBucket)
		if err != nil {
			return err
		}

		// pre-load sessions
		c := b.Cursor()

		for k, v := c.First(); k != nil; k, v = c.Next() {
			var session Session
			if err := session.deserialize(bytes.NewReader(v)); err != nil {
				return err
			}
			ss.sessions[string(k)] = &session
		}

		return nil
	})

	if err != nil {
		db.Close()
		return err
	}

	ss.storage = db
	return nil
}

func (ss *SessionStore) run() {
	for {
		select {
		case cmd := <-ss.cmds:
			cmd()
		case <-time.After(time.Minute * 5):
			ss.cleanToken()
		}
	}
}

func (ss *SessionStore) cleanToken() {
	now := time.Now()
	for token, session := range ss.sessions {
		expires := time.Unix(session.ExpiresAt, 0)
		if expires.Before(now) {
			delete(ss.sessions, token)
			ss.remove(token)
		}
	}
}

func (ss *SessionStore) remove(token string) {
	if ss.storage == nil {
		return
	}
	err := ss.storage.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket(sessionsBucket)
		return b.Delete([]byte(token))
	})
	if err != nil {
		log.Printf("error: %v\n", err)
	}
}

func (ss *SessionStore) Delete(token string) bool {
	res := make(chan bool)
	ss.cmds <- func() {
		if _, found := ss.sessions[token]; !found {
			res <- false
			return
		}
		delete(ss.sessions, token)
		ss.remove(token)
		res <- true
	}
	return <-res
}

func (ss *SessionStore) store(token string, session *Session) {
	if ss.storage == nil {
		return
	}
	err := ss.storage.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket(sessionsBucket)
		var buf bytes.Buffer
		if err := session.serialize(&buf); err != nil {
			return err
		}
		return b.Put([]byte(token), buf.Bytes())
	})
	if err != nil {
		log.Printf("error: %v\n", err)
	}
}

func (ss *SessionStore) Add(token string, session *Session) {
	res := make(chan struct{})

	ss.cmds <- func() {
		defer close(res)
		s := ss.sessions[token]
		if s == nil {
			s = session
			ss.sessions[token] = session
		}
		s.touch()
		ss.store(token, s)
	}

	<-res
}

func (ss *SessionStore) Renew(token string) (string, error) {

	type result struct {
		newToken string
		err      error
	}

	resCh := make(chan result)

	ss.cmds <- func() {
		session := ss.sessions[token]
		if session == nil {
			resCh <- result{err: ErrNoSuchToken}
		} else {
			delete(ss.sessions, token)
			ss.remove(token)
			newToken := GenerateSessionKey()
			// TODO: Ensure that this is not racy!
			session.ExpiresAt = time.Now().Add(maxTokenValid).Unix()
			ss.sessions[newToken] = session
			ss.store(newToken, session)
			resCh <- result{newToken: newToken}
		}
	}

	r := <-resCh
	return r.newToken, r.err
}

func (ss *SessionStore) Session(token string) *Session {
	res := make(chan *Session)
	ss.cmds <- func() {
		session := ss.sessions[token]
		if session == nil {
			res <- nil
		} else {
			session.touch()
			ss.store(token, session)
			res <- session
		}
	}
	return <-res
}

func (ss *SessionStore) Logout(user string) {
	ss.cmds <- func() {
		for token, session := range ss.sessions {
			if session.User == user {
				delete(ss.sessions, token)
				ss.remove(token)
			}
		}
	}
}

func (ss *SessionStore) Shutdown() error {
	if db := ss.storage; db != nil {
		log.Println("info: shutdown persistent session store.")
		ss.storage = nil
		return db.Close()
	}
	log.Println("info: shutdown in-memory session store.")
	return nil
}