diff --git a/core/services/addons/hostops/authority.go b/core/services/addons/hostops/authority.go new file mode 100644 index 0000000000..ba78d775f0 --- /dev/null +++ b/core/services/addons/hostops/authority.go @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +package hostops + +import ( + "slices" + "sort" +) + +// Reason says why Authority.Allows refused. It is for the log, the audit line +// and the administrator pages. It never reaches an add-on: a refused call +// answers the same text whatever the reason. +type Reason string + +const ( + // ReasonNone is the reason of an allowed call. + ReasonNone Reason = "" + // ReasonNotGranted means no source grants the operation. + ReasonNotGranted Reason = "not_granted" + // ReasonDeniedByPolicyFile means a deny rule of the policy file or flags + // forbids the operation. + ReasonDeniedByPolicyFile Reason = "denied_by_policy_file" + // ReasonDeniedByAdmin means a deny rule an administrator set in the UI + // forbids the operation. + ReasonDeniedByAdmin Reason = "denied_by_admin" + // ReasonIdentityMismatch means a grant exists but it belongs to another + // package than the one installed under this add-on ID. + ReasonIdentityMismatch Reason = "identity_mismatch" +) + +// Denied reports whether the reason is a deny rule, not a missing grant. +func (r Reason) Denied() bool { + return r == ReasonDeniedByPolicyFile || r == ReasonDeniedByAdmin +} + +// EffectiveSet is what one add-on may call at one revision of the grants. It +// is a view for admission and for listings; the check of a call is +// Authority.Allows. +type EffectiveSet struct { + // Revision is the revision of the grants the set was computed at. A static + // authority has revision 0. + Revision uint64 + // Prefixes are the operation prefixes the deployment grants (the policy + // file floor), sorted. + Prefixes []string + // Operations are exact operation names granted through the UI store or a + // blanket approval, sorted. + Operations []string + // Denied are the deny rules in force for the add-on, sorted. Each is a + // prefix, a literal operation name or "*". + Denied []string +} + +// Authority answers what an add-on may call right now. Safe for concurrent +// use. The broker asks it on every call, so a change applies to the next call +// and never to one that already runs. +type Authority interface { + // Allows reports whether the add-on may call op, and if not, why. + Allows(addonID, op string) (ok bool, why Reason) + // Snapshot returns the effective operations of an add-on and the revision + // they were computed at. It is for admission and for the Apps listing. + Snapshot(addonID string) EffectiveSet + // Entitled reports whether the add-on holds any operation at all, so the + // supervisor decides whether to open an endpoint. + Entitled(addonID string) bool +} + +// staticAuthority is the Authority of a fixed prefix map: today's behavior. +type staticAuthority struct{ prefixes map[string][]string } + +// NewStaticAuthority builds an Authority over a fixed map of add-on ID to +// operation prefixes. It copies the map. An empty prefix is ignored and an +// add-on that is not listed holds nothing. +func NewStaticAuthority(prefixes map[string][]string) Authority { + out := make(map[string][]string, len(prefixes)) + for id, list := range prefixes { + clean := make([]string, 0, len(list)) + for _, p := range list { + if p != "" { + clean = append(clean, p) + } + } + if len(clean) > 0 { + sort.Strings(clean) + out[id] = clean + } + } + return staticAuthority{prefixes: out} +} + +func (a staticAuthority) Allows(addonID, op string) (bool, Reason) { + if hasPrefix(a.prefixes[addonID], op) { + return true, ReasonNone + } + return false, ReasonNotGranted +} + +func (a staticAuthority) Snapshot(addonID string) EffectiveSet { + return EffectiveSet{Prefixes: slices.Clone(a.prefixes[addonID])} +} + +func (a staticAuthority) Entitled(addonID string) bool { return len(a.prefixes[addonID]) > 0 } diff --git a/core/services/addons/hostops/authority_test.go b/core/services/addons/hostops/authority_test.go new file mode 100644 index 0000000000..4c4031edc9 --- /dev/null +++ b/core/services/addons/hostops/authority_test.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +package hostops + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("static authority", func() { + It("matches the prefix rule of the broker", func() { + a := NewStaticAuthority(map[string][]string{"sandbox": {"workspace.", ""}, "x": {}}) + ok, why := a.Allows("sandbox", "workspace.commit") + Expect(ok).To(BeTrue()) + Expect(why).To(Equal(ReasonNone)) + ok, why = a.Allows("sandbox", "egress.check") + Expect(ok).To(BeFalse()) + Expect(why).To(Equal(ReasonNotGranted)) + ok, _ = a.Allows("other", "workspace.commit") + Expect(ok).To(BeFalse()) + Expect(a.Entitled("sandbox")).To(BeTrue()) + Expect(a.Entitled("x")).To(BeFalse()) + Expect(a.Entitled("other")).To(BeFalse()) + Expect(a.Snapshot("sandbox").Prefixes).To(Equal([]string{"workspace."})) + }) + It("does not follow later changes to the map it was built from", func() { + in := map[string][]string{"a": {"workspace."}} + a := NewStaticAuthority(in) + in["a"][0] = "egress." + delete(in, "a") + ok, _ := a.Allows("a", "workspace.commit") + Expect(ok).To(BeTrue()) + }) +})