From 69287d5a49cce12f23391b3dd1960c7ff7aa89fc Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 14:37:42 +0000 Subject: [PATCH] Add permission-aware help surface --- README.md | 3 +- cmd/hf/main.go | 149 +-------------------------- internal/help/help.go | 2 +- internal/help/surface.go | 211 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 147 deletions(-) create mode 100644 internal/help/surface.go diff --git a/README.md b/README.md index 787420f..ea7c864 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Implemented: - Config file resolution relative to binary directory - Runtime mode detection (`pass_mgr` present/absent) - Top-level and group/leaf help rendering system (`--help` / `--help-brief` / `not permitted` stubs) +- Permission-aware command visibility via `/auth/me/permissions` when a token is available - HTTP client wrapper - Output formatting (human-readable + `--json`) - `hf version`, `hf health`, `hf config` @@ -72,4 +73,4 @@ Implemented: Planned: - User, role, project, task, milestone, meeting, support, propose, monitor commands -- Permission-aware help rendering +- Rich per-command help/usage text beyond the current stub renderer diff --git a/cmd/hf/main.go b/cmd/hf/main.go index 7f20ce2..faf2796 100644 --- a/cmd/hf/main.go +++ b/cmd/hf/main.go @@ -17,15 +17,15 @@ func main() { args = parseGlobalFlags(args) if len(args) == 0 { - fmt.Print(help.RenderTopHelp(commands.Version, topGroups())) + fmt.Print(help.RenderTopHelp(commands.Version, help.CommandSurface())) return } switch args[0] { case "--help", "-h": - fmt.Print(help.RenderTopHelp(commands.Version, topGroups())) + fmt.Print(help.RenderTopHelp(commands.Version, help.CommandSurface())) case "--help-brief": - fmt.Print(help.RenderTopHelpBrief(commands.Version, topGroups())) + fmt.Print(help.RenderTopHelpBrief(commands.Version, help.CommandSurface())) case "version": handleLeafOrRun("version", args[1:], commands.RunVersion) case "health": @@ -240,7 +240,7 @@ func isHelpFlagOnly(args []string) bool { } func findGroup(name string) (help.Group, bool) { - for _, group := range topGroups() { + for _, group := range help.CommandSurface() { if group.Name == name { return group, true } @@ -256,144 +256,3 @@ func findSubCommand(group help.Group, name string) (help.Command, bool) { } return help.Command{}, false } - -// topGroups returns the full command tree for help rendering. -// TODO: permission awareness will be added when auth introspection is available. -func topGroups() []help.Group { - return []help.Group{ - {Name: "version", Description: "Show CLI version", Permitted: true}, - {Name: "health", Description: "Check API health", Permitted: true}, - {Name: "config", Description: "View and manage CLI configuration", Permitted: true}, - { - Name: "user", - Description: "Manage users", - Permitted: true, - SubCommands: []help.Command{ - {Name: "create", Description: "Create a user account (uses account-manager token flow)", Permitted: true}, - {Name: "list", Description: "List users", Permitted: true}, - {Name: "get", Description: "Show a user by username", Permitted: true}, - {Name: "update", Description: "Update a user", Permitted: true}, - {Name: "activate", Description: "Activate a user", Permitted: true}, - {Name: "deactivate", Description: "Deactivate a user", Permitted: true}, - {Name: "delete", Description: "Delete a user", Permitted: true}, - }, - }, - { - Name: "role", - Description: "Manage roles and permissions", - Permitted: true, - SubCommands: []help.Command{ - {Name: "list", Description: "List roles", Permitted: false}, - {Name: "get", Description: "Show a role by name", Permitted: false}, - {Name: "create", Description: "Create a role", Permitted: false}, - {Name: "update", Description: "Update a role", Permitted: false}, - {Name: "delete", Description: "Delete a role", Permitted: false}, - {Name: "set-permissions", Description: "Replace role permissions", Permitted: false}, - {Name: "add-permissions", Description: "Add permissions to a role", Permitted: false}, - {Name: "remove-permissions", Description: "Remove permissions from a role", Permitted: false}, - }, - }, - { - Name: "permission", - Description: "List permissions", - Permitted: true, - SubCommands: []help.Command{ - {Name: "list", Description: "List permissions", Permitted: false}, - }, - }, - { - Name: "project", - Description: "Manage projects", - Permitted: true, - SubCommands: []help.Command{ - {Name: "list", Description: "List projects", Permitted: false}, - {Name: "get", Description: "Show a project by code", Permitted: false}, - {Name: "create", Description: "Create a project", Permitted: false}, - {Name: "update", Description: "Update a project", Permitted: false}, - {Name: "delete", Description: "Delete a project", Permitted: false}, - {Name: "members", Description: "List project members", Permitted: false}, - {Name: "add-member", Description: "Add a project member", Permitted: false}, - {Name: "remove-member", Description: "Remove a project member", Permitted: false}, - }, - }, - { - Name: "milestone", - Description: "Manage milestones", - Permitted: true, - SubCommands: []help.Command{ - {Name: "list", Description: "List milestones", Permitted: false}, - {Name: "get", Description: "Show a milestone by code", Permitted: false}, - {Name: "create", Description: "Create a milestone", Permitted: false}, - {Name: "update", Description: "Update a milestone", Permitted: false}, - {Name: "delete", Description: "Delete a milestone", Permitted: false}, - {Name: "progress", Description: "Show milestone progress", Permitted: false}, - }, - }, - { - Name: "task", - Description: "Manage tasks", - Permitted: true, - SubCommands: []help.Command{ - {Name: "list", Description: "List tasks", Permitted: false}, - {Name: "get", Description: "Show a task by code", Permitted: false}, - {Name: "create", Description: "Create a task", Permitted: false}, - {Name: "update", Description: "Update a task", Permitted: false}, - {Name: "transition", Description: "Transition a task to a new status", Permitted: false}, - {Name: "take", Description: "Assign a task to the current user", Permitted: false}, - {Name: "delete", Description: "Delete a task", Permitted: false}, - {Name: "search", Description: "Search tasks", Permitted: false}, - }, - }, - { - Name: "meeting", - Description: "Manage meetings", - Permitted: true, - SubCommands: []help.Command{ - {Name: "list", Description: "List meetings", Permitted: false}, - {Name: "get", Description: "Show a meeting by code", Permitted: false}, - {Name: "create", Description: "Create a meeting", Permitted: false}, - {Name: "update", Description: "Update a meeting", Permitted: false}, - {Name: "attend", Description: "Attend a meeting", Permitted: false}, - {Name: "delete", Description: "Delete a meeting", Permitted: false}, - }, - }, - { - Name: "support", - Description: "Manage support tickets", - Permitted: true, - SubCommands: []help.Command{ - {Name: "list", Description: "List support tickets", Permitted: false}, - {Name: "get", Description: "Show a support ticket by code", Permitted: false}, - {Name: "create", Description: "Create a support ticket", Permitted: false}, - {Name: "update", Description: "Update a support ticket", Permitted: false}, - {Name: "take", Description: "Assign a support ticket to the current user", Permitted: false}, - {Name: "transition", Description: "Transition a support ticket to a new status", Permitted: false}, - {Name: "delete", Description: "Delete a support ticket", Permitted: false}, - }, - }, - { - Name: "propose", - Description: "Manage proposals", - Permitted: true, - SubCommands: []help.Command{ - {Name: "list", Description: "List proposals", Permitted: false}, - {Name: "get", Description: "Show a proposal by code", Permitted: false}, - {Name: "create", Description: "Create a proposal", Permitted: false}, - {Name: "update", Description: "Update a proposal", Permitted: false}, - {Name: "accept", Description: "Accept a proposal", Permitted: false}, - {Name: "reject", Description: "Reject a proposal", Permitted: false}, - {Name: "reopen", Description: "Reopen a proposal", Permitted: false}, - }, - }, - { - Name: "monitor", - Description: "Monitor servers and API keys", - Permitted: true, - SubCommands: []help.Command{ - {Name: "overview", Description: "Show monitor overview", Permitted: false}, - {Name: "server", Description: "Manage monitor servers", Permitted: false}, - {Name: "api-key", Description: "Manage monitor API keys", Permitted: false}, - }, - }, - } -} diff --git a/internal/help/help.go b/internal/help/help.go index 04788ee..a545cae 100644 --- a/internal/help/help.go +++ b/internal/help/help.go @@ -10,7 +10,7 @@ import ( type Command struct { Name string Description string - Permitted bool // whether the current user can execute this + Permitted bool // whether the current user can execute this SubCommands []Command } diff --git a/internal/help/surface.go b/internal/help/surface.go new file mode 100644 index 0000000..89bf7ec --- /dev/null +++ b/internal/help/surface.go @@ -0,0 +1,211 @@ +package help + +import ( + "encoding/json" + + "git.hangman-lab.top/zhi/HarborForge.Cli/internal/client" + "git.hangman-lab.top/zhi/HarborForge.Cli/internal/config" + "git.hangman-lab.top/zhi/HarborForge.Cli/internal/mode" + "git.hangman-lab.top/zhi/HarborForge.Cli/internal/passmgr" +) + +type permissionState struct { + Known bool + Permissions map[string]struct{} +} + +type permissionIntrospectionResponse struct { + Username string `json:"username"` + RoleName *string `json:"role_name"` + IsAdmin bool `json:"is_admin"` + Permissions []string `json:"permissions"` +} + +func CommandSurface() []Group { + perms := detectPermissionState() + + groups := []Group{ + {Name: "version", Description: "Show CLI version", Permitted: true}, + {Name: "health", Description: "Check API health", Permitted: true}, + {Name: "config", Description: "View and manage CLI configuration", Permitted: true}, + { + Name: "user", + Description: "Manage users", + SubCommands: []Command{ + {Name: "create", Description: "Create a user account (uses account-manager token flow)", Permitted: true}, + {Name: "list", Description: "List users", Permitted: has(perms, "user.manage")}, + {Name: "get", Description: "Show a user by username", Permitted: has(perms, "user.manage")}, + {Name: "update", Description: "Update 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: "delete", Description: "Delete a user", Permitted: has(perms, "user.manage")}, + }, + }, + { + Name: "role", + Description: "Manage roles and permissions", + SubCommands: []Command{ + {Name: "list", Description: "List roles", Permitted: has(perms, "role.manage")}, + {Name: "get", Description: "Show a role by name", Permitted: has(perms, "role.manage")}, + {Name: "create", Description: "Create a role", Permitted: has(perms, "role.manage")}, + {Name: "update", Description: "Update a role", Permitted: has(perms, "role.manage")}, + {Name: "delete", Description: "Delete a role", Permitted: has(perms, "role.manage")}, + {Name: "set-permissions", Description: "Replace role permissions", Permitted: has(perms, "role.manage")}, + {Name: "add-permissions", Description: "Add permissions to a role", Permitted: has(perms, "role.manage")}, + {Name: "remove-permissions", Description: "Remove permissions from a role", Permitted: has(perms, "role.manage")}, + }, + }, + { + Name: "permission", + Description: "List permissions", + SubCommands: []Command{{Name: "list", Description: "List permissions", Permitted: has(perms, "role.manage")}}, + }, + { + Name: "project", + Description: "Manage projects", + SubCommands: []Command{ + {Name: "list", Description: "List projects", Permitted: has(perms, "project.read")}, + {Name: "get", Description: "Show a project by code", Permitted: has(perms, "project.read")}, + {Name: "create", Description: "Create a project", Permitted: has(perms, "project.write")}, + {Name: "update", Description: "Update a project", Permitted: has(perms, "project.write")}, + {Name: "delete", Description: "Delete a project", Permitted: has(perms, "project.delete")}, + {Name: "members", Description: "List project members", Permitted: has(perms, "project.read")}, + {Name: "add-member", Description: "Add a project member", Permitted: has(perms, "project.manage_members")}, + {Name: "remove-member", Description: "Remove a project member", Permitted: has(perms, "project.manage_members")}, + }, + }, + { + Name: "milestone", + Description: "Manage milestones", + SubCommands: []Command{ + {Name: "list", Description: "List milestones", Permitted: has(perms, "milestone.read")}, + {Name: "get", Description: "Show a milestone by code", Permitted: has(perms, "milestone.read")}, + {Name: "create", Description: "Create a milestone", Permitted: has(perms, "milestone.create")}, + {Name: "update", Description: "Update a milestone", Permitted: has(perms, "milestone.write")}, + {Name: "delete", Description: "Delete a milestone", Permitted: has(perms, "milestone.delete")}, + {Name: "progress", Description: "Show milestone progress", Permitted: has(perms, "milestone.read")}, + }, + }, + { + Name: "task", + Description: "Manage tasks", + SubCommands: []Command{ + {Name: "list", Description: "List tasks", Permitted: has(perms, "task.read")}, + {Name: "get", Description: "Show a task by code", Permitted: has(perms, "task.read")}, + {Name: "create", Description: "Create a task", Permitted: has(perms, "task.create")}, + {Name: "update", Description: "Update a task", Permitted: has(perms, "task.write")}, + {Name: "transition", Description: "Transition a task to a new status", Permitted: has(perms, "task.write")}, + {Name: "take", Description: "Assign a task to the current user", Permitted: has(perms, "task.write")}, + {Name: "delete", Description: "Delete a task", Permitted: has(perms, "task.delete")}, + {Name: "search", Description: "Search tasks", Permitted: has(perms, "task.read")}, + }, + }, + { + Name: "meeting", + Description: "Manage meetings", + SubCommands: []Command{ + {Name: "list", Description: "List meetings", Permitted: has(perms, "task.read")}, + {Name: "get", Description: "Show a meeting by code", Permitted: has(perms, "task.read")}, + {Name: "create", Description: "Create a meeting", Permitted: has(perms, "task.create")}, + {Name: "update", Description: "Update a meeting", Permitted: has(perms, "task.write")}, + {Name: "attend", Description: "Attend a meeting", Permitted: has(perms, "task.write")}, + {Name: "delete", Description: "Delete a meeting", Permitted: has(perms, "task.delete")}, + }, + }, + { + Name: "support", + Description: "Manage support tickets", + SubCommands: []Command{ + {Name: "list", Description: "List support tickets", Permitted: has(perms, "task.read")}, + {Name: "get", Description: "Show a support ticket by code", Permitted: has(perms, "task.read")}, + {Name: "create", Description: "Create a support ticket", Permitted: has(perms, "task.create")}, + {Name: "update", Description: "Update a support ticket", Permitted: has(perms, "task.write")}, + {Name: "take", Description: "Assign a support ticket to the current user", Permitted: has(perms, "task.write")}, + {Name: "transition", Description: "Transition a support ticket to a new status", Permitted: has(perms, "task.write")}, + {Name: "delete", Description: "Delete a support ticket", Permitted: has(perms, "task.delete")}, + }, + }, + { + Name: "propose", + Description: "Manage proposals", + SubCommands: []Command{ + {Name: "list", Description: "List proposals", Permitted: has(perms, "project.read")}, + {Name: "get", Description: "Show a proposal by code", Permitted: has(perms, "project.read")}, + {Name: "create", Description: "Create a proposal", Permitted: has(perms, "task.create")}, + {Name: "update", Description: "Update a proposal", Permitted: has(perms, "task.write")}, + {Name: "accept", Description: "Accept a proposal", Permitted: has(perms, "propose.accept")}, + {Name: "reject", Description: "Reject a proposal", Permitted: has(perms, "propose.reject")}, + {Name: "reopen", Description: "Reopen a proposal", Permitted: has(perms, "propose.reopen")}, + }, + }, + { + Name: "monitor", + Description: "Monitor servers and API keys", + SubCommands: []Command{ + {Name: "overview", Description: "Show monitor overview", Permitted: has(perms, "monitor.read")}, + {Name: "server", Description: "Manage monitor servers", Permitted: has(perms, "monitor.manage") || has(perms, "monitor.read")}, + {Name: "api-key", Description: "Manage monitor API keys", Permitted: has(perms, "monitor.manage")}, + }, + }, + } + + for i := range groups { + groups[i].Permitted = groupPermitted(groups[i]) + } + return groups +} + +func detectPermissionState() permissionState { + if mode.IsPaddedCell() { + token, err := passmgr.GetToken() + if err != nil || token == "" { + return permissionState{Known: false, Permissions: map[string]struct{}{}} + } + return loadPermissionState(token) + } + return permissionState{Known: false, Permissions: map[string]struct{}{}} +} + +func loadPermissionState(token string) permissionState { + cfg, err := config.Load() + if err != nil || cfg.BaseURL == "" { + return permissionState{Known: false, Permissions: map[string]struct{}{}} + } + + c := client.New(cfg.BaseURL, token) + data, err := c.Get("/auth/me/permissions") + if err != nil { + return permissionState{Known: false, Permissions: map[string]struct{}{}} + } + + var resp permissionIntrospectionResponse + if err := json.Unmarshal(data, &resp); err != nil { + return permissionState{Known: false, Permissions: map[string]struct{}{}} + } + + perms := make(map[string]struct{}, len(resp.Permissions)) + for _, perm := range resp.Permissions { + perms[perm] = struct{}{} + } + return permissionState{Known: true, Permissions: perms} +} + +func has(state permissionState, perm string) bool { + if !state.Known { + return false + } + _, ok := state.Permissions[perm] + return ok +} + +func groupPermitted(group Group) bool { + if len(group.SubCommands) == 0 { + return group.Permitted + } + for _, cmd := range group.SubCommands { + if cmd.Permitted { + return true + } + } + return false +}