From 10704d946f5bcaec9932f8bd9e92d4079a424a0c Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Sun, 20 Mar 2022 23:02:36 +0100 Subject: [PATCH] Now server/auth is completely generic on the user type --- server/auth.go | 11 +++++-- server/auth/auth.go | 46 ++++++++++++++++------------ server/db/database.go | 4 +++ server/server.go | 68 ++++++++++++++++------------------------- server/util/generics.go | 5 +++ 5 files changed, 70 insertions(+), 64 deletions(-) create mode 100644 server/util/generics.go diff --git a/server/auth.go b/server/auth.go index 956fd85..f6dc0f9 100644 --- a/server/auth.go +++ b/server/auth.go @@ -56,10 +56,15 @@ func (service *simpleAuthenticator) SessionTokenFromUser(userID string) (string, return token, nil } -func (service *simpleAuthenticator) UserFromSessionToken(session string) (string, error) { - user, present := service.sessions[session] +func (service *simpleAuthenticator) UserFromSessionToken(session string) (*db.User, error) { + userID, present := service.sessions[session] if !present { - return "", auth.ErrNoUserForSession + return nil, auth.ErrNoUserForSession + } + + user, err := service.database.GetUser(userID) + if err != nil { + return nil, err } return user, nil diff --git a/server/auth/auth.go b/server/auth/auth.go index ca0ade2..334e501 100644 --- a/server/auth/auth.go +++ b/server/auth/auth.go @@ -7,24 +7,30 @@ import ( "errors" "net/http" "time" + + "git.phc.dm.unipi.it/aziis98/posti-dm/server/util" ) var ( ErrNoUserForSession = errors.New(`no user for session token`) ) +type User[IdType any] interface { + UID() IdType +} + // Authenticator should be used by clients to provide authentication functions and mapping of session tokens to users -type Authenticator interface { +type Authenticator[UserID any, U User[UserID]] interface { // CheckUserPassword is called to login a user and create a corresponding session, see also "SessionTokenFromUser" - CheckUserPassword(userID string, password string) error + CheckUserPassword(user UserID, password string) error // GetUserPermissions gets the list of permissions for this user - UserPermissions(userID string) ([]string, error) + UserPermissions(user UserID) ([]string, error) // SessionTokenFromUser returns a (new) session token that represents this user - SessionTokenFromUser(userID string) (string, error) + SessionTokenFromUser(user UserID) (string, error) // UserFromSessionToken returns the corresponding user for this session token or "service.ErrNoUserForSession" - UserFromSessionToken(session string) (string, error) + UserFromSessionToken(session string) (U, error) // AuthenticationFailed handles failed authentications AuthenticationFailed(error) http.Handler @@ -42,19 +48,19 @@ type MiddlewareConfig struct { } // AuthSessionService given an Authenticator provides functions to login and logout users and an http.Handler middleware that accept users based on permissions and login status -type AuthSessionService struct { +type AuthSessionService[UserID any, U User[UserID]] struct { SessionCookieName string SessionCookiePath string SessionCookieDuration time.Duration - Authenticator + Authenticator Authenticator[UserID, U] } // NewAuthSessionService creates a new "*AuthSessionService" with a default session cookie name and path -func NewAuthSessionService(auth Authenticator) *AuthSessionService { +func NewAuthSessionService[UserID any, U User[UserID]](auth Authenticator[UserID, U]) *AuthSessionService[UserID, U] { oneWeek := 7 * 24 * time.Hour - return &AuthSessionService{ + return &AuthSessionService[UserID, U]{ "session", "/", oneWeek, @@ -63,7 +69,7 @@ func NewAuthSessionService(auth Authenticator) *AuthSessionService { } // Login tries to login a user given its id and password -func (service *AuthSessionService) Login(w http.ResponseWriter, userID, password string) error { +func (service *AuthSessionService[UserID, U]) Login(w http.ResponseWriter, userID UserID, password string) error { if err := service.Authenticator.CheckUserPassword(userID, password); err != nil { return err } @@ -84,7 +90,7 @@ func (service *AuthSessionService) Login(w http.ResponseWriter, userID, password } // Logout clears the session cookie from a request effectively logging out the user for future requests -func (service *AuthSessionService) Logout(w http.ResponseWriter) { +func (service *AuthSessionService[UserID, U]) Logout(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: service.SessionCookieName, Path: service.SessionCookiePath, @@ -94,7 +100,7 @@ func (service *AuthSessionService) Logout(w http.ResponseWriter) { } // Middleware returns an http middleware that accepts users based on login status and permissions -func (service *AuthSessionService) Middleware(config *MiddlewareConfig) func(http.Handler) http.Handler { +func (service *AuthSessionService[UserID, U]) 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(service.SessionCookieName) @@ -112,7 +118,7 @@ func (service *AuthSessionService) Middleware(config *MiddlewareConfig) func(htt return } - userID, err := service.Authenticator.UserFromSessionToken(cookie.Value) + user, err := service.Authenticator.UserFromSessionToken(cookie.Value) if err == ErrNoUserForSession { service.Logout(w) w.WriteHeader(http.StatusUnauthorized) @@ -124,7 +130,7 @@ func (service *AuthSessionService) Middleware(config *MiddlewareConfig) func(htt } if config.RequireLogged { - userPerms, err := service.Authenticator.UserPermissions(userID) + userPerms, err := service.Authenticator.UserPermissions(user.UID()) if err != nil { service.Authenticator.OtherError(err).ServeHTTP(w, r) return @@ -168,7 +174,7 @@ func (service *AuthSessionService) Middleware(config *MiddlewareConfig) func(htt // Middleware(*AuthMiddlewareConfig) // // that only accepts logged in users, no special permissions are checked -func (service *AuthSessionService) LoggedMiddleware() func(http.Handler) http.Handler { +func (service *AuthSessionService[UserID, U]) LoggedMiddleware() func(http.Handler) http.Handler { return service.Middleware(&MiddlewareConfig{ RequireLogged: true, NeedPermissions: []string{}, @@ -181,16 +187,16 @@ func (service *AuthSessionService) LoggedMiddleware() func(http.Handler) http.Ha // func (auth *AuthService[U]) RequestUser(r *http.Request) *U // // and will just return nil if no user is present. -func (service *AuthSessionService) RequestUser(r *http.Request) (string, error) { +func (service *AuthSessionService[UserID, U]) RequestUser(r *http.Request) (U, error) { cookie, err := r.Cookie(service.SessionCookieName) if err != nil { - return "", err + return util.Zero[U](), err } - userID, err := service.Authenticator.UserFromSessionToken(cookie.Value) + user, err := service.Authenticator.UserFromSessionToken(cookie.Value) if err == ErrNoUserForSession { - return "", err + return util.Zero[U](), err } - return userID, nil + return user, nil } diff --git a/server/db/database.go b/server/db/database.go index 3618d37..0f31bcf 100644 --- a/server/db/database.go +++ b/server/db/database.go @@ -38,6 +38,10 @@ type User struct { // passwordSaltHash string } +func (user User) UID() string { + return user.ID +} + // 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 { ID string `json:"id"` diff --git a/server/server.go b/server/server.go index 071b2d7..f79bee7 100644 --- a/server/server.go +++ b/server/server.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "net/http" "git.phc.dm.unipi.it/aziis98/posti-dm/server/auth" @@ -11,7 +12,7 @@ import ( ) type Server struct { - auth *auth.AuthSessionService + auth *auth.AuthSessionService[string, *db.User] roomServerEventStreams map[string]serverevents.Handler @@ -24,7 +25,7 @@ func NewServer() *Server { server := &Server{ Database: database, - auth: auth.NewAuthSessionService( + auth: auth.NewAuthSessionService[string, *db.User]( &simpleAuthenticator{ make(map[string]string), database, @@ -84,17 +85,12 @@ func (server *Server) setupRoutes() { }) api.Get("/user", func(w http.ResponseWriter, r *http.Request) { - userID, err := auth.RequestUser(r) + user, err := auth.RequestUser(r) if err != nil { httputil.WriteJSON(w, nil) return } - user, err := database.GetUser(userID) - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - } - httputil.WriteJSON(w, user) }) @@ -134,38 +130,40 @@ func (server *Server) setupRoutes() { server.roomServerEventStreams[roomID[0]].ServeHTTP(w, r) }) + requestSeat := func(r *http.Request) (*db.Seat, error) { + seatID, ok := r.URL.Query()["id"] + if !ok { + return nil, fmt.Errorf(`missing seat id`) + } + + seat, err := database.GetSeat(seatID[0]) + if err != nil { + return nil, err + } + + return seat, nil + } + api.With(auth.LoggedMiddleware()). Post("/seat/occupy", func(w http.ResponseWriter, r *http.Request) { database.BeginTransaction() defer database.EndTransaction() - userID, err := auth.RequestUser(r) + user, err := auth.RequestUser(r) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } - user, err := database.GetUser(userID) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - seatID, ok := r.URL.Query()["id"] - if !ok { - http.Error(w, `missing seat id`, http.StatusBadRequest) - return - } - - seat, err := database.GetSeat(seatID[0]) + seat, err := requestSeat(r) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } // a seat can be occupied only if empty; simple users can occupy a seat only if they have occupied no other seat; admins and moderator (for now) can occupy even more than one seat (as occupied by an anonymous?) to fix the free seat count of the room if len(seat.OccupiedBy) == 0 { - userSeats, err := database.GetUserSeats(userID) + userSeats, err := database.GetUserSeats(user.ID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -176,7 +174,7 @@ func (server *Server) setupRoutes() { return } - if err := database.OccupySeat(seatID[0], userID); err != nil { + if err := database.OccupySeat(seat.ID, user.ID); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -190,34 +188,22 @@ func (server *Server) setupRoutes() { api.With(auth.LoggedMiddleware()). Post("/seat/leave", func(w http.ResponseWriter, r *http.Request) { - userID, err := auth.RequestUser(r) + user, err := auth.RequestUser(r) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } - user, err := database.GetUser(userID) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - seatID, ok := r.URL.Query()["id"] - if !ok { - http.Error(w, `missing seat id`, http.StatusBadRequest) - return - } - - seat, err := database.GetSeat(seatID[0]) + seat, err := requestSeat(r) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } // Check permissions, if the place is occupied then only its owner, a moderator or an admin can clear it if len(seat.OccupiedBy) > 0 { if user.ID == seat.OccupiedBy[0] || user.Permissions.HasAny(db.PermissionAdmin, db.PermissionModerator) { - if err := database.FreeSeat(seatID[0]); err != nil { + if err := database.FreeSeat(seat.ID); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/server/util/generics.go b/server/util/generics.go new file mode 100644 index 0000000..c6a8cb3 --- /dev/null +++ b/server/util/generics.go @@ -0,0 +1,5 @@ +package util + +func Zero[T any]() T { + return *new(T) +}