Skip to content

Commit

Permalink
Add API user authentication to auth module with caching of creds on u…
Browse files Browse the repository at this point in the history
…ser CRUD.
  • Loading branch information
knadh committed Oct 13, 2024
1 parent 0bea998 commit 5024ded
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 23 deletions.
7 changes: 6 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions cmd/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}

Expand All @@ -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})
}

Expand Down
50 changes: 43 additions & 7 deletions cmd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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})
}

Expand Down Expand Up @@ -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
}
22 changes: 13 additions & 9 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ type Callbacks struct {
}

type Auth struct {
tokens map[string]models.User
apiUsers map[string]models.User
sync.RWMutex

cfg Config
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 8 additions & 5 deletions internal/core/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,23 @@ 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.
if u.Type == models.UserTypeAPI {
// 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}
u.PasswordLogin = false
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)))
}
Expand All @@ -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.
Expand All @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down

0 comments on commit 5024ded

Please sign in to comment.