View consented permissions

This commit is contained in:
Jonathon Leight
2026-06-21 23:10:32 -04:00
parent 2cc1492b89
commit 67bff6f7ec
4 changed files with 114 additions and 17 deletions
+67 -16
View File
@@ -56,26 +56,11 @@ func (s *Handlers) pageContribute(w http.ResponseWriter, r *http.Request) {
http.Error(w, "could not load commands", http.StatusInternalServerError)
return
}
// Reuse the per-tier grouping; only commands in at least one tier are shown.
groups := groupPermissions(catalog, idSet(adminIDs), idSet(memberIDs))
var envelope []permGroup
for _, g := range groups {
var cmds []permChoice
for _, c := range g.Commands {
if c.AdminChecked || c.MemberChecked {
cmds = append(cmds, c)
}
}
if len(cmds) > 0 {
envelope = append(envelope, permGroup{Name: g.Name, Commands: cmds})
}
}
data := map[string]any{
"Repeater": rep,
"Org": org,
"Version": version,
"Envelope": envelope,
"Envelope": permEnvelope(catalog, idSet(adminIDs), idSet(memberIDs)),
}
// If already contributed and behind the current version, show what changed
@@ -104,6 +89,72 @@ func (s *Handlers) pageContribute(w http.ResponseWriter, r *http.Request) {
s.Render(w, r, "contribute.html", data)
}
// permEnvelope groups the catalog by category, keeping only commands granted to
// at least one tier in the given admin/member id sets — the set of commands a
// permission version actually allows.
func permEnvelope(catalog []*store.Command, adminSet, memberSet map[int64]bool) []permGroup {
var envelope []permGroup
for _, g := range groupPermissions(catalog, adminSet, memberSet) {
var cmds []permChoice
for _, c := range g.Commands {
if c.AdminChecked || c.MemberChecked {
cmds = append(cmds, c)
}
}
if len(cmds) > 0 {
envelope = append(envelope, permGroup{Name: g.Name, Commands: cmds})
}
}
return envelope
}
// pageConsented shows, read-only, the exact commands this repeater is currently
// consented to grant the org — the detail behind "consented to vN" on the
// sharing page. It renders the consented version, which may lag the org's
// current one.
func (s *Handlers) pageConsented(w http.ResponseWriter, r *http.Request) {
rep, orgID, ok := s.orgContext(w, r)
if !ok {
return
}
org, err := s.Store.GetOrg(r.Context(), orgID)
if err != nil {
http.NotFound(w, r)
return
}
cvID, contributed, err := s.Store.ConsentedVersionID(r.Context(), orgID, rep.ID)
if err != nil || !contributed {
http.NotFound(w, r) // not contributed → nothing consented to view
return
}
version, err := s.Store.VersionNumber(r.Context(), cvID)
if err != nil {
http.Error(w, "could not load policy", http.StatusInternalServerError)
return
}
adminIDs, memberIDs, err := s.Store.VersionCommandIDs(r.Context(), cvID)
if err != nil {
http.Error(w, "could not load policy", http.StatusInternalServerError)
return
}
catalog, err := s.Store.ListCommands(r.Context())
if err != nil {
http.Error(w, "could not load commands", http.StatusInternalServerError)
return
}
data := map[string]any{
"Repeater": rep,
"Org": org,
"Version": version,
"Envelope": permEnvelope(catalog, idSet(adminIDs), idSet(memberIDs)),
}
// Note (with a re-consent link) when the org has moved past this version.
if _, current, err := s.Store.CurrentVersion(r.Context(), orgID); err == nil && current > version {
data["CurrentVersion"] = current
}
s.Render(w, r, "consented.html", data)
}
// union returns the set union of two id sets.
func union(a, b map[int64]bool) map[int64]bool {
out := make(map[int64]bool, len(a)+len(b))
+45
View File
@@ -0,0 +1,45 @@
{{define "title"}}{{.Repeater.Name}} · {{.Org.Name}} commands · MeshTender{{end}}
{{define "header"}}
<div class="row g-2 align-items-center">
<div class="col">
<div class="page-pretitle">Consented to {{.Org.Name}} · v{{.Version}}</div>
<h2 class="page-title">{{.Repeater.Name}}</h2>
</div>
</div>
{{end}}
{{define "content"}}
<div class="card">
<div class="card-body">
<p class="text-secondary">
These are the commands {{.Org.Name}}'s admins and members can run on this repeater over the mesh — the
policy you consented to (v{{.Version}}). Org-admins also get everything members get.
</p>
{{if .CurrentVersion}}
<div class="alert alert-warning">
{{.Org.Name}} has since published v{{.CurrentVersion}}. This repeater stays on v{{.Version}} until you
<a class="alert-link" href="/repeaters/{{.Repeater.PublicID}}/orgs/{{.Org.Slug}}/contribute">review the changes and re-consent</a>.
</div>
{{end}}
{{if .Envelope}}
{{range .Envelope}}
<fieldset class="form-fieldset">
<div class="form-label fw-bold text-capitalize">{{.Name}}</div>
{{range .Commands}}
<div class="d-flex align-items-center flex-wrap gap-2 py-1">
<code class="flex-fill">{{.Template}}{{if .Risky}} <span class="badge bg-warning-lt">risky</span>{{end}}</code>
{{if .AdminChecked}}<span class="badge bg-success-lt">admin</span>{{end}}
{{if .MemberChecked}}<span class="badge bg-azure-lt">member</span>{{end}}
</div>
{{end}}
</fieldset>
{{end}}
{{else}}
<p class="text-secondary">This version grants no commands.</p>
{{end}}
<a class="back-link mt-3" href="/repeaters/{{.Repeater.PublicID}}/share">{{template "icon-arrow-left" "me-1"}}Back to sharing</a>
</div>
</div>
{{end}}
+1 -1
View File
@@ -34,7 +34,7 @@
{{range .Contributed}}
<div class="list-group-item d-flex align-items-center flex-wrap gap-2 px-0">
<span class="fw-bold">{{.OrgName}}</span>
<span class="text-secondary">consented to v{{.ConsentedVersion}}{{if .NeedsReconsent}} · org is now on v{{.CurrentVersion}}{{end}}</span>
<span class="text-secondary"><a class="link-secondary" href="/repeaters/{{$.Repeater.PublicID}}/orgs/{{.OrgSlug}}/consented">consented to v{{.ConsentedVersion}}</a>{{if .NeedsReconsent}} · org is now on v{{.CurrentVersion}}{{end}}</span>
{{if .NeedsReconsent}}
<a class="btn btn-sm" href="/repeaters/{{$.Repeater.PublicID}}/orgs/{{.OrgSlug}}/contribute">Review changes</a>
{{end}}
+1
View File
@@ -140,6 +140,7 @@ func (s *Handlers) appRouter() chi.Router {
r.Post("/repeaters/{id}/unshare", s.handleUnshare)
r.Get("/repeaters/{id}/share/{userID}/commands", s.pageShareCommands)
r.Post("/repeaters/{id}/share/{userID}/commands", s.handleSetShareCommands)
r.Get("/repeaters/{id}/orgs/{orgID}/consented", s.pageConsented)
r.Get("/repeaters/{id}/orgs/{orgID}/contribute", s.pageContribute)
r.Post("/repeaters/{id}/orgs/{orgID}/contribute", s.handleContribute)
r.Post("/repeaters/{id}/orgs/{orgID}/withdraw", s.handleWithdraw)