2022-05-21 15:46:49 +02:00
//go:build !noserver
2022-01-23 06:54:18 +01:00
package cmd
import (
"crypto/subtle"
"errors"
"fmt"
2022-12-25 17:41:38 +01:00
"heckel.io/ntfy/user"
2022-06-20 22:03:39 +02:00
"os"
2022-06-15 17:42:22 +02:00
"strings"
2022-01-23 06:54:18 +01:00
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/util"
)
2023-01-09 21:40:46 +01:00
const (
tierReset = "-"
)
2022-05-09 17:03:40 +02:00
func init ( ) {
commands = append ( commands , cmdUser )
}
2022-05-30 04:14:14 +02:00
var flagsUser = append (
flagsDefault ,
2022-06-25 01:13:10 +02:00
& cli . StringFlag { Name : "config" , Aliases : [ ] string { "c" } , EnvVars : [ ] string { "NTFY_CONFIG_FILE" } , Value : defaultServerConfigFile , DefaultText : defaultServerConfigFile , Usage : "config file" } ,
altsrc . NewStringFlag ( & cli . StringFlag { Name : "auth-file" , Aliases : [ ] string { "auth_file" , "H" } , EnvVars : [ ] string { "NTFY_AUTH_FILE" } , Usage : "auth database file used for access control" } ) ,
altsrc . NewStringFlag ( & cli . StringFlag { Name : "auth-default-access" , Aliases : [ ] string { "auth_default_access" , "p" } , EnvVars : [ ] string { "NTFY_AUTH_DEFAULT_ACCESS" } , Value : "read-write" , Usage : "default permissions if no matching entries in the auth database are found" } ) ,
2022-05-30 04:14:14 +02:00
)
2022-05-09 17:03:40 +02:00
2022-01-23 06:54:18 +01:00
var cmdUser = & cli . Command {
Name : "user" ,
2022-02-02 05:39:57 +01:00
Usage : "Manage/show users" ,
UsageText : "ntfy user [list|add|remove|change-pass|change-role] ..." ,
2022-01-23 06:54:18 +01:00
Flags : flagsUser ,
2022-06-01 22:57:35 +02:00
Before : initConfigFileInputSourceFunc ( "config" , flagsUser , initLogFunc ) ,
2022-01-23 07:00:38 +01:00
Category : categoryServer ,
2022-01-23 06:54:18 +01:00
Subcommands : [ ] * cli . Command {
{
2022-02-02 05:39:57 +01:00
Name : "add" ,
Aliases : [ ] string { "a" } ,
2022-02-03 22:10:15 +01:00
Usage : "Adds a new user" ,
2022-06-20 22:03:39 +02:00
UsageText : "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME" ,
2022-02-02 05:39:57 +01:00
Action : execUserAdd ,
2022-01-23 06:54:18 +01:00
Flags : [ ] cli . Flag {
2022-12-25 17:41:38 +01:00
& cli . StringFlag { Name : "role" , Aliases : [ ] string { "r" } , Value : string ( user . RoleUser ) , Usage : "user role" } ,
2022-01-23 06:54:18 +01:00
} ,
2022-02-02 05:39:57 +01:00
Description : ` Add a new user to the ntfy user database .
A user can be either a regular user , or an admin . A regular user has no read or write access ( unless
granted otherwise by the auth - default - access setting ) . An admin user has read and write access to all
topics .
Examples :
2022-06-20 22:03:39 +02:00
ntfy user add phil # Add regular user phil
ntfy user add -- role = admin phil # Add admin user phil
NTFY_PASSWORD = ... ntfy user add phil # Add user , using env variable to set password ( for scripts )
You may set the NTFY_PASSWORD environment variable to pass the password . This is useful if
you are creating users via scripts .
2022-02-02 05:39:57 +01:00
` ,
2022-01-23 06:54:18 +01:00
} ,
{
2022-02-02 05:39:57 +01:00
Name : "remove" ,
Aliases : [ ] string { "del" , "rm" } ,
2022-02-03 22:10:15 +01:00
Usage : "Removes a user" ,
2022-02-02 05:39:57 +01:00
UsageText : "ntfy user remove USERNAME" ,
Action : execUserDel ,
Description : ` Remove a user from the ntfy user database .
Example :
ntfy user del phil
` ,
2022-01-23 06:54:18 +01:00
} ,
{
2022-02-02 05:39:57 +01:00
Name : "change-pass" ,
Aliases : [ ] string { "chp" } ,
2022-02-03 22:10:15 +01:00
Usage : "Changes a user's password" ,
2022-06-20 22:03:39 +02:00
UsageText : "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME" ,
2022-02-02 05:39:57 +01:00
Action : execUserChangePass ,
Description : ` Change the password for the given user .
The new password will be read from STDIN , and it ' ll be confirmed by typing
it twice .
Example :
2022-06-20 22:03:39 +02:00
ntfy user change - pass phil
NTFY_PASSWORD = . . ntfy user change - pass phil
You may set the NTFY_PASSWORD environment variable to pass the new password . This is
useful if you are updating users via scripts .
2022-02-02 05:39:57 +01:00
` ,
2022-01-23 06:54:18 +01:00
} ,
2022-01-23 21:30:30 +01:00
{
2022-02-02 05:39:57 +01:00
Name : "change-role" ,
Aliases : [ ] string { "chr" } ,
2022-02-03 22:10:15 +01:00
Usage : "Changes the role of a user" ,
2022-02-02 05:39:57 +01:00
UsageText : "ntfy user change-role USERNAME ROLE" ,
Action : execUserChangeRole ,
Description : ` Change the role for the given user to admin or user .
This command can be used to change the role of a user either from a regular user
to an admin user , or the other way around :
- admin : an admin has read / write access to all topics
- user : a regular user only has access to what was explicitly granted via ' ntfy access '
When changing the role of a user to "admin" , all access control entries for that
user are removed , since they are no longer necessary .
Example :
ntfy user change - role phil admin # Make user phil an admin
ntfy user change - role phil user # Remove admin role from user phil
2023-01-09 21:40:46 +01:00
` ,
} ,
{
Name : "change-tier" ,
Aliases : [ ] string { "cht" } ,
Usage : "Changes the tier of a user" ,
UsageText : "ntfy user change-tier USERNAME (TIER|-)" ,
Action : execUserChangeTier ,
Description : ` Change the tier for the given user .
This command can be used to change the tier of a user . Tiers define usage limits , such
as messages per day , attachment file sizes , etc .
Example :
ntfy user change - tier phil pro # Change tier to "pro" for user "phil"
ntfy user change - tier phil - # Remove tier from user "phil" entirely
2022-02-02 05:39:57 +01:00
` ,
2022-01-23 21:30:30 +01:00
} ,
{
Name : "list" ,
2022-02-02 05:39:57 +01:00
Aliases : [ ] string { "l" } ,
2022-02-03 22:10:15 +01:00
Usage : "Shows a list of users" ,
2022-01-24 05:02:39 +01:00
Action : execUserList ,
2022-02-04 02:07:23 +01:00
Description : ` Shows a list of all configured users , including the everyone ( '*' ) user .
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 .
This command is an alias to calling ' ntfy access ' ( display access control list ) .
` ,
2022-01-23 21:30:30 +01:00
} ,
2022-01-23 06:54:18 +01:00
} ,
2022-02-02 05:39:57 +01:00
Description : ` Manage users of the ntfy server .
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 . Please also refer
to the related command ' ntfy access ' .
The command allows you to add / remove / change users in the ntfy user database , as well as change
passwords or roles .
Examples :
2022-06-20 22:03:39 +02:00
ntfy user list # Shows list of users ( alias : ' ntfy access ' )
ntfy user add phil # Add regular user phil
NTFY_PASSWORD = ... ntfy user add phil # As above , using env variable to set password ( for scripts )
ntfy user add -- role = admin phil # Add admin user phil
ntfy user del phil # Delete user phil
ntfy user change - pass phil # Change password for user phil
NTFY_PASSWORD = . . ntfy user change - pass phil # As above , using env variable to set password ( for scripts )
ntfy user change - role phil admin # Make user phil an admin
For the ' ntfy user add ' and ' ntfy user change - pass ' commands , you may set the NTFY_PASSWORD environment
variable to pass the new password . This is useful if you are creating / updating users via scripts .
2022-02-02 05:39:57 +01:00
` ,
2022-01-23 06:54:18 +01:00
}
func execUserAdd ( c * cli . Context ) error {
2022-06-20 19:26:13 +02:00
username := c . Args ( ) . Get ( 0 )
2022-12-25 17:41:38 +01:00
role := user . Role ( c . String ( "role" ) )
2022-06-20 22:03:39 +02:00
password := os . Getenv ( "NTFY_PASSWORD" )
2022-06-20 19:19:54 +02:00
if username == "" {
return errors . New ( "username expected, type 'ntfy user add --help' for help" )
} else if username == userEveryone {
return errors . New ( "username not allowed" )
2022-12-25 17:41:38 +01:00
} else if ! user . AllowedRole ( role ) {
2022-06-20 19:19:54 +02:00
return errors . New ( "role must be either 'user' or 'admin'" )
}
2022-12-28 04:14:14 +01:00
manager , err := createUserManager ( c )
2022-06-20 19:24:42 +02:00
if err != nil {
return err
}
if user , _ := manager . User ( username ) ; user != nil {
return fmt . Errorf ( "user %s already exists" , username )
}
2022-06-20 19:19:54 +02:00
if password == "" {
2022-06-15 17:42:22 +02:00
p , err := readPasswordAndConfirm ( c )
if err != nil {
return err
}
2022-06-20 19:19:54 +02:00
2022-06-15 17:42:22 +02:00
password = p
2022-01-23 06:54:18 +01:00
}
2022-02-03 22:10:15 +01:00
if err := manager . AddUser ( username , password , role ) ; err != nil {
2022-01-23 06:54:18 +01:00
return err
}
2022-02-03 22:10:15 +01:00
fmt . Fprintf ( c . App . ErrWriter , "user %s added with role %s\n" , username , role )
2022-01-23 06:54:18 +01:00
return nil
}
func execUserDel ( c * cli . Context ) error {
2022-01-23 21:30:30 +01:00
username := c . Args ( ) . Get ( 0 )
if username == "" {
2022-01-23 06:54:18 +01:00
return errors . New ( "username expected, type 'ntfy user del --help' for help" )
2022-02-02 05:39:57 +01:00
} else if username == userEveryone {
return errors . New ( "username not allowed" )
2022-01-23 06:54:18 +01:00
}
2022-12-28 04:14:14 +01:00
manager , err := createUserManager ( c )
2022-01-23 06:54:18 +01:00
if err != nil {
return err
}
2022-12-25 17:41:38 +01:00
if _ , err := manager . User ( username ) ; err == user . ErrNotFound {
2022-02-03 22:10:15 +01:00
return fmt . Errorf ( "user %s does not exist" , username )
}
2022-01-23 06:54:18 +01:00
if err := manager . RemoveUser ( username ) ; err != nil {
return err
}
2022-02-03 22:10:15 +01:00
fmt . Fprintf ( c . App . ErrWriter , "user %s removed\n" , username )
2022-01-23 06:54:18 +01:00
return nil
}
func execUserChangePass ( c * cli . Context ) error {
2022-01-23 21:30:30 +01:00
username := c . Args ( ) . Get ( 0 )
2022-06-20 22:03:39 +02:00
password := os . Getenv ( "NTFY_PASSWORD" )
2022-01-23 21:30:30 +01:00
if username == "" {
2022-01-23 06:54:18 +01:00
return errors . New ( "username expected, type 'ntfy user change-pass --help' for help" )
2022-02-02 05:39:57 +01:00
} else if username == userEveryone {
return errors . New ( "username not allowed" )
2022-01-23 06:54:18 +01:00
}
2022-12-28 04:14:14 +01:00
manager , err := createUserManager ( c )
2022-01-23 06:54:18 +01:00
if err != nil {
return err
}
2022-12-25 17:41:38 +01:00
if _ , err := manager . User ( username ) ; err == user . ErrNotFound {
2022-02-03 22:10:15 +01:00
return fmt . Errorf ( "user %s does not exist" , username )
}
2022-06-20 22:03:39 +02:00
if password == "" {
password , err = readPasswordAndConfirm ( c )
if err != nil {
return err
}
2022-01-23 06:54:18 +01:00
}
if err := manager . ChangePassword ( username , password ) ; err != nil {
return err
}
2022-02-03 22:10:15 +01:00
fmt . Fprintf ( c . App . ErrWriter , "changed password for user %s\n" , username )
2022-01-23 06:54:18 +01:00
return nil
}
2022-01-23 21:30:30 +01:00
func execUserChangeRole ( c * cli . Context ) error {
username := c . Args ( ) . Get ( 0 )
2022-12-25 17:41:38 +01:00
role := user . Role ( c . Args ( ) . Get ( 1 ) )
if username == "" || ! user . AllowedRole ( role ) {
2022-01-23 21:30:30 +01:00
return errors . New ( "username and new role expected, type 'ntfy user change-role --help' for help" )
2022-02-02 05:39:57 +01:00
} else if username == userEveryone {
return errors . New ( "username not allowed" )
2022-01-23 21:30:30 +01:00
}
2022-12-28 04:14:14 +01:00
manager , err := createUserManager ( c )
2022-01-23 21:30:30 +01:00
if err != nil {
return err
}
2022-12-25 17:41:38 +01:00
if _ , err := manager . User ( username ) ; err == user . ErrNotFound {
2022-02-03 22:10:15 +01:00
return fmt . Errorf ( "user %s does not exist" , username )
}
2022-01-23 21:30:30 +01:00
if err := manager . ChangeRole ( username , role ) ; err != nil {
return err
}
2022-02-03 22:10:15 +01:00
fmt . Fprintf ( c . App . ErrWriter , "changed role for user %s to %s\n" , username , role )
2022-01-23 21:30:30 +01:00
return nil
}
2023-01-09 21:40:46 +01:00
func execUserChangeTier ( c * cli . Context ) error {
username := c . Args ( ) . Get ( 0 )
tier := c . Args ( ) . Get ( 1 )
if username == "" {
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 {
return errors . New ( "username not allowed" )
}
manager , err := createUserManager ( c )
if err != nil {
return err
}
if _ , err := manager . User ( username ) ; err == user . ErrNotFound {
return fmt . Errorf ( "user %s does not exist" , username )
}
if tier == tierReset {
if err := manager . ResetTier ( username ) ; err != nil {
return err
}
fmt . Fprintf ( c . App . ErrWriter , "removed tier from user %s\n" , username )
} else {
if err := manager . ChangeTier ( username , tier ) ; err != nil {
return err
}
fmt . Fprintf ( c . App . ErrWriter , "changed tier for user %s to %s\n" , username , tier )
}
return nil
}
2022-01-24 05:02:39 +01:00
func execUserList ( c * cli . Context ) error {
2022-12-28 04:14:14 +01:00
manager , err := createUserManager ( c )
2022-01-24 05:02:39 +01:00
if err != nil {
return err
}
users , err := manager . Users ( )
if err != nil {
return err
}
2022-01-24 06:54:28 +01:00
return showUsers ( c , manager , users )
2022-01-24 05:02:39 +01:00
}
2022-12-28 04:14:14 +01:00
func createUserManager ( c * cli . Context ) ( * user . Manager , error ) {
2022-01-23 06:54:18 +01:00
authFile := c . String ( "auth-file" )
2023-01-05 21:20:44 +01:00
authStartupQueries := c . String ( "auth-startup-queries" )
2022-01-23 07:00:38 +01:00
authDefaultAccess := c . String ( "auth-default-access" )
2022-01-23 06:54:18 +01:00
if authFile == "" {
return nil , errors . New ( "option auth-file not set; auth is unconfigured for this server" )
} else if ! util . FileExists ( authFile ) {
return nil , errors . New ( "auth-file does not exist; please start the server at least once to create it" )
}
2023-01-03 03:12:42 +01:00
authDefault , err := user . ParsePermission ( authDefaultAccess )
if err != nil {
return nil , errors . New ( "if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'" )
}
2023-01-05 21:20:44 +01:00
return user . NewManager ( authFile , authStartupQueries , authDefault )
2022-01-23 06:54:18 +01:00
}
2022-02-04 02:20:50 +01:00
func readPasswordAndConfirm ( c * cli . Context ) ( string , error ) {
fmt . Fprint ( c . App . ErrWriter , "password: " )
2022-01-23 06:54:18 +01:00
password , err := util . ReadPassword ( c . App . Reader )
if err != nil {
return "" , err
}
2022-02-04 02:20:50 +01:00
fmt . Fprintf ( c . App . ErrWriter , "\r%s\rconfirm: " , strings . Repeat ( " " , 25 ) )
2022-01-23 06:54:18 +01:00
confirm , err := util . ReadPassword ( c . App . Reader )
if err != nil {
return "" , err
}
fmt . Fprintf ( c . App . ErrWriter , "\r%s\r" , strings . Repeat ( " " , 25 ) )
if subtle . ConstantTimeCompare ( confirm , password ) != 1 {
return "" , errors . New ( "passwords do not match: try it again, but this time type slooowwwlly" )
}
return string ( password ) , nil
}