feat(veans): discover /routes for permission-group negotiation
This commit is contained in:
parent
1f5abaa6fb
commit
d2c3f3244d
|
|
@ -0,0 +1,64 @@
|
|||
package client
|
||||
|
||||
import "context"
|
||||
|
||||
// RouteGroup mirrors models.APITokenRoute on the wire — the per-action
|
||||
// detail object is opaque to us.
|
||||
type RouteGroup map[string]struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
// Routes returns the API token route map. Used during bootstrap to
|
||||
// negotiate exactly which permission groups+actions exist on this Vikunja
|
||||
// instance, so the bot's API token only requests scopes the server knows
|
||||
// about — avoiding hard-coding a permission list that could drift.
|
||||
func (c *Client) Routes(ctx context.Context) (map[string]RouteGroup, error) {
|
||||
out := map[string]RouteGroup{}
|
||||
if err := c.Do(ctx, "GET", "/routes", nil, nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// PermissionsForBot picks a curated subset of route groups the veans bot
|
||||
// needs and projects the available actions of each. Groups not present on
|
||||
// the server are silently dropped, so the resulting permission map is
|
||||
// always valid for PUT /tokens regardless of Vikunja version.
|
||||
//
|
||||
// The set is intentionally tasks-centric — the bot doesn't need to manage
|
||||
// users, teams, or webhooks. We grant `read_one`/`read_all` on projects so
|
||||
// the bot can resolve PROJ-NN and #NN identifiers, but no project mutation.
|
||||
func PermissionsForBot(routes map[string]RouteGroup) map[string][]string {
|
||||
wanted := map[string][]string{
|
||||
"tasks": {"read_one", "read_all", "create", "update", "delete"},
|
||||
"projects": {"read_one", "read_all"},
|
||||
"projects_views": {"read_one", "read_all"},
|
||||
"buckets": {"read_one", "read_all", "create", "update", "delete"},
|
||||
"labels": {"read_one", "read_all", "create", "update", "delete"},
|
||||
"comments": {"read_one", "read_all", "create", "update", "delete"},
|
||||
"tasks_comments": {"read_one", "read_all", "create", "update", "delete"},
|
||||
"relations": {"create", "delete"},
|
||||
"tasks_relations": {"create", "delete"},
|
||||
"assignees": {"read_all", "create", "delete"},
|
||||
"tasks_assignees": {"read_all", "create", "delete"},
|
||||
"tasks_labels": {"create", "delete", "read_all"},
|
||||
}
|
||||
out := map[string][]string{}
|
||||
for group, actions := range wanted {
|
||||
avail, ok := routes[group]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var picked []string
|
||||
for _, a := range actions {
|
||||
if _, has := avail[a]; has {
|
||||
picked = append(picked, a)
|
||||
}
|
||||
}
|
||||
if len(picked) > 0 {
|
||||
out[group] = picked
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package client
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPermissionsForBot_DropsUnknownGroups(t *testing.T) {
|
||||
// Server only exposes a subset of what we ask for.
|
||||
server := map[string]RouteGroup{
|
||||
"tasks": {
|
||||
"read_one": {},
|
||||
"read_all": {},
|
||||
"create": {},
|
||||
"update": {},
|
||||
// "delete" intentionally absent
|
||||
},
|
||||
"projects": {
|
||||
"read_one": {},
|
||||
"read_all": {},
|
||||
},
|
||||
// no "labels", no "comments", etc.
|
||||
}
|
||||
got := PermissionsForBot(server)
|
||||
|
||||
if _, ok := got["tasks"]; !ok {
|
||||
t.Fatalf("expected tasks group in result")
|
||||
}
|
||||
for _, a := range got["tasks"] {
|
||||
if a == "delete" {
|
||||
t.Errorf("delete should have been dropped")
|
||||
}
|
||||
}
|
||||
if _, ok := got["projects"]; !ok {
|
||||
t.Fatalf("expected projects group")
|
||||
}
|
||||
if _, ok := got["labels"]; ok {
|
||||
t.Errorf("labels was not on server, should not appear in result")
|
||||
}
|
||||
if _, ok := got["nonexistent_group"]; ok {
|
||||
t.Errorf("phantom group leaked into result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermissionsForBot_EmptyWhenServerIsEmpty(t *testing.T) {
|
||||
got := PermissionsForBot(map[string]RouteGroup{})
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("expected empty map, got %v", got)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue