diff --git a/cmd/serve.go b/cmd/serve.go index ef4d98d5..516356c5 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -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-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.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-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)"}), @@ -157,6 +158,7 @@ func execServe(c *cli.Context) error { authFile := c.String("auth-file") authStartupQueries := c.String("auth-startup-queries") authDefaultAccess := c.String("auth-default-access") + authUsers := c.StringSlice("auth-users") attachmentCacheDir := c.String("attachment-cache-dir") attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit") attachmentFileSizeLimitStr := c.String("attachment-file-size-limit") @@ -406,6 +408,7 @@ func execServe(c *cli.Context) error { conf.AuthFile = authFile conf.AuthStartupQueries = authStartupQueries conf.AuthDefault = authDefault + conf.AuthUsers = nil // FIXME conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit diff --git a/cmd/user.go b/cmd/user.go index e6867b11..9902dace 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -94,7 +94,6 @@ Example: 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. - `, }, { diff --git a/server/config.go b/server/config.go index 59b11c16..67554021 100644 --- a/server/config.go +++ b/server/config.go @@ -93,6 +93,7 @@ type Config struct { AuthFile string AuthStartupQueries string AuthDefault user.Permission + AuthUsers []user.User AuthBcryptCost int AuthStatsQueueWriterInterval time.Duration AttachmentCacheDir string diff --git a/server/server.go b/server/server.go index bfa7eb6b..10ad7d8e 100644 --- a/server/server.go +++ b/server/server.go @@ -189,7 +189,14 @@ func New(conf *Config) (*Server, error) { } var userManager *user.Manager 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 { return nil, err } diff --git a/user/manager.go b/user/manager.go index 814ee827..04c3c878 100644 --- a/user/manager.go +++ b/user/manager.go @@ -441,36 +441,53 @@ var ( // Manager is an implementation of Manager. It stores users and access control list // in a SQLite database. type Manager struct { - db *sql.DB - defaultAccess Permission // Default permission if no ACL matches - 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) - bcryptCost int // Makes testing easier - mu sync.Mutex + config *Config + db *sql.DB + 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) + 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) // NewManager creates a new Manager instance -func NewManager(filename, startupQueries string, defaultAccess Permission, bcryptCost int, queueWriterInterval time.Duration) (*Manager, error) { - db, err := sql.Open("sqlite3", filename) +func NewManager(config *Config) (*Manager, error) { + // 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 { return nil, err } if err := setupDB(db); err != nil { return nil, err } - if err := runStartupQueries(db, startupQueries); err != nil { + if err := runStartupQueries(db, config.StartupQueries); err != nil { return nil, err } manager := &Manager{ - db: db, - defaultAccess: defaultAccess, - statsQueue: make(map[string]*Stats), - tokenQueue: make(map[string]*TokenUpdate), - bcryptCost: bcryptCost, + db: db, + config: config, + statsQueue: make(map[string]*Stats), + tokenQueue: make(map[string]*TokenUpdate), } - go manager.asyncQueueWriter(queueWriterInterval) + go manager.asyncQueueWriter(config.QueueWriterInterval) return manager, nil } @@ -843,7 +860,7 @@ func (a *Manager) Authorize(user *User, topic string, perm Permission) error { } defer rows.Close() if !rows.Next() { - return a.resolvePerms(a.defaultAccess, perm) + return a.resolvePerms(a.config.DefaultAccess, perm) } var read, write bool 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 { hash = []byte(password) } else { - hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) + hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost) if err != nil { return err } @@ -1205,7 +1222,7 @@ func (a *Manager) ChangePassword(username, password string, hashed bool) error { if hashed { hash = []byte(password) } else { - hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost) + hash, err = bcrypt.GenerateFromPassword([]byte(password), a.config.BcryptCost) if err != nil { 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 func (a *Manager) DefaultAccess() Permission { - return a.defaultAccess + return a.config.DefaultAccess } // AddTier creates a new tier in the database