Add API user authentication to auth module with caching of creds on user CRUD.

This commit is contained in:
Kailash Nadh 2024-07-15 23:04:00 +05:30
parent 0bea9989ec
commit 5024ded763
6 changed files with 81 additions and 23 deletions

View file

@ -994,7 +994,12 @@ func initAuth(db *sql.DB, ko *koanf.Koanf, co *core.Core) *auth.Auth {
Type: models.UserTypeAPI,
}
u.Role.ID = auth.SuperAdminRoleID
a.SetToken(username, u)
a.CacheAPIUsers([]models.User{u})
}
// Load all API users.
if err := cacheAPIUsers(co, a); err != nil {
lo.Fatalf("error loading API users: %v", err)
}
return a

View file

@ -76,6 +76,11 @@ func handleUpdateRole(c echo.Context) error {
return err
}
// Cache the API token for validating API queries without hitting the DB every time.
if err := cacheAPIUsers(app.core, app.auth); err != nil {
return err
}
return c.JSON(http.StatusOK, okResp{out})
}
@ -94,6 +99,11 @@ func handleDeleteRole(c echo.Context) error {
return err
}
// Cache the API token for validating API queries without hitting the DB every time.
if err := cacheAPIUsers(app.core, app.auth); err != nil {
return err
}
return c.JSON(http.StatusOK, okResp{true})
}

View file

@ -7,6 +7,7 @@ import (
"strings"
"github.com/knadh/listmonk/internal/auth"
"github.com/knadh/listmonk/internal/core"
"github.com/knadh/listmonk/internal/utils"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
@ -94,15 +95,20 @@ func handleCreateUser(c echo.Context) error {
}
// Create the user in the database.
out, err := app.core.CreateUser(u)
user, err := app.core.CreateUser(u)
if err != nil {
return err
}
if out.Type != models.UserTypeAPI {
out.Password = null.String{}
if user.Type != models.UserTypeAPI {
user.Password = null.String{}
}
return c.JSON(http.StatusOK, okResp{out})
// Cache the API token for validating API queries without hitting the DB every time.
if err := cacheAPIUsers(app.core, app.auth); err != nil {
return err
}
return c.JSON(http.StatusOK, okResp{user})
}
// handleUpdateUser handles user modification.
@ -170,13 +176,21 @@ func handleUpdateUser(c echo.Context) error {
u.Name = u.Username
}
out, err := app.core.UpdateUser(id, u)
// Update the user in the DB.
user, err := app.core.UpdateUser(id, u)
if err != nil {
return err
}
out.Password = null.String{}
return c.JSON(http.StatusOK, okResp{out})
// Clear the pasword before sending outside.
user.Password = null.String{}
// Cache the API token for validating API queries without hitting the DB every time.
if err := cacheAPIUsers(app.core, app.auth); err != nil {
return err
}
return c.JSON(http.StatusOK, okResp{user})
}
// handleDeleteUsers handles user deletion, either a single one (ID in the URI), or a list.
@ -199,6 +213,11 @@ func handleDeleteUsers(c echo.Context) error {
return err
}
// Cache the API token for validating API queries without hitting the DB every time.
if err := cacheAPIUsers(app.core, app.auth); err != nil {
return err
}
return c.JSON(http.StatusOK, okResp{true})
}
@ -250,3 +269,20 @@ func handleUpdateUserProfile(c echo.Context) error {
return c.JSON(http.StatusOK, okResp{out})
}
func cacheAPIUsers(co *core.Core, a *auth.Auth) error {
allUsers, err := co.GetUsers()
if err != nil {
return err
}
apiUsers := make([]models.User, 0, len(allUsers))
for _, u := range allUsers {
if u.Type == models.UserTypeAPI && u.Status == models.UserStatusEnabled {
apiUsers = append(apiUsers, u)
}
}
a.CacheAPIUsers(apiUsers)
return nil
}

View file

@ -68,7 +68,7 @@ type Callbacks struct {
}
type Auth struct {
tokens map[string]models.User
apiUsers map[string]models.User
sync.RWMutex
cfg Config
@ -86,7 +86,7 @@ func New(cfg Config, db *sql.DB, cb *Callbacks, lo *log.Logger) (*Auth, error) {
cb: cb,
log: lo,
tokens: map[string]models.User{},
apiUsers: map[string]models.User{},
}
// Initialize OIDC.
@ -138,17 +138,21 @@ func New(cfg Config, db *sql.DB, cb *Callbacks, lo *log.Logger) (*Auth, error) {
return a, nil
}
// SetToken caches tokens for authenticating API client calls.
func (o *Auth) SetToken(apiKey string, u models.User) {
// CacheAPIUsers caches API users for authenticating requests.
func (o *Auth) CacheAPIUsers(users []models.User) {
o.Lock()
o.tokens[apiKey] = u
o.apiUsers = map[string]models.User{}
for _, u := range users {
o.apiUsers[u.Username] = u
}
o.Unlock()
}
// GetToken validates an API user+token.
func (o *Auth) GetToken(user string, token string) (models.User, bool) {
// GetAPIToken validates an API user+token.
func (o *Auth) GetAPIToken(user string, token string) (models.User, bool) {
o.RLock()
t, ok := o.tokens[user]
t, ok := o.apiUsers[user]
o.RUnlock()
if !ok || subtle.ConstantTimeCompare([]byte(t.Password.String), []byte(token)) != 1 {
@ -221,7 +225,7 @@ func (o *Auth) Middleware(next echo.HandlerFunc) echo.HandlerFunc {
}
// Validate the token.
user, ok := o.GetToken(key, token)
user, ok := o.GetAPIToken(key, token)
if !ok {
c.Set(UserKey, echo.NewHTTPError(http.StatusForbidden, "invalid API credentials"))
return next(c)

View file

@ -33,7 +33,7 @@ func (c *Core) GetUser(id int, username, email string) (models.User, error) {
// CreateUser creates a new user.
func (c *Core) CreateUser(u models.User) (models.User, error) {
var out models.User
var id int
// If it's an API user, generate a random token for password
// and set the e-mail to default.
@ -41,7 +41,7 @@ func (c *Core) CreateUser(u models.User) (models.User, error) {
// Generate a random admin password.
tk, err := utils.GenerateRandomString(32)
if err != nil {
return out, err
return models.User{}, err
}
u.Email = null.String{String: u.Username + "@api", Valid: true}
@ -49,7 +49,7 @@ func (c *Core) CreateUser(u models.User) (models.User, error) {
u.Password = null.String{String: tk, Valid: true}
}
if err := c.q.CreateUser.Get(&out, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.RoleID, u.Status); err != nil {
if err := c.q.CreateUser.Get(&id, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.RoleID, u.Status); err != nil {
return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
}
@ -60,7 +60,8 @@ func (c *Core) CreateUser(u models.User) (models.User, error) {
u.Password = null.String{Valid: false}
}
return out, nil
out, err := c.GetUser(id, "", "")
return out, err
}
// UpdateUser updates a given user.
@ -75,7 +76,9 @@ func (c *Core) UpdateUser(id int, u models.User) (models.User, error) {
return models.User{}, echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("users.needSuper"))
}
return c.GetUser(id, "", "")
out, err := c.GetUser(id, "", "")
return out, err
}
// UpdateUserProfile updates the basic fields of a given uesr (name, email, password).

View file

@ -1039,7 +1039,7 @@ INSERT INTO users (username, password_login, password, email, name, type, role_i
THEN $3
ELSE NULL
END
), $4, $5, $6, $7, $8) RETURNING *;
), $4, $5, $6, $7, $8) RETURNING id;
-- name: update-user
WITH u AS (