Fix almost all tests

This commit is contained in:
binwiederhier 2022-12-27 22:14:14 -05:00
parent 95a8e64fbb
commit d9722a9825
17 changed files with 197 additions and 253 deletions

View file

@ -71,7 +71,7 @@ func execUserAccess(c *cli.Context) error {
if c.NArg() > 3 { if c.NArg() > 3 {
return errors.New("too many arguments, please check 'ntfy access --help' for usage details") return errors.New("too many arguments, please check 'ntfy access --help' for usage details")
} }
manager, err := createAuthManager(c) manager, err := createUserManager(c)
if err != nil { if err != nil {
return err return err
} }
@ -96,7 +96,7 @@ func execUserAccess(c *cli.Context) error {
return changeAccess(c, manager, username, topic, perms) return changeAccess(c, manager, username, topic, perms)
} }
func changeAccess(c *cli.Context, manager user.Manager, username string, topic string, perms string) error { func changeAccess(c *cli.Context, manager *user.Manager, username string, topic string, perms string) error {
if !util.Contains([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) { if !util.Contains([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) {
return errors.New("permission must be one of: read-write, read-only, write-only, or deny (or the aliases: read, ro, write, wo, none)") return errors.New("permission must be one of: read-write, read-only, write-only, or deny (or the aliases: read, ro, write, wo, none)")
} }
@ -123,7 +123,7 @@ func changeAccess(c *cli.Context, manager user.Manager, username string, topic s
return showUserAccess(c, manager, username) return showUserAccess(c, manager, username)
} }
func resetAccess(c *cli.Context, manager user.Manager, username, topic string) error { func resetAccess(c *cli.Context, manager *user.Manager, username, topic string) error {
if username == "" { if username == "" {
return resetAllAccess(c, manager) return resetAllAccess(c, manager)
} else if topic == "" { } else if topic == "" {
@ -132,7 +132,7 @@ func resetAccess(c *cli.Context, manager user.Manager, username, topic string) e
return resetUserTopicAccess(c, manager, username, topic) return resetUserTopicAccess(c, manager, username, topic)
} }
func resetAllAccess(c *cli.Context, manager user.Manager) error { func resetAllAccess(c *cli.Context, manager *user.Manager) error {
if err := manager.ResetAccess("", ""); err != nil { if err := manager.ResetAccess("", ""); err != nil {
return err return err
} }
@ -140,7 +140,7 @@ func resetAllAccess(c *cli.Context, manager user.Manager) error {
return nil return nil
} }
func resetUserAccess(c *cli.Context, manager user.Manager, username string) error { func resetUserAccess(c *cli.Context, manager *user.Manager, username string) error {
if err := manager.ResetAccess(username, ""); err != nil { if err := manager.ResetAccess(username, ""); err != nil {
return err return err
} }
@ -148,7 +148,7 @@ func resetUserAccess(c *cli.Context, manager user.Manager, username string) erro
return showUserAccess(c, manager, username) return showUserAccess(c, manager, username)
} }
func resetUserTopicAccess(c *cli.Context, manager user.Manager, username string, topic string) error { func resetUserTopicAccess(c *cli.Context, manager *user.Manager, username string, topic string) error {
if err := manager.ResetAccess(username, topic); err != nil { if err := manager.ResetAccess(username, topic); err != nil {
return err return err
} }
@ -156,14 +156,14 @@ func resetUserTopicAccess(c *cli.Context, manager user.Manager, username string,
return showUserAccess(c, manager, username) return showUserAccess(c, manager, username)
} }
func showAccess(c *cli.Context, manager user.Manager, username string) error { func showAccess(c *cli.Context, manager *user.Manager, username string) error {
if username == "" { if username == "" {
return showAllAccess(c, manager) return showAllAccess(c, manager)
} }
return showUserAccess(c, manager, username) return showUserAccess(c, manager, username)
} }
func showAllAccess(c *cli.Context, manager user.Manager) error { func showAllAccess(c *cli.Context, manager *user.Manager) error {
users, err := manager.Users() users, err := manager.Users()
if err != nil { if err != nil {
return err return err
@ -171,7 +171,7 @@ func showAllAccess(c *cli.Context, manager user.Manager) error {
return showUsers(c, manager, users) return showUsers(c, manager, users)
} }
func showUserAccess(c *cli.Context, manager user.Manager, username string) error { func showUserAccess(c *cli.Context, manager *user.Manager, username string) error {
users, err := manager.User(username) users, err := manager.User(username)
if err == user.ErrNotFound { if err == user.ErrNotFound {
return fmt.Errorf("user %s does not exist", username) return fmt.Errorf("user %s does not exist", username)
@ -181,7 +181,7 @@ func showUserAccess(c *cli.Context, manager user.Manager, username string) error
return showUsers(c, manager, []*user.User{users}) return showUsers(c, manager, []*user.User{users})
} }
func showUsers(c *cli.Context, manager user.Manager, users []*user.User) error { func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error {
for _, u := range users { for _, u := range users {
fmt.Fprintf(c.App.ErrWriter, "user %s (%s)\n", u.Name, u.Role) fmt.Fprintf(c.App.ErrWriter, "user %s (%s)\n", u.Name, u.Role)
if u.Role == user.RoleAdmin { if u.Role == user.RoleAdmin {

View file

@ -1,5 +1,4 @@
//go:build linux || dragonfly || freebsd || netbsd || openbsd //go:build linux || dragonfly || freebsd || netbsd || openbsd
// +build linux dragonfly freebsd netbsd openbsd
package cmd package cmd

View file

@ -161,7 +161,7 @@ func execUserAdd(c *cli.Context) error {
} else if !user.AllowedRole(role) { } else if !user.AllowedRole(role) {
return errors.New("role must be either 'user' or 'admin'") return errors.New("role must be either 'user' or 'admin'")
} }
manager, err := createAuthManager(c) manager, err := createUserManager(c)
if err != nil { if err != nil {
return err return err
} }
@ -190,7 +190,7 @@ func execUserDel(c *cli.Context) error {
} else if username == userEveryone { } else if username == userEveryone {
return errors.New("username not allowed") return errors.New("username not allowed")
} }
manager, err := createAuthManager(c) manager, err := createUserManager(c)
if err != nil { if err != nil {
return err return err
} }
@ -212,7 +212,7 @@ func execUserChangePass(c *cli.Context) error {
} else if username == userEveryone { } else if username == userEveryone {
return errors.New("username not allowed") return errors.New("username not allowed")
} }
manager, err := createAuthManager(c) manager, err := createUserManager(c)
if err != nil { if err != nil {
return err return err
} }
@ -240,7 +240,7 @@ func execUserChangeRole(c *cli.Context) error {
} else if username == userEveryone { } else if username == userEveryone {
return errors.New("username not allowed") return errors.New("username not allowed")
} }
manager, err := createAuthManager(c) manager, err := createUserManager(c)
if err != nil { if err != nil {
return err return err
} }
@ -255,7 +255,7 @@ func execUserChangeRole(c *cli.Context) error {
} }
func execUserList(c *cli.Context) error { func execUserList(c *cli.Context) error {
manager, err := createAuthManager(c) manager, err := createUserManager(c)
if err != nil { if err != nil {
return err return err
} }
@ -266,7 +266,7 @@ func execUserList(c *cli.Context) error {
return showUsers(c, manager, users) return showUsers(c, manager, users)
} }
func createAuthManager(c *cli.Context) (user.Manager, error) { func createUserManager(c *cli.Context) (*user.Manager, error) {
authFile := c.String("auth-file") authFile := c.String("auth-file")
authDefaultAccess := c.String("auth-default-access") authDefaultAccess := c.String("auth-default-access")
if authFile == "" { if authFile == "" {
@ -278,7 +278,7 @@ func createAuthManager(c *cli.Context) (user.Manager, error) {
} }
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only" authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only" authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
return user.NewSQLiteAuthManager(authFile, authDefaultRead, authDefaultWrite) return user.NewManager(authFile, authDefaultRead, authDefaultWrite)
} }
func readPasswordAndConfirm(c *cli.Context) (string, error) { func readPasswordAndConfirm(c *cli.Context) (string, error) {

29
go.sum
View file

@ -1,18 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww=
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU=
cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0= cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k=
cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
cloud.google.com/go/iam v0.9.0 h1:bK6Or6mxhuL8lnj1i9j0yMo2wE/IeTO2cWlfUrf/TZs= cloud.google.com/go/iam v0.9.0 h1:bK6Or6mxhuL8lnj1i9j0yMo2wE/IeTO2cWlfUrf/TZs=
cloud.google.com/go/iam v0.9.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM= cloud.google.com/go/iam v0.9.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
@ -21,14 +15,11 @@ cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcb
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4= firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A= firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A=
github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/MicahParks/keyfunc v1.7.0 h1:LBd4tBj6FwGs2S4GXniQbgrG0PXzIldyGDKWch8slhg=
github.com/MicahParks/keyfunc v1.7.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -84,8 +75,6 @@ github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1V
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg= github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg=
github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
@ -94,11 +83,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI= github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI=
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -109,13 +95,10 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli/v2 v2.23.6 h1:iWmtKD+prGo1nKUtLO0Wg4z9esfBM4rAV4QRLQiEmJ4=
github.com/urfave/cli/v2 v2.23.6/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY= github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
@ -124,8 +107,6 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -141,13 +122,9 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -164,8 +141,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -184,8 +159,6 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8= google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8=
google.golang.org/api v0.105.0/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI= google.golang.org/api v0.105.0/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@ -197,8 +170,6 @@ google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4Ho
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd h1:OjndDrsik+Gt+e6fs45z9AxiewiKyLKYpA45W5Kpkks=
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70= google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=

View file

@ -56,13 +56,6 @@ func TestFileCache_Write_FailedTotalSizeLimit(t *testing.T) {
require.NoFileExists(t, dir+"/abcdefghijkX") require.NoFileExists(t, dir+"/abcdefghijkX")
} }
func TestFileCache_Write_FailedFileSizeLimit(t *testing.T) {
dir, c := newTestFileCache(t)
_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1025)))
require.Equal(t, util.ErrLimitReached, err)
require.NoFileExists(t, dir+"/abcdefghijkl")
}
func TestFileCache_Write_FailedAdditionalLimiter(t *testing.T) { func TestFileCache_Write_FailedAdditionalLimiter(t *testing.T) {
dir, c := newTestFileCache(t) dir, c := newTestFileCache(t)
_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1001)), util.NewFixedLimiter(1000)) _, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1001)), util.NewFixedLimiter(1000))
@ -95,7 +88,7 @@ func TestFileCache_RemoveExpired(t *testing.T) {
func newTestFileCache(t *testing.T) (dir string, cache *fileCache) { func newTestFileCache(t *testing.T) (dir string, cache *fileCache) {
dir = t.TempDir() dir = t.TempDir()
cache, err := newFileCache(dir, 10*1024, 1*1024) cache, err := newFileCache(dir, 10*1024)
require.Nil(t, err) require.Nil(t, err)
return dir, cache return dir, cache
} }

View file

@ -40,7 +40,7 @@ const (
attachment_expires INT NOT NULL, attachment_expires INT NOT NULL,
attachment_url TEXT NOT NULL, attachment_url TEXT NOT NULL,
sender TEXT NOT NULL, sender TEXT NOT NULL,
user TEXT NOT NULL, user TEXT NOT NULL,
encoding TEXT NOT NULL, encoding TEXT NOT NULL,
published INT NOT NULL published INT NOT NULL
); );
@ -95,7 +95,7 @@ const (
// Schema management queries // Schema management queries
const ( const (
currentSchemaVersion = 9 currentSchemaVersion = 10
createSchemaVersionTableQuery = ` createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion ( CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY, id INT PRIMARY KEY,
@ -193,6 +193,11 @@ const (
migrate8To9AlterMessagesTableQuery = ` migrate8To9AlterMessagesTableQuery = `
CREATE INDEX IF NOT EXISTS idx_time ON messages (time); CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
` `
// 9 -> 10
migrate9To10AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN user TEXT NOT NULL DEFAULT('');
`
) )
type messageCache struct { type messageCache struct {
@ -614,8 +619,9 @@ func setupCacheDB(db *sql.DB, startupQueries string) error {
return migrateFrom7(db) return migrateFrom7(db)
} else if schemaVersion == 8 { } else if schemaVersion == 8 {
return migrateFrom8(db) return migrateFrom8(db)
} else if schemaVersion == 9 {
return migrateFrom9(db)
} }
// TODO add user column
return fmt.Errorf("unexpected schema version found: %d", schemaVersion) return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
} }
@ -731,5 +737,16 @@ func migrateFrom8(db *sql.DB) error {
if _, err := db.Exec(updateSchemaVersion, 9); err != nil { if _, err := db.Exec(updateSchemaVersion, 9); err != nil {
return err return err
} }
return migrateFrom9(db)
}
func migrateFrom9(db *sql.DB) error {
log.Info("Migrating cache database schema: from 9 to 10")
if _, err := db.Exec(migrate9To10AlterMessagesTableQuery); err != nil {
return err
}
if _, err := db.Exec(updateSchemaVersion, 10); err != nil {
return err
}
return nil // Update this when a new version is added return nil // Update this when a new version is added
} }

View file

@ -43,12 +43,15 @@ import (
"user list" shows * twice "user list" shows * twice
"ntfy access everyone user4topic <bla>" twice -> UNIQUE constraint error "ntfy access everyone user4topic <bla>" twice -> UNIQUE constraint error
Account usage not updated "in real time" Account usage not updated "in real time"
Attachment expiration based on plan
Plan: Keep 10000 messages or keep X days?
Sync: Sync:
- "mute" setting - "mute" setting
- figure out what settings are "web" or "phone" - figure out what settings are "web" or "phone"
UI: UI:
- Subscription dotmenu dropdown: Move to nav bar, or make same as profile dropdown - Subscription dotmenu dropdown: Move to nav bar, or make same as profile dropdown
- "Logout and delete local storage" option - "Logout and delete local storage" option
- Delete local storage when deleting account
Pages: Pages:
- Home - Home
- Password reset - Password reset
@ -61,7 +64,8 @@ import (
- APIs - APIs
- CRUD tokens - CRUD tokens
- Expire tokens - Expire tokens
- - userManager can be nil
- visitor with/without user
*/ */
// Server is the main server, providing the UI and API for ntfy // Server is the main server, providing the UI and API for ntfy
@ -77,7 +81,7 @@ type Server struct {
visitors map[string]*visitor // ip:<ip> or user:<user> visitors map[string]*visitor // ip:<ip> or user:<user>
firebaseClient *firebaseClient firebaseClient *firebaseClient
messages int64 messages int64
userManager user.Manager userManager *user.Manager // Might be nil!
messageCache *messageCache messageCache *messageCache
fileCache *fileCache fileCache *fileCache
closeChan chan bool closeChan chan bool
@ -165,9 +169,9 @@ func New(conf *Config) (*Server, error) {
return nil, err return nil, err
} }
} }
var auther user.Manager var userManager *user.Manager
if conf.AuthFile != "" { if conf.AuthFile != "" {
auther, err = user.NewSQLiteAuthManager(conf.AuthFile, conf.AuthDefaultRead, conf.AuthDefaultWrite) userManager, err = user.NewManager(conf.AuthFile, conf.AuthDefaultRead, conf.AuthDefaultWrite)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -178,7 +182,7 @@ func New(conf *Config) (*Server, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
firebaseClient = newFirebaseClient(sender, auther) firebaseClient = newFirebaseClient(sender, userManager)
} }
return &Server{ return &Server{
config: conf, config: conf,
@ -187,7 +191,7 @@ func New(conf *Config) (*Server, error) {
firebaseClient: firebaseClient, firebaseClient: firebaseClient,
smtpSender: mailer, smtpSender: mailer,
topics: topics, topics: topics,
userManager: auther, userManager: userManager,
visitors: make(map[string]*visitor), visitors: make(map[string]*visitor),
}, nil }, nil
} }
@ -341,27 +345,27 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath { } else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
return s.ensureWebEnabled(s.handleWebConfig)(w, r, v) return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == accountPath { } else if r.Method == http.MethodPost && r.URL.Path == accountPath {
return s.handleAccountCreate(w, r, v) return s.ensureAccountsEnabled(s.handleAccountCreate)(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == accountPath { } else if r.Method == http.MethodGet && r.URL.Path == accountPath {
return s.handleAccountGet(w, r, v) return s.handleAccountGet(w, r, v) // Allowed by anonymous
} else if r.Method == http.MethodDelete && r.URL.Path == accountPath { } else if r.Method == http.MethodDelete && r.URL.Path == accountPath {
return s.handleAccountDelete(w, r, v) return s.ensureWithAccount(s.handleAccountDelete)(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == accountPasswordPath { } else if r.Method == http.MethodPost && r.URL.Path == accountPasswordPath {
return s.handleAccountPasswordChange(w, r, v) return s.ensureWithAccount(s.handleAccountPasswordChange)(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == accountTokenPath { } else if r.Method == http.MethodPost && r.URL.Path == accountTokenPath {
return s.handleAccountTokenIssue(w, r, v) return s.ensureWithAccount(s.handleAccountTokenIssue)(w, r, v)
} else if r.Method == http.MethodPatch && r.URL.Path == accountTokenPath { } else if r.Method == http.MethodPatch && r.URL.Path == accountTokenPath {
return s.handleAccountTokenExtend(w, r, v) return s.ensureWithAccount(s.handleAccountTokenExtend)(w, r, v)
} else if r.Method == http.MethodDelete && r.URL.Path == accountTokenPath { } else if r.Method == http.MethodDelete && r.URL.Path == accountTokenPath {
return s.handleAccountTokenDelete(w, r, v) return s.ensureWithAccount(s.handleAccountTokenDelete)(w, r, v)
} else if r.Method == http.MethodPatch && r.URL.Path == accountSettingsPath { } else if r.Method == http.MethodPatch && r.URL.Path == accountSettingsPath {
return s.handleAccountSettingsChange(w, r, v) return s.ensureWithAccount(s.handleAccountSettingsChange)(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == accountSubscriptionPath { } else if r.Method == http.MethodPost && r.URL.Path == accountSubscriptionPath {
return s.handleAccountSubscriptionAdd(w, r, v) return s.ensureWithAccount(s.handleAccountSubscriptionAdd)(w, r, v)
} else if r.Method == http.MethodPatch && accountSubscriptionSingleRegex.MatchString(r.URL.Path) { } else if r.Method == http.MethodPatch && accountSubscriptionSingleRegex.MatchString(r.URL.Path) {
return s.handleAccountSubscriptionChange(w, r, v) return s.ensureWithAccount(s.handleAccountSubscriptionChange)(w, r, v)
} else if r.Method == http.MethodDelete && accountSubscriptionSingleRegex.MatchString(r.URL.Path) { } else if r.Method == http.MethodDelete && accountSubscriptionSingleRegex.MatchString(r.URL.Path) {
return s.handleAccountSubscriptionDelete(w, r, v) return s.ensureWithAccount(s.handleAccountSubscriptionDelete)(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath { } else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
return s.handleMatrixDiscovery(w) return s.handleMatrixDiscovery(w)
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { } else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
@ -804,7 +808,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
} else if m.Time > time.Now().Add(s.config.AttachmentExpiryDuration).Unix() { } else if m.Time > time.Now().Add(s.config.AttachmentExpiryDuration).Unix() {
return errHTTPBadRequestAttachmentsExpiryBeforeDelivery return errHTTPBadRequestAttachmentsExpiryBeforeDelivery
} }
stats, err := v.Stats() stats, err := v.Info()
if err != nil { if err != nil {
return err return err
} }
@ -1182,7 +1186,7 @@ func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
return topics, nil return topics, nil
} }
func (s *Server) updateStatsAndPrune() { func (s *Server) execManager() {
log.Debug("Manager: Starting") log.Debug("Manager: Starting")
defer log.Debug("Manager: Finished") defer log.Debug("Manager: Finished")
@ -1203,8 +1207,10 @@ func (s *Server) updateStatsAndPrune() {
log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors) log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors)
// Delete expired user tokens // Delete expired user tokens
if err := s.userManager.RemoveExpiredTokens(); err != nil { if s.userManager != nil {
log.Warn("Error expiring user tokens: %s", err.Error()) if err := s.userManager.RemoveExpiredTokens(); err != nil {
log.Warn("Error expiring user tokens: %s", err.Error())
}
} }
// Delete expired attachments // Delete expired attachments
@ -1293,7 +1299,7 @@ func (s *Server) runManager() {
for { for {
select { select {
case <-time.After(s.config.ManagerInterval): case <-time.After(s.config.ManagerInterval):
s.updateStatsAndPrune() s.execManager()
case <-s.closeChan: case <-s.closeChan:
return return
} }
@ -1399,6 +1405,24 @@ func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
} }
} }
func (s *Server) ensureAccountsEnabled(next handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
if s.userManager != nil {
return errHTTPNotFound
}
return next(w, r, v)
}
}
func (s *Server) ensureWithAccount(next handleFunc) handleFunc {
return s.ensureAccountsEnabled(func(w http.ResponseWriter, r *http.Request, v *visitor) error {
if v.user != nil {
return errHTTPNotFound
}
return next(w, r, v)
})
}
// transformBodyJSON peeks the request body, reads the JSON, and converts it to headers // transformBodyJSON peeks the request body, reads the JSON, and converts it to headers
// before passing it on to the next handler. This is meant to be used in combination with handlePublish. // before passing it on to the next handler. This is meant to be used in combination with handlePublish.
func (s *Server) transformBodyJSON(next handleFunc) handleFunc { func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
@ -1502,17 +1526,17 @@ func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc
// Note that this function will always return a visitor, even if an error occurs. // Note that this function will always return a visitor, even if an error occurs.
func (s *Server) visitor(r *http.Request) (v *visitor, err error) { func (s *Server) visitor(r *http.Request) (v *visitor, err error) {
ip := extractIPAddress(r, s.config.BehindProxy) ip := extractIPAddress(r, s.config.BehindProxy)
var user *user.User // may stay nil if no auth header! var u *user.User // may stay nil if no auth header!
if user, err = s.authenticate(r); err != nil { if u, err = s.authenticate(r); err != nil {
log.Debug("authentication failed: %s", err.Error()) log.Debug("authentication failed: %s", err.Error())
err = errHTTPUnauthorized // Always return visitor, even when error occurs! err = errHTTPUnauthorized // Always return visitor, even when error occurs!
} }
if user != nil { if u != nil {
v = s.visitorFromUser(user, ip) v = s.visitorFromUser(u, ip)
} else { } else {
v = s.visitorFromIP(ip) v = s.visitorFromIP(ip)
} }
v.user = user // Update user -- FIXME race? v.user = u // Update user -- FIXME race?
return v, err // Always return visitor, even when error occurs! return v, err // Always return visitor, even when error occurs!
} }
@ -1521,17 +1545,19 @@ func (s *Server) visitor(r *http.Request) (v *visitor, err error) {
// support the WebSocket JavaScript class, which does not support passing headers during the initial request. The auth // support the WebSocket JavaScript class, which does not support passing headers during the initial request. The auth
// query param is effectively double base64 encoded. Its format is base64(Basic base64(user:pass)). // query param is effectively double base64 encoded. Its format is base64(Basic base64(user:pass)).
func (s *Server) authenticate(r *http.Request) (user *user.User, err error) { func (s *Server) authenticate(r *http.Request) (user *user.User, err error) {
value := r.Header.Get("Authorization") value := strings.TrimSpace(r.Header.Get("Authorization"))
queryParam := readQueryParam(r, "authorization", "auth") queryParam := readQueryParam(r, "authorization", "auth")
if queryParam != "" { if queryParam != "" {
a, err := base64.RawURLEncoding.DecodeString(queryParam) a, err := base64.RawURLEncoding.DecodeString(queryParam)
if err != nil { if err != nil {
return nil, err return nil, err
} }
value = string(a) value = strings.TrimSpace(string(a))
} }
if value == "" { if value == "" {
return nil, nil return nil, nil
} else if s.userManager == nil {
return nil, errHTTPUnauthorized
} }
if strings.HasPrefix(value, "Bearer") { if strings.HasPrefix(value, "Bearer") {
return s.authenticateBearerAuth(value) return s.authenticateBearerAuth(value)

View file

@ -45,11 +45,11 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *visitor) error { func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *visitor) error {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
stats, err := v.Stats() stats, err := v.Info()
if err != nil { if err != nil {
return err return err
} }
response := &apiAccountSettingsResponse{ response := &apiAccountResponse{
Stats: &apiAccountStats{ Stats: &apiAccountStats{
Messages: stats.Messages, Messages: stats.Messages,
MessagesRemaining: stats.MessagesRemaining, MessagesRemaining: stats.MessagesRemaining,

View file

@ -28,10 +28,10 @@ var (
// The actual Firebase implementation is implemented in firebaseSenderImpl, to make it testable. // The actual Firebase implementation is implemented in firebaseSenderImpl, to make it testable.
type firebaseClient struct { type firebaseClient struct {
sender firebaseSender sender firebaseSender
auther user.Manager auther user.Auther
} }
func newFirebaseClient(sender firebaseSender, auther user.Manager) *firebaseClient { func newFirebaseClient(sender firebaseSender, auther user.Auther) *firebaseClient {
return &firebaseClient{ return &firebaseClient{
sender: sender, sender: sender,
auther: auther, auther: auther,
@ -112,7 +112,7 @@ func (c *firebaseSenderImpl) Send(m *messaging.Message) error {
// On Android, this will trigger the app to poll the topic and thereby displaying new messages. // On Android, this will trigger the app to poll the topic and thereby displaying new messages.
// - If UpstreamBaseURL is set, messages are forwarded as poll requests to an upstream server and then forwarded // - If UpstreamBaseURL is set, messages are forwarded as poll requests to an upstream server and then forwarded
// to Firebase here. This is mainly for iOS to support self-hosted servers. // to Firebase here. This is mainly for iOS to support self-hosted servers.
func toFirebaseMessage(m *message, auther user.Manager) (*messaging.Message, error) { func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, error) {
var data map[string]string // Mostly matches https://ntfy.sh/docs/subscribe/api/#json-message-format var data map[string]string // Mostly matches https://ntfy.sh/docs/subscribe/api/#json-message-format
var apnsConfig *messaging.APNSConfig var apnsConfig *messaging.APNSConfig
switch m.Event { switch m.Event {

View file

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"heckel.io/ntfy/user"
"net/netip" "net/netip"
"strings" "strings"
"sync" "sync"
@ -17,7 +18,9 @@ type testAuther struct {
Allow bool Allow bool
} }
func (t testAuther) AuthenticateUser(_, _ string) (*user.User, error) { var _ user.Auther = (*testAuther)(nil)
func (t testAuther) Authenticate(_, _ string) (*user.User, error) {
return nil, errors.New("not used") return nil, errors.New("not used")
} }
@ -323,7 +326,7 @@ func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) {
func TestToFirebaseSender_Abuse(t *testing.T) { func TestToFirebaseSender_Abuse(t *testing.T) {
sender := &testFirebaseSender{allowed: 2} sender := &testFirebaseSender{allowed: 2}
client := newFirebaseClient(sender, &testAuther{}) client := newFirebaseClient(sender, &testAuther{})
visitor := newVisitor(newTestConfig(t), newMemTestCache(t), netip.MustParseAddr("1.2.3.4")) visitor := newVisitor(newTestConfig(t), newMemTestCache(t), netip.MustParseAddr("1.2.3.4"), nil)
require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"})) require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
require.Equal(t, 1, len(sender.Messages())) require.Equal(t, 1, len(sender.Messages()))

View file

@ -72,7 +72,7 @@ func TestMatrix_WriteMatrixDiscoveryResponse(t *testing.T) {
func TestMatrix_WriteMatrixError(t *testing.T) { func TestMatrix_WriteMatrixError(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", nil) r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", nil)
v := newVisitor(newTestConfig(t), nil, netip.MustParseAddr("1.2.3.4")) v := newVisitor(newTestConfig(t), nil, netip.MustParseAddr("1.2.3.4"), nil)
require.Nil(t, writeMatrixError(w, r, v, &errMatrix{"https://ntfy.example.com/upABCDEFGHI?up=1", errHTTPBadRequestMatrixPushkeyBaseURLMismatch})) require.Nil(t, writeMatrixError(w, r, v, &errMatrix{"https://ntfy.example.com/upABCDEFGHI?up=1", errHTTPBadRequestMatrixPushkeyBaseURLMismatch}))
require.Equal(t, 200, w.Result().StatusCode) require.Equal(t, 200, w.Result().StatusCode)
require.Equal(t, `{"rejected":["https://ntfy.example.com/upABCDEFGHI?up=1"]}`+"\n", w.Body.String()) require.Equal(t, `{"rejected":["https://ntfy.example.com/upABCDEFGHI?up=1"]}`+"\n", w.Body.String())

View file

@ -6,6 +6,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"heckel.io/ntfy/user"
"io" "io"
"log" "log"
"math/rand" "math/rand"
@ -171,7 +172,7 @@ func TestServer_StaticSites(t *testing.T) {
rr = request(t, s, "GET", "/static/css/home.css", "", nil) rr = request(t, s, "GET", "/static/css/home.css", "", nil)
require.Equal(t, 200, rr.Code) require.Equal(t, 200, rr.Code)
require.Contains(t, rr.Body.String(), `html, body {`) require.Contains(t, rr.Body.String(), `/* general styling */`)
rr = request(t, s, "GET", "/docs", "", nil) rr = request(t, s, "GET", "/docs", "", nil)
require.Equal(t, 301, rr.Code) require.Equal(t, 301, rr.Code)
@ -353,7 +354,7 @@ func TestServer_PublishAtAndPrune(t *testing.T) {
"In": "1h", "In": "1h",
}) })
require.Equal(t, 200, response.Code) require.Equal(t, 200, response.Code)
s.updateStatsAndPrune() // Fire pruning s.execManager() // Fire pruning
response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil)
messages := toMessages(t, response.Body.String()) messages := toMessages(t, response.Body.String())
@ -625,8 +626,7 @@ func TestServer_Auth_Success_Admin(t *testing.T) {
c.AuthFile = filepath.Join(t.TempDir(), "user.db") c.AuthFile = filepath.Join(t.TempDir(), "user.db")
s := newTestServer(t, c) s := newTestServer(t, c)
manager := s.userManager.(user.Manager) require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin))
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
"Authorization": basicAuth("phil:phil"), "Authorization": basicAuth("phil:phil"),
@ -642,9 +642,8 @@ func TestServer_Auth_Success_User(t *testing.T) {
c.AuthDefaultWrite = false c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
manager := s.userManager.(user.Manager) require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser)) require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, manager.AllowAccess("ben", "mytopic", true, true))
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
"Authorization": basicAuth("ben:ben"), "Authorization": basicAuth("ben:ben"),
@ -659,10 +658,9 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
c.AuthDefaultWrite = false c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
manager := s.userManager.(user.Manager) require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser)) require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, manager.AllowAccess("ben", "mytopic", true, true)) require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", true, true))
require.Nil(t, manager.AllowAccess("ben", "anothertopic", true, true))
response := request(t, s, "GET", "/mytopic,anothertopic/auth", "", map[string]string{ response := request(t, s, "GET", "/mytopic,anothertopic/auth", "", map[string]string{
"Authorization": basicAuth("ben:ben"), "Authorization": basicAuth("ben:ben"),
@ -682,8 +680,7 @@ func TestServer_Auth_Fail_InvalidPass(t *testing.T) {
c.AuthDefaultWrite = false c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
manager := s.userManager.(user.Manager) require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin))
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
"Authorization": basicAuth("phil:INVALID"), "Authorization": basicAuth("phil:INVALID"),
@ -698,9 +695,8 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
c.AuthDefaultWrite = false c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
manager := s.userManager.(user.Manager) require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser)) require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", true, true)) // Not mytopic!
require.Nil(t, manager.AllowAccess("ben", "sometopic", true, true)) // Not mytopic!
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
"Authorization": basicAuth("ben:ben"), "Authorization": basicAuth("ben:ben"),
@ -715,10 +711,9 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
c.AuthDefaultWrite = true // Open by default c.AuthDefaultWrite = true // Open by default
s := newTestServer(t, c) s := newTestServer(t, c)
manager := s.userManager.(user.Manager) require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin)) require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", false, false))
require.Nil(t, manager.AllowAccess(user.Everyone, "private", false, false)) require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", true, false))
require.Nil(t, manager.AllowAccess(user.Everyone, "announcements", true, false))
response := request(t, s, "PUT", "/mytopic", "test", nil) response := request(t, s, "PUT", "/mytopic", "test", nil)
require.Equal(t, 200, response.Code) require.Equal(t, 200, response.Code)
@ -748,8 +743,7 @@ func TestServer_Auth_ViaQuery(t *testing.T) {
c.AuthDefaultWrite = false c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
manager := s.userManager.(user.Manager) require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin))
require.Nil(t, manager.AddUser("ben", "some pass", user.RoleAdmin))
u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:some pass")))) u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:some pass"))))
response := request(t, s, "GET", u, "", nil) response := request(t, s, "GET", u, "", nil)
@ -760,27 +754,6 @@ func TestServer_Auth_ViaQuery(t *testing.T) {
require.Equal(t, 401, response.Code) require.Equal(t, 401, response.Code)
} }
/*
func TestServer_Curl_Publish_Poll(t *testing.T) {
s, port := test.StartServer(t)
defer test.StopServer(t, s, port)
cmd := exec.Command("sh", "-c", fmt.Sprintf(`curl -sd "This is a test" localhost:%d/mytopic`, port))
require.Nil(t, cmd.Run())
b, err := cmd.CombinedOutput()
require.Nil(t, err)
msg := toMessage(t, string(b))
require.Equal(t, "This is a test", msg.Message)
cmd = exec.Command("sh", "-c", fmt.Sprintf(`curl "localhost:%d/mytopic?poll=1"`, port))
require.Nil(t, cmd.Run())
b, err = cmd.CombinedOutput()
require.Nil(t, err)
msg = toMessage(t, string(b))
require.Equal(t, "This is a test", msg.Message)
}
*/
type testMailer struct { type testMailer struct {
count int count int
mu sync.Mutex mu sync.Mutex
@ -1306,7 +1279,7 @@ func TestServer_PublishAttachmentAndPrune(t *testing.T) {
// Prune and makes sure it's gone // Prune and makes sure it's gone
time.Sleep(time.Second) // Sigh ... time.Sleep(time.Second) // Sigh ...
s.updateStatsAndPrune() s.execManager()
require.NoFileExists(t, file) require.NoFileExists(t, file)
response = request(t, s, "GET", path, "", nil) response = request(t, s, "GET", path, "", nil)
require.Equal(t, 404, response.Code) require.Equal(t, 404, response.Code)
@ -1360,7 +1333,7 @@ func TestServer_PublishAttachmentBandwidthLimitUploadOnly(t *testing.T) {
require.Equal(t, 41301, err.Code) require.Equal(t, 41301, err.Code)
} }
func TestServer_PublishAttachmentUserStats(t *testing.T) { func TestServer_PublishAttachmentAccountStats(t *testing.T) {
content := util.RandomString(4999) // > 4096 content := util.RandomString(4999) // > 4096
c := newTestConfig(t) c := newTestConfig(t)
@ -1374,14 +1347,14 @@ func TestServer_PublishAttachmentUserStats(t *testing.T) {
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
// User stats // User stats
response = request(t, s, "GET", "/user/stats", "", nil) response = request(t, s, "GET", "/v1/account", "", nil)
require.Equal(t, 200, response.Code) require.Equal(t, 200, response.Code)
var stats visitorStats var account *apiAccountResponse
require.Nil(t, json.NewDecoder(strings.NewReader(response.Body.String())).Decode(&stats)) require.Nil(t, json.NewDecoder(strings.NewReader(response.Body.String())).Decode(&account))
require.Equal(t, int64(5000), stats.AttachmentFileSizeLimit) require.Equal(t, int64(5000), account.Limits.AttachmentFileSize)
require.Equal(t, int64(6000), stats.VisitorAttachmentBytesTotal) require.Equal(t, int64(6000), account.Limits.AttachmentTotalSize)
require.Equal(t, int64(4999), stats.AttachmentBytes) require.Equal(t, int64(4999), account.Stats.AttachmentTotalSize)
require.Equal(t, int64(1001), stats.VisitorAttachmentBytesRemaining) require.Equal(t, int64(1001), account.Stats.AttachmentTotalSizeRemaining)
} }
func TestServer_Visitor_XForwardedFor_None(t *testing.T) { func TestServer_Visitor_XForwardedFor_None(t *testing.T) {
@ -1391,7 +1364,8 @@ func TestServer_Visitor_XForwardedFor_None(t *testing.T) {
r, _ := http.NewRequest("GET", "/bla", nil) r, _ := http.NewRequest("GET", "/bla", nil)
r.RemoteAddr = "8.9.10.11" r.RemoteAddr = "8.9.10.11"
r.Header.Set("X-Forwarded-For", " ") // Spaces, not empty! r.Header.Set("X-Forwarded-For", " ") // Spaces, not empty!
v := s.visitor(r) v, err := s.visitor(r)
require.Nil(t, err)
require.Equal(t, "8.9.10.11", v.ip.String()) require.Equal(t, "8.9.10.11", v.ip.String())
} }
@ -1402,7 +1376,8 @@ func TestServer_Visitor_XForwardedFor_Single(t *testing.T) {
r, _ := http.NewRequest("GET", "/bla", nil) r, _ := http.NewRequest("GET", "/bla", nil)
r.RemoteAddr = "8.9.10.11" r.RemoteAddr = "8.9.10.11"
r.Header.Set("X-Forwarded-For", "1.1.1.1") r.Header.Set("X-Forwarded-For", "1.1.1.1")
v := s.visitor(r) v, err := s.visitor(r)
require.Nil(t, err)
require.Equal(t, "1.1.1.1", v.ip.String()) require.Equal(t, "1.1.1.1", v.ip.String())
} }
@ -1413,7 +1388,8 @@ func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) {
r, _ := http.NewRequest("GET", "/bla", nil) r, _ := http.NewRequest("GET", "/bla", nil)
r.RemoteAddr = "8.9.10.11" r.RemoteAddr = "8.9.10.11"
r.Header.Set("X-Forwarded-For", "1.2.3.4 , 2.4.4.2,234.5.2.1 ") r.Header.Set("X-Forwarded-For", "1.2.3.4 , 2.4.4.2,234.5.2.1 ")
v := s.visitor(r) v, err := s.visitor(r)
require.Nil(t, err)
require.Equal(t, "234.5.2.1", v.ip.String()) require.Equal(t, "234.5.2.1", v.ip.String())
} }
@ -1442,7 +1418,7 @@ func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
go func() { go func() {
log.Printf("Updating stats") log.Printf("Updating stats")
start := time.Now() start := time.Now()
s.updateStatsAndPrune() s.execManager()
log.Printf("Done: Updating stats; took %s", time.Since(start).Round(time.Millisecond)) log.Printf("Done: Updating stats; took %s", time.Since(start).Round(time.Millisecond))
statsChan <- true statsChan <- true
}() }()

View file

@ -252,7 +252,7 @@ type apiAccountStats struct {
AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"` AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
} }
type apiAccountSettingsResponse struct { type apiAccountResponse struct {
Username string `json:"username"` Username string `json:"username"`
Role string `json:"role,omitempty"` Role string `json:"role,omitempty"`
Language string `json:"language,omitempty"` Language string `json:"language,omitempty"`

View file

@ -40,7 +40,7 @@ type visitor struct {
mu sync.Mutex mu sync.Mutex
} }
type visitorStats struct { type visitorInfo struct {
Basis string // "ip", "role" or "plan" Basis string // "ip", "role" or "plan"
Messages int64 Messages int64
MessagesLimit int64 MessagesLimit int64
@ -165,30 +165,30 @@ func (v *visitor) IncrEmails() {
} }
} }
func (v *visitor) Stats() (*visitorStats, error) { func (v *visitor) Info() (*visitorInfo, error) {
v.mu.Lock() v.mu.Lock()
messages := v.messages messages := v.messages
emails := v.emails emails := v.emails
v.mu.Unlock() v.mu.Unlock()
stats := &visitorStats{} info := &visitorInfo{}
if v.user != nil && v.user.Role == user.RoleAdmin { if v.user != nil && v.user.Role == user.RoleAdmin {
stats.Basis = "role" info.Basis = "role"
stats.MessagesLimit = 0 info.MessagesLimit = 0
stats.EmailsLimit = 0 info.EmailsLimit = 0
stats.AttachmentTotalSizeLimit = 0 info.AttachmentTotalSizeLimit = 0
stats.AttachmentFileSizeLimit = 0 info.AttachmentFileSizeLimit = 0
} else if v.user != nil && v.user.Plan != nil { } else if v.user != nil && v.user.Plan != nil {
stats.Basis = "plan" info.Basis = "plan"
stats.MessagesLimit = v.user.Plan.MessagesLimit info.MessagesLimit = v.user.Plan.MessagesLimit
stats.EmailsLimit = v.user.Plan.EmailsLimit info.EmailsLimit = v.user.Plan.EmailsLimit
stats.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit info.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit
stats.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit info.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit
} else { } else {
stats.Basis = "ip" info.Basis = "ip"
stats.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish) info.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish)
stats.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish) info.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish)
stats.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit info.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit
stats.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit info.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit
} }
var attachmentsBytesUsed int64 var attachmentsBytesUsed int64
var err error var err error
@ -200,13 +200,13 @@ func (v *visitor) Stats() (*visitorStats, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
stats.Messages = messages info.Messages = messages
stats.MessagesRemaining = zeroIfNegative(stats.MessagesLimit - stats.Messages) info.MessagesRemaining = zeroIfNegative(info.MessagesLimit - info.Messages)
stats.Emails = emails info.Emails = emails
stats.EmailsRemaining = zeroIfNegative(stats.EmailsLimit - stats.Emails) info.EmailsRemaining = zeroIfNegative(info.EmailsLimit - info.Emails)
stats.AttachmentTotalSize = attachmentsBytesUsed info.AttachmentTotalSize = attachmentsBytesUsed
stats.AttachmentTotalSizeRemaining = zeroIfNegative(stats.AttachmentTotalSizeLimit - stats.AttachmentTotalSize) info.AttachmentTotalSizeRemaining = zeroIfNegative(info.AttachmentTotalSizeLimit - info.AttachmentTotalSize)
return stats, nil return info, nil
} }
func zeroIfNegative(value int64) int64 { func zeroIfNegative(value int64) int64 {

View file

@ -121,9 +121,9 @@ const (
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
) )
// SQLiteManager 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 SQLiteManager struct { type Manager struct {
db *sql.DB db *sql.DB
defaultRead bool defaultRead bool
defaultWrite bool defaultWrite bool
@ -131,10 +131,10 @@ type SQLiteManager struct {
mu sync.Mutex mu sync.Mutex
} }
var _ Manager = (*SQLiteManager)(nil) var _ Auther = (*Manager)(nil)
// NewSQLiteAuthManager creates a new SQLiteManager instance // NewManager creates a new Manager instance
func NewSQLiteAuthManager(filename string, defaultRead, defaultWrite bool) (*SQLiteManager, error) { func NewManager(filename string, defaultRead, defaultWrite bool) (*Manager, error) {
db, err := sql.Open("sqlite3", filename) db, err := sql.Open("sqlite3", filename)
if err != nil { if err != nil {
return nil, err return nil, err
@ -142,7 +142,7 @@ func NewSQLiteAuthManager(filename string, defaultRead, defaultWrite bool) (*SQL
if err := setupAuthDB(db); err != nil { if err := setupAuthDB(db); err != nil {
return nil, err return nil, err
} }
manager := &SQLiteManager{ manager := &Manager{
db: db, db: db,
defaultRead: defaultRead, defaultRead: defaultRead,
defaultWrite: defaultWrite, defaultWrite: defaultWrite,
@ -155,7 +155,7 @@ func NewSQLiteAuthManager(filename string, defaultRead, defaultWrite bool) (*SQL
// Authenticate checks username and password and returns a user if correct. The method // Authenticate checks username and password and returns a user if correct. The method
// returns in constant-ish time, regardless of whether the user exists or the password is // returns in constant-ish time, regardless of whether the user exists or the password is
// correct or incorrect. // correct or incorrect.
func (a *SQLiteManager) Authenticate(username, password string) (*User, error) { func (a *Manager) Authenticate(username, password string) (*User, error) {
if username == Everyone { if username == Everyone {
return nil, ErrUnauthenticated return nil, ErrUnauthenticated
} }
@ -171,7 +171,7 @@ func (a *SQLiteManager) Authenticate(username, password string) (*User, error) {
return user, nil return user, nil
} }
func (a *SQLiteManager) AuthenticateToken(token string) (*User, error) { func (a *Manager) AuthenticateToken(token string) (*User, error) {
user, err := a.userByToken(token) user, err := a.userByToken(token)
if err != nil { if err != nil {
return nil, ErrUnauthenticated return nil, ErrUnauthenticated
@ -180,7 +180,7 @@ func (a *SQLiteManager) AuthenticateToken(token string) (*User, error) {
return user, nil return user, nil
} }
func (a *SQLiteManager) CreateToken(user *User) (*Token, error) { func (a *Manager) CreateToken(user *User) (*Token, error) {
token := util.RandomString(tokenLength) token := util.RandomString(tokenLength)
expires := time.Now().Add(userTokenExpiryDuration) expires := time.Now().Add(userTokenExpiryDuration)
if _, err := a.db.Exec(insertTokenQuery, user.Name, token, expires.Unix()); err != nil { if _, err := a.db.Exec(insertTokenQuery, user.Name, token, expires.Unix()); err != nil {
@ -192,7 +192,7 @@ func (a *SQLiteManager) CreateToken(user *User) (*Token, error) {
}, nil }, nil
} }
func (a *SQLiteManager) ExtendToken(user *User) (*Token, error) { func (a *Manager) ExtendToken(user *User) (*Token, error) {
newExpires := time.Now().Add(userTokenExpiryDuration) newExpires := time.Now().Add(userTokenExpiryDuration)
if _, err := a.db.Exec(updateTokenExpiryQuery, newExpires.Unix(), user.Name, user.Token); err != nil { if _, err := a.db.Exec(updateTokenExpiryQuery, newExpires.Unix(), user.Name, user.Token); err != nil {
return nil, err return nil, err
@ -203,7 +203,7 @@ func (a *SQLiteManager) ExtendToken(user *User) (*Token, error) {
}, nil }, nil
} }
func (a *SQLiteManager) RemoveToken(user *User) error { func (a *Manager) RemoveToken(user *User) error {
if user.Token == "" { if user.Token == "" {
return ErrUnauthorized return ErrUnauthorized
} }
@ -213,14 +213,14 @@ func (a *SQLiteManager) RemoveToken(user *User) error {
return nil return nil
} }
func (a *SQLiteManager) RemoveExpiredTokens() error { func (a *Manager) RemoveExpiredTokens() error {
if _, err := a.db.Exec(deleteExpiredTokensQuery, time.Now().Unix()); err != nil { if _, err := a.db.Exec(deleteExpiredTokensQuery, time.Now().Unix()); err != nil {
return err return err
} }
return nil return nil
} }
func (a *SQLiteManager) ChangeSettings(user *User) error { func (a *Manager) ChangeSettings(user *User) error {
settings, err := json.Marshal(user.Prefs) settings, err := json.Marshal(user.Prefs)
if err != nil { if err != nil {
return err return err
@ -231,13 +231,13 @@ func (a *SQLiteManager) ChangeSettings(user *User) error {
return nil return nil
} }
func (a *SQLiteManager) EnqueueStats(user *User) { func (a *Manager) EnqueueStats(user *User) {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
a.statsQueue[user.Name] = user a.statsQueue[user.Name] = user
} }
func (a *SQLiteManager) userStatsQueueWriter() { func (a *Manager) userStatsQueueWriter() {
ticker := time.NewTicker(userStatsQueueWriterInterval) ticker := time.NewTicker(userStatsQueueWriterInterval)
for range ticker.C { for range ticker.C {
if err := a.writeUserStatsQueue(); err != nil { if err := a.writeUserStatsQueue(); err != nil {
@ -246,7 +246,7 @@ func (a *SQLiteManager) userStatsQueueWriter() {
} }
} }
func (a *SQLiteManager) writeUserStatsQueue() error { func (a *Manager) writeUserStatsQueue() error {
a.mu.Lock() a.mu.Lock()
if len(a.statsQueue) == 0 { if len(a.statsQueue) == 0 {
a.mu.Unlock() a.mu.Unlock()
@ -273,7 +273,7 @@ func (a *SQLiteManager) writeUserStatsQueue() error {
// Authorize returns nil if the given user has access to the given topic using the desired // Authorize returns nil if the given user has access to the given topic using the desired
// permission. The user param may be nil to signal an anonymous user. // permission. The user param may be nil to signal an anonymous user.
func (a *SQLiteManager) Authorize(user *User, topic string, perm Permission) error { func (a *Manager) Authorize(user *User, topic string, perm Permission) error {
if user != nil && user.Role == RoleAdmin { if user != nil && user.Role == RoleAdmin {
return nil // Admin can do everything return nil // Admin can do everything
} }
@ -301,7 +301,7 @@ func (a *SQLiteManager) Authorize(user *User, topic string, perm Permission) err
return a.resolvePerms(read, write, perm) return a.resolvePerms(read, write, perm)
} }
func (a *SQLiteManager) resolvePerms(read, write bool, perm Permission) error { func (a *Manager) resolvePerms(read, write bool, perm Permission) error {
if perm == PermissionRead && read { if perm == PermissionRead && read {
return nil return nil
} else if perm == PermissionWrite && write { } else if perm == PermissionWrite && write {
@ -312,7 +312,7 @@ func (a *SQLiteManager) resolvePerms(read, write bool, perm Permission) error {
// AddUser adds a user with the given username, password and role. The password should be hashed // AddUser adds a user with the given username, password and role. The password should be hashed
// before it is stored in a persistence layer. // before it is stored in a persistence layer.
func (a *SQLiteManager) AddUser(username, password string, role Role) error { func (a *Manager) AddUser(username, password string, role Role) error {
if !AllowedUsername(username) || !AllowedRole(role) { if !AllowedUsername(username) || !AllowedRole(role) {
return ErrInvalidArgument return ErrInvalidArgument
} }
@ -328,7 +328,7 @@ func (a *SQLiteManager) AddUser(username, password string, role Role) error {
// RemoveUser deletes the user with the given username. The function returns nil on success, even // RemoveUser deletes the user with the given username. The function returns nil on success, even
// if the user did not exist in the first place. // if the user did not exist in the first place.
func (a *SQLiteManager) RemoveUser(username string) error { func (a *Manager) RemoveUser(username string) error {
if !AllowedUsername(username) { if !AllowedUsername(username) {
return ErrInvalidArgument return ErrInvalidArgument
} }
@ -345,7 +345,7 @@ func (a *SQLiteManager) RemoveUser(username string) error {
} }
// Users returns a list of users. It always also returns the Everyone user ("*"). // Users returns a list of users. It always also returns the Everyone user ("*").
func (a *SQLiteManager) Users() ([]*User, error) { func (a *Manager) Users() ([]*User, error) {
rows, err := a.db.Query(selectUsernamesQuery) rows, err := a.db.Query(selectUsernamesQuery)
if err != nil { if err != nil {
return nil, err return nil, err
@ -380,7 +380,7 @@ func (a *SQLiteManager) Users() ([]*User, error) {
// User returns the user with the given username if it exists, or ErrNotFound otherwise. // User returns the user with the given username if it exists, or ErrNotFound otherwise.
// You may also pass Everyone to retrieve the anonymous user and its Grant list. // You may also pass Everyone to retrieve the anonymous user and its Grant list.
func (a *SQLiteManager) User(username string) (*User, error) { func (a *Manager) User(username string) (*User, error) {
if username == Everyone { if username == Everyone {
return a.everyoneUser() return a.everyoneUser()
} }
@ -391,7 +391,7 @@ func (a *SQLiteManager) User(username string) (*User, error) {
return a.readUser(rows) return a.readUser(rows)
} }
func (a *SQLiteManager) userByToken(token string) (*User, error) { func (a *Manager) userByToken(token string) (*User, error) {
rows, err := a.db.Query(selectUserByTokenQuery, token) rows, err := a.db.Query(selectUserByTokenQuery, token)
if err != nil { if err != nil {
return nil, err return nil, err
@ -399,7 +399,7 @@ func (a *SQLiteManager) userByToken(token string) (*User, error) {
return a.readUser(rows) return a.readUser(rows)
} }
func (a *SQLiteManager) readUser(rows *sql.Rows) (*User, error) { func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
defer rows.Close() defer rows.Close()
var username, hash, role string var username, hash, role string
var settings, planCode sql.NullString var settings, planCode sql.NullString
@ -446,7 +446,7 @@ func (a *SQLiteManager) readUser(rows *sql.Rows) (*User, error) {
return user, nil return user, nil
} }
func (a *SQLiteManager) everyoneUser() (*User, error) { func (a *Manager) everyoneUser() (*User, error) {
grants, err := a.readGrants(Everyone) grants, err := a.readGrants(Everyone)
if err != nil { if err != nil {
return nil, err return nil, err
@ -459,7 +459,7 @@ func (a *SQLiteManager) everyoneUser() (*User, error) {
}, nil }, nil
} }
func (a *SQLiteManager) readGrants(username string) ([]Grant, error) { func (a *Manager) readGrants(username string) ([]Grant, error) {
rows, err := a.db.Query(selectUserAccessQuery, username) rows, err := a.db.Query(selectUserAccessQuery, username)
if err != nil { if err != nil {
return nil, err return nil, err
@ -484,7 +484,7 @@ func (a *SQLiteManager) readGrants(username string) ([]Grant, error) {
} }
// ChangePassword changes a user's password // ChangePassword changes a user's password
func (a *SQLiteManager) ChangePassword(username, password string) error { func (a *Manager) ChangePassword(username, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil { if err != nil {
return err return err
@ -497,7 +497,7 @@ func (a *SQLiteManager) ChangePassword(username, password string) error {
// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin, // ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
// all existing access control entries (Grant) are removed, since they are no longer needed. // all existing access control entries (Grant) are removed, since they are no longer needed.
func (a *SQLiteManager) ChangeRole(username string, role Role) error { func (a *Manager) ChangeRole(username string, role Role) error {
if !AllowedUsername(username) || !AllowedRole(role) { if !AllowedUsername(username) || !AllowedRole(role) {
return ErrInvalidArgument return ErrInvalidArgument
} }
@ -514,7 +514,7 @@ func (a *SQLiteManager) ChangeRole(username string, role Role) error {
// AllowAccess adds or updates an entry in th access control list for a specific user. It controls // AllowAccess adds or updates an entry in th access control list for a specific user. It controls
// read/write access to a topic. The parameter topicPattern may include wildcards (*). // read/write access to a topic. The parameter topicPattern may include wildcards (*).
func (a *SQLiteManager) AllowAccess(username string, topicPattern string, read bool, write bool) error { func (a *Manager) AllowAccess(username string, topicPattern string, read bool, write bool) error {
if (!AllowedUsername(username) && username != Everyone) || !AllowedTopicPattern(topicPattern) { if (!AllowedUsername(username) && username != Everyone) || !AllowedTopicPattern(topicPattern) {
return ErrInvalidArgument return ErrInvalidArgument
} }
@ -526,7 +526,7 @@ func (a *SQLiteManager) AllowAccess(username string, topicPattern string, read b
// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is // ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
// empty) for an entire user. The parameter topicPattern may include wildcards (*). // empty) for an entire user. The parameter topicPattern may include wildcards (*).
func (a *SQLiteManager) ResetAccess(username string, topicPattern string) error { func (a *Manager) ResetAccess(username string, topicPattern string) error {
if !AllowedUsername(username) && username != Everyone && username != "" { if !AllowedUsername(username) && username != Everyone && username != "" {
return ErrInvalidArgument return ErrInvalidArgument
} else if !AllowedTopicPattern(topicPattern) && topicPattern != "" { } else if !AllowedTopicPattern(topicPattern) && topicPattern != "" {
@ -544,7 +544,7 @@ func (a *SQLiteManager) ResetAccess(username string, topicPattern 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 *SQLiteManager) DefaultAccess() (read bool, write bool) { func (a *Manager) DefaultAccess() (read bool, write bool) {
return a.defaultRead, a.defaultWrite return a.defaultRead, a.defaultWrite
} }

View file

@ -2,6 +2,7 @@ package user_test
import ( import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"heckel.io/ntfy/user"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
@ -234,9 +235,9 @@ func TestSQLiteAuth_ChangeRole(t *testing.T) {
require.Equal(t, 0, len(ben.Grants)) require.Equal(t, 0, len(ben.Grants))
} }
func newTestAuth(t *testing.T, defaultRead, defaultWrite bool) *user.SQLiteAuthManager { func newTestAuth(t *testing.T, defaultRead, defaultWrite bool) *user.Manager {
filename := filepath.Join(t.TempDir(), "user.db") filename := filepath.Join(t.TempDir(), "user.db")
a, err := user.NewSQLiteAuthManager(filename, defaultRead, defaultWrite) a, err := user.NewManager(filename, defaultRead, defaultWrite)
require.Nil(t, err) require.Nil(t, err)
return a return a
} }

View file

@ -6,57 +6,15 @@ import (
"regexp" "regexp"
) )
// Manager is a generic interface to implement password and token based authentication and authorization type Auther interface {
type Manager interface {
// Authenticate checks username and password and returns a user if correct. The method // Authenticate checks username and password and returns a user if correct. The method
// returns in constant-ish time, regardless of whether the user exists or the password is // returns in constant-ish time, regardless of whether the user exists or the password is
// correct or incorrect. // correct or incorrect.
Authenticate(username, password string) (*User, error) Authenticate(username, password string) (*User, error)
AuthenticateToken(token string) (*User, error)
CreateToken(user *User) (*Token, error)
ExtendToken(user *User) (*Token, error)
RemoveToken(user *User) error
RemoveExpiredTokens() error
ChangeSettings(user *User) error
EnqueueStats(user *User)
// Authorize returns nil if the given user has access to the given topic using the desired // Authorize returns nil if the given user has access to the given topic using the desired
// permission. The user param may be nil to signal an anonymous user. // permission. The user param may be nil to signal an anonymous user.
Authorize(user *User, topic string, perm Permission) error Authorize(user *User, topic string, perm Permission) error
// AddUser adds a user with the given username, password and role. The password should be hashed
// before it is stored in a persistence layer.
AddUser(username, password string, role Role) error
// RemoveUser deletes the user with the given username. The function returns nil on success, even
// if the user did not exist in the first place.
RemoveUser(username string) error
// Users returns a list of users. It always also returns the Everyone user ("*").
Users() ([]*User, error)
// User returns the user with the given username if it exists, or ErrNotFound otherwise.
// You may also pass Everyone to retrieve the anonymous user and its Grant list.
User(username string) (*User, error)
// ChangePassword changes a user's password
ChangePassword(username, password string) error
// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
// all existing access control entries (Grant) are removed, since they are no longer needed.
ChangeRole(username string, role Role) error
// AllowAccess adds or updates an entry in th access control list for a specific user. It controls
// read/write access to a topic. The parameter topicPattern may include wildcards (*).
AllowAccess(username string, topicPattern string, read bool, write bool) error
// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
// empty) for an entire user. The parameter topicPattern may include wildcards (*).
ResetAccess(username string, topicPattern string) error
// DefaultAccess returns the default read/write access if no access control entry matches
DefaultAccess() (read bool, write bool)
} }
// User is a struct that represents a user // User is a struct that represents a user