From 3b3bc4c7751f2996a5bdd772a3a09c5e1e8dc84e Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 20 Apr 2026 18:55:51 +0200 Subject: [PATCH] feat(cli): add user set-admin command (license-gated) --- pkg/cmd/user.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/user.go b/pkg/cmd/user.go index abd7c8c27..371cdcfdd 100644 --- a/pkg/cmd/user.go +++ b/pkg/cmd/user.go @@ -26,6 +26,7 @@ import ( "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/initialize" + "code.vikunja.io/api/pkg/license" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" @@ -48,6 +49,8 @@ var ( userFlagDisableUser bool userFlagDeleteNow bool userFlagDeleteConfirm bool + userFlagMakeAdmin bool + userFlagRemoveAdmin bool ) func init() { @@ -81,10 +84,68 @@ func init() { // Bypass confirm prompt userDeleteCmd.Flags().BoolVarP(&userFlagDeleteConfirm, "confirm", "c", false, "Bypasses any prompts confirming the deletion request, use with caution!") - userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeStatusCmd, userDeleteCmd) + userSetAdminCmd.Flags().BoolVar(&userFlagMakeAdmin, "admin", false, "Promote the user to instance admin.") + userSetAdminCmd.Flags().BoolVar(&userFlagRemoveAdmin, "no-admin", false, "Revoke instance admin from the user.") + userSetAdminCmd.MarkFlagsMutuallyExclusive("admin", "no-admin") + userSetAdminCmd.MarkFlagsOneRequired("admin", "no-admin") + + userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeStatusCmd, userDeleteCmd, userSetAdminCmd) rootCmd.AddCommand(userCmd) } +func setUserAdmin(s *xorm.Session, identifier string, value bool) error { + filter := &user.User{} + id, err := strconv.ParseInt(identifier, 10, 64) + if err != nil { + filter.Username = identifier + } else { + filter.ID = id + } + u, err := user.GetUserWithEmail(s, filter) + if err != nil && !user.IsErrUserStatusError(err) { + return err + } + if !value { + if err := user.GuardLastAdmin(s, u); err != nil { + return err + } + } + u.IsAdmin = value + _, err = s.ID(u.ID).Cols("is_admin").Update(u) + return err +} + +var userSetAdminCmd = &cobra.Command{ + Use: "set-admin [username-or-id]", + Short: "Set or remove the instance-admin flag on a user.", + Args: cobra.ExactArgs(1), + PreRun: func(_ *cobra.Command, _ []string) { + initialize.FullInit() + }, + Run: func(_ *cobra.Command, args []string) { + // Refuse on a free instance; the is_admin bypass is gated by the admin-panel entitlement everywhere else. + if !license.IsFeatureEnabled(license.FeatureAdminPanel) { + log.Fatalf("The admin-panel license feature is not active; refusing to change the is_admin flag.") + } + + s := db.NewSession() + defer s.Close() + value := userFlagMakeAdmin + if err := setUserAdmin(s, args[0], value); err != nil { + _ = s.Rollback() + log.Fatalf("Could not update admin flag: %s", err) + } + if err := s.Commit(); err != nil { + log.Fatalf("Could not commit: %s", err) + } + if value { + fmt.Printf("User %q is now an instance admin.\n", args[0]) + } else { + fmt.Printf("User %q is no longer an instance admin.\n", args[0]) + } + }, +} + func getPasswordFromFlagOrInput() (pw string) { pw = userFlagPassword if userFlagPassword == "" { @@ -376,6 +437,10 @@ var userDeleteCmd = &cobra.Command{ u := getUserFromArg(s, args[0]) if userFlagDeleteNow { + if err := user.GuardLastAdmin(s, u); err != nil { + _ = s.Rollback() + log.Fatalf("Error removing the user: %s", err) + } err := models.DeleteUser(s, u) if err != nil { _ = s.Rollback()