view pkg/auth/session.go @ 5711:2dd155cc95ec revive-cleanup

Fix all revive issue (w/o machine generated stuff).
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Tue, 20 Feb 2024 22:22:57 +0100
parents 91f4b3f56ce2
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 (
	"encoding/base64"
	"errors"
	"io"
	"sync"
	"time"

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

// Roles is a list of roles a logged in user has.
type Roles []string

// Session stores the informations about a logged in user.
type Session struct {
	// ExpiresAt is a unix timestamp when the session
	// of the user expires.
	ExpiresAt int64 `json:"expires"`

	// User is the login name of the user.
	User string `json:"user"`

	// Roles is the list of roles of the user.
	Roles Roles `json:"roles"`

	// private fields for managing expiration.
	access time.Time
	mu     sync.Mutex
}

// Has checks if a certain role is amongst the roles.
func (r Roles) Has(role string) bool {
	for _, x := range r {
		if x == role {
			return true
		}
	}
	return false
}

// HasAny checks if any of the given roles is in the role list.
func (r Roles) HasAny(roles ...string) bool {
	for _, y := range roles {
		if r.Has(y) {
			return true
		}
	}
	return false
}

const (
	sessionKeyLength = 20
)

// newSession creates a new session.
func newSession(user string, roles Roles) *Session {

	// Create the Claims
	return &Session{
		ExpiresAt: time.Now().Add(config.SessionTimeout()).Unix(),
		User:      user,
		Roles:     roles,
	}
}

func (s *Session) serialize(w io.Writer) error {

	access, err := s.last().MarshalText()
	if err != nil {
		return err
	}

	wr := BinWriter{Writer: w, Err: nil}
	wr.WriteBin(s.ExpiresAt)
	wr.WriteString(s.User)
	wr.WriteBin(uint32(len(s.Roles)))
	for _, role := range s.Roles {
		wr.WriteString(role)
	}
	wr.WriteBin(uint32(len(access)))
	wr.WriteBin(access)
	return wr.Err
}

func (s *Session) deserialize(r io.Reader) error {

	var n uint32
	rd := BinReader{Reader: r, Err: nil}
	rd.ReadBin(&s.ExpiresAt)
	rd.ReadString(&s.User)
	rd.ReadBin(&n)
	s.Roles = make(Roles, n)

	for i := uint32(0); n > 0 && i < n; i++ {
		rd.ReadString(&s.Roles[i])
	}

	if rd.Err != nil {
		return rd.Err
	}

	var l uint32
	rd.ReadBin(&l)
	access := make([]byte, l)
	rd.ReadBin(access)

	if rd.Err != nil {
		return rd.Err
	}

	var t time.Time
	if err := t.UnmarshalText(access); err != nil {
		return err
	}

	s.access = t

	return nil
}

func (s *Session) touch() {
	s.mu.Lock()
	s.access = time.Now()
	s.mu.Unlock()
}

func (s *Session) last() time.Time {
	s.mu.Lock()
	access := s.access
	s.mu.Unlock()
	return access
}

func generateSessionKey() string {
	return base64.URLEncoding.EncodeToString(
		common.GenerateRandomKey(sessionKeyLength))
}

// ErrInvalidRole is returned if a given role does not exsist in this system.
var ErrInvalidRole = errors.New("invalid role")

// GenerateSession creates a new session for a given user and password
// backed by the roles of this user in the database.
func GenerateSession(user, password string) (string, *Session, error) {
	roles, err := AllOtherRoles(user, password)
	if err != nil {
		return "", nil, err
	}
	// TODO: Make this a configuration.
	if !roles.HasAny("sys_admin", "waterway_admin", "waterway_user") {
		return "", nil, ErrInvalidRole
	}
	token := generateSessionKey()
	session := newSession(user, roles)
	Sessions.Add(token, session)
	return token, session, nil
}