mirror of
https://github.com/knadh/listmonk.git
synced 2024-11-13 02:55:04 +08:00
Add API user authentication to auth module with caching of creds on user CRUD.
This commit is contained in:
parent
0bea9989ec
commit
5024ded763
6 changed files with 81 additions and 23 deletions
|
@ -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
|
||||
|
|
10
cmd/roles.go
10
cmd/roles.go
|
@ -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})
|
||||
}
|
||||
|
||||
|
|
50
cmd/users.go
50
cmd/users.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Reference in a new issue