From de95ba3590f1bc8efcfc64e9deeaeec49dd31b8a Mon Sep 17 00:00:00 2001 From: Jonathon Leight Date: Tue, 7 Jul 2026 20:33:21 -0400 Subject: [PATCH] Switch permissions back to per-repeater --- internal/core/endpoints_org_test.go | 13 +- internal/core/ignored_errors_test.go | 25 ---- internal/core/org_participation.go | 125 ------------------ internal/core/templates/org.html | 1 - internal/core/templates/org_commands.html | 93 ------------- internal/core/web.go | 2 - internal/e2e/limit_commands_test.go | 103 --------------- internal/e2e/org_actions_test.go | 56 -------- .../0037_repeater_org_command_optin.sql | 47 +++++++ internal/store/org_command_optin.go | 43 ------ internal/store/org_repeater_command_optin.go | 45 +++++++ internal/store/orgs_resolution_test.go | 91 ++++++++++++- internal/store/share_commands.go | 12 +- 13 files changed, 188 insertions(+), 468 deletions(-) delete mode 100644 internal/core/templates/org_commands.html delete mode 100644 internal/e2e/limit_commands_test.go delete mode 100644 internal/e2e/org_actions_test.go create mode 100644 internal/store/migrations/0037_repeater_org_command_optin.sql delete mode 100644 internal/store/org_command_optin.go create mode 100644 internal/store/org_repeater_command_optin.go diff --git a/internal/core/endpoints_org_test.go b/internal/core/endpoints_org_test.go index d4bcac7..a171146 100644 --- a/internal/core/endpoints_org_test.go +++ b/internal/core/endpoints_org_test.go @@ -9,10 +9,10 @@ import ( ) // Black-box coverage for the org-management POST endpoints (create/edit/links/ -// members/my-commands and join/leave). Each asserts the 303 redirect target and a -// cheap store side-effect; none render anything. +// members and join/leave). Each asserts the 303 redirect target and a cheap store +// side-effect; none render anything. -// #43 create, #44 edit, #104 links, #48 member role, #50 my-commands. +// #43 create, #44 edit, #104 links, #48 member role. func TestOrgManagementPosts(t *testing.T) { t.Parallel() st, ctx, ts, h := splitServer(t) @@ -65,13 +65,6 @@ func TestOrgManagementPosts(t *testing.T) { if admin, _ := st.IsOrgAdmin(ctx, orgID, other.ID); !admin { t.Fatal("promote did not make the member an admin") } - - // #50 my-commands — "clear" removes any restriction, redirecting to the editor. - cmds := post(t, ts, h.app, "/orgs/"+slug+"/my-commands", url.Values{"clear": {"1"}}, sess) - cmds.Body.Close() - if loc, _ := url.Parse(cmds.Header.Get("Location")); cmds.StatusCode != http.StatusSeeOther || loc.Path != "/orgs/"+slug+"/my-commands" { - t.Fatalf("save my-commands = %d %q, want 303 → my-commands", cmds.StatusCode, cmds.Header.Get("Location")) - } } // #56 update config profile, #57 delete config profile. (Create #54 and regions diff --git a/internal/core/ignored_errors_test.go b/internal/core/ignored_errors_test.go index 0a0331a..ca04ac2 100644 --- a/internal/core/ignored_errors_test.go +++ b/internal/core/ignored_errors_test.go @@ -42,28 +42,3 @@ func TestShareCommandsPageReadErrorFailsClosed(t *testing.T) { t.Fatalf("status = %d, want 500 (a swallowed error would render 200 with an empty, data-wiping form)", resp.StatusCode) } } - -// TestOrgCommandsPageReadErrorFailsClosed: if loading a member's per-org opt-in -// list fails, the page must 500 rather than render as "permissive" (all checked), -// which a Save would persist as clearing the member's real restriction. -func TestOrgCommandsPageReadErrorFailsClosed(t *testing.T) { - t.Parallel() - st, ctx, ts, h := splitServer(t) - member, sess := appLogin(t, ts, st, ctx, h.app, "orgmember") - org, err := st.CreateOrg(ctx, "Org", member.ID) // creator is an admin member - if err != nil { - t.Fatal(err) - } - - // Make OrgOptInCommandIDs (SELECT … FROM org_command_optin) fail; the handler's - // earlier queries (org, role, catalog ceiling) use other tables. - if _, err := st.Pool().Exec(ctx, `DROP TABLE org_command_optin`); err != nil { - t.Fatalf("drop org_command_optin: %v", err) - } - - resp := do(t, ts, h.app, "/orgs/"+org.Slug+"/my-commands", sess) - resp.Body.Close() - if resp.StatusCode != http.StatusInternalServerError { - t.Fatalf("status = %d, want 500 (a swallowed error would render 200 as permissive, wiping the restriction on save)", resp.StatusCode) - } -} diff --git a/internal/core/org_participation.go b/internal/core/org_participation.go index 0ab4654..a6e29b2 100644 --- a/internal/core/org_participation.go +++ b/internal/core/org_participation.go @@ -2,7 +2,6 @@ package core import ( "net/http" - "strconv" "github.com/go-chi/chi/v5" @@ -47,127 +46,3 @@ func (s *Handlers) handleSetRepeaterOrg(w http.ResponseWriter, r *http.Request) } http.Redirect(w, r, sharePath(rep.PublicID), http.StatusSeeOther) } - -// pageOrgCommands lets a member restrict, for one org, which of the commands that -// org is permitted to run actually run on the member's repeaters. No restriction -// (the default) means every command in the org's ceiling can run. -func (s *Handlers) pageOrgCommands(w http.ResponseWriter, r *http.Request) { - uid := s.Auth.CurrentUserID(r.Context()) - id, ok := s.orgID(r) - if !ok { - s.NotFound(w, r) - return - } - org, err := s.Store.GetOrg(r.Context(), id) - if err != nil { - s.NotFound(w, r) - return - } - role, isMember, err := s.Store.OrgRole(r.Context(), id, uid) - if err != nil || !isMember { - s.NotFound(w, r) - return - } - ceiling, err := s.orgCeilingCommands(r) - if err != nil { - s.ServerError(w, r, "could not load commands", err) - return - } - // Must not swallow this error: an empty list reads as "permissive" (everything - // checked), and saving that would clear the member's real restriction. - optIn, err := s.Store.OrgOptInCommandIDs(r.Context(), id, uid) - if err != nil { - s.ServerError(w, r, "could not load commands", err) - return - } - restricted := len(optIn) > 0 - // Permissive (no list) shows everything checked: all ceiling commands may run. - checked := make(map[int64]bool, len(ceiling)) - if restricted { - for _, cid := range optIn { - checked[cid] = true - } - } else { - for _, c := range ceiling { - checked[c.ID] = true - } - } - s.Render(w, r, "org_commands.html", map[string]any{ - "Org": org, - "Nav": s.OrgNavFor(r.Context(), org.ID, org.Slug, "", true, role == "admin"), - "Groups": groupCommands(ceiling, checked), - "Restricted": restricted, - }) -} - -// handleSaveOrgCommands saves the member's per-org opt-in list. If every ceiling -// command is selected (or none of the modes restrict), the list is cleared back to -// permissive so we don't persist a redundant full allowlist. -func (s *Handlers) handleSaveOrgCommands(w http.ResponseWriter, r *http.Request) { - uid := s.Auth.CurrentUserID(r.Context()) - id, ok := s.orgID(r) - if !ok { - s.NotFound(w, r) - return - } - if _, isMember, err := s.Store.OrgRole(r.Context(), id, uid); err != nil || !isMember { - s.NotFound(w, r) - return - } - if err := r.ParseForm(); err != nil { - http.Error(w, "bad form", http.StatusBadRequest) - return - } - // "Remove restriction" clears the list regardless of checkboxes. - if r.FormValue("clear") != "" { - if err := s.Store.SetOrgOptIn(r.Context(), id, uid, nil); err != nil { - s.ServerError(w, r, "could not save", err) - return - } - http.Redirect(w, r, "/orgs/"+orgParam(r)+"/my-commands", http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin - return - } - ceiling, err := s.orgCeilingCommands(r) - if err != nil { - s.ServerError(w, r, "could not load commands", err) - return - } - chosen := parseCommandIDs(r.Form["cmd"]) - // Selecting the full ceiling is equivalent to permissive — store nothing. - if len(chosen) >= len(ceiling) { - chosen = nil - } - if err := s.Store.SetOrgOptIn(r.Context(), id, uid, chosen); err != nil { - s.ServerError(w, r, "could not save", err) - return - } - http.Redirect(w, r, "/orgs/"+orgParam(r)+"/my-commands", http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin -} - -// orgCeilingCommands returns the catalog commands an org is ever permitted to run -// (member or admin tier) — the universe the opt-in editor restricts within. -func (s *Handlers) orgCeilingCommands(r *http.Request) ([]*store.Command, error) { - catalog, err := s.Store.ListCommands(r.Context()) - if err != nil { - return nil, err - } - var out []*store.Command - for _, c := range catalog { - if c.OrgMemberAllowed || c.OrgAdminAllowed { - out = append(out, c) - } - } - return out, nil -} - -// parseCommandIDs parses a slice of form values into catalog command ids, skipping -// any that aren't valid integers. -func parseCommandIDs(values []string) []int64 { - var ids []int64 - for _, v := range values { - if cid, err := strconv.ParseInt(v, 10, 64); err == nil { - ids = append(ids, cid) - } - } - return ids -} diff --git a/internal/core/templates/org.html b/internal/core/templates/org.html index 6dffa24..a733878 100644 --- a/internal/core/templates/org.html +++ b/internal/core/templates/org.html @@ -10,7 +10,6 @@