Simplify tables
This commit is contained in:
		
							parent
							
								
									86b20e8ccd
								
							
						
					
					
						commit
						1287594505
					
				
					 6 changed files with 217 additions and 208 deletions
				
			
		
							
								
								
									
										13
									
								
								cmd/serve.go
									
										
									
									
									
								
							
							
						
						
									
										13
									
								
								cmd/serve.go
									
										
									
									
									
								
							|  | @ -24,6 +24,8 @@ var flagsServe = []cli.Flag{ | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}), | ||||||
| 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}), | 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}), | ||||||
|  | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), | ||||||
|  | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-permissions", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_PERMISSIONS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "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{"A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}), | ||||||
|  | @ -80,6 +82,8 @@ func execServe(c *cli.Context) error { | ||||||
| 	firebaseKeyFile := c.String("firebase-key-file") | 	firebaseKeyFile := c.String("firebase-key-file") | ||||||
| 	cacheFile := c.String("cache-file") | 	cacheFile := c.String("cache-file") | ||||||
| 	cacheDuration := c.Duration("cache-duration") | 	cacheDuration := c.Duration("cache-duration") | ||||||
|  | 	authFile := c.String("auth-file") | ||||||
|  | 	authDefaultPermissions := c.String("auth-default-permissions") | ||||||
| 	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") | ||||||
|  | @ -126,8 +130,14 @@ func execServe(c *cli.Context) error { | ||||||
| 		return errors.New("if attachment-cache-dir is set, base-url must also be set") | 		return errors.New("if attachment-cache-dir is set, base-url must also be set") | ||||||
| 	} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") { | 	} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") { | ||||||
| 		return errors.New("if set, base-url must start with http:// or https://") | 		return errors.New("if set, base-url must start with http:// or https://") | ||||||
|  | 	} else if !util.InStringList([]string{"read-write", "read-only", "deny-all"}, authDefaultPermissions) { | ||||||
|  | 		return errors.New("if set, auth-default-permissions must start set to 'read-write', 'read-only' or 'deny-all'") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Default auth permissions | ||||||
|  | 	authDefaultRead := authDefaultPermissions == "read-write" || authDefaultPermissions == "read-only" | ||||||
|  | 	authDefaultWrite := authDefaultPermissions == "read-write" | ||||||
|  | 
 | ||||||
| 	// Special case: Unset default | 	// Special case: Unset default | ||||||
| 	if listenHTTP == "-" { | 	if listenHTTP == "-" { | ||||||
| 		listenHTTP = "" | 		listenHTTP = "" | ||||||
|  | @ -164,6 +174,9 @@ func execServe(c *cli.Context) error { | ||||||
| 	conf.FirebaseKeyFile = firebaseKeyFile | 	conf.FirebaseKeyFile = firebaseKeyFile | ||||||
| 	conf.CacheFile = cacheFile | 	conf.CacheFile = cacheFile | ||||||
| 	conf.CacheDuration = cacheDuration | 	conf.CacheDuration = cacheDuration | ||||||
|  | 	conf.AuthFile = authFile | ||||||
|  | 	conf.AuthDefaultRead = authDefaultRead | ||||||
|  | 	conf.AuthDefaultWrite = authDefaultWrite | ||||||
| 	conf.AttachmentCacheDir = attachmentCacheDir | 	conf.AttachmentCacheDir = attachmentCacheDir | ||||||
| 	conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit | 	conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit | ||||||
| 	conf.AttachmentFileSizeLimit = attachmentFileSizeLimit | 	conf.AttachmentFileSizeLimit = attachmentFileSizeLimit | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								server/auth.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								server/auth.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | package server | ||||||
|  | 
 | ||||||
|  | // auth is a generic interface to implement password-based authentication and authorization | ||||||
|  | type auth interface { | ||||||
|  | 	Authenticate(user, pass string) (*user, error) | ||||||
|  | 	Authorize(user *user, topic string, perm int) error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type user struct { | ||||||
|  | 	Name string | ||||||
|  | 	Role string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	permRead  = 1 | ||||||
|  | 	permWrite = 2 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	roleAdmin = "admin" | ||||||
|  | 	roleUser  = "user" | ||||||
|  | 	roleNone  = "none" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var everyone = &user{ | ||||||
|  | 	Name: "", | ||||||
|  | 	Role: roleNone, | ||||||
|  | } | ||||||
|  | @ -1,197 +0,0 @@ | ||||||
| package server |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"database/sql" |  | ||||||
| 	"fmt" |  | ||||||
| 	"golang.org/x/crypto/bcrypt" |  | ||||||
| 	"log" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| /* |  | ||||||
| 
 |  | ||||||
| SELECT * FROM user; |  | ||||||
| SELECT * FROM topic; |  | ||||||
| SELECT * FROM topic_user; |  | ||||||
| 
 |  | ||||||
| INSERT INTO user VALUES('phil','$2a$06$.4W0LI5mcxzxhpjUvpTaNeu0MhRO0T7B.CYnmAkRnlztIy7PrSODu', 'admin'); |  | ||||||
| INSERT INTO user VALUES('ben','$2a$06$skJK/AecWCUmiCjr69ke.Ow/hFA616RdvJJPxnI221zyohsRlyXL.', 'user'); |  | ||||||
| INSERT INTO user VALUES('marian','$2a$06$N/BcXR0g6XUlmWttMqciWugR6xQKm2lVj31HLid6Mc4cnzpeOMgnq', 'user'); |  | ||||||
| 
 |  | ||||||
| INSERT INTO topic_user VALUES('alerts','ben',1,1); |  | ||||||
| INSERT INTO topic_user VALUES('alerts','marian',1,0); |  | ||||||
| 
 |  | ||||||
| INSERT INTO topic VALUES('announcements',1,0); |  | ||||||
| 
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	permRead  = 1 |  | ||||||
| 	permWrite = 2 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	roleAdmin = "admin" |  | ||||||
| 	roleUser  = "user" |  | ||||||
| 	roleNone  = "none" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	createAuthTablesQueries = ` |  | ||||||
| 		BEGIN; |  | ||||||
| 		CREATE TABLE IF NOT EXISTS user ( |  | ||||||
| 			user TEXT NOT NULL PRIMARY KEY, |  | ||||||
| 			pass TEXT NOT NULL, |  | ||||||
| 			role TEXT NOT NULL |  | ||||||
| 		); |  | ||||||
| 		CREATE TABLE IF NOT EXISTS topic ( |  | ||||||
| 			topic TEXT NOT NULL PRIMARY KEY, |  | ||||||
| 			anon_read INT NOT NULL, |  | ||||||
| 			anon_write INT NOT NULL			 |  | ||||||
| 		); |  | ||||||
| 		CREATE TABLE IF NOT EXISTS topic_user ( |  | ||||||
| 			topic TEXT NOT NULL, |  | ||||||
| 			user TEXT NOT NULL,		 |  | ||||||
| 			read INT NOT NULL, |  | ||||||
| 			write INT NOT NULL, |  | ||||||
| 			PRIMARY KEY (topic, user) |  | ||||||
| 		); |  | ||||||
| 		CREATE TABLE IF NOT EXISTS schema_version ( |  | ||||||
| 			id INT PRIMARY KEY, |  | ||||||
| 			version INT NOT NULL |  | ||||||
| 		); |  | ||||||
| 		COMMIT; |  | ||||||
| 	` |  | ||||||
| 	selectUserQuery           = `SELECT pass FROM user WHERE user = ?` |  | ||||||
| 	selectTopicPermsAnonQuery = `SELECT ?, anon_read, anon_write FROM topic WHERE topic = ?` |  | ||||||
| 	selectTopicPermsUserQuery = ` |  | ||||||
| 		SELECT role, IFNULL(read, 0), IFNULL(write, 0) |  | ||||||
| 		FROM user |  | ||||||
| 		LEFT JOIN topic_user ON user.user = topic_user.user AND topic_user.topic = ? |  | ||||||
| 		WHERE user.user = ? |  | ||||||
| 	` |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type auther interface { |  | ||||||
| 	Authenticate(user, pass string) error |  | ||||||
| 	Authorize(user, topic string, perm int) error |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type memAuther struct { |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *memAuther) Authenticate(user, pass string) error { |  | ||||||
| 	if user == "phil" && pass == "phil" { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	return errHTTPUnauthorized |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *memAuther) Authorize(user, topic string, perm int) error { |  | ||||||
| 	if perm == permRead { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	if user == "phil" && topic == "mytopic" { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	return errHTTPUnauthorized |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type sqliteAuther struct { |  | ||||||
| 	db           *sql.DB |  | ||||||
| 	defaultRead  bool |  | ||||||
| 	defaultWrite bool |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var _ auther = (*sqliteAuther)(nil) |  | ||||||
| 
 |  | ||||||
| func newSqliteAuther(filename string, defaultRead, defaultWrite bool) (*sqliteAuther, error) { |  | ||||||
| 	db, err := sql.Open("sqlite3", filename) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if err := setupNewAuthDB(db); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return &sqliteAuther{ |  | ||||||
| 		db:           db, |  | ||||||
| 		defaultRead:  defaultRead, |  | ||||||
| 		defaultWrite: defaultWrite, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func setupNewAuthDB(db *sql.DB) error { |  | ||||||
| 	if _, err := db.Exec(createAuthTablesQueries); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	// FIXME schema version |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (a *sqliteAuther) Authenticate(user, pass string) error { |  | ||||||
| 	rows, err := a.db.Query(selectUserQuery, user) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	defer rows.Close() |  | ||||||
| 	var hash string |  | ||||||
| 	if !rows.Next() { |  | ||||||
| 		return fmt.Errorf("user %s not found", user) |  | ||||||
| 	} |  | ||||||
| 	if err := rows.Scan(&hash); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} else if err := rows.Err(); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (a *sqliteAuther) Authorize(user, topic string, perm int) error { |  | ||||||
| 	if user == "" { |  | ||||||
| 		return a.authorizeAnon(topic, perm) |  | ||||||
| 	} |  | ||||||
| 	return a.authorizeUser(user, topic, perm) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (a *sqliteAuther) authorizeAnon(topic string, perm int) error { |  | ||||||
| 	rows, err := a.db.Query(selectTopicPermsAnonQuery, roleNone, topic) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return a.checkPerms(rows, perm) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (a *sqliteAuther) authorizeUser(user string, topic string, perm int) error { |  | ||||||
| 	rows, err := a.db.Query(selectTopicPermsUserQuery, topic, user) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return a.checkPerms(rows, perm) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (a *sqliteAuther) checkPerms(rows *sql.Rows, perm int) error { |  | ||||||
| 	defer rows.Close() |  | ||||||
| 	if !rows.Next() { |  | ||||||
| 		return a.resolvePerms(a.defaultRead, a.defaultWrite, perm) |  | ||||||
| 	} |  | ||||||
| 	var role string |  | ||||||
| 	var read, write bool |  | ||||||
| 	if err := rows.Scan(&role, &read, &write); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} else if err := rows.Err(); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	log.Printf("%#v, %#v, %#v", role, read, write) |  | ||||||
| 	if role == roleAdmin { |  | ||||||
| 		return nil // Admin can do everything |  | ||||||
| 	} |  | ||||||
| 	return a.resolvePerms(read, write, perm) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (a *sqliteAuther) resolvePerms(read, write bool, perm int) error { |  | ||||||
| 	if perm == permRead && read { |  | ||||||
| 		return nil |  | ||||||
| 	} else if perm == permWrite && write { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	return errHTTPUnauthorized |  | ||||||
| } |  | ||||||
							
								
								
									
										157
									
								
								server/auth_sqlite.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								server/auth_sqlite.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,157 @@ | ||||||
|  | package server | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"database/sql" | ||||||
|  | 	"golang.org/x/crypto/bcrypt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 
 | ||||||
|  | SELECT * FROM user; | ||||||
|  | SELECT * FROM user_topic; | ||||||
|  | 
 | ||||||
|  | INSERT INTO user VALUES ('phil','$2a$06$.4W0LI5mcxzxhpjUvpTaNeu0MhRO0T7B.CYnmAkRnlztIy7PrSODu', 'admin'); | ||||||
|  | INSERT INTO user VALUES ('ben','$2a$06$skJK/AecWCUmiCjr69ke.Ow/hFA616RdvJJPxnI221zyohsRlyXL.', 'user'); | ||||||
|  | INSERT INTO user VALUES ('marian','$2a$06$N/BcXR0g6XUlmWttMqciWugR6xQKm2lVj31HLid6Mc4cnzpeOMgnq', 'user'); | ||||||
|  | 
 | ||||||
|  | INSERT INTO user_topic VALUES ('ben','alerts',1,1); | ||||||
|  | INSERT INTO user_topic VALUES ('marian','alerts',1,0); | ||||||
|  | INSERT INTO user_topic VALUES ('','announcements',1,0); | ||||||
|  | INSERT INTO user_topic VALUES ('','write-all',1,1); | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | dabbling for CLI | ||||||
|  | 	ntfy user add phil --role=admin | ||||||
|  | 	ntfy user del phil | ||||||
|  | 	ntfy user change-pass phil | ||||||
|  | 	ntfy user allow phil mytopic | ||||||
|  | 	ntfy user allow phil mytopic --read-only | ||||||
|  | 	ntfy user deny phil mytopic | ||||||
|  | 	ntfy user list | ||||||
|  | 	   phil (admin) | ||||||
|  | 	   - read-write access to everything | ||||||
|  | 	   ben (user) | ||||||
|  | 	   - read-write access to a topic alerts | ||||||
|  | 	   - read access to | ||||||
|  |        everyone (no user) | ||||||
|  |        - read-only access to topic announcements | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	createAuthTablesQueries = ` | ||||||
|  | 		BEGIN; | ||||||
|  | 		CREATE TABLE IF NOT EXISTS user ( | ||||||
|  | 			user TEXT NOT NULL PRIMARY KEY, | ||||||
|  | 			pass TEXT NOT NULL, | ||||||
|  | 			role TEXT NOT NULL | ||||||
|  | 		); | ||||||
|  | 		CREATE TABLE IF NOT EXISTS user_topic ( | ||||||
|  | 			user TEXT NOT NULL,		 | ||||||
|  | 			topic TEXT NOT NULL, | ||||||
|  | 			read INT NOT NULL, | ||||||
|  | 			write INT NOT NULL, | ||||||
|  | 			PRIMARY KEY (topic, user) | ||||||
|  | 		); | ||||||
|  | 		CREATE TABLE IF NOT EXISTS schema_version ( | ||||||
|  | 			id INT PRIMARY KEY, | ||||||
|  | 			version INT NOT NULL | ||||||
|  | 		); | ||||||
|  | 		COMMIT; | ||||||
|  | 	` | ||||||
|  | 	selectUserQuery       = `SELECT pass, role FROM user WHERE user = ?` | ||||||
|  | 	selectTopicPermsQuery = ` | ||||||
|  | 		SELECT read, write  | ||||||
|  | 		FROM user_topic  | ||||||
|  | 		WHERE user IN ('', ?) AND topic = ? | ||||||
|  | 		ORDER BY user DESC | ||||||
|  | 	` | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type sqliteAuth struct { | ||||||
|  | 	db           *sql.DB | ||||||
|  | 	defaultRead  bool | ||||||
|  | 	defaultWrite bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ auth = (*sqliteAuth)(nil) | ||||||
|  | 
 | ||||||
|  | func newSqliteAuth(filename string, defaultRead, defaultWrite bool) (*sqliteAuth, error) { | ||||||
|  | 	db, err := sql.Open("sqlite3", filename) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if err := setupNewAuthDB(db); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return &sqliteAuth{ | ||||||
|  | 		db:           db, | ||||||
|  | 		defaultRead:  defaultRead, | ||||||
|  | 		defaultWrite: defaultWrite, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func setupNewAuthDB(db *sql.DB) error { | ||||||
|  | 	if _, err := db.Exec(createAuthTablesQueries); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	// FIXME schema version | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *sqliteAuth) Authenticate(username, password string) (*user, error) { | ||||||
|  | 	rows, err := a.db.Query(selectUserQuery, username) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer rows.Close() | ||||||
|  | 	var hash, role string | ||||||
|  | 	if rows.Next() { | ||||||
|  | 		if err := rows.Scan(&hash, &role); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} else if err := rows.Err(); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return &user{ | ||||||
|  | 		Name: username, | ||||||
|  | 		Role: role, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *sqliteAuth) Authorize(user *user, topic string, perm int) error { | ||||||
|  | 	if user.Role == roleAdmin { | ||||||
|  | 		return nil // Admin can do everything | ||||||
|  | 	} | ||||||
|  | 	// Select the read/write permissions for this user/topic combo. The query may return two | ||||||
|  | 	// rows (one for everyone, and one for the user), but prioritizes the user. The value for | ||||||
|  | 	// user.Name may be empty (= everyone). | ||||||
|  | 	rows, err := a.db.Query(selectTopicPermsQuery, user.Name, topic) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer rows.Close() | ||||||
|  | 	if !rows.Next() { | ||||||
|  | 		return a.resolvePerms(a.defaultRead, a.defaultWrite, perm) | ||||||
|  | 	} | ||||||
|  | 	var read, write bool | ||||||
|  | 	if err := rows.Scan(&read, &write); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} else if err := rows.Err(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return a.resolvePerms(read, write, perm) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a *sqliteAuth) resolvePerms(read, write bool, perm int) error { | ||||||
|  | 	if perm == permRead && read { | ||||||
|  | 		return nil | ||||||
|  | 	} else if perm == permWrite && write { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return errHTTPUnauthorized | ||||||
|  | } | ||||||
|  | @ -55,6 +55,9 @@ type Config struct { | ||||||
| 	FirebaseKeyFile                      string | 	FirebaseKeyFile                      string | ||||||
| 	CacheFile                            string | 	CacheFile                            string | ||||||
| 	CacheDuration                        time.Duration | 	CacheDuration                        time.Duration | ||||||
|  | 	AuthFile                             string | ||||||
|  | 	AuthDefaultRead                      bool | ||||||
|  | 	AuthDefaultWrite                     bool | ||||||
| 	AttachmentCacheDir                   string | 	AttachmentCacheDir                   string | ||||||
| 	AttachmentTotalSizeLimit             int64 | 	AttachmentTotalSizeLimit             int64 | ||||||
| 	AttachmentFileSizeLimit              int64 | 	AttachmentFileSizeLimit              int64 | ||||||
|  | @ -97,6 +100,9 @@ func NewConfig() *Config { | ||||||
| 		FirebaseKeyFile:                      "", | 		FirebaseKeyFile:                      "", | ||||||
| 		CacheFile:                            "", | 		CacheFile:                            "", | ||||||
| 		CacheDuration:                        DefaultCacheDuration, | 		CacheDuration:                        DefaultCacheDuration, | ||||||
|  | 		AuthFile:                             "", | ||||||
|  | 		AuthDefaultRead:                      true, | ||||||
|  | 		AuthDefaultWrite:                     true, | ||||||
| 		AttachmentCacheDir:                   "", | 		AttachmentCacheDir:                   "", | ||||||
| 		AttachmentTotalSizeLimit:             DefaultAttachmentTotalSizeLimit, | 		AttachmentTotalSizeLimit:             DefaultAttachmentTotalSizeLimit, | ||||||
| 		AttachmentFileSizeLimit:              DefaultAttachmentFileSizeLimit, | 		AttachmentFileSizeLimit:              DefaultAttachmentFileSizeLimit, | ||||||
|  |  | ||||||
|  | @ -46,7 +46,7 @@ type Server struct { | ||||||
| 	firebase     subscriber | 	firebase     subscriber | ||||||
| 	mailer       mailer | 	mailer       mailer | ||||||
| 	messages     int64 | 	messages     int64 | ||||||
| 	auther       auther | 	auth         auth | ||||||
| 	cache        cache | 	cache        cache | ||||||
| 	fileCache    *fileCache | 	fileCache    *fileCache | ||||||
| 	closeChan    chan bool | 	closeChan    chan bool | ||||||
|  | @ -141,10 +141,13 @@ func New(conf *Config) (*Server, error) { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	auther, err := newSqliteAuther("user.db", false, false) | 	var auth auth | ||||||
|  | 	if conf.AuthFile != "" { | ||||||
|  | 		auth, err = newSqliteAuth(conf.AuthFile, conf.AuthDefaultRead, conf.AuthDefaultWrite) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 	return &Server{ | 	return &Server{ | ||||||
| 		config:    conf, | 		config:    conf, | ||||||
| 		cache:     cache, | 		cache:     cache, | ||||||
|  | @ -152,7 +155,7 @@ func New(conf *Config) (*Server, error) { | ||||||
| 		firebase:  firebaseSubscriber, | 		firebase:  firebaseSubscriber, | ||||||
| 		mailer:    mailer, | 		mailer:    mailer, | ||||||
| 		topics:    topics, | 		topics:    topics, | ||||||
| 		auther:    auther, | 		auth:      auth, | ||||||
| 		visitors:  make(map[string]*visitor), | 		visitors:  make(map[string]*visitor), | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  | @ -1123,23 +1126,22 @@ func (s *Server) authRead(next handleFunc) handleFunc { | ||||||
| 
 | 
 | ||||||
| func (s *Server) withAuth(next handleFunc, perm int) handleFunc { | func (s *Server) withAuth(next handleFunc, perm int) handleFunc { | ||||||
| 	return func(w http.ResponseWriter, r *http.Request, v *visitor) error { | 	return func(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||||
| 		if s.auther == nil { | 		if s.auth == nil { | ||||||
| 			return next(w, r, v) | 			return next(w, r, v) | ||||||
| 		} | 		} | ||||||
| 		t, err := s.topicFromPath(r.URL.Path) | 		t, err := s.topicFromPath(r.URL.Path) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		user, pass, ok := r.BasicAuth() | 		user := everyone | ||||||
|  | 		username, password, ok := r.BasicAuth() | ||||||
| 		if ok { | 		if ok { | ||||||
| 			if err := s.auther.Authenticate(user, pass); err != nil { | 			if user, err = s.auth.Authenticate(username, password); err != nil { | ||||||
| 				log.Printf("authentication failed: %s", err.Error()) | 				log.Printf("authentication failed: %s", err.Error()) | ||||||
| 				return errHTTPUnauthorized | 				return errHTTPUnauthorized | ||||||
| 			} | 			} | ||||||
| 		} else { |  | ||||||
| 			user = "" // Just in case |  | ||||||
| 		} | 		} | ||||||
| 		if err := s.auther.Authorize(user, t.ID, perm); err != nil { | 		if err := s.auth.Authorize(user, t.ID, perm); err != nil { | ||||||
| 			log.Printf("unauthorized: %s", err.Error()) | 			log.Printf("unauthorized: %s", err.Error()) | ||||||
| 			return errHTTPUnauthorized | 			return errHTTPUnauthorized | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue