diff --git a/server/auth.go b/server/auth.go new file mode 100644 index 0000000..956fd85 --- /dev/null +++ b/server/auth.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "git.phc.dm.unipi.it/aziis98/posti-dm/server/auth" + "git.phc.dm.unipi.it/aziis98/posti-dm/server/db" + "git.phc.dm.unipi.it/aziis98/posti-dm/server/util" +) + +// simpleAuthenticator holds an in memory map of session tokens and a reference to the main database interface +type simpleAuthenticator struct { + // sessions is a map from a sessionToken to userID + sessions map[string]string + database db.Database +} + +func (service *simpleAuthenticator) CheckUserPassword(userID, password string) error { + if password != "phc" { + return fmt.Errorf(`invalid password`) + } + + // FIXME: al momento quando la password è giusta creiamo tutti gli account necessari + err := service.database.CreateUser(&db.User{ + ID: userID, + Permissions: make(util.StringSet), + }) + if err != nil { + log.Printf(`got "%v" while trying to log as @%s`, err, userID) + return nil + } + + return nil +} + +func (service *simpleAuthenticator) UserPermissions(userID string) ([]string, error) { + user, err := service.database.GetUser(userID) + if err != nil { + return nil, err + } + + return user.Permissions.Elements(), nil +} + +func (service *simpleAuthenticator) SessionTokenFromUser(userID string) (string, error) { + user, err := service.database.GetUser(userID) + if err != nil { + return "", err + } + + token := util.RandomHash(20) + service.sessions[token] = user.ID + + return token, nil +} + +func (service *simpleAuthenticator) UserFromSessionToken(session string) (string, error) { + user, present := service.sessions[session] + if !present { + return "", auth.ErrNoUserForSession + } + + return user, nil +} + +func (service *simpleAuthenticator) AuthenticationFailed(err error) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, err.Error(), http.StatusUnauthorized) + }) +} + +func (service *simpleAuthenticator) OtherError(err error) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }) +} diff --git a/server/auth/auth.go b/server/auth/auth.go index 216ad1c..0173ea2 100644 --- a/server/auth/auth.go +++ b/server/auth/auth.go @@ -13,67 +13,58 @@ var ( ErrNoUserForSession = errors.New(`no user for session token`) ) -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 { - // RequireLogged rejects not logged users if true - RequireLogged bool - - // WithPermissions is the list of permissions the user should have to pass the middleware - WithPermissions []string -} - -// // Authenticator is the spec of this library -// type Authenticator interface { -// // Login checks user credentials and adds a session cookie to the user if successfull -// Login(w http.ResponseWriter, r *http.Request, userID, password string) -// // 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(*AuthMiddlewareConfig) func(http.Handler) http.Handler -// // LoggedMiddleware accepts all logged users without checking for specific permissions -// LoggedMiddleware() func(http.Handler) http.Handler - -// // RequestUser returns the userID for this cookie session token -// RequestUser(r *http.Request) (string, error) -// } - -// 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 { +// Authenticator 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 Authenticator interface { // CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser" - CheckUserPassword func(userID string, password string) error + CheckUserPassword(userID string, password string) error // GetUserPermissions gets the list of permissions for this user - UserPermissions func(userID string) ([]string, error) + UserPermissions(userID string) ([]string, error) // SessionTokenFromUser returns a (new) session token that represents this user - SessionTokenFromUser func(userID string) (string, error) - // UserFromSessionToken returns the corresponing user for this session token or "auth.ErrNoUserForSession" - UserFromSessionToken func(session string) (string, error) + SessionTokenFromUser(userID string) (string, error) + // UserFromSessionToken returns the corresponding user for this session token or "service.ErrNoUserForSession" + UserFromSessionToken(session string) (string, error) // AuthenticationFailed handles failed authentications - AuthenticationFailed func(error) http.Handler + AuthenticationFailed(error) http.Handler // OtherError handles other errors - OtherError func(error) http.Handler + OtherError(error) http.Handler +} + +// MiddlewareConfig configures the middleware to only accept logged users (if "RequireLogged" is true) and with certain permissions (user must have all permissions inside "WithPermissions") +type MiddlewareConfig struct { + // RequireLogged rejects not logged users if true + RequireLogged bool + + // NeedPermissions is the list of permissions the user should have to pass the middleware + NeedPermissions []string +} + +// AuthService is the spec of this library +type AuthSessionService struct { + SessionCookieName string + Authenticator +} + +// NewAuthSessionService creates a new *AuthSessionService with a default session cookie name +func NewAuthSessionService(auth Authenticator) *AuthSessionService { + return &AuthSessionService{"session", auth} } // 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 { +func (service *AuthSessionService) Login(w http.ResponseWriter, userID, password string) error { + if err := service.Authenticator.CheckUserPassword(userID, password); err != nil { return err } - token, err := auth.SessionTokenFromUser(userID) + token, err := service.Authenticator.SessionTokenFromUser(userID) if err != nil { return err } http.SetCookie(w, &http.Cookie{ - Name: SessionCookieName, + Name: service.SessionCookieName, Path: "/", // TODO: Make configurable Value: token, Expires: time.Now().Add(7 * 24 * time.Hour), // TODO: Make configurable @@ -82,10 +73,10 @@ func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, p 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) { +// Logout clears the session cookie from a request effectively logging out the user for future requests +func (service *AuthSessionService) Logout(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ - Name: SessionCookieName, + Name: service.SessionCookieName, Path: "/", Value: "", Expires: time.Now(), @@ -93,39 +84,39 @@ func (auth *AuthService) Logout(w http.ResponseWriter) { } // 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 (service *AuthSessionService) Middleware(config *MiddlewareConfig) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie(SessionCookieName) + cookie, err := r.Cookie(service.SessionCookieName) if err == http.ErrNoCookie { if !config.RequireLogged { // Login not required next.ServeHTTP(w, r) return } - auth.AuthenticationFailed(err).ServeHTTP(w, r) + service.Authenticator.AuthenticationFailed(err).ServeHTTP(w, r) return } if err != nil { - auth.OtherError(err).ServeHTTP(w, r) + service.Authenticator.OtherError(err).ServeHTTP(w, r) return } - userID, err := auth.UserFromSessionToken(cookie.Value) + userID, err := service.Authenticator.UserFromSessionToken(cookie.Value) if err == ErrNoUserForSession { - auth.Logout(w) + service.Logout(w) w.WriteHeader(http.StatusUnauthorized) return } if err != nil { - auth.OtherError(err).ServeHTTP(w, r) + service.Authenticator.OtherError(err).ServeHTTP(w, r) return } if config.RequireLogged { - userPerms, err := auth.UserPermissions(userID) + userPerms, err := service.Authenticator.UserPermissions(userID) if err != nil { - auth.OtherError(err).ServeHTTP(w, r) + service.Authenticator.OtherError(err).ServeHTTP(w, r) return } @@ -136,7 +127,7 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand // check the user has all the permissions to access the route hasAll := true - for _, perm := range config.WithPermissions { + for _, perm := range config.NeedPermissions { if _, present := userPermsMap[perm]; !present { hasAll = false break @@ -144,14 +135,14 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand } if !hasAll { - auth.AuthenticationFailed(err).ServeHTTP(w, r) + service.Authenticator.AuthenticationFailed(err).ServeHTTP(w, r) return } } // Refresh session cookie expiration http.SetCookie(w, &http.Cookie{ - Name: SessionCookieName, + Name: service.SessionCookieName, Path: "/", Value: cookie.Value, Expires: time.Now().Add(7 * 24 * time.Hour), @@ -167,10 +158,10 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand // Middleware(*AuthMiddlewareConfig) // // that checks if a user is logged, no extra permissions are checked -func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler { - return auth.Middleware(&AuthMiddlewareConfig{ +func (service *AuthSessionService) LoggedMiddleware() func(http.Handler) http.Handler { + return service.Middleware(&MiddlewareConfig{ RequireLogged: true, - WithPermissions: []string{}, + NeedPermissions: []string{}, }) } @@ -180,13 +171,13 @@ func (auth *AuthService) LoggedMiddleware() func(http.Handler) http.Handler { // 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) { - cookie, err := r.Cookie(SessionCookieName) +func (service *AuthSessionService) RequestUser(r *http.Request) (string, error) { + cookie, err := r.Cookie(service.SessionCookieName) if err != nil { return "", err } - userID, err := auth.UserFromSessionToken(cookie.Value) + userID, err := service.Authenticator.UserFromSessionToken(cookie.Value) if err == ErrNoUserForSession { return "", err } diff --git a/server/db/database.go b/server/db/database.go index 0ef0448..3618d37 100644 --- a/server/db/database.go +++ b/server/db/database.go @@ -96,8 +96,8 @@ type Seat struct { // Database Interfaces // -// Store is the main interface for interacting with database implementations, for now the only implementation is "memDB". -type Store interface { +// Database is the main interface for interacting with database implementations, for now the only implementation is "memDB". +type Database interface { BeginTransaction() EndTransaction() @@ -127,7 +127,7 @@ type Store interface { // TODO: Create an SQLite implementation // NewInMemoryStore creates an instance of memDB hidden behind the Store interface. -func NewInMemoryStore() Store { +func NewInMemoryStore() Database { db := &memDB{ &sync.Mutex{}, diff --git a/server/server.go b/server/server.go index 5357db9..071b2d7 100644 --- a/server/server.go +++ b/server/server.go @@ -1,89 +1,35 @@ package main import ( - "fmt" - "log" "net/http" "git.phc.dm.unipi.it/aziis98/posti-dm/server/auth" "git.phc.dm.unipi.it/aziis98/posti-dm/server/db" "git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil" "git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil/serverevents" - "git.phc.dm.unipi.it/aziis98/posti-dm/server/util" "github.com/go-chi/chi/v5" ) type Server struct { - authService *auth.AuthService + auth *auth.AuthSessionService roomServerEventStreams map[string]serverevents.Handler - Database db.Store + Database db.Database ApiRoute chi.Router } func NewServer() *Server { - sessions := make(map[string]*db.User) database := db.NewInMemoryStore() server := &Server{ Database: database, - authService: &auth.AuthService{ - CheckUserPassword: func(userID, password string) error { - if password != "phc" { - return fmt.Errorf(`invalid password`) - } - - // FIXME: al momento quando la password è giusta creiamo tutti gli account necessari - err := database.CreateUser(&db.User{ - ID: userID, - Permissions: make(util.StringSet), - }) - if err != nil { - log.Printf(`got "%v" while trying to log as @%s`, err, userID) - return nil - } - - return nil - }, - UserPermissions: func(userID string) ([]string, error) { - user, err := database.GetUser(userID) - if err != nil { - return nil, err - } - - return user.Permissions.Elements(), nil - }, - SessionTokenFromUser: func(userID string) (string, error) { - user, err := database.GetUser(userID) - if err != nil { - return "", err - } - - token := util.RandomHash(20) - sessions[token] = user - - return token, nil - }, - UserFromSessionToken: func(session string) (string, error) { - user, present := sessions[session] - if !present { - return "", auth.ErrNoUserForSession - } - - return user.ID, nil - }, - AuthenticationFailed: func(err error) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, err.Error(), http.StatusUnauthorized) - }) - }, - OtherError: func(err error) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, err.Error(), http.StatusInternalServerError) - }) + auth: auth.NewAuthSessionService( + &simpleAuthenticator{ + make(map[string]string), + database, }, - }, + ), roomServerEventStreams: make(map[string]serverevents.Handler), @@ -103,7 +49,7 @@ func NewServer() *Server { func (server *Server) setupRoutes() { api := server.ApiRoute database := server.Database - auth := server.authService + auth := server.auth // FIXME: in realtà tutte le routes che interagiscono con il db dovrebbero essere in transazione con // database.BeginTransaction() @@ -122,7 +68,7 @@ func (server *Server) setupRoutes() { return } - err := auth.Login(w, r, credentials.Username, credentials.Password) + err := auth.Login(w, credentials.Username, credentials.Password) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return