Add permission-aware help surface
This commit is contained in:
@@ -65,6 +65,7 @@ Implemented:
|
|||||||
- Config file resolution relative to binary directory
|
- Config file resolution relative to binary directory
|
||||||
- Runtime mode detection (`pass_mgr` present/absent)
|
- Runtime mode detection (`pass_mgr` present/absent)
|
||||||
- Top-level and group/leaf help rendering system (`--help` / `--help-brief` / `not permitted` stubs)
|
- 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
|
- HTTP client wrapper
|
||||||
- Output formatting (human-readable + `--json`)
|
- Output formatting (human-readable + `--json`)
|
||||||
- `hf version`, `hf health`, `hf config`
|
- `hf version`, `hf health`, `hf config`
|
||||||
@@ -72,4 +73,4 @@ Implemented:
|
|||||||
|
|
||||||
Planned:
|
Planned:
|
||||||
- User, role, project, task, milestone, meeting, support, propose, monitor commands
|
- 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
|
||||||
|
|||||||
149
cmd/hf/main.go
149
cmd/hf/main.go
@@ -17,15 +17,15 @@ func main() {
|
|||||||
args = parseGlobalFlags(args)
|
args = parseGlobalFlags(args)
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
fmt.Print(help.RenderTopHelp(commands.Version, topGroups()))
|
fmt.Print(help.RenderTopHelp(commands.Version, help.CommandSurface()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "--help", "-h":
|
case "--help", "-h":
|
||||||
fmt.Print(help.RenderTopHelp(commands.Version, topGroups()))
|
fmt.Print(help.RenderTopHelp(commands.Version, help.CommandSurface()))
|
||||||
case "--help-brief":
|
case "--help-brief":
|
||||||
fmt.Print(help.RenderTopHelpBrief(commands.Version, topGroups()))
|
fmt.Print(help.RenderTopHelpBrief(commands.Version, help.CommandSurface()))
|
||||||
case "version":
|
case "version":
|
||||||
handleLeafOrRun("version", args[1:], commands.RunVersion)
|
handleLeafOrRun("version", args[1:], commands.RunVersion)
|
||||||
case "health":
|
case "health":
|
||||||
@@ -240,7 +240,7 @@ func isHelpFlagOnly(args []string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findGroup(name string) (help.Group, bool) {
|
func findGroup(name string) (help.Group, bool) {
|
||||||
for _, group := range topGroups() {
|
for _, group := range help.CommandSurface() {
|
||||||
if group.Name == name {
|
if group.Name == name {
|
||||||
return group, true
|
return group, true
|
||||||
}
|
}
|
||||||
@@ -256,144 +256,3 @@ func findSubCommand(group help.Group, name string) (help.Command, bool) {
|
|||||||
}
|
}
|
||||||
return help.Command{}, false
|
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},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
211
internal/help/surface.go
Normal file
211
internal/help/surface.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user