user.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. package cmd
  2. import (
  3. "crypto/subtle"
  4. "errors"
  5. "fmt"
  6. "github.com/urfave/cli/v2"
  7. "github.com/urfave/cli/v2/altsrc"
  8. "heckel.io/ntfy/auth"
  9. "heckel.io/ntfy/util"
  10. "strings"
  11. )
  12. /*
  13. ---
  14. dabbling for CLI
  15. ntfy user allow phil mytopic
  16. ntfy user allow phil mytopic --read-only
  17. ntfy user deny phil mytopic
  18. ntfy user list
  19. phil (admin)
  20. - read-write access to everything
  21. ben (user)
  22. - read-write access to a topic alerts
  23. - read access to
  24. everyone (no user)
  25. - read-only access to topic announcements
  26. */
  27. var flagsUser = userCommandFlags()
  28. var cmdUser = &cli.Command{
  29. Name: "user",
  30. Usage: "Manage users and access to topics",
  31. UsageText: "ntfy user [add|del|...] ...",
  32. Flags: flagsUser,
  33. Before: initConfigFileInputSource("config", flagsUser),
  34. Category: categoryServer,
  35. Subcommands: []*cli.Command{
  36. {
  37. Name: "add",
  38. Aliases: []string{"a"},
  39. Usage: "add user to auth database",
  40. Action: execUserAdd,
  41. Flags: []cli.Flag{
  42. &cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(auth.RoleUser), Usage: "user role"},
  43. },
  44. },
  45. {
  46. Name: "remove",
  47. Aliases: []string{"del", "rm"},
  48. Usage: "remove user from auth database",
  49. Action: execUserDel,
  50. },
  51. {
  52. Name: "change-pass",
  53. Aliases: []string{"chp"},
  54. Usage: "change user password",
  55. Action: execUserChangePass,
  56. },
  57. {
  58. Name: "change-role",
  59. Aliases: []string{"chr"},
  60. Usage: "change user role",
  61. Action: execUserChangeRole,
  62. },
  63. {
  64. Name: "list",
  65. Aliases: []string{"chr"},
  66. Usage: "change user role",
  67. Action: execUserList,
  68. },
  69. },
  70. }
  71. func execUserAdd(c *cli.Context) error {
  72. username := c.Args().Get(0)
  73. role := auth.Role(c.String("role"))
  74. if username == "" {
  75. return errors.New("username expected, type 'ntfy user add --help' for help")
  76. } else if !auth.AllowedRole(role) {
  77. return errors.New("role must be either 'user' or 'admin'")
  78. }
  79. password, err := readPassword(c)
  80. if err != nil {
  81. return err
  82. }
  83. manager, err := createAuthManager(c)
  84. if err != nil {
  85. return err
  86. }
  87. if err := manager.AddUser(username, password, auth.Role(role)); err != nil {
  88. return err
  89. }
  90. fmt.Fprintf(c.App.ErrWriter, "User %s added with role %s\n", username, role)
  91. return nil
  92. }
  93. func execUserDel(c *cli.Context) error {
  94. username := c.Args().Get(0)
  95. if username == "" {
  96. return errors.New("username expected, type 'ntfy user del --help' for help")
  97. }
  98. manager, err := createAuthManager(c)
  99. if err != nil {
  100. return err
  101. }
  102. if err := manager.RemoveUser(username); err != nil {
  103. return err
  104. }
  105. fmt.Fprintf(c.App.ErrWriter, "User %s removed\n", username)
  106. return nil
  107. }
  108. func execUserChangePass(c *cli.Context) error {
  109. username := c.Args().Get(0)
  110. if username == "" {
  111. return errors.New("username expected, type 'ntfy user change-pass --help' for help")
  112. }
  113. password, err := readPassword(c)
  114. if err != nil {
  115. return err
  116. }
  117. manager, err := createAuthManager(c)
  118. if err != nil {
  119. return err
  120. }
  121. if err := manager.ChangePassword(username, password); err != nil {
  122. return err
  123. }
  124. fmt.Fprintf(c.App.ErrWriter, "Changed password for user %s\n", username)
  125. return nil
  126. }
  127. func execUserChangeRole(c *cli.Context) error {
  128. username := c.Args().Get(0)
  129. role := auth.Role(c.Args().Get(1))
  130. if username == "" || !auth.AllowedRole(role) {
  131. return errors.New("username and new role expected, type 'ntfy user change-role --help' for help")
  132. }
  133. manager, err := createAuthManager(c)
  134. if err != nil {
  135. return err
  136. }
  137. if err := manager.ChangeRole(username, role); err != nil {
  138. return err
  139. }
  140. fmt.Fprintf(c.App.ErrWriter, "Changed role for user %s to %s\n", username, role)
  141. return nil
  142. }
  143. func execUserList(c *cli.Context) error {
  144. manager, err := createAuthManager(c)
  145. if err != nil {
  146. return err
  147. }
  148. users, err := manager.Users()
  149. if err != nil {
  150. return err
  151. }
  152. return showUsers(c, users)
  153. }
  154. func showUsers(c *cli.Context, users []*auth.User) error {
  155. for _, user := range users {
  156. fmt.Fprintf(c.App.Writer, "User %s (%s)\n", user.Name, user.Role)
  157. if user.Role == auth.RoleAdmin {
  158. fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
  159. } else if len(user.Grants) > 0 {
  160. for _, grant := range user.Grants {
  161. if grant.Read && grant.Write {
  162. fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.Topic)
  163. } else if grant.Read {
  164. fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.Topic)
  165. } else if grant.Write {
  166. fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.Topic)
  167. } else {
  168. fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.Topic)
  169. }
  170. }
  171. } else {
  172. fmt.Fprintf(c.App.ErrWriter, "- no topic-specific permissions\n")
  173. }
  174. }
  175. return nil
  176. }
  177. func createAuthManager(c *cli.Context) (auth.Manager, error) {
  178. authFile := c.String("auth-file")
  179. authDefaultAccess := c.String("auth-default-access")
  180. if authFile == "" {
  181. return nil, errors.New("option auth-file not set; auth is unconfigured for this server")
  182. } else if !util.FileExists(authFile) {
  183. return nil, errors.New("auth-file does not exist; please start the server at least once to create it")
  184. } else if !util.InStringList([]string{"read-write", "read-only", "deny-all"}, authDefaultAccess) {
  185. return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'")
  186. }
  187. authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
  188. authDefaultWrite := authDefaultAccess == "read-write"
  189. return auth.NewSQLiteAuth(authFile, authDefaultRead, authDefaultWrite)
  190. }
  191. func readPassword(c *cli.Context) (string, error) {
  192. fmt.Fprint(c.App.ErrWriter, "Enter Password: ")
  193. password, err := util.ReadPassword(c.App.Reader)
  194. if err != nil {
  195. return "", err
  196. }
  197. fmt.Fprintf(c.App.ErrWriter, "\r%s\rConfirm: ", strings.Repeat(" ", 25))
  198. confirm, err := util.ReadPassword(c.App.Reader)
  199. if err != nil {
  200. return "", err
  201. }
  202. fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 25))
  203. if subtle.ConstantTimeCompare(confirm, password) != 1 {
  204. return "", errors.New("passwords do not match: try it again, but this time type slooowwwlly")
  205. }
  206. return string(password), nil
  207. }
  208. func userCommandFlags() []cli.Flag {
  209. return []cli.Flag{
  210. &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
  211. altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
  212. altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
  213. }
  214. }