Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions libraries/authority-core/go/authority/consent_fence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package authority_test

import (
"context"
"strings"
"sync"
"testing"

"github.com/codefly-dev/authority/libraries/authority-core/go/authority"
"github.com/codefly-dev/authority/libraries/authority-core/go/domain"
"github.com/codefly-dev/authority/libraries/authority-core/go/memstore"
basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
)

// hookStore runs after once, right after the Read that finds it armed
// returns: a narrowing the host commits between the core's decision and its
// mint.
type hookStore struct {
*memstore.Store
mu sync.Mutex
after func()
}

func (h *hookStore) Read(ctx context.Context, fn func(authority.Tx) error) error {
err := h.Store.Read(ctx, fn)
h.mu.Lock()
f := h.after
h.after = nil
h.mu.Unlock()
if f != nil {
f()
}
return err
}

func (h *hookStore) arm(f func()) {
h.mu.Lock()
h.after = f
h.mu.Unlock()
}

// A narrowing the host commits between an issue's decision and its mint is
// never handed out: the capability decided under the withdrawn consent is
// refused at issuance, on every path, by the revision fence.
func TestANarrowingBetweenDecisionAndMintIsNeverHandedOut(t *testing.T) {
for _, path := range []string{"workload", "child"} {
t.Run(path, func(t *testing.T) {
h := newHarness(t)
cfg := h.cfg
cfg.HostInstallsModules = true
mem := memstore.New()
store := &hookStore{Store: mem}
svc, err := authority.New(store, cfg)
if err != nil {
t.Fatal(err)
}
mem.PutOrganization(domain.Organization{ID: acme, Name: acme})
mem.PutOrgRole(domain.OrgRole{Organization: acme, Name: "reader", Scopes: []domain.RoleScope{{Kind: legacyKind, Actions: []string{"read", "write"}}, {Kind: kind, Actions: []string{"read"}}}})
mem.PutPerson(domain.Person{ID: "alex", Organization: acme, Roles: []string{"reader"}})
relayScopes := []*basev0.WorkScopeV1{sc(legacyKind, "read", "write")}
for _, m := range []domain.Module{
{Name: "relay", Principal: "relay", Kind: domain.KindModule, Generation: 1, ResourceKinds: relayScopes},
{Name: "store", Principal: "store", Kind: domain.KindModule, Generation: 1, ResourceKinds: []*basev0.WorkScopeV1{sc(kind, "read")},
Bindings: map[string]domain.Binding{"relay": {ID: "relay", Audience: "relay", InvokeScopes: relayScopes, HeadlessScopes: relayScopes}}},
} {
if _, err := svc.ApplyModule(h.ctx, m); err != nil {
t.Fatal(err)
}
}
put := func(revision uint64, bindings map[string]domain.Binding) {
if err := mem.Write(h.ctx, func(tx authority.Tx) error {
return tx.PutInstallation(domain.Installation{ID: "st-1", Organization: acme, Target: "store", Status: domain.Active, Revision: revision, Bindings: bindings})
}); err != nil {
t.Fatal(err)
}
}
put(1, map[string]domain.Binding{"relay": {ID: "relay", InvokeScopes: relayScopes, HeadlessScopes: relayScopes}})
narrow := func() { put(2, map[string]domain.Binding{}) }
if path == "workload" {
store.arm(narrow)
_, err = svc.IssueForWorkload(h.ctx, authority.Workload{Module: "store"}, authority.IssueRequest{Binding: "relay", Organization: acme})
} else {
parent, perr := svc.IssueForPerson(h.ctx, authority.Person{ID: "alex", Organization: acme, SessionID: "s"}, authority.IssueRequest{Audience: "store"})
if perr != nil {
t.Fatal(perr)
}
// IssueChild reads twice: the first resolves the binding, the
// second decides; the narrowing lands after the second.
store.arm(func() { store.arm(narrow) })
_, err = svc.IssueChild(h.ctx, parent.Token, authority.Workload{Module: "store"}, authority.IssueRequest{Binding: "relay"})
}
if err == nil {
t.Fatal("a capability decided under the consent the host withdrew before the mint was handed out")
}
_, err = svc.IssueForWorkload(h.ctx, authority.Workload{Module: "store"}, authority.IssueRequest{Binding: "relay", Organization: acme})
code(t, err, domain.Denied)
})
}
}

// consented replaces an installation's consent with bindings, as a host
// that installs by consent narrows it.
func (h *harness) consented(org, target string, bindings map[string]domain.Binding) {
h.t.Helper()
if err := h.store.Write(h.ctx, func(tx authority.Tx) error {
inst, err := tx.Installation(authority.StableInstallationID(org, target))
if err != nil {
return err
}
inst.Bindings, inst.Revision = bindings, inst.Revision+1
return tx.PutInstallation(inst)
}); err != nil {
h.t.Fatal(err)
}
}

func refusedByConsent(t *testing.T, err error) {
t.Helper()
code(t, err, domain.Denied)
if !strings.Contains(err.Error(), "consents to") {
t.Fatalf("refused for another reason than the consent: %v", err)
}
}

// The consent bounds an installation acting for itself through its
// solution's binding, and an issue from a delegation through the module's.
func TestTheConsentBoundsInstallationAndDelegationIssues(t *testing.T) {
t.Run("an installation acting for itself", func(t *testing.T) {
h := newHarness(t)
h.must(h.agent("a", "store"))
h.consented(acme, "a", map[string]domain.Binding{"legacy": {ID: "legacy", InvokeScopes: []*basev0.WorkScopeV1{sc(legacyKind, "read")}}})
_, err := h.agent("a", "store")
refusedByConsent(t, err)
})
t.Run("an issue from a delegation", func(t *testing.T) {
h := newHarness(t)
rob := h.person("rob")
d, err := h.svc.Delegate(h.ctx, rob, "store", []*basev0.WorkScopeV1{owned(kind, "", []string{"read"}, "store-x")}, "a job")
if err != nil {
t.Fatal(err)
}
h.consented(acme, "store", map[string]domain.Binding{})
_, err = h.svc.IssueForDelegation(h.ctx, d.ID, authority.Workload{Module: "store"}, authority.IssueRequest{Binding: "legacy"})
refusedByConsent(t, err)
})
}
3 changes: 3 additions & 0 deletions libraries/authority-core/go/authority/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ func (s *Service) IssueForDelegation(ctx context.Context, id string, actor Workl
if !ok {
return domain.Missing("operation binding %q is not one %s declares", r.Binding, am.Name)
}
if binding, err = ceiled(tx, d.Organization, am, binding); err != nil {
return err
}
if audience, err = tx.Module(r.Audience); err != nil {
return notFound(err)
}
Expand Down
122 changes: 120 additions & 2 deletions libraries/authority-core/go/authority/host_installs_modules_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package authority_test

import (
"strings"
"testing"

"github.com/codefly-dev/authority/libraries/authority-core/go/authority"
Expand Down Expand Up @@ -33,10 +34,17 @@ func newHostInstalled(t *testing.T) *hostInstalled {
}
store.PutOrgRole(domain.OrgRole{Organization: acme, Name: "admin"})
store.PutPerson(domain.Person{ID: "alex", Organization: acme, Roles: []string{"admin"}})
if _, err := svc.ApplyModule(h.ctx, domain.Module{Name: "relay", Principal: "relay", Kind: domain.KindModule, Generation: 1,
ResourceKinds: []*basev0.WorkScopeV1{sc(legacyKind, "read")}}); err != nil {
t.Fatal(err)
}
if _, err := svc.ApplyModule(h.ctx, domain.Module{Name: "store", Principal: "store", Kind: domain.KindModule, Generation: 1,
ResourceKinds: []*basev0.WorkScopeV1{sc(kind, "read", "write")},
Bindings: map[string]domain.Binding{"self": {ID: "self", Audience: "store",
InvokeScopes: []*basev0.WorkScopeV1{sc(kind, "read", "write")}, HeadlessScopes: []*basev0.WorkScopeV1{sc(kind, "read")}}},
Bindings: map[string]domain.Binding{
"self": {ID: "self", Audience: "store",
InvokeScopes: []*basev0.WorkScopeV1{sc(kind, "read", "write")}, HeadlessScopes: []*basev0.WorkScopeV1{sc(kind, "read")}},
"relay": {ID: "relay", Audience: "relay", InvokeScopes: []*basev0.WorkScopeV1{sc(legacyKind, "read")}},
},
}); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -183,3 +191,113 @@ func TestAWorkloadIsIssuedOnlyWhereItsHostInstalledIt(t *testing.T) {
}
})
}

// recordConsent writes the host's installation record of the store module
// with the bindings its consent allows.
func (h *hostInstalled) recordConsent(org, id string, bindings map[string]domain.Binding) {
h.t.Helper()
if err := h.store.Write(h.ctx, func(tx authority.Tx) error {
return tx.PutInstallation(domain.Installation{ID: id, Organization: org, Target: "store", Status: domain.Active, Revision: 1, Bindings: bindings})
}); err != nil {
h.t.Fatal(err)
}
}

// What a host's consent allows of a module's bindings at one installation
// bounds every issue through them, whatever the delivery declares: a binding
// the consent does not name issues nothing, one it narrows issues within the
// narrowing, and a redelivery that widens the binding issues no more than was
// consented.
func TestAnInstallationsConsentBoundsItsModulesBindings(t *testing.T) {
read, readWrite := []*basev0.WorkScopeV1{sc(kind, "read")}, []*basev0.WorkScopeV1{sc(kind, "read", "write")}
for _, tc := range []struct {
name string
consent map[string]domain.Binding
widen bool
want []string
refusal domain.Code
refusedB bool
}{
{name: "no ceiling: as delivered", want: []string{"read"}},
{name: "the binding consented as delivered", consent: map[string]domain.Binding{"self": {ID: "self", HeadlessScopes: read, InvokeScopes: readWrite}}, want: []string{"read"}},
{name: "a consent naming no binding", consent: map[string]domain.Binding{}, refusedB: true, refusal: domain.Denied},
{name: "a consent naming another binding", consent: map[string]domain.Binding{"other": {ID: "other", HeadlessScopes: read}}, refusedB: true, refusal: domain.Denied},
{name: "a widening redelivery stays within the consent", consent: map[string]domain.Binding{"self": {ID: "self", HeadlessScopes: read}}, widen: true, want: []string{"read"}},
{name: "a widening redelivery with no ceiling widens", widen: true, want: []string{"read", "write"}},
} {
t.Run(tc.name, func(t *testing.T) {
h := newHostInstalled(t)
h.recordConsent(acme, "host-1", tc.consent)
if tc.widen {
if _, err := h.svc.ApplyModule(h.ctx, domain.Module{Name: "store", Principal: "store", Kind: domain.KindModule, Generation: 2,
ResourceKinds: readWrite,
Bindings: map[string]domain.Binding{"self": {ID: "self", Audience: "store", InvokeScopes: readWrite, HeadlessScopes: readWrite}},
}); err != nil {
t.Fatal(err)
}
}
issued, err := h.issue(authority.Workload{Module: "store"}, acme)
if tc.refusedB {
code(t, err, tc.refusal)
return
}
if err != nil {
t.Fatal(err)
}
got := effective(issued)
if len(got) != 1 || got[0].GetResourceKind() != kind || !sameActions(got[0].GetActions(), tc.want) {
t.Fatalf("issued %v, want %s %v", got, kind, tc.want)
}
})
}

t.Run("a hop back to the module's own audience is bounded by its parent, not by the consent", func(t *testing.T) {
h := newHostInstalled(t)
h.recordConsent(acme, "host-1", map[string]domain.Binding{})
h.store.PutOrgRole(domain.OrgRole{Organization: acme, Name: "reader", Scopes: []domain.RoleScope{{Kind: kind, Actions: []string{"read"}}}})
h.store.PutPerson(domain.Person{ID: "alex", Organization: acme, Roles: []string{"admin", "reader"}})
parent, err := h.svc.IssueForPerson(h.ctx, authority.Person{ID: "alex", Organization: acme, SessionID: "s"}, authority.IssueRequest{Audience: "store"})
if err != nil {
t.Fatal(err)
}
child, err := h.svc.IssueChild(h.ctx, parent.Token, authority.Workload{Module: "store"}, authority.IssueRequest{Binding: "self"})
if err != nil {
t.Fatal(err)
}
if got := effective(child); len(got) != 1 || !sameActions(got[0].GetActions(), []string{"read"}) {
t.Fatalf("a hop to itself issued %v, want read", got)
}
})

t.Run("a person's hop through a binding the consent does not name is refused", func(t *testing.T) {
h := newHostInstalled(t)
h.recordConsent(acme, "host-1", map[string]domain.Binding{})
h.store.PutOrgRole(domain.OrgRole{Organization: acme, Name: "reader", Scopes: []domain.RoleScope{{Kind: kind, Actions: []string{"read"}}}})
h.store.PutPerson(domain.Person{ID: "alex", Organization: acme, Roles: []string{"admin", "reader"}})
parent, err := h.svc.IssueForPerson(h.ctx, authority.Person{ID: "alex", Organization: acme, SessionID: "s"}, authority.IssueRequest{Audience: "store"})
if err != nil {
t.Fatal(err)
}
_, err = h.svc.IssueChild(h.ctx, parent.Token, authority.Workload{Module: "store"}, authority.IssueRequest{Binding: "relay"})
code(t, err, domain.Denied)
if !strings.Contains(err.Error(), "consents to") {
t.Fatalf("refused for another reason: %v", err)
}
})
}

func sameActions(got, want []string) bool {
if len(got) != len(want) {
return false
}
seen := map[string]bool{}
for _, a := range got {
seen[a] = true
}
for _, a := range want {
if !seen[a] {
return false
}
}
return true
}
27 changes: 26 additions & 1 deletion libraries/authority-core/go/authority/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ func (s *Service) IssueForWorkload(ctx context.Context, w Workload, r IssueReque
var module, audience domain.Module
var through, scopes Scopes
var org, instID string
var f fence
err := s.store.Read(ctx, func(tx Tx) error {
var err error
if module, err = tx.ModuleNamed(w.Module); err != nil {
Expand Down Expand Up @@ -299,6 +300,9 @@ func (s *Service) IssueForWorkload(ctx context.Context, w Workload, r IssueReque
if !ok {
return fmt.Errorf("%w: operation binding %q is not one %s declares", workcontext.ErrNoBinding, r.Binding, module.Name)
}
if binding, err = ceiled(tx, org, module, binding); err != nil {
return err
}
if binding.Audience != audience.Principal {
return domain.Refuse("binding %s is addressed to %s, not %s", r.Binding, binding.Audience, audience.Principal)
}
Expand All @@ -313,6 +317,12 @@ func (s *Service) IssueForWorkload(ctx context.Context, w Workload, r IssueReque
return domain.NotNow("%s is not installed at %s", module.Name, org)
}
instID = inst.ID
// What the decision read: the installation's revision moves with
// the consent its bindings came from, so a capability minted after a
// narrowing committed is refused rather than handed out.
if f, err = s.src.fenceIn(tx, org, module.Principal, inst.ID); err != nil {
return err
}
return decide.OwnerAware(audience, scopes)
})
if err != nil {
Expand All @@ -326,6 +336,9 @@ func (s *Service) IssueForWorkload(ctx context.Context, w Workload, r IssueReque
if err != nil {
return Issued{}, err
}
if err := f.holds(signed); err != nil {
return Issued{}, err
}
if signed.GetSeal().GetPrincipalEpoch() != module.Generation || signed.GetOperationBinding().GetRevision() != module.Generation {
return Issued{}, domain.Refuse("workload binding changed during issuance")
}
Expand Down Expand Up @@ -385,6 +398,9 @@ func (s *Service) IssueForInstallation(ctx context.Context, w Workload, installa
if !ok {
return domain.Missing("operation binding %q is not one %s declares", r.Binding, solution.Name)
}
if binding, err = ceiled(tx, inst.Organization, solution, binding); err != nil {
return err
}
if binding.Audience != audience.Principal {
return domain.Refuse("binding %q is addressed to %s, not %s", r.Binding, binding.Audience, audience.Principal)
}
Expand Down Expand Up @@ -490,6 +506,16 @@ func (s *Service) IssueChild(ctx context.Context, parent string, actor Workload,
err = s.store.Read(ctx, func(tx Tx) error {
tenant := claims.GetTenantId()
var heldHere, sourceCeiling Scopes
var err error
// A hop back to the module's own audience reaches nothing the parent,
// addressed to it already, did not: it is bounded by that parent and
// the owner's grants, so the consent that bounds what a module reaches
// elsewhere does not apply to it.
if binding.Audience != am.Principal {
if binding, err = ceiled(tx, tenant, am, binding); err != nil {
return err
}
}
bounded := false
switch claims.GetOwnerPrincipalKind() {
case "human":
Expand Down Expand Up @@ -520,7 +546,6 @@ func (s *Service) IssueChild(ctx context.Context, parent string, actor Workload,
if (bounded || claims.GetOwnerPrincipalKind() == "agent") && !audience.OwnerAware {
return decide.OwnerAware(audience, decide.Scopes{{ResourceKind: audience.Name, ResourceOwnerId: claims.GetOwnerPrincipalId()}})
}
var err error
if through, err = decide.ScopesFor(binding, r.Operation, "invoke"); err != nil {
return err
}
Expand Down
Loading
Loading