view pkg/auth/store.go @ 5490:5f47eeea988d logging

Use own logging package.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Mon, 20 Sep 2021 17:45:39 +0200
parents 866eae1bd888
children
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2018 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>

package auth

import (
	"bytes"
	"errors"
	"time"

	bolt "go.etcd.io/bbolt"

	"gemma.intevation.de/gemma/pkg/config"
	"gemma.intevation.de/gemma/pkg/log"
)

// ErrNoSuchToken is returned if a given token does not
// exists th the session store.
var ErrNoSuchToken = errors.New("no such token")

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

// SessionStore encapsulates a set of currently active sessions.
type SessionStore struct {
	storage  *bolt.DB
	sessions map[string]*Session
	cmds     chan func()
}

var sessionsBucket = []byte("sessions")

// NewSessionStore creates a new session store.
// If the filename is empty the session are only hold in memory.
// If the filename is not empty the sessions are mirrored to
// a file with this name. Use the later option if you want
// a persistent session store.
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.Errorf("%v\n", err)
	}
}

// Delete removes a session identified by its token from the
// session store. Returns true if there was such s session.
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.Errorf("%v\n", err)
	}
}

// Add puts a session into the session store identified by
// a given token. An old session with the same key will
// be replaced.
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
}

// Renew refreshes a session. It takes an old token to
// identify a session and returns a new token with the
// freshed up one.
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(config.SessionTimeout()).Unix()
			ss.sessions[newToken] = session
			ss.store(newToken, session)
			resCh <- result{newToken: newToken}
		}
	}

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

// Session returns the session associated with given token.
// Returns nil if no matching session was found.
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
}

// Logout removes all sessions of a given user from the session store.
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)
			}
		}
	}
}

// Shutdown closes the session store.
// If using the persistent mode the backing session database is closed.
func (ss *SessionStore) Shutdown() error {
	if db := ss.storage; db != nil {
		log.Infof("shutdown persistent session store.")
		ss.storage = nil
		return db.Close()
	}
	log.Infof("shutdown in-memory session store.")
	return nil
}