Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b666d21
feat(authority-core): one membership per organization, the delivery r…
antoinetoussaint-byte Oct 27, 2026
fa3794f
style: gofmt
antoinetoussaint-byte Oct 27, 2026
2ad39f7
feat(authority-core): every organization holds every composed module,…
antoinetoussaint-byte Oct 27, 2026
de8beb5
feat(authority-core): every change of authority is journaled in the t…
antoinetoussaint-byte Oct 27, 2026
827d29c
feat(authority-core): read a composed module as delivered
antoinetoussaint-byte Oct 27, 2026
7d2dfd1
feat(authority-core): a delegation is to an installed module with a b…
antoinetoussaint-byte Oct 27, 2026
f8e81ae
test(conformance): every installation call stays in its organization,…
antoinetoussaint-byte Oct 27, 2026
8093104
feat(authority-core): an install of a target already installed names …
antoinetoussaint-byte Oct 27, 2026
62f9b2e
perf(authority-core): reconciling organizations writes only where a m…
antoinetoussaint-byte Oct 27, 2026
8846431
test(storetest): an empty id names no member
antoinetoussaint-byte Oct 27, 2026
a2f1c56
core: a resource-scoped organization role is issued on its ids, never…
antoinetoussaint-byte Oct 27, 2026
76c3759
Merge main into core/hosts-share-the-rules
antoinetoussaint-byte Oct 27, 2026
ddc87c4
test(authority-core): current installation asks for the store-x its r…
antoinetoussaint-byte Oct 27, 2026
b5a4a69
feat(authority-core): a host that installs modules by consent is the …
antoinetoussaint-byte Oct 27, 2026
52951b6
test(storetest): a failed transaction's installation is a solution's,…
antoinetoussaint-byte Oct 27, 2026
e9b9c69
feat(conformance): a host that installs modules by consent installs t…
antoinetoussaint-byte Oct 27, 2026
6384474
Merge origin/main (656d23bf, #32) into core/hosts-share-the-rules
antoinetoussaint-byte Oct 27, 2026
e0b04b2
test(conformance): an admission key registration carries its proof, a…
antoinetoussaint-byte Oct 27, 2026
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
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -784,15 +784,22 @@ the suite, and by any module deciding which of its own resources a caller sees.
The authority knows resources by id only; each module maps ids onto its own.

On the local authority a role's scope may carry `resources`. A scope with
none covers the whole kind. A person's answer is what her roles grant. A
module's or an installation's own work is bounded by its capability alone. A
capability narrowed to named ids covers only the ones the owner's grants also
cover. A capability that does not hold the action is refused.
none covers the whole kind. A scope with resources is never issued as the
whole kind (2026-10-26): its ids are the capability's resource ids, so an ask
naming none is issued on exactly those ids, and an ask naming any other id, or
the whole kind, is refused, at the root and at every hop. A token cannot carry
a prefix or a pattern, so one holds only the ids an ask (or the capability a
hop derives from) names that it matches, and an ask naming none carries none
of it. A person's answer is what her roles grant, cut to the ids her
capability names. A module's or an installation's own work is bounded by its
capability alone. A capability that does not hold the action is refused.

The suite line "the authority answers which resources a context covers, by
grant" holds an implementation to a fixture's covered and uncovered ids, an
unheld action, and a narrowed capability. It fails on an implementation that
answers every resource.
unheld action, an empty ask that never carries the whole kind, and the refusal
of an ask naming an uncovered id or the whole kind. It fails on an
implementation that answers every resource or issues a resource-scoped role as
the whole kind.

Building it exposed an attenuation bug: the local authority's scope operations dropped
resource ids, so a capability asked for two named resources was issued for the
Expand Down
6 changes: 4 additions & 2 deletions libraries/authority-core/go/authority/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ func (s *Service) AttachResource(ctx context.Context, r AttachmentRequest) (stri
return err
}
owner = inst.ID
return nil
return s.journal(ctx, tx, domain.Event{Kind: domain.ResourceAttached, Organization: inst.Organization, Actor: r.Store.Module, Subject: inst.ID,
Detail: map[string]string{"kind": r.Kind, "resource": r.ResourceID}})
})
return owner, err
}
Expand Down Expand Up @@ -136,7 +137,8 @@ func (s *Service) DetachResource(ctx context.Context, caller Workload, org, kind
return err
}
owner = inst.ID
return nil
return s.journal(ctx, tx, domain.Event{Kind: domain.ResourceDetached, Organization: org, Actor: caller.Module, Subject: inst.ID,
Detail: map[string]string{"kind": kind, "resource": id}})
}
return nil
})
Expand Down
32 changes: 31 additions & 1 deletion libraries/authority-core/go/authority/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,36 @@ func (s *Service) Check(ctx context.Context, token, audience string) (*workconte
return verified, revision, nil
}

// CheckMethod is Check, then what the audience module's delivery declares
// for method applied to what the call's current actor holds (the last hop's
// grant, never the owner's authority above it): decide.AdmitMethod. An empty
// method is Check alone. Advisory decides the method but does not enforce it:
// a sound capability is answered with the method's refusal beside it, the
// same refusal a check that enforces it returns as its error.
func (s *Service) CheckMethod(ctx context.Context, token, audience, method string, advisory bool) (verified *workcontext.Verified, revision uint64, refusal error, err error) {
if audience == "" {
return nil, 0, nil, domain.Refuse("a check names the audience the caller is")
}
if verified, revision, err = s.Check(ctx, token, audience); err != nil || method == "" {
return verified, revision, nil, err
}
err = s.store.Read(ctx, func(tx Tx) error {
m, err := tx.Module(audience)
if err != nil {
return notFound(err)
}
refusal = decide.AdmitMethod(m, method, verified.EffectiveScopes())
return nil
})
if err != nil {
return nil, 0, nil, err
}
if refusal != nil && !advisory {
return nil, 0, nil, refusal
}
return verified, revision, refusal, nil
}

// Grant is one way a capability covers resources of a kind: a resource match
// and, when set, the owner the resource must have.
type Grant struct {
Expand Down Expand Up @@ -81,7 +111,7 @@ func (s *Service) ListResources(ctx context.Context, token, audience, kind, acti
err = s.store.Read(ctx, func(tx Tx) error {
rows := effective
if wc.GetOwnerPrincipalKind() == "human" {
p, err := tx.Person(wc.GetOwnerPrincipalId())
p, err := tx.Member(wc.GetTenantId(), wc.GetOwnerPrincipalId())
if err != nil {
return domain.Refuse("the owner %s is no longer a member", wc.GetOwnerPrincipalId())
}
Expand Down
16 changes: 14 additions & 2 deletions libraries/authority-core/go/authority/consent.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,28 @@ import (
)

func administrator(tx Tx, caller Person, org string) error {
p, err := tx.Person(caller.ID)
p, err := memberAs(tx, caller, org)
if err != nil && !errors.Is(err, domain.ErrNotFound) {
return err
}
if caller.ID == "" || err != nil || p.Organization != org || !decide.IsAdministrator(p) {
if err != nil || !decide.IsAdministrator(p) {
return domain.Refuse("installation administration requires an administrator or owner of organization %q", org)
}
return nil
}

// memberAs is the caller as a member of org, read now. The caller acts in the
// organization their session was opened at: a person who belongs to several
// organizations administers or reads another one only from a session there,
// so a session never carries one organization's authority into another.
// Anything else is ErrNotFound, as an absent member is.
func memberAs(tx Tx, caller Person, org string) (domain.Person, error) {
if caller.ID == "" || org == "" || caller.Organization != org {
return domain.Person{}, fmt.Errorf("%q acting at %q: %w", caller.ID, org, domain.ErrNotFound)
}
return tx.Member(org, caller.ID)
}

// offerDigest covers every authority declaration in the reviewed module. JSON
// sorts map keys; the receipt therefore covers roles, ceilings and bindings too.
func offerDigest(m domain.Module) (string, error) {
Expand Down
5 changes: 3 additions & 2 deletions libraries/authority-core/go/authority/current_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (

func TestCurrentInstallationIsConfinedToTheAuthenticatedAudience(t *testing.T) {
h := newHarness(t)
parent := h.must(h.root("rob", "store", sc(kind, "read")))
// rob and omar read store-x alone: each asks for what they hold.
parent := h.must(h.root("rob", "store", owned(kind, "", []string{"read"}, "store-x")))
inst, err := h.svc.CurrentInstallation(h.ctx, parent.Token, authority.Workload{Module: "store"})
if err != nil || inst.ID != authority.StableInstallationID(acme, "store") || inst.Organization != acme || inst.Target != "store" {
t.Fatalf("current installation: %+v %v", inst, err)
Expand All @@ -25,7 +26,7 @@ func TestCurrentInstallationIsConfinedToTheAuthenticatedAudience(t *testing.T) {
}
h.store.PutPerson(domain.Person{ID: "omar", Organization: globex, Roles: []string{"store-reader"}})
h.store.PutOrgRole(domain.OrgRole{Organization: globex, Name: "store-reader", Scopes: []domain.RoleScope{{Kind: kind, Actions: []string{"read"}, Resources: []resources.Grant{{ID: "store-x"}}}}})
foreign := h.must(h.root("omar", "store", sc(kind, "read")))
foreign := h.must(h.root("omar", "store", owned(kind, "", []string{"read"}, "store-x")))
other, err := h.svc.CurrentInstallation(h.ctx, foreign.Token, authority.Workload{Module: "store"})
if err != nil || other.Organization != globex || other.ID == inst.ID {
t.Fatalf("tenant isolation: %+v %v", other, err)
Expand Down
49 changes: 38 additions & 11 deletions libraries/authority-core/go/authority/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package authority

import (
"context"
"errors"

"github.com/codefly-dev/authority/libraries/authority-core/go/decide"
"github.com/codefly-dev/authority/libraries/authority-core/go/domain"
Expand All @@ -15,7 +16,7 @@ import (
func (s *Service) Delegate(ctx context.Context, who Person, module string, scopes Scopes, purpose string) (domain.Delegation, error) {
var d domain.Delegation
err := s.store.Write(ctx, func(tx Tx) error {
p, err := tx.Person(who.ID)
p, err := tx.Member(who.Organization, who.ID)
if err != nil {
return notFound(err)
}
Expand All @@ -26,7 +27,15 @@ func (s *Service) Delegate(ctx context.Context, who Person, module string, scope
if len(scopes) == 0 {
return domain.Refuse("a delegation names what it grants")
}
holdings, err := held(tx, p, m)
if len(purpose) > MaxDelegationPurpose {
return domain.Malformed("a delegation's purpose is at most %d characters, not %d", MaxDelegationPurpose, len(purpose))
}
// A module not installed at the grantor's organization could never
// issue from the delegation: it is refused rather than recorded.
if inst, err := installationOf(tx, p.Organization, m.Name); err != nil || inst.Status != domain.Active {
return domain.NotNow("module %q is not installed in organization %s, so nothing could issue from a delegation to it", m.Name, p.Organization)
}
holdings, err := held(tx, p, m, scopes)
if err != nil {
return err
}
Expand All @@ -35,12 +44,12 @@ func (s *Service) Delegate(ctx context.Context, who Person, module string, scope
if err != nil {
continue
}
there, _, _, err := throughSolutions(tx, p, target, []string{m.Principal})
there, _, _, err := throughSolutions(tx, p, target, []string{m.Principal}, scopes)
if err != nil {
return err
}
if m.Kind != domain.KindSolution {
if there, err = held(tx, p, target); err != nil {
if there, err = held(tx, p, target, scopes); err != nil {
return err
}
}
Expand All @@ -51,13 +60,21 @@ func (s *Service) Delegate(ctx context.Context, who Person, module string, scope
return domain.Refuse("%s delegates what they do not hold", p.ID)
}
d = domain.Delegation{ID: s.cfg.NewID(), Organization: p.Organization, Grantor: p.ID, Module: m.Name, Purpose: purpose, Scopes: scope.Normalize(scopes)}
return tx.PutDelegation(d)
if err := tx.PutDelegation(d); err != nil {
return err
}
return s.journal(ctx, tx, domain.Event{Kind: domain.DelegationGranted, Organization: d.Organization, Actor: d.Grantor, Subject: d.ID,
Detail: map[string]string{"module": d.Module, "purpose": d.Purpose}})
})
return d, err
}

// RevokeDelegation ends a delegation; only its grantor may. Its capabilities
// are refused at once.
// MaxDelegationPurpose bounds the purpose a delegation records.
const MaxDelegationPurpose = 1024

// RevokeDelegation ends a delegation: its grantor may, and so may an
// administrator of its organization acting there. Anyone else learns nothing,
// not even that it exists. Its capabilities are refused at once.
func (s *Service) RevokeDelegation(ctx context.Context, who Person, id string) (domain.Delegation, error) {
var d domain.Delegation
err := s.store.Write(ctx, func(tx Tx) error {
Expand All @@ -66,7 +83,13 @@ func (s *Service) RevokeDelegation(ctx context.Context, who Person, id string) (
return notFound(err)
}
if d.Grantor != who.ID {
return domain.Refuse("only the grantor of %s may revoke it", id)
admin, err := memberAs(tx, who, d.Organization)
if err != nil && !errors.Is(err, domain.ErrNotFound) {
return err
}
if err != nil || !decide.IsAdministrator(admin) {
return domain.Missing("delegation %q", id)
}
}
if d.Revoked {
return nil
Expand All @@ -75,7 +98,11 @@ func (s *Service) RevokeDelegation(ctx context.Context, who Person, id string) (
if err := tx.PutDelegation(d); err != nil {
return err
}
return tx.BumpAuthorizationRevision(d.Organization)
if err := tx.BumpAuthorizationRevision(d.Organization); err != nil {
return err
}
return s.journal(ctx, tx, domain.Event{Kind: domain.DelegationRevoked, Organization: d.Organization, Actor: who.ID, Subject: d.ID,
Detail: map[string]string{"module": d.Module}})
})
return d, err
}
Expand Down Expand Up @@ -138,11 +165,11 @@ func (s *Service) IssueForDelegation(ctx context.Context, id string, actor Workl
if through, err = decide.ScopesFor(binding, r.Operation, "invoke"); err != nil {
return err
}
p, err := tx.Person(d.Grantor)
p, err := tx.Member(d.Organization, d.Grantor)
if err != nil {
return domain.Refuse("the grantor %s is no longer a member", d.Grantor)
}
h, _, _, err := throughSolutions(tx, p, audience, []string{am.Principal})
h, _, _, err := throughSolutions(tx, p, audience, []string{am.Principal}, d.Scopes)
if err != nil {
return err
}
Expand Down
68 changes: 68 additions & 0 deletions libraries/authority-core/go/authority/delegation_rules_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package authority_test

import (
"strings"
"testing"

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

// A delegation is to a module installed at the grantor's organization, with a
// bounded purpose; its grantor or an administrator of its organization ends
// it, and anyone else learns nothing of it.
func TestDelegationRules(t *testing.T) {
h := newHarness(t)
read := []*basev0.WorkScopeV1{sc(kind, "read")}
rob := h.person("rob")

t.Run("a purpose past its bound is malformed", func(t *testing.T) {
_, err := h.svc.Delegate(h.ctx, rob, "store", read, strings.Repeat("p", authority.MaxDelegationPurpose+1))
code(t, err, domain.Invalid)
})
t.Run("a module not installed at the grantor's organization is refused", func(t *testing.T) {
if err := h.svc.UninstallAsHost(h.ctx, acme, authority.StableInstallationID(acme, "store")); err != nil {
t.Fatal(err)
}
_, err := h.svc.Delegate(h.ctx, rob, "store", read, "a job")
code(t, err, domain.Precondition)
refused(t, err, "is not installed")
})
}

func TestADelegationIsEndedByItsGrantorOrAnAdministratorAlone(t *testing.T) {
h := newHarness(t)
// Rob reads store-x alone: he delegates that, never the whole kind.
read := []*basev0.WorkScopeV1{owned(kind, "", []string{"read"}, "store-x")}
rob, maya, alex, zoe := h.person("rob"), h.person("maya"), h.person("alex"), h.person("zoe")
for _, tc := range []struct {
name string
who authority.Person
ok bool
reason domain.Code
}{
{"another member", maya, false, domain.NotFound},
{"another organization's administrator", zoe, false, domain.NotFound},
{"an administrator of its organization", alex, true, 0},
{"its grantor", rob, true, 0},
} {
t.Run(tc.name, func(t *testing.T) {
d, err := h.svc.Delegate(h.ctx, rob, "store", read, "a job")
if err != nil {
t.Fatal(err)
}
ended, err := h.svc.RevokeDelegation(h.ctx, tc.who, d.ID)
if !tc.ok {
code(t, err, tc.reason)
return
}
if err != nil || !ended.Revoked {
t.Fatalf("revoked %+v %v", ended, err)
}
if again, err := h.svc.RevokeDelegation(h.ctx, tc.who, d.ID); err != nil || !again.Revoked {
t.Fatalf("revoking twice answers the delegation as it stands: %+v %v", again, err)
}
})
}
}
Loading
Loading