binwiederhier 3 лет назад
Родитель
Сommit
d717bf39ac
4 измененных файлов с 219 добавлено и 6 удалено
  1. 212 0
      cmd/token.go
  2. 5 5
      cmd/user.go
  3. 1 0
      server/server.go
  4. 1 1
      user/manager.go

+ 212 - 0
cmd/token.go

@@ -0,0 +1,212 @@
+//go:build !noserver
+
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"github.com/urfave/cli/v2"
+	"heckel.io/ntfy/user"
+	"heckel.io/ntfy/util"
+	"net/netip"
+	"time"
+)
+
+func init() {
+	commands = append(commands, cmdToken)
+}
+
+var flagsToken = append(
+	flagsUser,
+)
+
+var cmdToken = &cli.Command{
+	Name:      "token",
+	Usage:     "Create, list or delete user tokens",
+	UsageText: "ntfy token [list|add|remove] ...",
+	Flags:     flagsToken,
+	Before:    initConfigFileInputSourceFunc("config", flagsToken, initLogFunc),
+	Category:  categoryServer,
+	Subcommands: []*cli.Command{
+		{
+			Name:      "add",
+			Aliases:   []string{"a"},
+			Usage:     "Create a new token",
+			UsageText: "ntfy token add [--expires=<duration>] [--label=..] USERNAME",
+			Action:    execTokenAdd,
+			Flags: []cli.Flag{
+				&cli.StringFlag{Name: "expires", Aliases: []string{"e"}, Value: "", Usage: "token expires after"},
+				&cli.StringFlag{Name: "label", Aliases: []string{"l"}, Value: "", Usage: "token label"},
+			},
+			Description: `Create a new user access token.
+
+User access tokens can be used to publish, subscribe, or perform any other user-specific tasks.
+Tokens have full access, and can perform any task a user can do. They are meant to be used to 
+avoid spreading the password to various places.
+
+Examples:
+  ntfy token add phil                   # Create token for user phil which never expires
+  ntfy token add --expires=2d phil      # Create token for user phil which expires in 2 days
+  ntfy token add -e "tuesday, 8pm" phil # Create token for user phil which expires next Tuesday
+  ntfy token add -l backups phil        # Create token for user phil with label "backups"
+`,
+		},
+		{
+			Name:      "remove",
+			Aliases:   []string{"del", "rm"},
+			Usage:     "Removes a token",
+			UsageText: "ntfy token remove USERNAME TOKEN",
+			Action:    execTokenDel,
+			Description: `Remove a token from the ntfy user database.
+
+Example:
+  ntfy token del phil tk_th2srHVlxrANQHAso5t0HuQ1J1TjN
+`,
+		},
+		{
+			Name:    "list",
+			Aliases: []string{"l"},
+			Usage:   "Shows a list of tokens",
+			Action:  execTokenList,
+			Description: `Shows a list of all tokens.
+
+This is a server-only command. It directly reads from the user.db as defined in the server config
+file server.yml. The command only works if 'auth-file' is properly defined.
+`,
+		},
+	},
+	Description: `Manage access tokens for individual users.
+
+User access tokens can be used to publish, subscribe, or perform any other user-specific tasks.
+Tokens have full access, and can perform any task a user can do. They are meant to be used to 
+avoid spreading the password to various places.
+
+This is a server-only command. It directly manages the user.db as defined in the server config
+file server.yml. The command only works if 'auth-file' is properly defined.
+
+Examples:
+  ntfy token list                               # Shows list of tokens for all users
+  ntfy token list phil                          # Shows list of tokens for user phil
+  ntfy token add phil                           # Create token for user phil which never expires
+  ntfy token add --expires=2d phil              # Create token for user phil which expires in 2 days
+  ntfy token del phil tk_th2srHVlxr...          # Delete token
+`,
+}
+
+func execTokenAdd(c *cli.Context) error {
+	username := c.Args().Get(0)
+	expiresStr := c.String("expires")
+	label := c.String("label")
+	if username == "" {
+		return errors.New("username expected, type 'ntfy token add --help' for help")
+	} else if username == userEveryone || username == user.Everyone {
+		return errors.New("username not allowed")
+	}
+	expires := time.Unix(0, 0)
+	if expiresStr != "" {
+		var err error
+		expires, err = util.ParseFutureTime(expiresStr, time.Now())
+		if err != nil {
+			return err
+		}
+	}
+	manager, err := createUserManager(c)
+	if err != nil {
+		return err
+	}
+	u, err := manager.User(username)
+	if err == user.ErrUserNotFound {
+		return fmt.Errorf("user %s does not exist", username)
+	} else if err != nil {
+		return err
+	}
+	token, err := manager.CreateToken(u.ID, label, expires, netip.IPv4Unspecified())
+	if err != nil {
+		return err
+	}
+	if expires.Unix() == 0 {
+		fmt.Fprintf(c.App.ErrWriter, "token %s created for user %s, never expires\n", token.Value, u.Name)
+	} else {
+		fmt.Fprintf(c.App.ErrWriter, "token %s created for user %s, expires %v\n", token.Value, u.Name, expires.Format(time.UnixDate))
+	}
+	return nil
+}
+
+func execTokenDel(c *cli.Context) error {
+	username, token := c.Args().Get(0), c.Args().Get(1)
+	if username == "" || token == "" {
+		return errors.New("username and token expected, type 'ntfy token del --help' for help")
+	} else if username == userEveryone || username == user.Everyone {
+		return errors.New("username not allowed")
+	}
+	manager, err := createUserManager(c)
+	if err != nil {
+		return err
+	}
+	u, err := manager.User(username)
+	if err == user.ErrUserNotFound {
+		return fmt.Errorf("user %s does not exist", username)
+	} else if err != nil {
+		return err
+	}
+	if err := manager.RemoveToken(u.ID, token); err != nil {
+		return err
+	}
+	fmt.Fprintf(c.App.ErrWriter, "token %s for user %s removed\n", token, username)
+	return nil
+}
+
+func execTokenList(c *cli.Context) error {
+	username := c.Args().Get(0)
+	if username == userEveryone || username == user.Everyone {
+		return errors.New("username not allowed")
+	}
+	manager, err := createUserManager(c)
+	if err != nil {
+		return err
+	}
+	var users []*user.User
+	if username != "" {
+		u, err := manager.User(username)
+		if err == user.ErrUserNotFound {
+			return fmt.Errorf("user %s does not exist", username)
+		} else if err != nil {
+			return err
+		}
+		users = append(users, u)
+	} else {
+		users, err = manager.Users()
+		if err != nil {
+			return err
+		}
+	}
+	if len(users) == 0 {
+		fmt.Fprintf(c.App.ErrWriter, "no users\n")
+	} else {
+		for _, u := range users {
+			tokens, err := manager.Tokens(u.ID)
+			if err != nil {
+				return err
+			} else if len(tokens) == 0 && username != "" {
+				fmt.Fprintf(c.App.ErrWriter, "user %s has no access tokens\n", username)
+				return nil
+			} else if len(tokens) == 0 {
+				continue
+			}
+			fmt.Fprintf(c.App.ErrWriter, "user %s\n", u.Name)
+			for _, t := range tokens {
+				var label, expires string
+				if t.Label != "" {
+					label = fmt.Sprintf(" (%s)", t.Label)
+				}
+				if t.Expires.Unix() == 0 {
+					expires = "never expires"
+				} else {
+					expires = fmt.Sprintf("expires %s", t.Expires.Format(time.RFC822))
+				}
+				fmt.Fprintf(c.App.ErrWriter, "- %s%s, %s, accessed from %s at %s\n", t.Value, label, expires, t.LastOrigin.String(), t.LastAccess.Format(time.RFC822))
+			}
+		}
+	}
+	return nil
+}

+ 5 - 5
cmd/user.go

@@ -176,7 +176,7 @@ func execUserAdd(c *cli.Context) error {
 	password := os.Getenv("NTFY_PASSWORD")
 	if username == "" {
 		return errors.New("username expected, type 'ntfy user add --help' for help")
-	} else if username == userEveryone {
+	} else if username == userEveryone || username == user.Everyone {
 		return errors.New("username not allowed")
 	} else if !user.AllowedRole(role) {
 		return errors.New("role must be either 'user' or 'admin'")
@@ -207,7 +207,7 @@ func execUserDel(c *cli.Context) error {
 	username := c.Args().Get(0)
 	if username == "" {
 		return errors.New("username expected, type 'ntfy user del --help' for help")
-	} else if username == userEveryone {
+	} else if username == userEveryone || username == user.Everyone {
 		return errors.New("username not allowed")
 	}
 	manager, err := createUserManager(c)
@@ -229,7 +229,7 @@ func execUserChangePass(c *cli.Context) error {
 	password := os.Getenv("NTFY_PASSWORD")
 	if username == "" {
 		return errors.New("username expected, type 'ntfy user change-pass --help' for help")
-	} else if username == userEveryone {
+	} else if username == userEveryone || username == user.Everyone {
 		return errors.New("username not allowed")
 	}
 	manager, err := createUserManager(c)
@@ -257,7 +257,7 @@ func execUserChangeRole(c *cli.Context) error {
 	role := user.Role(c.Args().Get(1))
 	if username == "" || !user.AllowedRole(role) {
 		return errors.New("username and new role expected, type 'ntfy user change-role --help' for help")
-	} else if username == userEveryone {
+	} else if username == userEveryone || username == user.Everyone {
 		return errors.New("username not allowed")
 	}
 	manager, err := createUserManager(c)
@@ -281,7 +281,7 @@ func execUserChangeTier(c *cli.Context) error {
 		return errors.New("username and new tier expected, type 'ntfy user change-tier --help' for help")
 	} else if !user.AllowedTier(tier) && tier != tierReset {
 		return errors.New("invalid tier, must be tier code, or - to reset")
-	} else if username == userEveryone {
+	} else if username == userEveryone || username == user.Everyone {
 		return errors.New("username not allowed")
 	}
 	manager, err := createUserManager(c)

+ 1 - 0
server/server.go

@@ -36,6 +36,7 @@ import (
 
 - HIGH Rate limiting: Sensitive endpoints (account/login/change-password/...)
 - HIGH Docs
+- HIGH CLI
 - MEDIUM: Test new token endpoints & never-expiring token
 - MEDIUM: Make sure account endpoints make sense for admins
 - MEDIUM: Reservation (UI): Show "This topic is reserved" error message when trying to reserve a reserved topic (Thorben)

+ 1 - 1
user/manager.go

@@ -27,7 +27,7 @@ const (
 	userHardDeleteAfterDuration     = 7 * 24 * time.Hour
 	tokenPrefix                     = "tk_"
 	tokenLength                     = 32
-	tokenMaxCount                   = 10 // Only keep this many tokens in the table per user
+	tokenMaxCount                   = 20 // Only keep this many tokens in the table per user
 )
 
 // Default constants that may be overridden by configs