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
69 changes: 69 additions & 0 deletions api/datalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,75 @@ func LabelCheck(required map[string]string) (biscuit.Check, error) {
return parser.FromStringCheck("check if " + strings.Join(clauses, " or "))
}

// LabelFloorCheck compiles an operator's egress floor (see Egress.RequireLabels)
// into a single fail-closed check satisfied only when the token carries *every*
// pair: `check if label("jurisdiction", "eu"), label("compliance", "gdpr")`.
//
// The conjunction is the whole difference from LabelCheck, which is a
// disjunction because a caller naming several labels means "any of these will
// do". A floor cannot mean that: a peer attesting only the most permissive of
// several alternatives would satisfy the floor while sitting outside the
// boundary the operator drew. So a floor takes a map — one value per key, no
// way to spell an alternative — and requires all of it.
//
// Both are ordinary Biscuit checks, so a caller's requirement and a floor are
// combined by adding each to the authorizer and letting it AND them; neither
// needs to know about the other.
func LabelFloorCheck(required map[string]string) (biscuit.Check, error) {
if len(required) == 0 {
return biscuit.Check{}, fmt.Errorf("no required labels")
}
keys := make([]string, 0, len(required))
for k := range required {
keys = append(keys, k)
}
sort.Strings(keys)
clauses := make([]string, 0, len(required))
for _, k := range keys {
if err := ValidateLabelKey(k); err != nil {
return biscuit.Check{}, err
}
v := required[k]
if err := ValidateLabelValue(v); err != nil {
return biscuit.Check{}, err
}
clauses = append(clauses, fmt.Sprintf("%s(%q, %q)", FactLabel, k, v))
}
return parser.FromStringCheck("check if " + strings.Join(clauses, ", "))
}

// LabelsSatisfyFloor reports whether claimed satisfies every pair of the floor.
// It is the non-attested counterpart of LabelFloorCheck, for the one provider
// class that has no Biscuit to check: a service local to this node, whose
// labels are its own configuration and so are complete. An empty floor is
// satisfied by anything, so callers may pass one unconditionally.
func LabelsSatisfyFloor(floor, claimed map[string]string) bool {
for k, v := range floor {
if claimed[k] != v {
return false
}
}
return true
}
Comment on lines +620 to +627

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For defensive programming and clarity, explicitly handle the case where claimed is nil or floor is empty. Although map indexing on a nil map is safe in Go, explicit checks make the code more robust and self-documenting.

Suggested change
func LabelsSatisfyFloor(floor, claimed map[string]string) bool {
for k, v := range floor {
if claimed[k] != v {
return false
}
}
return true
}
func LabelsSatisfyFloor(floor, claimed map[string]string) bool {
if len(floor) == 0 {
return true
}
if claimed == nil {
return false
}
for k, v := range floor {
if claimed[k] != v {
return false
}
}
return true
}


// LabelsContradictFloor reports whether claimed states a *different* value for
// some key the floor requires.
//
// Absence is not contradiction, which is the whole distinction from
// LabelsSatisfyFloor. Gossiped claims are a discovery hint and may carry only
// part of what a peer attests, so a peer silent on one pair of the floor may
// still satisfy all of it in its Biscuit. Only a conflicting value is grounds
// to skip such a peer before the gate has seen its attested facts; treating
// silence as failure would drop providers that are inside the boundary.
func LabelsContradictFloor(floor, claimed map[string]string) bool {
for k, v := range floor {
if got, stated := claimed[k]; stated && got != v {
return true
}
}
return false
}
Comment on lines +638 to +645

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For defensive programming, explicitly handle the case where claimed is nil or floor is empty. This avoids executing the loop when there are no claims or no floor constraints.

Suggested change
func LabelsContradictFloor(floor, claimed map[string]string) bool {
for k, v := range floor {
if got, stated := claimed[k]; stated && got != v {
return true
}
}
return false
}
func LabelsContradictFloor(floor, claimed map[string]string) bool {
if len(floor) == 0 || claimed == nil {
return false
}
for k, v := range floor {
if got, stated := claimed[k]; stated && got != v {
return true
}
}
return false
}


// isExactService reports whether serviceStr resolves to a plain exact-match grant, as opposed to a
// wildcard/prefix/suffix pattern which already collapses to a single, cheap fact via BuildServiceDatalogFact.
// The classification is asked of BuildServiceDatalogFact rather than repeated here: a new wildcard shape
Expand Down
90 changes: 90 additions & 0 deletions api/datalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,3 +618,93 @@ func TestLabelFactsAndCheck(t *testing.T) {
})
}
}

// A caller's requirement is a disjunction ("any of these will do") and an
// operator's egress floor is a conjunction ("all of these"). The difference is
// the reason both exist, so it is pinned on the compiled shape rather than on
// the rendered string: a disjunction becomes one query body per alternative, a
// conjunction one body carrying every predicate.
func TestLabelFloorCheckIsConjunctionUnlikeLabelCheck(t *testing.T) {
both := map[string]string{"jurisdiction": "eu", "compliance": "gdpr"}

floor, err := LabelFloorCheck(both)
if err != nil {
t.Fatalf("LabelFloorCheck: %v", err)
}
if len(floor.Queries) != 1 {
t.Fatalf("a floor must compile to a single conjunctive body, got %d alternatives", len(floor.Queries))
}
if got := len(floor.Queries[0].Body); got != len(both) {
t.Errorf("the floor's body carries %d predicates, want all %d", got, len(both))
}

caller, err := LabelCheck(both)
if err != nil {
t.Fatalf("LabelCheck: %v", err)
}
if len(caller.Queries) != len(both) {
t.Errorf("a caller requirement must compile to one body per alternative, got %d", len(caller.Queries))
}
}

func TestLabelFloorCheckRejectsBadInput(t *testing.T) {
if _, err := LabelFloorCheck(nil); err == nil {
t.Error("LabelFloorCheck(nil): expected error, got nil")
}
if _, err := LabelFloorCheck(map[string]string{"region": "bad,value"}); err == nil {
t.Error("LabelFloorCheck(invalid value): expected error, got nil")
}
if _, err := LabelFloorCheck(map[string]string{"bad key!": "v"}); err == nil {
t.Error("LabelFloorCheck(invalid key): expected error, got nil")
}
}

func TestLabelsSatisfyFloor(t *testing.T) {
floor := map[string]string{"jurisdiction": "eu", "compliance": "gdpr"}
tests := []struct {
name string
claimed map[string]string
want bool
}{
{"every pair present", map[string]string{"jurisdiction": "eu", "compliance": "gdpr", "region": "de"}, true},
{"one pair missing", map[string]string{"jurisdiction": "eu"}, false},
{"one pair wrong", map[string]string{"jurisdiction": "eu", "compliance": "hipaa"}, false},
{"nothing claimed", nil, false},
}
for _, tt := range tests {
if got := LabelsSatisfyFloor(floor, tt.claimed); got != tt.want {
t.Errorf("%s: LabelsSatisfyFloor = %v, want %v", tt.name, got, tt.want)
}
}
// An empty floor constrains nothing, so callers can pass it unconditionally.
if !LabelsSatisfyFloor(nil, nil) {
t.Error("an empty floor must be satisfied by anything")
}
}

// The two floor predicates differ on one case, and it is the case that
// matters: a peer silent on a pair the floor requires. Gossip is partial, so
// silence must not read as failure before the gate has seen attested facts.
func TestLabelsContradictFloorTreatsSilenceAsUnknown(t *testing.T) {
floor := map[string]string{"jurisdiction": "eu", "compliance": "gdpr"}

partial := map[string]string{"jurisdiction": "eu"}
if LabelsContradictFloor(floor, partial) {
t.Error("a claim silent on one pair does not contradict the floor")
}
if LabelsSatisfyFloor(floor, partial) {
t.Error("but it does not satisfy it either")
}

conflicting := map[string]string{"jurisdiction": "us"}
if !LabelsContradictFloor(floor, conflicting) {
t.Error("a different value for a required key is a contradiction")
}

if LabelsContradictFloor(floor, nil) {
t.Error("no claims at all cannot contradict anything")
}
if LabelsContradictFloor(floor, map[string]string{"jurisdiction": "eu", "compliance": "gdpr"}) {
t.Error("a fully satisfying claim must not read as a contradiction")
}
}
34 changes: 30 additions & 4 deletions api/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,14 @@ type ServiceConfig struct {

// NodeConfig defines the optional attenuation rules and static services for a specific SAM Node.
type NodeConfig struct {
Version string `yaml:"version"`
Attenuation Attenuation `yaml:"attenuation"`
Services []ServiceConfig `yaml:"services"`
Labels map[string]string `yaml:"labels,omitempty"`
Version string `yaml:"version"`
Attenuation Attenuation `yaml:"attenuation"`
Services []ServiceConfig `yaml:"services"`
// Labels is what this node is, attested at enrollment; Egress is what it
// demands of the peers it talks to. Adjacent because they are read
// together and mean opposite directions.
Labels map[string]string `yaml:"labels,omitempty"`
Egress Egress `yaml:"egress"`
}

// NodeConfigVersionV1Alpha1 is the only node config schema this build understands.
Expand All @@ -66,3 +70,25 @@ type Attenuation struct {
Checks []string `yaml:"checks"`
Rules []string `yaml:"rules"`
}

// Egress is the operator's outbound policy: what this node demands of the peers
// it talks to. Attenuation is the mirror of it — what this node demands of the
// peers that talk to *it* — and the two are deliberately separate blocks
// because they answer opposite questions.
type Egress struct {
// RequireLabels is a floor every remote provider must attest before this
// node will send it anything, whatever the caller asked for. Absent means
// no floor, which is the historical behaviour: the requirement is then
// whatever the caller supplied, and a caller that supplies nothing is
// unconstrained.
//
// Every pair must hold (AND), unlike a caller's requirement, where any one
// pair is enough (see LabelCheck vs LabelFloorCheck). A map gives one value
// per key, so a floor cannot express alternatives — that is the point: a
// floor with alternatives would let the weakest of them stand in for the
// rest.
//
// A floor naming a label no peer attests reaches nothing, which is a
// usable egress kill switch.
RequireLabels map[string]string `yaml:"require_labels,omitempty"`
}
18 changes: 18 additions & 0 deletions internal/node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ type NodeConfigComplete struct {
Rules []biscuit.Rule
Services []api.ServiceConfig
Labels map[string]string

// EgressRequireLabels is the operator's egress floor (api.Egress). Nil
// means no floor, so a caller's requirement — or the absence of one —
// stands on its own, as it always has.
EgressRequireLabels map[string]string
}

// LoadNodeConfig loads the node configuration from the specified path.
Expand Down Expand Up @@ -75,6 +80,19 @@ func CompleteNodeConfig(config api.NodeConfig) (*NodeConfigComplete, error) {
Labels: config.Labels,
}

// Rejected at load, not at first use: a floor that cannot compile would
// otherwise fail open on the request that needed it, and an operator who
// wrote one is entitled to find out at startup instead.
if len(config.Egress.RequireLabels) > 0 {
if err := api.ValidateLabels(config.Egress.RequireLabels); err != nil {
return nil, fmt.Errorf("invalid egress.require_labels: %w", err)
}
if _, err := api.LabelFloorCheck(config.Egress.RequireLabels); err != nil {
return nil, fmt.Errorf("invalid egress.require_labels: %w", err)
}
complete.EgressRequireLabels = config.Egress.RequireLabels
}

for i, svc := range config.Services {
if err := api.ValidateServiceFormat(svc.Type + "://" + svc.Name); err != nil {
return nil, fmt.Errorf("invalid service config at index %d: %w", i, err)
Expand Down
61 changes: 61 additions & 0 deletions internal/node/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,64 @@ func TestCompleteNodeConfig(t *testing.T) {
t.Fatal("CompleteNodeConfig() with invalid Datalog: want error, got nil")
}
}

func TestLoadNodeConfigEgressFloor(t *testing.T) {
write := func(t *testing.T, body string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "sam-node.yaml")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
return path
}

t.Run("floor is loaded", func(t *testing.T) {
cfg, err := LoadNodeConfig(write(t, `
version: v1alpha1
egress:
require_labels:
jurisdiction: eu
compliance: gdpr
`))
if err != nil {
t.Fatalf("LoadNodeConfig: %v", err)
}
if len(cfg.EgressRequireLabels) != 2 ||
cfg.EgressRequireLabels["jurisdiction"] != "eu" ||
cfg.EgressRequireLabels["compliance"] != "gdpr" {
t.Errorf("EgressRequireLabels = %v", cfg.EgressRequireLabels)
}
})

t.Run("absent block means no floor", func(t *testing.T) {
cfg, err := LoadNodeConfig(write(t, "version: v1alpha1\n"))
if err != nil {
t.Fatalf("LoadNodeConfig: %v", err)
}
if cfg.EgressRequireLabels != nil {
t.Errorf("no egress block must leave the floor nil, got %v", cfg.EgressRequireLabels)
}
})

// Rejected at load: a floor that cannot compile would otherwise fail open
// on the first request that needed it.
t.Run("a malformed floor fails startup", func(t *testing.T) {
for _, body := range []string{
"version: v1alpha1\negress:\n require_labels:\n \"bad key!\": eu\n",
"version: v1alpha1\negress:\n require_labels:\n jurisdiction: \"has,comma\"\n",
"version: v1alpha1\negress:\n require_labels:\n jurisdiction: \"\"\n",
} {
if _, err := LoadNodeConfig(write(t, body)); err == nil {
t.Errorf("expected a load error for %q", body)
}
}
})

// The schema is strict, so a typo in the block name is refused rather than
// silently leaving the node with no floor.
t.Run("a misspelled key is refused, not ignored", func(t *testing.T) {
if _, err := LoadNodeConfig(write(t, "version: v1alpha1\negress:\n required_labels:\n jurisdiction: eu\n")); err == nil {
t.Error("require_labels misspelled as required_labels must fail the strict schema")
}
})
}
Loading
Loading