diff --git a/cmd/access.go b/cmd/access.go index e03abd0..5de39ed 100644 --- a/cmd/access.go +++ b/cmd/access.go @@ -71,7 +71,7 @@ func execUserAccess(c *cli.Context) error { if c.NArg() > 3 { 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 { return err } @@ -96,7 +96,7 @@ func execUserAccess(c *cli.Context) error { 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) { 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) } -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 == "" { return resetAllAccess(c, manager) } 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) } -func resetAllAccess(c *cli.Context, manager user.Manager) error { +func resetAllAccess(c *cli.Context, manager *user.Manager) error { if err := manager.ResetAccess("", ""); err != nil { return err } @@ -140,7 +140,7 @@ func resetAllAccess(c *cli.Context, manager user.Manager) error { 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 { return err } @@ -148,7 +148,7 @@ func resetUserAccess(c *cli.Context, manager user.Manager, username string) erro 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 { return err } @@ -156,14 +156,14 @@ func resetUserTopicAccess(c *cli.Context, manager user.Manager, username string, 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 == "" { return showAllAccess(c, manager) } 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() if err != nil { return err @@ -171,7 +171,7 @@ func showAllAccess(c *cli.Context, manager user.Manager) error { 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) if err == user.ErrNotFound { 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}) } -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 { fmt.Fprintf(c.App.ErrWriter, "user %s (%s)\n", u.Name, u.Role) if u.Role == user.RoleAdmin { diff --git a/cmd/subscribe_unix.go b/cmd/subscribe_unix.go index 6e5128d..8b91fed 100644 --- a/cmd/subscribe_unix.go +++ b/cmd/subscribe_unix.go @@ -1,5 +1,4 @@ //go:build linux || dragonfly || freebsd || netbsd || openbsd -// +build linux dragonfly freebsd netbsd openbsd package cmd diff --git a/cmd/user.go b/cmd/user.go index 8fe1b30..3562e30 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -161,7 +161,7 @@ func execUserAdd(c *cli.Context) error { } else if !user.AllowedRole(role) { return errors.New("role must be either 'user' or 'admin'") } - manager, err := createAuthManager(c) + manager, err := createUserManager(c) if err != nil { return err } @@ -190,7 +190,7 @@ func execUserDel(c *cli.Context) error { } else if username == userEveryone { return errors.New("username not allowed") } - manager, err := createAuthManager(c) + manager, err := createUserManager(c) if err != nil { return err } @@ -212,7 +212,7 @@ func execUserChangePass(c *cli.Context) error { } else if username == userEveryone { return errors.New("username not allowed") } - manager, err := createAuthManager(c) + manager, err := createUserManager(c) if err != nil { return err } @@ -240,7 +240,7 @@ func execUserChangeRole(c *cli.Context) error { } else if username == userEveryone { return errors.New("username not allowed") } - manager, err := createAuthManager(c) + manager, err := createUserManager(c) if err != nil { return err } @@ -255,7 +255,7 @@ func execUserChangeRole(c *cli.Context) error { } func execUserList(c *cli.Context) error { - manager, err := createAuthManager(c) + manager, err := createUserManager(c) if err != nil { return err } @@ -266,7 +266,7 @@ func execUserList(c *cli.Context) error { 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") authDefaultAccess := c.String("auth-default-access") if authFile == "" { @@ -278,7 +278,7 @@ func createAuthManager(c *cli.Context) (user.Manager, error) { } authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-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) { diff --git a/go.sum b/go.sum index d62e01e..38822aa 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,12 @@ 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/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/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/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= 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/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/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM= 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= 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= -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/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= 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/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/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= 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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 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/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= 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/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/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/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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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.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/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/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 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= 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.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/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= 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-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.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/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.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/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= 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/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.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/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 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-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= 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/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI= 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-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-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/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= diff --git a/server/file_cache_test.go b/server/file_cache_test.go index 971cff1..cc010fa 100644 --- a/server/file_cache_test.go +++ b/server/file_cache_test.go @@ -56,13 +56,6 @@ func TestFileCache_Write_FailedTotalSizeLimit(t *testing.T) { 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) { dir, c := newTestFileCache(t) _, 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) { dir = t.TempDir() - cache, err := newFileCache(dir, 10*1024, 1*1024) + cache, err := newFileCache(dir, 10*1024) require.Nil(t, err) return dir, cache } diff --git a/server/message_cache.go b/server/message_cache.go index 7172e1b..d2ab20d 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -40,7 +40,7 @@ const ( attachment_expires INT NOT NULL, attachment_url TEXT NOT NULL, sender TEXT NOT NULL, - user TEXT NOT NULL, + user TEXT NOT NULL, encoding TEXT NOT NULL, published INT NOT NULL ); @@ -95,7 +95,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 9 + currentSchemaVersion = 10 createSchemaVersionTableQuery = ` CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, @@ -193,6 +193,11 @@ const ( migrate8To9AlterMessagesTableQuery = ` 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 { @@ -614,8 +619,9 @@ func setupCacheDB(db *sql.DB, startupQueries string) error { return migrateFrom7(db) } else if schemaVersion == 8 { return migrateFrom8(db) + } else if schemaVersion == 9 { + return migrateFrom9(db) } - // TODO add user column 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 { 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 } diff --git a/server/server.go b/server/server.go index 8846547..fcb9a04 100644 --- a/server/server.go +++ b/server/server.go @@ -43,12 +43,15 @@ import ( "user list" shows * twice "ntfy access everyone user4topic " twice -> UNIQUE constraint error Account usage not updated "in real time" + Attachment expiration based on plan + Plan: Keep 10000 messages or keep X days? Sync: - "mute" setting - figure out what settings are "web" or "phone" UI: - Subscription dotmenu dropdown: Move to nav bar, or make same as profile dropdown - "Logout and delete local storage" option + - Delete local storage when deleting account Pages: - Home - Password reset @@ -61,7 +64,8 @@ import ( - APIs - CRUD tokens - Expire tokens - - + - userManager can be nil + - visitor with/without user */ // Server is the main server, providing the UI and API for ntfy @@ -77,7 +81,7 @@ type Server struct { visitors map[string]*visitor // ip: or user: firebaseClient *firebaseClient messages int64 - userManager user.Manager + userManager *user.Manager // Might be nil! messageCache *messageCache fileCache *fileCache closeChan chan bool @@ -165,9 +169,9 @@ func New(conf *Config) (*Server, error) { return nil, err } } - var auther user.Manager + var userManager *user.Manager 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 { return nil, err } @@ -178,7 +182,7 @@ func New(conf *Config) (*Server, error) { if err != nil { return nil, err } - firebaseClient = newFirebaseClient(sender, auther) + firebaseClient = newFirebaseClient(sender, userManager) } return &Server{ config: conf, @@ -187,7 +191,7 @@ func New(conf *Config) (*Server, error) { firebaseClient: firebaseClient, smtpSender: mailer, topics: topics, - userManager: auther, + userManager: userManager, visitors: make(map[string]*visitor), }, 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 { return s.ensureWebEnabled(s.handleWebConfig)(w, r, v) } 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 { - 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 { - return s.handleAccountDelete(w, r, v) + return s.ensureWithAccount(s.handleAccountDelete)(w, r, v) } 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 { - return s.handleAccountTokenIssue(w, r, v) + return s.ensureWithAccount(s.handleAccountTokenIssue)(w, r, v) } 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 { - return s.handleAccountTokenDelete(w, r, v) + return s.ensureWithAccount(s.handleAccountTokenDelete)(w, r, v) } 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 { - 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) { - 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) { - return s.handleAccountSubscriptionDelete(w, r, v) + return s.ensureWithAccount(s.handleAccountSubscriptionDelete)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath { return s.handleMatrixDiscovery(w) } 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() { return errHTTPBadRequestAttachmentsExpiryBeforeDelivery } - stats, err := v.Stats() + stats, err := v.Info() if err != nil { return err } @@ -1182,7 +1186,7 @@ func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) { return topics, nil } -func (s *Server) updateStatsAndPrune() { +func (s *Server) execManager() { log.Debug("Manager: Starting") defer log.Debug("Manager: Finished") @@ -1203,8 +1207,10 @@ func (s *Server) updateStatsAndPrune() { log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors) // Delete expired user tokens - if err := s.userManager.RemoveExpiredTokens(); err != nil { - log.Warn("Error expiring user tokens: %s", err.Error()) + if s.userManager != nil { + if err := s.userManager.RemoveExpiredTokens(); err != nil { + log.Warn("Error expiring user tokens: %s", err.Error()) + } } // Delete expired attachments @@ -1293,7 +1299,7 @@ func (s *Server) runManager() { for { select { case <-time.After(s.config.ManagerInterval): - s.updateStatsAndPrune() + s.execManager() case <-s.closeChan: 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 // 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 { @@ -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. func (s *Server) visitor(r *http.Request) (v *visitor, err error) { ip := extractIPAddress(r, s.config.BehindProxy) - var user *user.User // may stay nil if no auth header! - if user, err = s.authenticate(r); err != nil { + var u *user.User // may stay nil if no auth header! + if u, err = s.authenticate(r); err != nil { log.Debug("authentication failed: %s", err.Error()) err = errHTTPUnauthorized // Always return visitor, even when error occurs! } - if user != nil { - v = s.visitorFromUser(user, ip) + if u != nil { + v = s.visitorFromUser(u, ip) } else { 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! } @@ -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 // 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) { - value := r.Header.Get("Authorization") + value := strings.TrimSpace(r.Header.Get("Authorization")) queryParam := readQueryParam(r, "authorization", "auth") if queryParam != "" { a, err := base64.RawURLEncoding.DecodeString(queryParam) if err != nil { return nil, err } - value = string(a) + value = strings.TrimSpace(string(a)) } if value == "" { return nil, nil + } else if s.userManager == nil { + return nil, errHTTPUnauthorized } if strings.HasPrefix(value, "Bearer") { return s.authenticateBearerAuth(value) diff --git a/server/server_account.go b/server/server_account.go index 7f3d0d8..1568ccf 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -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 { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this - stats, err := v.Stats() + stats, err := v.Info() if err != nil { return err } - response := &apiAccountSettingsResponse{ + response := &apiAccountResponse{ Stats: &apiAccountStats{ Messages: stats.Messages, MessagesRemaining: stats.MessagesRemaining, diff --git a/server/server_firebase.go b/server/server_firebase.go index 629aebc..20f880f 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -28,10 +28,10 @@ var ( // The actual Firebase implementation is implemented in firebaseSenderImpl, to make it testable. type firebaseClient struct { 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{ sender: sender, 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. // - 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. -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 apnsConfig *messaging.APNSConfig switch m.Event { diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index 034511f..9a21834 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "heckel.io/ntfy/user" "net/netip" "strings" "sync" @@ -17,7 +18,9 @@ type testAuther struct { 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") } @@ -323,7 +326,7 @@ func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) { func TestToFirebaseSender_Abuse(t *testing.T) { sender := &testFirebaseSender{allowed: 2} 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.Equal(t, 1, len(sender.Messages())) diff --git a/server/server_matrix_test.go b/server/server_matrix_test.go index 4b5a66c..ad94da5 100644 --- a/server/server_matrix_test.go +++ b/server/server_matrix_test.go @@ -72,7 +72,7 @@ func TestMatrix_WriteMatrixDiscoveryResponse(t *testing.T) { func TestMatrix_WriteMatrixError(t *testing.T) { w := httptest.NewRecorder() 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.Equal(t, 200, w.Result().StatusCode) require.Equal(t, `{"rejected":["https://ntfy.example.com/upABCDEFGHI?up=1"]}`+"\n", w.Body.String()) diff --git a/server/server_test.go b/server/server_test.go index dc047e7..e151762 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "heckel.io/ntfy/user" "io" "log" "math/rand" @@ -171,7 +172,7 @@ func TestServer_StaticSites(t *testing.T) { rr = request(t, s, "GET", "/static/css/home.css", "", nil) 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) require.Equal(t, 301, rr.Code) @@ -353,7 +354,7 @@ func TestServer_PublishAtAndPrune(t *testing.T) { "In": "1h", }) 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) 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") s := newTestServer(t, c) - manager := s.userManager.(user.Manager) - require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ "Authorization": basicAuth("phil:phil"), @@ -642,9 +642,8 @@ func TestServer_Auth_Success_User(t *testing.T) { c.AuthDefaultWrite = false s := newTestServer(t, c) - manager := s.userManager.(user.Manager) - require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser)) - require.Nil(t, manager.AllowAccess("ben", "mytopic", true, true)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", true, true)) response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ "Authorization": basicAuth("ben:ben"), @@ -659,10 +658,9 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) { c.AuthDefaultWrite = false s := newTestServer(t, c) - manager := s.userManager.(user.Manager) - require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser)) - require.Nil(t, manager.AllowAccess("ben", "mytopic", true, true)) - require.Nil(t, manager.AllowAccess("ben", "anothertopic", true, true)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", true, true)) + require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", true, true)) response := request(t, s, "GET", "/mytopic,anothertopic/auth", "", map[string]string{ "Authorization": basicAuth("ben:ben"), @@ -682,8 +680,7 @@ func TestServer_Auth_Fail_InvalidPass(t *testing.T) { c.AuthDefaultWrite = false s := newTestServer(t, c) - manager := s.userManager.(user.Manager) - require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ "Authorization": basicAuth("phil:INVALID"), @@ -698,9 +695,8 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) { c.AuthDefaultWrite = false s := newTestServer(t, c) - manager := s.userManager.(user.Manager) - require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser)) - require.Nil(t, manager.AllowAccess("ben", "sometopic", true, true)) // Not mytopic! + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", true, true)) // Not mytopic! response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ "Authorization": basicAuth("ben:ben"), @@ -715,10 +711,9 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) { c.AuthDefaultWrite = true // Open by default s := newTestServer(t, c) - manager := s.userManager.(user.Manager) - require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin)) - require.Nil(t, manager.AllowAccess(user.Everyone, "private", false, false)) - require.Nil(t, manager.AllowAccess(user.Everyone, "announcements", true, false)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", false, false)) + require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", true, false)) response := request(t, s, "PUT", "/mytopic", "test", nil) require.Equal(t, 200, response.Code) @@ -748,8 +743,7 @@ func TestServer_Auth_ViaQuery(t *testing.T) { c.AuthDefaultWrite = false s := newTestServer(t, c) - manager := s.userManager.(user.Manager) - require.Nil(t, manager.AddUser("ben", "some pass", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin)) u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:some pass")))) response := request(t, s, "GET", u, "", nil) @@ -760,27 +754,6 @@ func TestServer_Auth_ViaQuery(t *testing.T) { 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 { count int mu sync.Mutex @@ -1306,7 +1279,7 @@ func TestServer_PublishAttachmentAndPrune(t *testing.T) { // Prune and makes sure it's gone time.Sleep(time.Second) // Sigh ... - s.updateStatsAndPrune() + s.execManager() require.NoFileExists(t, file) response = request(t, s, "GET", path, "", nil) require.Equal(t, 404, response.Code) @@ -1360,7 +1333,7 @@ func TestServer_PublishAttachmentBandwidthLimitUploadOnly(t *testing.T) { require.Equal(t, 41301, err.Code) } -func TestServer_PublishAttachmentUserStats(t *testing.T) { +func TestServer_PublishAttachmentAccountStats(t *testing.T) { content := util.RandomString(4999) // > 4096 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/") // User stats - response = request(t, s, "GET", "/user/stats", "", nil) + response = request(t, s, "GET", "/v1/account", "", nil) require.Equal(t, 200, response.Code) - var stats visitorStats - require.Nil(t, json.NewDecoder(strings.NewReader(response.Body.String())).Decode(&stats)) - require.Equal(t, int64(5000), stats.AttachmentFileSizeLimit) - require.Equal(t, int64(6000), stats.VisitorAttachmentBytesTotal) - require.Equal(t, int64(4999), stats.AttachmentBytes) - require.Equal(t, int64(1001), stats.VisitorAttachmentBytesRemaining) + var account *apiAccountResponse + require.Nil(t, json.NewDecoder(strings.NewReader(response.Body.String())).Decode(&account)) + require.Equal(t, int64(5000), account.Limits.AttachmentFileSize) + require.Equal(t, int64(6000), account.Limits.AttachmentTotalSize) + require.Equal(t, int64(4999), account.Stats.AttachmentTotalSize) + require.Equal(t, int64(1001), account.Stats.AttachmentTotalSizeRemaining) } 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.RemoteAddr = "8.9.10.11" 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()) } @@ -1402,7 +1376,8 @@ func TestServer_Visitor_XForwardedFor_Single(t *testing.T) { r, _ := http.NewRequest("GET", "/bla", nil) r.RemoteAddr = "8.9.10.11" 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()) } @@ -1413,7 +1388,8 @@ func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) { r, _ := http.NewRequest("GET", "/bla", nil) r.RemoteAddr = "8.9.10.11" 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()) } @@ -1442,7 +1418,7 @@ func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) { go func() { log.Printf("Updating stats") start := time.Now() - s.updateStatsAndPrune() + s.execManager() log.Printf("Done: Updating stats; took %s", time.Since(start).Round(time.Millisecond)) statsChan <- true }() diff --git a/server/types.go b/server/types.go index e6e5186..4084d3f 100644 --- a/server/types.go +++ b/server/types.go @@ -252,7 +252,7 @@ type apiAccountStats struct { AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"` } -type apiAccountSettingsResponse struct { +type apiAccountResponse struct { Username string `json:"username"` Role string `json:"role,omitempty"` Language string `json:"language,omitempty"` diff --git a/server/visitor.go b/server/visitor.go index 9b53033..c39b47e 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -40,7 +40,7 @@ type visitor struct { mu sync.Mutex } -type visitorStats struct { +type visitorInfo struct { Basis string // "ip", "role" or "plan" Messages 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() messages := v.messages emails := v.emails v.mu.Unlock() - stats := &visitorStats{} + info := &visitorInfo{} if v.user != nil && v.user.Role == user.RoleAdmin { - stats.Basis = "role" - stats.MessagesLimit = 0 - stats.EmailsLimit = 0 - stats.AttachmentTotalSizeLimit = 0 - stats.AttachmentFileSizeLimit = 0 + info.Basis = "role" + info.MessagesLimit = 0 + info.EmailsLimit = 0 + info.AttachmentTotalSizeLimit = 0 + info.AttachmentFileSizeLimit = 0 } else if v.user != nil && v.user.Plan != nil { - stats.Basis = "plan" - stats.MessagesLimit = v.user.Plan.MessagesLimit - stats.EmailsLimit = v.user.Plan.EmailsLimit - stats.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit - stats.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit + info.Basis = "plan" + info.MessagesLimit = v.user.Plan.MessagesLimit + info.EmailsLimit = v.user.Plan.EmailsLimit + info.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit + info.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit } else { - stats.Basis = "ip" - stats.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish) - stats.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish) - stats.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit - stats.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit + info.Basis = "ip" + info.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish) + info.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish) + info.AttachmentTotalSizeLimit = v.config.VisitorAttachmentTotalSizeLimit + info.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit } var attachmentsBytesUsed int64 var err error @@ -200,13 +200,13 @@ func (v *visitor) Stats() (*visitorStats, error) { if err != nil { return nil, err } - stats.Messages = messages - stats.MessagesRemaining = zeroIfNegative(stats.MessagesLimit - stats.Messages) - stats.Emails = emails - stats.EmailsRemaining = zeroIfNegative(stats.EmailsLimit - stats.Emails) - stats.AttachmentTotalSize = attachmentsBytesUsed - stats.AttachmentTotalSizeRemaining = zeroIfNegative(stats.AttachmentTotalSizeLimit - stats.AttachmentTotalSize) - return stats, nil + info.Messages = messages + info.MessagesRemaining = zeroIfNegative(info.MessagesLimit - info.Messages) + info.Emails = emails + info.EmailsRemaining = zeroIfNegative(info.EmailsLimit - info.Emails) + info.AttachmentTotalSize = attachmentsBytesUsed + info.AttachmentTotalSizeRemaining = zeroIfNegative(info.AttachmentTotalSizeLimit - info.AttachmentTotalSize) + return info, nil } func zeroIfNegative(value int64) int64 { diff --git a/user/manager.go b/user/manager.go index 5df8c84..c7b28b1 100644 --- a/user/manager.go +++ b/user/manager.go @@ -121,9 +121,9 @@ const ( 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. -type SQLiteManager struct { +type Manager struct { db *sql.DB defaultRead bool defaultWrite bool @@ -131,10 +131,10 @@ type SQLiteManager struct { mu sync.Mutex } -var _ Manager = (*SQLiteManager)(nil) +var _ Auther = (*Manager)(nil) -// NewSQLiteAuthManager creates a new SQLiteManager instance -func NewSQLiteAuthManager(filename string, defaultRead, defaultWrite bool) (*SQLiteManager, error) { +// NewManager creates a new Manager instance +func NewManager(filename string, defaultRead, defaultWrite bool) (*Manager, error) { db, err := sql.Open("sqlite3", filename) if err != nil { return nil, err @@ -142,7 +142,7 @@ func NewSQLiteAuthManager(filename string, defaultRead, defaultWrite bool) (*SQL if err := setupAuthDB(db); err != nil { return nil, err } - manager := &SQLiteManager{ + manager := &Manager{ db: db, defaultRead: defaultRead, 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 // returns in constant-ish time, regardless of whether the user exists or the password is // correct or incorrect. -func (a *SQLiteManager) Authenticate(username, password string) (*User, error) { +func (a *Manager) Authenticate(username, password string) (*User, error) { if username == Everyone { return nil, ErrUnauthenticated } @@ -171,7 +171,7 @@ func (a *SQLiteManager) Authenticate(username, password string) (*User, error) { return user, nil } -func (a *SQLiteManager) AuthenticateToken(token string) (*User, error) { +func (a *Manager) AuthenticateToken(token string) (*User, error) { user, err := a.userByToken(token) if err != nil { return nil, ErrUnauthenticated @@ -180,7 +180,7 @@ func (a *SQLiteManager) AuthenticateToken(token string) (*User, error) { return user, nil } -func (a *SQLiteManager) CreateToken(user *User) (*Token, error) { +func (a *Manager) CreateToken(user *User) (*Token, error) { token := util.RandomString(tokenLength) expires := time.Now().Add(userTokenExpiryDuration) 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 } -func (a *SQLiteManager) ExtendToken(user *User) (*Token, error) { +func (a *Manager) ExtendToken(user *User) (*Token, error) { newExpires := time.Now().Add(userTokenExpiryDuration) if _, err := a.db.Exec(updateTokenExpiryQuery, newExpires.Unix(), user.Name, user.Token); err != nil { return nil, err @@ -203,7 +203,7 @@ func (a *SQLiteManager) ExtendToken(user *User) (*Token, error) { }, nil } -func (a *SQLiteManager) RemoveToken(user *User) error { +func (a *Manager) RemoveToken(user *User) error { if user.Token == "" { return ErrUnauthorized } @@ -213,14 +213,14 @@ func (a *SQLiteManager) RemoveToken(user *User) error { return nil } -func (a *SQLiteManager) RemoveExpiredTokens() error { +func (a *Manager) RemoveExpiredTokens() error { if _, err := a.db.Exec(deleteExpiredTokensQuery, time.Now().Unix()); err != nil { return err } return nil } -func (a *SQLiteManager) ChangeSettings(user *User) error { +func (a *Manager) ChangeSettings(user *User) error { settings, err := json.Marshal(user.Prefs) if err != nil { return err @@ -231,13 +231,13 @@ func (a *SQLiteManager) ChangeSettings(user *User) error { return nil } -func (a *SQLiteManager) EnqueueStats(user *User) { +func (a *Manager) EnqueueStats(user *User) { a.mu.Lock() defer a.mu.Unlock() a.statsQueue[user.Name] = user } -func (a *SQLiteManager) userStatsQueueWriter() { +func (a *Manager) userStatsQueueWriter() { ticker := time.NewTicker(userStatsQueueWriterInterval) for range ticker.C { 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() if len(a.statsQueue) == 0 { 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 // 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 { 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) } -func (a *SQLiteManager) resolvePerms(read, write bool, perm Permission) error { +func (a *Manager) resolvePerms(read, write bool, perm Permission) error { if perm == PermissionRead && read { return nil } 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 // 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) { 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 // 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) { 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 ("*"). -func (a *SQLiteManager) Users() ([]*User, error) { +func (a *Manager) Users() ([]*User, error) { rows, err := a.db.Query(selectUsernamesQuery) if err != nil { 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. // 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 { return a.everyoneUser() } @@ -391,7 +391,7 @@ func (a *SQLiteManager) User(username string) (*User, error) { 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) if err != nil { return nil, err @@ -399,7 +399,7 @@ func (a *SQLiteManager) userByToken(token string) (*User, error) { 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() var username, hash, role string var settings, planCode sql.NullString @@ -446,7 +446,7 @@ func (a *SQLiteManager) readUser(rows *sql.Rows) (*User, error) { return user, nil } -func (a *SQLiteManager) everyoneUser() (*User, error) { +func (a *Manager) everyoneUser() (*User, error) { grants, err := a.readGrants(Everyone) if err != nil { return nil, err @@ -459,7 +459,7 @@ func (a *SQLiteManager) everyoneUser() (*User, error) { }, nil } -func (a *SQLiteManager) readGrants(username string) ([]Grant, error) { +func (a *Manager) readGrants(username string) ([]Grant, error) { rows, err := a.db.Query(selectUserAccessQuery, username) if err != nil { return nil, err @@ -484,7 +484,7 @@ func (a *SQLiteManager) readGrants(username string) ([]Grant, error) { } // 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) if err != nil { 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, // 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) { 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 // 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) { 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 // 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 != "" { return ErrInvalidArgument } 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 -func (a *SQLiteManager) DefaultAccess() (read bool, write bool) { +func (a *Manager) DefaultAccess() (read bool, write bool) { return a.defaultRead, a.defaultWrite } diff --git a/user/manager_test.go b/user/manager_test.go index 40af95f..8e38a95 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -2,6 +2,7 @@ package user_test import ( "github.com/stretchr/testify/require" + "heckel.io/ntfy/user" "path/filepath" "strings" "testing" @@ -234,9 +235,9 @@ func TestSQLiteAuth_ChangeRole(t *testing.T) { 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") - a, err := user.NewSQLiteAuthManager(filename, defaultRead, defaultWrite) + a, err := user.NewManager(filename, defaultRead, defaultWrite) require.Nil(t, err) return a } diff --git a/user/types.go b/user/types.go index 94febde..276348e 100644 --- a/user/types.go +++ b/user/types.go @@ -6,57 +6,15 @@ import ( "regexp" ) -// Manager is a generic interface to implement password and token based authentication and authorization -type Manager interface { +type Auther interface { // 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 // correct or incorrect. 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 // permission. The user param may be nil to signal an anonymous user. 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