Added some documentation

main
Antonio De Lucreziis 4 years ago
parent 3732ba245e
commit 0f22ca3437

@ -1,18 +1,21 @@
// This package provides a utility for managing session cookies.
//
// The main struct is "AuthService" that provides a pluggable system for login and logout of users, creating and storing their session tokens and middlewares for accepting only logged users or ones with some specified permissions.
package auth package auth
import ( import (
"errors" "errors"
"net/http" "net/http"
"time" "time"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil"
) )
var ( var (
ErrNoUserForSession = errors.New(`no user for session token`) ErrNoUserForSession = errors.New(`no user for session token`)
) )
var SessionCookieName = "session" // TODO: Make configurable var SessionCookieName = "session" // TODO: Make configurable
// AuthMiddlewareConfig configures the middleware to only accept logged users (if "RequireLogged" is true) and with certain permissions (user must have all permissions inside "WithPermissions")
type AuthMiddlewareConfig struct { type AuthMiddlewareConfig struct {
// RequireLogged rejects not logged users if true // RequireLogged rejects not logged users if true
RequireLogged bool RequireLogged bool
@ -21,52 +24,52 @@ type AuthMiddlewareConfig struct {
WithPermissions []string WithPermissions []string
} }
type Authenticator interface { // // Authenticator is the spec of this library
// Login checks user credentials and adds a session cookie to the user if successfull // type Authenticator interface {
Login(w http.ResponseWriter, r *http.Request, userID, password string) // // Login checks user credentials and adds a session cookie to the user if successfull
// Logout clears the user session cookies (by setting the session cookie timeout to 0) // Login(w http.ResponseWriter, r *http.Request, userID, password string)
Logout(w http.ResponseWriter) // // Logout clears the user session cookies (by setting the session cookie timeout to 0)
// Logout(w http.ResponseWriter)
// Middleware is a configurable middleware to authenticate http routes based on logged status and permissions // // Middleware is a configurable middleware to authenticate http routes based on logged status and permissions
Middleware(*AuthMiddlewareConfig) func(http.Handler) http.Handler // Middleware(*AuthMiddlewareConfig) func(http.Handler) http.Handler
// LoggedMiddleware accepts all logged users without checking for specific permissions // // LoggedMiddleware accepts all logged users without checking for specific permissions
LoggedMiddleware() func(http.Handler) http.Handler // LoggedMiddleware() func(http.Handler) http.Handler
// RequestUser returns the userID for this cookie session token // // RequestUser returns the userID for this cookie session token
RequestUser(r *http.Request) (string, error) // RequestUser(r *http.Request) (string, error)
} // }
var _ Authenticator = &AuthService{} // var _ Authenticator = &AuthService{}
// AuthService handles cookies, authentication and authorization of http routes by providing middlewares, logint/logout methods, user sessions and retriving the userID of an authenticated request.
type AuthService struct { type AuthService struct {
// CheckUserPassword // CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser"
CheckUserPassword func(userID string, password string) error CheckUserPassword func(userID string, password string) error
// GetUserPermissions gets the list of permissions for this user // GetUserPermissions gets the list of permissions for this user
UserPermissions func(userID string) ([]string, error) UserPermissions func(userID string) ([]string, error)
// SessionTokenFromUser returns a session token that represents this user // SessionTokenFromUser returns a (new) session token that represents this user
SessionTokenFromUser func(userID string) (string, error) SessionTokenFromUser func(userID string) (string, error)
// UserFromSessionToken returns the corresponing user for this session token or "auth.ErrNoUserForSession" // UserFromSessionToken returns the corresponing user for this session token or "auth.ErrNoUserForSession"
UserFromSessionToken func(session string) (string, error) UserFromSessionToken func(session string) (string, error)
// AuthenticationFailed handles failed authentications // AuthenticationFailed handles failed authentications
AuthenticationFailed func(error) http.Handler AuthenticationFailed func(error) http.Handler
// OtherError handles other errors // OtherError handles other errors
OtherError func(error) http.Handler OtherError func(error) http.Handler
} }
func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, password string) { // Login tries to login a user given its id and password
func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, password string) error {
if err := auth.CheckUserPassword(userID, password); err != nil { if err := auth.CheckUserPassword(userID, password); err != nil {
auth.AuthenticationFailed(err).ServeHTTP(w, r) return err
return
} }
token, err := auth.SessionTokenFromUser(userID) token, err := auth.SessionTokenFromUser(userID)
if err != nil { if err != nil {
auth.OtherError(err).ServeHTTP(w, r) return err
return
} }
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
@ -76,9 +79,10 @@ func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, p
Expires: time.Now().Add(7 * 24 * time.Hour), // TODO: Make configurable Expires: time.Now().Add(7 * 24 * time.Hour), // TODO: Make configurable
}) })
httputil.WriteJSON(w, "ok") return nil
} }
// Logout clears the session cookie from a request effectivly logging out the user for future requests
func (auth *AuthService) Logout(w http.ResponseWriter) { func (auth *AuthService) Logout(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: SessionCookieName, Name: SessionCookieName,
@ -86,11 +90,9 @@ func (auth *AuthService) Logout(w http.ResponseWriter) {
Value: "", Value: "",
Expires: time.Now(), Expires: time.Now(),
}) })
httputil.WriteJSON(w, "ok")
} }
// Middleware checks if the user is logged or not and if it has all fo the permissions set in "config.WithPermissions" // Middleware checks if the user is logged or not and if the user has all the permissions set in "config.WithPermissions"
func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Handler) http.Handler { func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -160,7 +162,11 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
} }
} }
// LoggedMiddleware is a shortcut for "Middleware(*AuthMiddlewareConfig)" that checks if a user is logged, no extra permissions are checked // LoggedMiddleware is a shortcut for
//
// Middleware(*AuthMiddlewareConfig)
//
// that checks if a user is logged, no extra permissions are checked
func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler { func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler {
return auth.Middleware(&AuthMiddlewareConfig{ return auth.Middleware(&AuthMiddlewareConfig{
RequireLogged: true, RequireLogged: true,
@ -168,7 +174,12 @@ func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler {
}) })
} }
// RequestUser retrives the "userID" from the given request based on the cookie session token // RequestUser retrives the "userID" from the given request based on the cookie session token.
// When generics arrive this will become something like
//
// func (auth *AuthService[U]) RequestUser(r *http.Request) *U
//
// and will just return nil if no user is present.
func (auth *AuthService) RequestUser(r *http.Request) (string, error) { func (auth *AuthService) RequestUser(r *http.Request) (string, error) {
cookie, err := r.Cookie(SessionCookieName) cookie, err := r.Cookie(SessionCookieName)
if err != nil { if err != nil {

@ -1,3 +1,4 @@
// Loads ".env" file and provides environment variables as global values.
package config package config
import ( import (
@ -8,11 +9,16 @@ import (
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
// Mode represents the MODE environment variable
var Mode string var Mode string
// Host represents the HOST environment variable
var Host string var Host string
// Url represents the URL environment variable
var Url string var Url string
// Email represents the EMAIL environment variable
var Email string var Email string
func loadEnv(target *string, name, defaultValue string) { func loadEnv(target *string, name, defaultValue string) {
@ -25,6 +31,7 @@ func loadEnv(target *string, name, defaultValue string) {
log.Printf("%s = %v", name, *target) log.Printf("%s = %v", name, *target)
} }
// Load calls "godotenv.Load()" and sets all the above exported variables
func Load() { func Load() {
godotenv.Load() godotenv.Load()

@ -1,3 +1,6 @@
// This package contains the database interface and an in memory implementation.
//
// For the in-memory implementation see the "NewInMemoryStore()" function.
package db package db
import ( import (
@ -5,16 +8,29 @@ import (
"fmt" "fmt"
) )
// ErrAlreadyExists is the error thrown from Store functions when an entity already exists.
var ErrAlreadyExists = errors.New(`object already exists in database`) var ErrAlreadyExists = errors.New(`object already exists in database`)
// ErrDoesntExist is the error thrown from Store functions when an entity doesn't exist.
var ErrDoesntExist = errors.New(`object doesn't exist in database`) var ErrDoesntExist = errors.New(`object doesn't exist in database`)
// Entities // Entities
// type FrontendUser struct {
// Anonymous bool `json:"anonymous,omitempty"`
// ID string `json:"id,omitempty"`
// Permissions []string `json:"permissions,omitempty"`
// }
// User represents a user in the database.
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`
Permissions []string `json:"permissions"` Permissions []string `json:"permissions"`
// passwordSaltHash string
} }
// HasPermission checks if the user has a specific permission.
func (user User) HasPermission(neededPerm string) bool { func (user User) HasPermission(neededPerm string) bool {
for _, perm := range user.Permissions { for _, perm := range user.Permissions {
if perm == neededPerm { if perm == neededPerm {
@ -25,6 +41,7 @@ func (user User) HasPermission(neededPerm string) bool {
return false return false
} }
// Room represents a room in the database, a room has an id, a name and a collection of seatIDs of this room.
type Room struct { type Room struct {
ID string `json:"id"` ID string `json:"id"`
SeatIDs []string `json:"seatIds"` SeatIDs []string `json:"seatIds"`
@ -32,6 +49,7 @@ type Room struct {
// gridRows, gridCols int // gridRows, gridCols int
} }
// Seat represents a seat in the database, it belongs to a single room and can be free or occupied by some user (referenced by userID).
type Seat struct { type Seat struct {
ID string `json:"id"` ID string `json:"id"`
RoomID string `json:"roomId"` RoomID string `json:"roomId"`
@ -42,8 +60,26 @@ type Seat struct {
// x, y, w, h int // x, y, w, h int
} }
// type FrontendSeat struct {
// ID string `json:"id"`
// RoomID string `json:"roomId"`
// // OccupiedBy is an empty list or a singleton of a userID
// OccupiedBy *FrontendUser `json:"occupiedBy"`
// DiagramRect struct {
// X int `json:"x"`
// Y int `json:"y"`
// Width int `json:"width"`
// Height int `json:"height"`
// } `json:"diagramRect"`
// }
//
// Database Interfaces // Database Interfaces
//
// Store is the main interface for interacting with database implementations, for now the only implementation is "memDB".
type Store interface { type Store interface {
CreateUser(user *User) error CreateUser(user *User) error
CreateRoom(room *Room) error CreateRoom(room *Room) error
@ -70,12 +106,17 @@ type Store interface {
// TODO: Create an SQLite implementation // TODO: Create an SQLite implementation
// memDB is the first Store implementation used for testing.
type memDB struct { type memDB struct {
// FIXME: Giusto per la cronaca fare le modifiche in questo modo alle mappe non è per niente thread safe, servirebbe come minimo usare un mutex per quando si scrive su una di queste variabili
// mutex *sync.Mutex
users map[string]*User users map[string]*User
rooms map[string]*Room rooms map[string]*Room
seats map[string]*Seat seats map[string]*Seat
} }
// NewInMemoryStore creates an instance of memDB hidden behind the Store interface.
func NewInMemoryStore() Store { func NewInMemoryStore() Store {
db := &memDB{ db := &memDB{
make(map[string]*User), make(map[string]*User),
@ -115,6 +156,10 @@ func NewInMemoryStore() Store {
return db return db
} }
//
// memDB Implementation
//
func (db *memDB) CreateUser(user *User) error { func (db *memDB) CreateUser(user *User) error {
if _, present := db.users[user.ID]; present { if _, present := db.users[user.ID]; present {
return ErrAlreadyExists return ErrAlreadyExists

@ -115,12 +115,19 @@ func (server *Server) setupRoutes() {
return return
} }
auth.Login(w, r, credentials.Username, credentials.Password) err := auth.Login(w, r, credentials.Username, credentials.Password)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
httputil.WriteJSON(w, "ok")
}) })
api.With(auth.LoggedMiddleware()). api.With(auth.LoggedMiddleware()).
Post("/logout", func(w http.ResponseWriter, r *http.Request) { Post("/logout", func(w http.ResponseWriter, r *http.Request) {
auth.Logout(w) auth.Logout(w)
httputil.WriteJSON(w, "ok")
}) })
api.Get("/user", func(w http.ResponseWriter, r *http.Request) { api.Get("/user", func(w http.ResponseWriter, r *http.Request) {

Loading…
Cancel
Save