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
168 changes: 168 additions & 0 deletions libraries/authority-core/go/authority/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package authority

import (
"context"
"errors"
"slices"

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

// Check verifies a capability for the audience the caller is, and refuses
// owner-selected scopes the audience can no longer honour: an audience that
// does not declare owner-aware checking now, or one that withdrew it after
// the capability was issued through a binding to it (never revived).
func (s *Service) Check(ctx context.Context, token, audience string) (*workcontext.Verified, uint64, error) {
verified, err := s.Verify(ctx, token, audience)
if errors.Is(err, workcontext.ErrRevoked) {
// Verified up to the revocation: name an owner-aware withdrawal,
// which core reports as a binding incarnation that moved.
if decoded, derr := workcontext.Decode(token); derr == nil {
wc := decoded.Context()
if scope.HasOwner(effectiveOf(wc)) && wc.GetOperationBinding().GetBindingId() != "" {
if current, berr := s.src.OperationBinding(ctx, wc.GetOperationBinding().GetBindingId()); berr == nil && current.Incarnation != wc.GetOperationBinding().GetIncarnation() {
return nil, 0, domain.Refuse("issued through a binding re-created since, or before %s last withdrew owner-aware checking; only a fresh capability carries owner-selected scopes", audience)
}
}
}
}
if err != nil {
return nil, 0, err
}
var revision uint64
err = s.store.Read(ctx, func(tx Tx) error {
m, err := tx.Module(audience)
if err != nil {
return notFound(err)
}
if err := decide.OwnerAware(m, verified.EffectiveScopes()); err != nil {
return err
}
revision, err = tx.AuthorizationRevision(verified.Context().GetTenantId())
return err
})
if err != nil {
return nil, 0, err
}
return verified, revision, 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 {
resources.Grant
Owner string
}

// ListResources answers which resources of kind a checked capability may act
// on with action, decided now: a person's through a solution are its roles on
// the principal's data, a person's own their organization roles' matchers,
// cut to any ids the capability names; a module's or an installation's own
// work is bounded by the capability alone.
func (s *Service) ListResources(ctx context.Context, token, audience, kind, action string) ([]Grant, error) {
if audience == "" || kind == "" || action == "" {
return nil, domain.Refuse("a resource question names the audience, a kind and an action")
}
verified, _, err := s.Check(ctx, token, audience)
if err != nil {
return nil, err
}
effective := verified.EffectiveScopes()
if !scope.HoldsAction(effective, kind, action) {
return nil, domain.Refuse("the capability holds no %s:%s", kind, action)
}
wc := verified.Context()
var out []Grant
err = s.store.Read(ctx, func(tx Tx) error {
rows := effective
if wc.GetOwnerPrincipalKind() == "human" {
p, err := tx.Person(wc.GetOwnerPrincipalId())
if err != nil {
return domain.Refuse("the owner %s is no longer a member", wc.GetOwnerPrincipalId())
}
ceiling, bounded, err := solutionCeiling(tx, p, solutionPrincipals(wc, wc.GetAudience()))
if err != nil {
return err
}
if bounded {
rows = scope.Intersect(effective, ceiling)
} else {
roles, err := orgRoles(tx, p)
if err != nil {
return err
}
out = narrow(decide.ResourceGrants(roles, kind, action), effective, kind, action)
return nil
}
}
out = grantsOf(rows, kind, action)
return nil
})
return out, err
}

func holds(sc *basev0.WorkScopeV1, kind, action string) bool {
return sc.GetResourceKind() == kind && (slices.Contains(sc.GetActions(), action) || slices.Contains(sc.GetActions(), scope.AnyAction))
}

// grantsOf projects the rows of kind holding action: their ids, or every
// resource, each qualified by the row's owner.
func grantsOf(rows Scopes, kind, action string) []Grant {
out := []Grant{}
for _, sc := range rows {
if !holds(sc, kind, action) {
continue
}
if len(sc.GetResourceIds()) == 0 {
out = append(out, Grant{Grant: resources.Grant{All: true}, Owner: sc.GetResourceOwnerId()})
continue
}
for _, id := range sc.GetResourceIds() {
out = append(out, Grant{Grant: resources.Grant{ID: id}, Owner: sc.GetResourceOwnerId()})
}
}
return out
}

// narrow cuts an organization role's matchers to the ids the capability
// names, when it names any.
func narrow(matchers []resources.Grant, effective Scopes, kind, action string) []Grant {
var ids []string
for _, sc := range effective {
if !holds(sc, kind, action) {
continue
}
if len(sc.GetResourceIds()) == 0 {
out := make([]Grant, 0, len(matchers))
for _, m := range matchers {
out = append(out, Grant{Grant: m})
}
return out
}
ids = append(ids, sc.GetResourceIds()...)
}
covered, err := resources.Compile(matchers)
out := []Grant{}
if err != nil {
return out
}
for _, id := range ids {
if covered.Matches(id) {
out = append(out, Grant{Grant: resources.Grant{ID: id}})
}
}
return out
}

// effectiveOf is a context's effective scopes: its last actor's, else its own.
func effectiveOf(wc *basev0.WorkContextV1) Scopes {
if chain := wc.GetActorChain(); len(chain) > 0 {
return chain[len(chain)-1].GetGrantedScopes()
}
return wc.GetAuthorityScopes()
}
59 changes: 59 additions & 0 deletions libraries/authority-core/go/authority/child_intersection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package authority_test

import (
"slices"
"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 child is the owner's current grants intersected with the binding it is
// issued through: a scope the parent holds outside the binding never reaches
// the child, whatever the parent carried (rule A, r122 on runtime #311).
func TestAChildHoldsOnlyWhatTheBindingAndTheOwnersGrantsShare(t *testing.T) {
h := newHarness(t)
page := domain.Module{Name: "page", Principal: "page", Kind: domain.KindModule, Generation: 1,
ResourceKinds: []*basev0.WorkScopeV1{sc("page.entry", "read"), sc(kind, "read", "write"), sc("profiles", "read")},
Bindings: map[string]domain.Binding{"store": {ID: "store", Audience: "store", InvokeScopes: []*basev0.WorkScopeV1{sc(kind, "read", "write")}}}}
if _, err := h.svc.ApplyModule(h.ctx, page); err != nil {
t.Fatal(err)
}
h.store.PutOrgRole(domain.OrgRole{Organization: acme, Name: "wide", Scopes: []domain.RoleScope{
{Kind: "page.entry", Actions: []string{"read"}}, {Kind: kind, Actions: []string{"read", "write"}}, {Kind: "profiles", Actions: []string{"read"}}}})
h.store.PutPerson(domain.Person{ID: "wren", Organization: acme, Roles: []string{"wide"}})

parent := h.must(h.root("wren", "page"))
if kinds := kindsOf(effective(parent)); !slices.Equal(kinds, []string{kind, "page.entry", "profiles"}) {
t.Fatalf("the parent holds %v; the test needs scopes outside the binding", kinds)
}
for name, ask := range map[string][]*basev0.WorkScopeV1{"everything held": nil, "the binding's kind": {sc(kind, "read", "write")}} {
t.Run(name, func(t *testing.T) {
child := h.must(h.svc.IssueChild(h.ctx, parent.Token, authority.Workload{Module: "page"}, authority.IssueRequest{Binding: "store", Scopes: ask}))
exactly(t, child, kind, "", []string{"read", "write"})
verified, _, err := h.svc.Check(h.ctx, child.Token, "store")
if err != nil {
t.Fatal(err)
}
if kinds := kindsOf(verified.EffectiveScopes()); !slices.Equal(kinds, []string{kind}) {
t.Fatalf("the checked child holds %v, want only %s", kinds, kind)
}
})
}
t.Run("a scope outside the binding", func(t *testing.T) {
_, err := h.svc.IssueChild(h.ctx, parent.Token, authority.Workload{Module: "page"}, authority.IssueRequest{Binding: "store", Scopes: []*basev0.WorkScopeV1{sc("profiles", "read")}})
refused(t, err, "widens")
})
}

func kindsOf(scopes []*basev0.WorkScopeV1) []string {
var out []string
for _, s := range scopes {
if len(s.GetActions()) > 0 && !slices.Contains(out, s.GetResourceKind()) {
out = append(out, s.GetResourceKind())
}
}
slices.Sort(out)
return out
}
Loading
Loading