Compare commits
6 Commits
cd22642472
...
docs/readm
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ed99d7347 | |||
| f1ebc52cca | |||
| de0ea39b2a | |||
| 6dae490257 | |||
| 53b5b88fc2 | |||
| 6252039fc5 |
15
README.md
15
README.md
@@ -1,6 +1,8 @@
|
|||||||
# HarborForge.Cli
|
# HarborForge.Cli
|
||||||
|
|
||||||
`HarborForge.Cli` is the Go-based `hf` binary for HarborForge.
|
`HarborForge.Cli` is the Go-based `hf` command-line client for HarborForge.
|
||||||
|
|
||||||
|
Part of the [HarborForge](../README.md) platform. `hf` is a thin, scriptable client over the `HarborForge.Backend` REST API (default `http://127.0.0.1:8000`). It is permission-aware (command visibility derives from the caller's backend permissions) and supports both automatic secret resolution (padded-cell mode) and explicit `--token` auth (manual mode).
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -205,14 +207,15 @@ There is not yet a finer-grained exit-code taxonomy; callers should currently tr
|
|||||||
- Top-level and group/leaf help rendering (`--help` / `--help-brief`)
|
- Top-level and group/leaf help rendering (`--help` / `--help-brief`)
|
||||||
- Permission-aware command visibility via `/auth/me/permissions`
|
- Permission-aware command visibility via `/auth/me/permissions`
|
||||||
- Detailed leaf help text for all commands, with padded-cell/manual auth flag differences
|
- Detailed leaf help text for all commands, with padded-cell/manual auth flag differences
|
||||||
- Nested help coverage for `config`, `monitor server`, and `monitor api-key` subtrees
|
- Nested help coverage for `config`, `monitor server`, `monitor api-key`, and `proposal essential` subtrees
|
||||||
- `(not permitted)` rendering for unauthorized commands
|
- `(not permitted)` rendering for unauthorized commands
|
||||||
|
|
||||||
**Core commands:**
|
**Core commands:**
|
||||||
- `hf version`, `hf health`, `hf config` (show / `--url` / `--acc-mgr-token`)
|
- `hf version`, `hf health`, `hf config` (show / `--url` / `--acc-mgr-token`)
|
||||||
|
- `hf update-discord-id <username> [discord-id]` — top-level convenience command
|
||||||
|
|
||||||
**Resource commands (all implemented with list/get/create/update/delete + special actions):**
|
**Resource commands (all implemented with list/get/create/update/delete + special actions):**
|
||||||
- `hf user` — create, list, get, update, activate, deactivate, delete
|
- `hf user` — create, list, get, update, activate, deactivate, delete, reset-apikey
|
||||||
- `hf role` — list, get, create, update, delete, set-permissions, add-permissions, remove-permissions
|
- `hf role` — list, get, create, update, delete, set-permissions, add-permissions, remove-permissions
|
||||||
- `hf permission` — list
|
- `hf permission` — list
|
||||||
- `hf project` — list, get, create, update, delete, members, add-member, remove-member
|
- `hf project` — list, get, create, update, delete, members, add-member, remove-member
|
||||||
@@ -220,7 +223,9 @@ There is not yet a finer-grained exit-code taxonomy; callers should currently tr
|
|||||||
- `hf task` — list, get, create, update, transition, take, delete, search
|
- `hf task` — list, get, create, update, transition, take, delete, search
|
||||||
- `hf meeting` — list, get, create, update, attend, delete
|
- `hf meeting` — list, get, create, update, attend, delete
|
||||||
- `hf support` — list, get, create, update, take, transition, delete
|
- `hf support` — list, get, create, update, take, transition, delete
|
||||||
- `hf propose` — list, get, create, update, accept, reject, reopen
|
- `hf proposal` (alias: `hf propose`) — list, get, create, update, accept, reject, reopen
|
||||||
|
- `hf proposal essential` — list, create, update, delete
|
||||||
|
- `hf calendar` — schedule, show, edit, cancel, date-list, plan-schedule, plan-list, plan-edit, plan-cancel
|
||||||
- `hf comment` — add, list
|
- `hf comment` — add, list
|
||||||
- `hf worklog` — add, list
|
- `hf worklog` — add, list
|
||||||
- `hf monitor` — overview, server (list/get/create/delete), api-key (generate/revoke)
|
- `hf monitor` — overview, server (list/get/create/delete), api-key (generate/revoke)
|
||||||
@@ -229,4 +234,4 @@ There is not yet a finer-grained exit-code taxonomy; callers should currently tr
|
|||||||
|
|
||||||
- Backend code-based endpoint support (some commands still use id-based API routes)
|
- Backend code-based endpoint support (some commands still use id-based API routes)
|
||||||
- Release automation beyond local `make release` packaging (checksums / archives / CI publishing)
|
- Release automation beyond local `make release` packaging (checksums / archives / CI publishing)
|
||||||
- Integration tests
|
- Broader test coverage (unit tests exist for the calendar and proposal commands; end-to-end coverage is still partial)
|
||||||
|
|||||||
@@ -341,6 +341,11 @@ func handleUserCommand(subCmd string, args []string) {
|
|||||||
output.Error("usage: hf user delete <username>")
|
output.Error("usage: hf user delete <username>")
|
||||||
}
|
}
|
||||||
commands.RunUserDelete(filtered[0], tokenFlag)
|
commands.RunUserDelete(filtered[0], tokenFlag)
|
||||||
|
case "reset-apikey":
|
||||||
|
if len(filtered) < 1 {
|
||||||
|
output.Error("usage: hf user reset-apikey <username>")
|
||||||
|
}
|
||||||
|
commands.RunUserResetAPIKey(filtered[0], tokenFlag, accMgrTokenFlag)
|
||||||
default:
|
default:
|
||||||
output.Errorf("hf user %s is not implemented yet", subCmd)
|
output.Errorf("hf user %s is not implemented yet", subCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func RunConfigURL(url string) {
|
|||||||
fmt.Printf("base-url set to %s\n", url)
|
fmt.Printf("base-url set to %s\n", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunConfigAccMgrToken stores the account-manager token via pass_mgr.
|
// RunConfigAccMgrToken stores the account-manager token via secret-mgr.
|
||||||
func RunConfigAccMgrToken(token string) {
|
func RunConfigAccMgrToken(token string) {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
output.Error("usage: hf config --acc-mgr-token <token>")
|
output.Error("usage: hf config --acc-mgr-token <token>")
|
||||||
|
|||||||
@@ -390,3 +390,60 @@ func RunUserDelete(username, tokenFlag string) {
|
|||||||
}
|
}
|
||||||
fmt.Printf("user deleted: %s\n", username)
|
fmt.Printf("user deleted: %s\n", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetAPIKeyResponse matches the backend reset-apikey response.
|
||||||
|
type resetAPIKeyResponse struct {
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
APIKey string `json:"api_key"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunUserResetAPIKey implements `hf user reset-apikey <username>`.
|
||||||
|
func RunUserResetAPIKey(username, tokenFlag, accMgrTokenFlag string) {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
output.Errorf("config error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try acc-mgr-token first (allows provisioning without existing user token)
|
||||||
|
var c *client.Client
|
||||||
|
if accMgrTokenFlag != "" {
|
||||||
|
c = client.NewWithAPIKey(cfg.BaseURL, accMgrTokenFlag)
|
||||||
|
} else if mode.IsPaddedCell() {
|
||||||
|
if tok, err := passmgr.GetAccountManagerToken(); err == nil && tok != "" {
|
||||||
|
c = client.NewWithAPIKey(cfg.BaseURL, tok)
|
||||||
|
} else {
|
||||||
|
token := ResolveToken(tokenFlag)
|
||||||
|
c = client.New(cfg.BaseURL, token)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
token := ResolveToken(tokenFlag)
|
||||||
|
c = client.New(cfg.BaseURL, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := c.Post("/users/"+username+"/reset-apikey", nil)
|
||||||
|
if err != nil {
|
||||||
|
output.Errorf("failed to reset API key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if output.JSONMode {
|
||||||
|
var raw json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
output.Errorf("invalid JSON response: %v", err)
|
||||||
|
}
|
||||||
|
output.PrintJSON(raw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var r resetAPIKeyResponse
|
||||||
|
if err := json.Unmarshal(data, &r); err != nil {
|
||||||
|
fmt.Printf("API key reset for: %s\n", username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output.PrintKeyValue(
|
||||||
|
"username", r.Username,
|
||||||
|
"api-key", r.APIKey,
|
||||||
|
"message", r.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,9 +95,9 @@ func leafHelpSpec(group, cmd string) (leafHelp, bool) {
|
|||||||
Notes: []string{"Writes base-url into .hf-config.json next to the hf binary."},
|
Notes: []string{"Writes base-url into .hf-config.json next to the hf binary."},
|
||||||
},
|
},
|
||||||
"config/acc-mgr-token": {
|
"config/acc-mgr-token": {
|
||||||
Summary: "Store the account-manager token via pass_mgr",
|
Summary: "Store the account-manager token via secret-mgr",
|
||||||
Usage: []string{"hf config --acc-mgr-token <token>"},
|
Usage: []string{"hf config --acc-mgr-token <token>"},
|
||||||
Notes: []string{"Only available in padded-cell mode with pass_mgr installed."},
|
Notes: []string{"Only available in padded-cell mode with secret-mgr installed."},
|
||||||
},
|
},
|
||||||
"user/create": {
|
"user/create": {
|
||||||
Summary: "Create a user account",
|
Summary: "Create a user account",
|
||||||
@@ -105,7 +105,7 @@ func leafHelpSpec(group, cmd string) (leafHelp, bool) {
|
|||||||
Flags: accountManagerFlagHelp(),
|
Flags: accountManagerFlagHelp(),
|
||||||
Notes: []string{
|
Notes: []string{
|
||||||
"This command uses the account-manager token flow, not the normal user token flow.",
|
"This command uses the account-manager token flow, not the normal user token flow.",
|
||||||
"In padded-cell mode, --acc-mgr-token is hidden and password generation can fall back to pass_mgr.",
|
"In padded-cell mode, --acc-mgr-token is hidden and password generation can fall back to secret-mgr.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"user/list": {Summary: "List users", Usage: []string{"hf user list"}, Flags: authFlagHelp()},
|
"user/list": {Summary: "List users", Usage: []string{"hf user list"}, Flags: authFlagHelp()},
|
||||||
@@ -114,6 +114,7 @@ func leafHelpSpec(group, cmd string) (leafHelp, bool) {
|
|||||||
"user/activate": {Summary: "Activate a user", Usage: []string{"hf user activate <username>"}, Flags: authFlagHelp()},
|
"user/activate": {Summary: "Activate a user", Usage: []string{"hf user activate <username>"}, Flags: authFlagHelp()},
|
||||||
"user/deactivate": {Summary: "Deactivate a user", Usage: []string{"hf user deactivate <username>"}, Flags: authFlagHelp()},
|
"user/deactivate": {Summary: "Deactivate a user", Usage: []string{"hf user deactivate <username>"}, Flags: authFlagHelp()},
|
||||||
"user/delete": {Summary: "Delete a user", Usage: []string{"hf user delete <username>"}, Flags: authFlagHelp()},
|
"user/delete": {Summary: "Delete a user", Usage: []string{"hf user delete <username>"}, Flags: authFlagHelp()},
|
||||||
|
"user/reset-apikey": {Summary: "Reset a user's API key", Usage: []string{"hf user reset-apikey <username>"}, Flags: authFlagHelp(), Notes: []string{"The new API key is shown once and cannot be retrieved again."}},
|
||||||
"role/list": {Summary: "List roles", Usage: []string{"hf role list"}, Flags: authFlagHelp()},
|
"role/list": {Summary: "List roles", Usage: []string{"hf role list"}, Flags: authFlagHelp()},
|
||||||
"role/get": {Summary: "Show a role by name", Usage: []string{"hf role get <role-name>"}, Flags: authFlagHelp()},
|
"role/get": {Summary: "Show a role by name", Usage: []string{"hf role get <role-name>"}, Flags: authFlagHelp()},
|
||||||
"role/create": {Summary: "Create a role", Usage: []string{"hf role create --name <role-name> [--desc <desc>] [--global <true|false>]"}, Flags: authFlagHelp()},
|
"role/create": {Summary: "Create a role", Usage: []string{"hf role create --name <role-name> [--desc <desc>] [--global <true|false>]"}, Flags: authFlagHelp()},
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func CommandSurface() []Group {
|
|||||||
{Name: "activate", Description: "Activate a user", Permitted: has(perms, "user.manage")},
|
{Name: "activate", Description: "Activate a user", Permitted: has(perms, "user.manage")},
|
||||||
{Name: "deactivate", Description: "Deactivate a user", Permitted: has(perms, "user.manage")},
|
{Name: "deactivate", Description: "Deactivate a user", Permitted: has(perms, "user.manage")},
|
||||||
{Name: "delete", Description: "Delete a user", Permitted: has(perms, "user.manage")},
|
{Name: "delete", Description: "Delete a user", Permitted: has(perms, "user.manage")},
|
||||||
|
{Name: "reset-apikey", Description: "Reset a user's API key", Permitted: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type RuntimeMode int
|
|||||||
const (
|
const (
|
||||||
// ManualMode requires explicit --token / --acc-mgr-token flags.
|
// ManualMode requires explicit --token / --acc-mgr-token flags.
|
||||||
ManualMode RuntimeMode = iota
|
ManualMode RuntimeMode = iota
|
||||||
// PaddedCellMode resolves secrets via pass_mgr automatically.
|
// PaddedCellMode resolves secrets via secret-mgr automatically.
|
||||||
PaddedCellMode
|
PaddedCellMode
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,11 +21,11 @@ var (
|
|||||||
detectOnce sync.Once
|
detectOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// Detect checks whether pass_mgr is available and returns the runtime mode.
|
// Detect checks whether secret-mgr is available and returns the runtime mode.
|
||||||
// The result is cached after the first call.
|
// The result is cached after the first call.
|
||||||
func Detect() RuntimeMode {
|
func Detect() RuntimeMode {
|
||||||
detectOnce.Do(func() {
|
detectOnce.Do(func() {
|
||||||
_, err := exec.LookPath("pass_mgr")
|
_, err := exec.LookPath("secret-mgr")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
detectedMode = PaddedCellMode
|
detectedMode = PaddedCellMode
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Package passmgr wraps calls to the pass_mgr binary for secret resolution.
|
// Package passmgr wraps calls to the secret-mgr binary for secret resolution.
|
||||||
package passmgr
|
package passmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,49 +7,49 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSecret calls: pass_mgr get-secret [--public] --key <key>
|
// GetSecret calls: secret-mgr get-secret [--public] --key <key>
|
||||||
func GetSecret(key string, public bool) (string, error) {
|
func GetSecret(key string, public bool) (string, error) {
|
||||||
args := []string{"get-secret"}
|
args := []string{"get-secret"}
|
||||||
if public {
|
if public {
|
||||||
args = append(args, "--public")
|
args = append(args, "--public")
|
||||||
}
|
}
|
||||||
args = append(args, "--key", key)
|
args = append(args, "--key", key)
|
||||||
out, err := exec.Command("pass_mgr", args...).Output()
|
out, err := exec.Command("secret-mgr", args...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("pass_mgr get-secret --key %s failed: %w", key, err)
|
return "", fmt.Errorf("secret-mgr get-secret --key %s failed: %w", key, err)
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(string(out)), nil
|
return strings.TrimSpace(string(out)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSecret calls: pass_mgr set [--public] --key <key> --secret <secret>
|
// SetSecret calls: secret-mgr set [--public] --key <key> --secret <secret>
|
||||||
func SetSecret(key, secret string, public bool) error {
|
func SetSecret(key, secret string, public bool) error {
|
||||||
args := []string{"set"}
|
args := []string{"set"}
|
||||||
if public {
|
if public {
|
||||||
args = append(args, "--public")
|
args = append(args, "--public")
|
||||||
}
|
}
|
||||||
args = append(args, "--key", key, "--secret", secret)
|
args = append(args, "--key", key, "--secret", secret)
|
||||||
if err := exec.Command("pass_mgr", args...).Run(); err != nil {
|
if err := exec.Command("secret-mgr", args...).Run(); err != nil {
|
||||||
return fmt.Errorf("pass_mgr set --key %s failed: %w", key, err)
|
return fmt.Errorf("secret-mgr set --key %s failed: %w", key, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneratePassword calls: pass_mgr generate --key <key> --username <username>
|
// GeneratePassword calls: secret-mgr generate --key <key> --username <username>
|
||||||
func GeneratePassword(key, username string) (string, error) {
|
func GeneratePassword(key, username string) (string, error) {
|
||||||
args := []string{"generate", "--key", key, "--username", username}
|
args := []string{"generate", "--key", key, "--username", username}
|
||||||
out, err := exec.Command("pass_mgr", args...).Output()
|
out, err := exec.Command("secret-mgr", args...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("pass_mgr generate failed: %w", err)
|
return "", fmt.Errorf("secret-mgr generate failed: %w", err)
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(string(out)), nil
|
return strings.TrimSpace(string(out)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetToken retrieves the normal hf-token via pass_mgr.
|
// GetToken retrieves the normal hf-token via secret-mgr.
|
||||||
func GetToken() (string, error) {
|
func GetToken() (string, error) {
|
||||||
return GetSecret("hf-token", false)
|
return GetSecret("hf-token", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccountManagerToken retrieves the public hf-acc-mgr-token via pass_mgr.
|
// GetAccountManagerToken retrieves the public hf-acc-mgr-token via secret-mgr.
|
||||||
func GetAccountManagerToken() (string, error) {
|
func GetAccountManagerToken() (string, error) {
|
||||||
return GetSecret("hf-acc-mgr-token", true)
|
return GetSecret("hf-acc-mgr-token", true)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user