"ntfy token" CLI

pull/600/head
binwiederhier 2023-01-29 21:42:40 -05:00
parent c12ecb9f21
commit d717bf39ac
4 changed files with 219 additions and 6 deletions

212
cmd/token.go 100644
View File

@ -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
}

View File

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

View File

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

View File

@ -27,7 +27,7 @@ const (
userHardDeleteAfterDuration = 7 * 24 * time.Hour userHardDeleteAfterDuration = 7 * 24 * time.Hour
tokenPrefix = "tk_" tokenPrefix = "tk_"
tokenLength = 32 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 // Default constants that may be overridden by configs