~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
254 lines · 5.7 KB Raw History
package bus

import (
	"errors"
	"expvar"
	"fmt"
	"log/slog"
	"slices"
	"strings"
	"sync/atomic"

	"k8s.io/apimachinery/pkg/labels"

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
)

const (
	CapabilityXRPC = "xrpc.call"

	CapabilityStream = "stream.dial"

	ScopeAll = "*"
)

var ErrLinkDenied = errors.New("bus: no accepted ComponentLink allows this call")

type Capability struct {
	Name  string
	Scope []string
}

func (c Capability) String() string {
	if len(c.Scope) == 0 {
		return c.Name
	}
	return c.Name + "{" + strings.Join(c.Scope, ",") + "}"
}

type LinkFrom struct {
	Ref *commonv1.ObjectRef

	Group string

	Kind string

	Workspace string

	Selector labels.Selector
}

type Link struct {
	Workspace string

	Name string

	From LinkFrom

	To commonv1.ObjectRef

	Capabilities []Capability
}

type LinkDeniedError struct {
	From commonv1.ObjectRef

	To commonv1.ObjectRef

	Capability Capability
}

func (e *LinkDeniedError) Error() string {
	if e == nil {
		return ErrLinkDenied.Error()
	}
	return fmt.Sprintf("bus: no accepted ComponentLink allows %s to call %s with %s", refString(e.From), refString(e.To), e.Capability)
}

func (e *LinkDeniedError) Unwrap() error {
	return ErrLinkDenied
}

func LinkAllows(link Link, from Caller, to commonv1.ObjectRef, c Capability) bool {
	if !link.From.matches(from) {
		return false
	}
	if !refMatches(link.To, to) {
		return false
	}
	for _, granted := range link.Capabilities {
		if strings.EqualFold(strings.TrimSpace(granted.Name), strings.TrimSpace(c.Name)) && scopeCovers(granted.Scope, c.Scope) {
			return true
		}
	}
	return false
}

func (f LinkFrom) matches(caller Caller) bool {
	if f.Ref != nil {
		return refMatches(*f.Ref, caller.Ref)
	}
	if f.Selector == nil || strings.TrimSpace(f.Kind) == "" {
		return false
	}
	if !strings.EqualFold(strings.TrimSpace(f.Kind), strings.TrimSpace(caller.Ref.Kind)) {
		return false
	}
	if !groupMatches(f.Group, caller.Ref.Group) {
		return false
	}
	if !workspaceMatches(f.Workspace, caller.Ref.Workspace) {
		return false
	}
	return f.Selector.Matches(labels.Set(caller.Labels))
}

func workspaceMatches(pattern, workspace string) bool {
	pattern, workspace = strings.TrimSpace(pattern), strings.TrimSpace(workspace)
	if pattern == ScopeAll {
		return workspace != ""
	}
	if parent, ok := strings.CutSuffix(pattern, ":"+ScopeAll); ok {
		return strings.HasPrefix(workspace, parent+":")
	}
	return pattern == workspace
}

func refMatches(pattern, ref commonv1.ObjectRef) bool {
	if strings.TrimSpace(pattern.Kind) == "" || strings.TrimSpace(pattern.Name) == "" || strings.TrimSpace(pattern.Workspace) == "" {
		return false
	}
	return strings.TrimSpace(pattern.Workspace) == strings.TrimSpace(ref.Workspace) &&
		strings.EqualFold(strings.TrimSpace(pattern.Kind), strings.TrimSpace(ref.Kind)) &&
		strings.EqualFold(strings.TrimSpace(pattern.Name), strings.TrimSpace(ref.Name)) &&
		groupMatches(pattern.Group, ref.Group)
}

func groupMatches(pattern, group string) bool {
	pattern, group = strings.TrimSpace(pattern), strings.TrimSpace(group)
	return pattern == "" || group == "" || strings.EqualFold(pattern, group)
}

func scopeCovers(granted, requested []string) bool {
	if len(granted) == 0 || slices.Contains(granted, ScopeAll) {
		return true
	}
	if len(requested) == 0 {
		return false
	}
	for _, want := range requested {
		if !scopeEntryCovered(granted, strings.TrimSpace(want)) {
			return false
		}
	}
	return true
}

func scopeEntryCovered(granted []string, want string) bool {
	for _, entry := range granted {
		entry = strings.TrimSpace(entry)
		if entry == want {
			return true
		}
		if prefix, ok := strings.CutSuffix(entry, ScopeAll); ok && strings.HasPrefix(want, prefix) {
			return true
		}
	}
	return false
}

func refString(ref commonv1.ObjectRef) string {
	if ref.IsZero() {
		return "an unnamed caller"
	}
	return ref.String()
}

func cloneLinks(links []Link) []Link {
	if links == nil {
		return nil
	}
	out := make([]Link, len(links))
	for i, link := range links {
		out[i] = link
		if link.From.Ref != nil {
			ref := *link.From.Ref
			out[i].From.Ref = &ref
		}
		out[i].Capabilities = cloneCapabilities(link.Capabilities)
	}
	return out
}

func cloneCapabilities(in []Capability) []Capability {
	if in == nil {
		return nil
	}
	out := make([]Capability, len(in))
	for i, c := range in {
		out[i] = Capability{Name: c.Name, Scope: slices.Clone(c.Scope)}
	}
	return out
}

func linksEqual(a, b []Link) bool {
	return slices.EqualFunc(a, b, func(x, y Link) bool {
		if x.Workspace != y.Workspace || x.Name != y.Name || x.To != y.To {
			return false
		}
		if (x.From.Ref == nil) != (y.From.Ref == nil) || (x.From.Ref != nil && *x.From.Ref != *y.From.Ref) {
			return false
		}
		if x.From.Group != y.From.Group || x.From.Kind != y.From.Kind || x.From.Workspace != y.From.Workspace {
			return false
		}
		if selectorString(x.From.Selector) != selectorString(y.From.Selector) {
			return false
		}
		return slices.EqualFunc(x.Capabilities, y.Capabilities, func(p, q Capability) bool {
			return p.Name == q.Name && slices.Equal(p.Scope, q.Scope)
		})
	})
}

func selectorString(s labels.Selector) string {
	if s == nil {
		return ""
	}
	return s.String()
}

type denials struct {
	count atomic.Uint64

	byCapability expvar.Map

	log atomic.Pointer[slog.Logger]

	changed chan struct{}
}

func (d *denials) deny(from Caller, to commonv1.ObjectRef, c Capability) error {
	d.count.Add(1)
	d.byCapability.Add(c.Name, 1)
	select {
	case d.changed <- struct{}{}:
	default:
	}
	log := d.log.Load()
	if log == nil {
		log = slog.Default()
	}
	log.Warn("bus: link denied", "from", refString(from.Ref), "to", refString(to), "capability", c.String())
	return &LinkDeniedError{From: from.Ref, To: to, Capability: Capability{Name: c.Name, Scope: slices.Clone(c.Scope)}}
}