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-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/auth"
"heckel.io/ntfy/util"
)
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 ,
& 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" } ,
altsrc . NewStringFlag ( & cli . StringFlag { Name : "auth-file" , Aliases : [ ] string { "H" } , EnvVars : [ ] string { "NTFY_AUTH_FILE" } , Usage : "auth database file used for access control" } ) ,
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" } ) ,
)
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-02-02 05:39:57 +01:00
UsageText : "ntfy user add [--role=admin|user] USERNAME" ,
Action : execUserAdd ,
2022-01-23 06:54:18 +01:00
Flags : [ ] cli . Flag {
& cli . StringFlag { Name : "role" , Aliases : [ ] string { "r" } , Value : string ( auth . RoleUser ) , Usage : "user role" } ,
2022-06-20 19:19:54 +02:00
& cli . StringFlag { Name : "password" , Aliases : [ ] string { "p" } , EnvVars : [ ] string { "NTFY_PASSWORD" } , Usage : "user password" } ,
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 :
ntfy user add phil # Add regular user phil
ntfy user add -- role = admin phil # Add admin user phil
` ,
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-02-02 05:39:57 +01:00
UsageText : "ntfy user change-pass USERNAME" ,
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 :
ntfy user change - pass phil
` ,
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
` ,
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-02-04 02:07:23 +01:00
ntfy user list # Shows list of users ( alias : ' ntfy access ' )
2022-02-02 05:39:57 +01:00
ntfy user add phil # Add regular user phil
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 user change - role phil admin # Make user phil an admin
` ,
2022-01-23 06:54:18 +01:00
}
func execUserAdd ( c * cli . Context ) error {
2022-06-20 19:19:54 +02:00
username = c . Args ( ) . Get ( 0 )
2022-06-20 19:25:31 +02:00
role := auth . Role ( c . String ( "role" ) )
password := c . String ( "user" )
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" )
} else if ! auth . AllowedRole ( role ) {
return errors . New ( "role must be either 'user' or 'admin'" )
}
2022-06-20 19:24:42 +02:00
manager , err := createAuthManager ( c )
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 the password env var was not set, read it from stdin
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
}
manager , err := createAuthManager ( c )
if err != nil {
return err
}
2022-02-03 22:10:15 +01:00
if _ , err := manager . User ( username ) ; err == auth . ErrNotFound {
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 )
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-02-03 22:10:15 +01:00
manager , err := createAuthManager ( c )
2022-01-23 06:54:18 +01:00
if err != nil {
return err
}
2022-02-03 22:10:15 +01:00
if _ , err := manager . User ( username ) ; err == auth . ErrNotFound {
return fmt . Errorf ( "user %s does not exist" , username )
}
2022-02-04 02:20:50 +01:00
password , err := readPasswordAndConfirm ( c )
2022-01-23 06:54:18 +01:00
if err != nil {
return err
}
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 )
role := auth . Role ( c . Args ( ) . Get ( 1 ) )
if username == "" || ! auth . AllowedRole ( role ) {
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
}
manager , err := createAuthManager ( c )
if err != nil {
return err
}
2022-02-03 22:10:15 +01:00
if _ , err := manager . User ( username ) ; err == auth . ErrNotFound {
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
}
2022-01-24 05:02:39 +01:00
func execUserList ( c * cli . Context ) error {
manager , err := createAuthManager ( c )
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-01-23 06:54:18 +01:00
func createAuthManager ( c * cli . Context ) ( auth . Manager , error ) {
authFile := c . String ( "auth-file" )
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" )
2022-02-02 05:39:57 +01:00
} else if ! util . InStringList ( [ ] string { "read-write" , "read-only" , "write-only" , "deny-all" } , authDefaultAccess ) {
2022-01-23 07:00:38 +01:00
return nil , errors . New ( "if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'" )
2022-01-23 06:54:18 +01:00
}
2022-01-23 07:00:38 +01:00
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
2022-02-02 05:39:57 +01:00
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
2022-01-23 06:54:18 +01:00
return auth . NewSQLiteAuth ( authFile , authDefaultRead , authDefaultWrite )
}
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
}