Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ D8 provides comprehensive cluster management capabilities:
| [**backup**](internal/backup/) | Backup operations | ETCD snapshots, configuration backups, data export |
| [**mirror**](internal/mirror/) | Module mirroring | Registry operations, image synchronization, air-gapped deployments |
| [**system**](internal/system/) | System diagnostics | Debug info collection, logs analysis, troubleshooting |
| **user-operation** | Local user operations | Request `UserOperation` in `user-authn` (ResetPassword/Reset2FA/Lock/Unlock) |

### 🚀 Module Management

Expand Down Expand Up @@ -140,6 +141,27 @@ d8 --version

---

## 🧰 User management (user-authn)

Manage Dex static users via `UserOperation` custom resources.

```bash
# Reset user's 2FA (TOTP)
d8 user reset2fa test-user --timeout 5m

# Lock user for 10 minutes
d8 user lock test-user 10m --timeout 5m

# Unlock user
d8 user unlock test-user --timeout 5m

# Reset password (bcrypt hash is required)
HASH="$(echo -n 'Test12345!' | htpasswd -BinC 10 \"\" | cut -d: -f2 | tr -d '\n')"
d8 user reset-password test-user "$HASH" --timeout 5m
```

---

## 🤝 Contributing

We welcome contributions! Here's how you can help:
Expand Down
2 changes: 2 additions & 0 deletions cmd/d8/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
status "github.com/deckhouse/deckhouse-cli/internal/status/cmd"
system "github.com/deckhouse/deckhouse-cli/internal/system/cmd"
"github.com/deckhouse/deckhouse-cli/internal/tools"
useroperation "github.com/deckhouse/deckhouse-cli/internal/useroperation/cmd"
"github.com/deckhouse/deckhouse-cli/internal/version"
)

Expand Down Expand Up @@ -104,6 +105,7 @@ func (r *RootCommand) registerCommands() {
r.cmd.AddCommand(data.NewCommand())
r.cmd.AddCommand(mirror.NewCommand())
r.cmd.AddCommand(status.NewCommand())
r.cmd.AddCommand(useroperation.NewCommand())
r.cmd.AddCommand(tools.NewCommand())
r.cmd.AddCommand(commands.NewVirtualizationCommand())
r.cmd.AddCommand(commands.NewKubectlCommand())
Expand Down
83 changes: 83 additions & 0 deletions internal/useroperation/cmd/lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package useroperation

import (
"fmt"
"time"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func newLockCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "lock <username> <lockDuration>",
Short: "Lock local user in Dex for a period of time",
Args: cobra.ExactArgs(2),
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
username := args[0]
lockDuration := args[1]
// Validate duration format (must be parseable by time.ParseDuration; supports s/m/h).
if _, err := time.ParseDuration(lockDuration); err != nil {
return fmt.Errorf("invalid lockDuration %q: %w", lockDuration, err)
}

wf, err := getWaitFlags(cmd)
if err != nil {
return err
}

dyn, err := newDynamicClient(cmd)
if err != nil {
return err
}

name := fmt.Sprintf("op-lock-%d", time.Now().Unix())
obj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "deckhouse.io/v1",
"kind": "UserOperation",
"metadata": map[string]any{
"name": name,
},
"spec": map[string]any{
"user": username,
"type": "Lock",
"initiatorType": "admin",
"lock": map[string]any{
"for": lockDuration,
},
},
},
}

_, err = createUserOperation(cmd.Context(), dyn, obj)
if err != nil {
return fmt.Errorf("create UserOperation: %w", err)
}

if !wf.wait {
cmd.Printf("%s\n", name)
return nil
}

result, err := waitUserOperation(cmd.Context(), dyn, name, wf.timeout)
if err != nil {
return fmt.Errorf("wait UserOperation: %w", err)
}

phase, _, _ := unstructured.NestedString(result.Object, "status", "phase")
message, _, _ := unstructured.NestedString(result.Object, "status", "message")
if phase == "Failed" {
return fmt.Errorf("Lock failed: %s", message)
}
cmd.Printf("Succeeded: %s\n", name)
return nil
},
}

cmd.Long = "Lock local user in Dex for a period of time.\n\nThe lockDuration argument must be a duration string (e.g. 30s, 10m, 1h)."
addWaitFlags(cmd)
return cmd
}
74 changes: 74 additions & 0 deletions internal/useroperation/cmd/reset2fa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package useroperation

import (
"fmt"
"time"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func newReset2FACommand() *cobra.Command {
cmd := &cobra.Command{
Use: "reset2fa <username>",
Short: "Reset local user's 2FA (TOTP) in Dex",
Args: cobra.ExactArgs(1),
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
username := args[0]
wf, err := getWaitFlags(cmd)
if err != nil {
return err
}

dyn, err := newDynamicClient(cmd)
if err != nil {
return err
}

name := fmt.Sprintf("op-reset2fa-%d", time.Now().Unix())
obj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "deckhouse.io/v1",
"kind": "UserOperation",
"metadata": map[string]any{
"name": name,
},
"spec": map[string]any{
"user": username,
"type": "Reset2FA",
"initiatorType": "admin",
},
},
}

_, err = createUserOperation(cmd.Context(), dyn, obj)
if err != nil {
return fmt.Errorf("create UserOperation: %w", err)
}

if !wf.wait {
cmd.Printf("%s\n", name)
return nil
}

result, err := waitUserOperation(cmd.Context(), dyn, name, wf.timeout)
if err != nil {
return fmt.Errorf("wait UserOperation: %w", err)
}

phase, _, _ := unstructured.NestedString(result.Object, "status", "phase")
message, _, _ := unstructured.NestedString(result.Object, "status", "message")
if phase == "Failed" {
return fmt.Errorf("Reset2FA failed: %s", message)
}
cmd.Printf("Succeeded: %s\n", name)
return nil
},
}

cmd.Long = "Reset local user's 2FA (TOTP) in Dex.\n\nThis requests a UserOperation of type Reset2FA and waits for completion by default."
addWaitFlags(cmd)
return cmd
}
79 changes: 79 additions & 0 deletions internal/useroperation/cmd/reset_password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package useroperation

import (
"fmt"
"time"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func newResetPasswordCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "reset-password <username> <bcryptHash>",
Aliases: []string{"resetpass"},
Short: "Reset local user's password in Dex (requires bcrypt hash)",
Args: cobra.ExactArgs(2),
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
username := args[0]
bcryptHash := args[1]
wf, err := getWaitFlags(cmd)
if err != nil {
return err
}

dyn, err := newDynamicClient(cmd)
if err != nil {
return err
}

name := fmt.Sprintf("op-resetpass-%d", time.Now().Unix())
obj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "deckhouse.io/v1",
"kind": "UserOperation",
"metadata": map[string]any{
"name": name,
},
"spec": map[string]any{
"user": username,
"type": "ResetPassword",
"initiatorType": "admin",
"resetPassword": map[string]any{
"newPasswordHash": bcryptHash,
},
},
},
}

_, err = createUserOperation(cmd.Context(), dyn, obj)
if err != nil {
return fmt.Errorf("create UserOperation: %w", err)
}

if !wf.wait {
cmd.Printf("%s\n", name)
return nil
}

result, err := waitUserOperation(cmd.Context(), dyn, name, wf.timeout)
if err != nil {
return fmt.Errorf("wait UserOperation: %w", err)
}

phase, _, _ := unstructured.NestedString(result.Object, "status", "phase")
message, _, _ := unstructured.NestedString(result.Object, "status", "message")
if phase == "Failed" {
return fmt.Errorf("ResetPassword failed: %s", message)
}
cmd.Printf("Succeeded: %s\n", name)
return nil
},
}

cmd.Long = "Reset local user's password in Dex.\n\nThe second argument must be a bcrypt hash (e.g. produced by `htpasswd -BinC 10`)."
addWaitFlags(cmd)
return cmd
}
Loading