WIP: Predefined users

This commit is contained in:
binwiederhier
2025-07-07 22:36:01 +02:00
parent 3c8ac4a1e1
commit efef587671
5 changed files with 48 additions and 21 deletions

View File

@@ -52,6 +52,7 @@ var flagsServe = append(
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-users", Aliases: []string{"auth_users"}, EnvVars: []string{"NTFY_AUTH_USERS"}, Usage: "pre-provisioned declarative users"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
@@ -157,6 +158,7 @@ func execServe(c *cli.Context) error {
authFile := c.String("auth-file") authFile := c.String("auth-file")
authStartupQueries := c.String("auth-startup-queries") authStartupQueries := c.String("auth-startup-queries")
authDefaultAccess := c.String("auth-default-access") authDefaultAccess := c.String("auth-default-access")
authUsers := c.StringSlice("auth-users")
attachmentCacheDir := c.String("attachment-cache-dir") attachmentCacheDir := c.String("attachment-cache-dir")
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit") attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
attachmentFileSizeLimitStr := c.String("attachment-file-size-limit") attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
@@ -406,6 +408,7 @@ func execServe(c *cli.Context) error {
conf.AuthFile = authFile conf.AuthFile = authFile
conf.AuthStartupQueries = authStartupQueries conf.AuthStartupQueries = authStartupQueries
conf.AuthDefault = authDefault conf.AuthDefault = authDefault
conf.AuthUsers = nil // FIXME
conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentCacheDir = attachmentCacheDir
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
conf.AttachmentFileSizeLimit = attachmentFileSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit

View File

@@ -94,7 +94,6 @@ Example:
You may set the NTFY_PASSWORD environment variable to pass the new password or NTFY_PASSWORD_HASH to pass You may set the NTFY_PASSWORD environment variable to pass the new password or NTFY_PASSWORD_HASH to pass
directly the bcrypt hash. This is useful if you are updating users via scripts. directly the bcrypt hash. This is useful if you are updating users via scripts.
`, `,
}, },
{ {

View File

@@ -93,6 +93,7 @@ type Config struct {
AuthFile string AuthFile string
AuthStartupQueries string AuthStartupQueries string
AuthDefault user.Permission AuthDefault user.Permission
AuthUsers []user.User
AuthBcryptCost int AuthBcryptCost int
AuthStatsQueueWriterInterval time.Duration AuthStatsQueueWriterInterval time.Duration
AttachmentCacheDir string AttachmentCacheDir string

View File

@@ -189,7 +189,14 @@ func New(conf *Config) (*Server, error) {
} }
var userManager *user.Manager var userManager *user.Manager
if conf.AuthFile != "" { if conf.AuthFile != "" {
userManager, err = user.NewManager(conf.AuthFile, conf.AuthStartupQueries, conf.AuthDefault, conf.AuthBcryptCost, conf.AuthStatsQueueWriterInterval) authConfig := &user.Config{
Filename: conf.AuthFile,
StartupQueries: conf.AuthStartupQueries,
DefaultAccess: conf.AuthDefault,
BcryptCost: conf.AuthBcryptCost,
QueueWriterInterval: conf.AuthStatsQueueWriterInterval,
}
userManager, err = user.NewManager(authConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -441,36 +441,53 @@ var (
// Manager is an implementation of Manager. It stores users and access control list // Manager is an implementation of Manager. It stores users and access control list
// in a SQLite database. // in a SQLite database.
type Manager struct { type Manager struct {
db *sql.DB config *Config
defaultAccess Permission // Default permission if no ACL matches db *sql.DB
statsQueue map[string]*Stats // "Queue" to asynchronously write user stats to the database (UserID -> Stats) statsQueue map[string]*Stats // "Queue" to asynchronously write user stats to the database (UserID -> Stats)
tokenQueue map[string]*TokenUpdate // "Queue" to asynchronously write token access stats to the database (Token ID -> TokenUpdate) tokenQueue map[string]*TokenUpdate // "Queue" to asynchronously write token access stats to the database (Token ID -> TokenUpdate)
bcryptCost int // Makes testing easier mu sync.Mutex
mu sync.Mutex }
type Config struct {
Filename string
StartupQueries string
DefaultAccess Permission // Default permission if no ACL matches
ProvisionedUsers []*User // Predefined users to create on startup
ProvisionedAccess map[string][]*Grant // Predefined access grants to create on startup
BcryptCost int // Makes testing easier
QueueWriterInterval time.Duration
} }
var _ Auther = (*Manager)(nil) var _ Auther = (*Manager)(nil)
// NewManager creates a new Manager instance // NewManager creates a new Manager instance
func NewManager(filename, startupQueries string, defaultAccess Permission, bcryptCost int, queueWriterInterval time.Duration) (*Manager, error) { func NewManager(config *Config) (*Manager, error) {
db, err := sql.Open("sqlite3", filename) // Set defaults
if config.BcryptCost <= 0 {
config.BcryptCost = DefaultUserPasswordBcryptCost
}
if config.QueueWriterInterval.Seconds() <= 0 {
config.QueueWriterInterval = DefaultUserStatsQueueWriterInterval
}
// Open DB and run setup queries
db, err := sql.Open("sqlite3", config.Filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := setupDB(db); err != nil { if err := setupDB(db); err != nil {
return nil, err return nil, err
} }
if err := runStartupQueries(db, startupQueries); err != nil { if err := runStartupQueries(db, config.StartupQueries); err != nil {
return nil, err return nil, err
} }
manager := &Manager{ manager := &Manager{
db: db, db: db,
defaultAccess: defaultAccess, config: config,
statsQueue: make(map[string]*Stats), statsQueue: make(map[string]*Stats),
tokenQueue: make(map[string]*TokenUpdate), tokenQueue: make(map[string]*TokenUpdate),
bcryptCost: bcryptCost,
} }
go manager.asyncQueueWriter(queueWriterInterval) go manager.asyncQueueWriter(config.QueueWriterInterval)
return manager, nil return manager, nil
} }
@@ -843,7 +860,7 @@ func (a *Manager) Authorize(user *User, topic string, perm Permission) error {
} }
defer rows.Close() defer rows.Close()
if !rows.Next() { if !rows.Next() {
return a.resolvePerms(a.defaultAccess, perm) return a.resolvePerms(a.config.DefaultAccess, perm)
} }
var read, write bool var read, write bool
if err := rows.Scan(&read, &write); err != nil { if err := rows.Scan(&read, &write); err != nil {
@@ -873,7 +890,7 @@ func (a *Manager) AddUser(username, password string, role Role, hashed bool) err
if hashed { if hashed {
hash = []byte(password) hash = []byte(password)
} else { } else {
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
if err != nil { if err != nil {
return err return err
} }
@@ -1205,7 +1222,7 @@ func (a *Manager) ChangePassword(username, password string, hashed bool) error {
if hashed { if hashed {
hash = []byte(password) hash = []byte(password)
} else { } else {
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost)
if err != nil { if err != nil {
return err return err
} }
@@ -1387,7 +1404,7 @@ func (a *Manager) RemoveReservations(username string, topics ...string) error {
// DefaultAccess returns the default read/write access if no access control entry matches // DefaultAccess returns the default read/write access if no access control entry matches
func (a *Manager) DefaultAccess() Permission { func (a *Manager) DefaultAccess() Permission {
return a.defaultAccess return a.config.DefaultAccess
} }
// AddTier creates a new tier in the database // AddTier creates a new tier in the database