An assignment says how far an endpoint reaches, and three things read it
novox/hq ADR 0138. Reachability was settled three times over: the filter read a listen's source with expose able to override it; the proxy composed a public name and an internal name for every route it was given, because it could; and the certificate authority followed from which names existed. Each was defensible and the combination was unstated, so "this endpoint must not be public" could not be written and was enforced by nothing — while a public certificate for that name was obtained anyway. Measured on the control node: an identity provider holding a 90-day public certificate and a 24-hour internal one, neither asked for. `reach` is one value per endpoint, per node — machine, internal, public or both — and the filter's source and the composed names both follow it. The authority needs no work: the proxy already asks the public authority for a route's own name and its internal authority for the internal one, so controlling the names controls the authority. Joined by the port, which a route already names: 35 of the catalogue's 36 route entries name a port the same module declares a listen on, and the one that does not is a path-level refusal — a rule about a name rather than an endpoint, left alone. Nothing said composes both names and follows the manifest's `from`, so every mesh already running is unchanged until an assignment speaks. A port that says both reach and expose is refused: they say the same thing in different words, and the filter would follow one while the names followed the other.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// a web module with one routed endpoint, the shape almost every routed module in the catalogue has.
|
||||
func aRoutedWeb() Manifest {
|
||||
return Manifest{
|
||||
Module: "web",
|
||||
Listens: []Listening{{Port: 3000, From: FromMesh}},
|
||||
Contributes: map[string]map[string]any{
|
||||
"route": {"label": "app", "port": 3000},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func reachSet(reach string) SettingsBy {
|
||||
return SettingsBy{"web": {{From: "node anchor",
|
||||
Values: map[string]any{ReachSetting: map[string]any{"3000": reach}}}}}
|
||||
}
|
||||
|
||||
// namesFor renders the contribution a routed module makes and returns the two names it carries.
|
||||
func namesFor(t *testing.T, m Manifest, settings SettingsBy) (public, internal string) {
|
||||
t.Helper()
|
||||
r := Resolution{Node: "anchor", Modules: []Manifest{m},
|
||||
PublicDomain: "example.test", At: "anchor.internal"}
|
||||
given, err := r.contributions(settings, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("contributions: %v", err)
|
||||
}
|
||||
for _, c := range given["route"] {
|
||||
p, _ := c.Values["name"].(string)
|
||||
i, _ := c.Values["internal-name"].(string)
|
||||
return p, i
|
||||
}
|
||||
t.Fatal("the module contributed no route")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// **Nothing said composes both names, exactly as before.** This is the assertion that keeps every
|
||||
// mesh already running identical until an assignment speaks, and it is the one that would break first
|
||||
// if reach were read where it should not be.
|
||||
func TestAnEndpointWithNoReachKeepsBothNames(t *testing.T) {
|
||||
public, internal := namesFor(t, aRoutedWeb(), nil)
|
||||
if public != "app.example.test" || internal != "app.anchor.internal" {
|
||||
t.Fatalf("names are %q and %q, want both composed as before", public, internal)
|
||||
}
|
||||
}
|
||||
|
||||
// An internal endpoint has an internal name and no public one — so the proxy serves it inside, and
|
||||
// the public authority is never asked for a name nobody wanted. This is what "must not be public"
|
||||
// could not say before.
|
||||
func TestAnInternalEndpointHasNoPublicName(t *testing.T) {
|
||||
public, internal := namesFor(t, aRoutedWeb(), reachSet(ReachInternal))
|
||||
if public != "" {
|
||||
t.Fatalf("an internal endpoint composed the public name %q", public)
|
||||
}
|
||||
if internal != "app.anchor.internal" {
|
||||
t.Fatalf("internal name is %q, want app.anchor.internal", internal)
|
||||
}
|
||||
}
|
||||
|
||||
// And the mirror: a public endpoint gets the public name and not the internal one, so the mesh's own
|
||||
// authority is not asked to certify a name the service is not reached by.
|
||||
func TestAPublicEndpointHasNoInternalName(t *testing.T) {
|
||||
public, internal := namesFor(t, aRoutedWeb(), reachSet(ReachPublic))
|
||||
if internal != "" {
|
||||
t.Fatalf("a public endpoint composed the internal name %q", internal)
|
||||
}
|
||||
if public != "app.example.test" {
|
||||
t.Fatalf("public name is %q, want app.example.test", public)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBothComposesBothNames(t *testing.T) {
|
||||
public, internal := namesFor(t, aRoutedWeb(), reachSet(ReachBoth))
|
||||
if public == "" || internal == "" {
|
||||
t.Fatalf("both should compose both names, got %q and %q", public, internal)
|
||||
}
|
||||
}
|
||||
|
||||
// **The filter reads the same value.** One statement, and the rule it produces is the one the reach
|
||||
// means — which is the whole claim of ADR 0138 and the reason reach is not two settings.
|
||||
func TestTheFilterFollowsTheSameReach(t *testing.T) {
|
||||
for _, c := range []struct{ reach, want string }{
|
||||
{ReachInternal, FromMesh},
|
||||
{ReachPublic, FromEverywhere},
|
||||
{ReachBoth, FromEverywhere},
|
||||
{ReachMachine, FromMachine},
|
||||
} {
|
||||
r := Resolution{Node: "anchor", Modules: []Manifest{aRoutedWeb()}}
|
||||
rules, err := r.Rules(Rendering{Settings: reachSet(c.reach)})
|
||||
if err != nil {
|
||||
t.Fatalf("%s: rules: %v", c.reach, err)
|
||||
}
|
||||
found := false
|
||||
for _, rule := range rules {
|
||||
if rule.Port == 3000 {
|
||||
found = true
|
||||
if rule.From != c.want {
|
||||
t.Fatalf("reach %q made the filter say %q, want %q", c.reach, rule.From, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("reach %q produced no rule for the port", c.reach)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A reach for a port the module does not listen on reaches nothing, and is refused where it is
|
||||
// written rather than accepted and ignored.
|
||||
func TestAReachForAPortTheModuleDoesNotListenOnIsRefused(t *testing.T) {
|
||||
_, err := Reaches(aRoutedWeb(), []Layer{{From: "node anchor",
|
||||
Values: map[string]any{ReachSetting: map[string]any{"9999": ReachInternal}}}})
|
||||
if err == nil || !strings.Contains(err.Error(), "reaches nothing") {
|
||||
t.Fatalf("a reach naming an undeclared port was accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A value that is not a reach is refused, and the refusal names the four so a reader is one edit from
|
||||
// right. "mesh" is the tempting wrong answer, because that is the filter's word for nearly the same
|
||||
// thing.
|
||||
func TestAValueThatIsNotAReachIsRefused(t *testing.T) {
|
||||
for _, wrong := range []string{"mesh", "anywhere", "private", "true"} {
|
||||
_, err := Reaches(aRoutedWeb(), []Layer{{From: "node anchor",
|
||||
Values: map[string]any{ReachSetting: map[string]any{"3000": wrong}}}})
|
||||
if err == nil || !strings.Contains(err.Error(), "a reach is") {
|
||||
t.Fatalf("%q was accepted as a reach: %v", wrong, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **A port that says both reach and expose is refused.** They say the same thing in different words,
|
||||
// and accepting both would have the filter follow one while the names followed the other — the
|
||||
// disagreement ADR 0138 exists to remove, reintroduced by the migration away from the older word.
|
||||
func TestReachAndExposeForOnePortAreRefused(t *testing.T) {
|
||||
_, err := Reaches(aRoutedWeb(), []Layer{{From: "node anchor", Values: map[string]any{
|
||||
ReachSetting: map[string]any{"3000": ReachInternal},
|
||||
ExposeSetting: map[string]any{"3000": FromEverywhere},
|
||||
}}})
|
||||
if err == nil || !strings.Contains(err.Error(), "same thing in different") {
|
||||
t.Fatalf("a port set both ways was accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A path-level refusal carries no port: it is a rule about a name, not an endpoint, and it inherits
|
||||
// whatever that name turned out to be. Narrowing the endpoint must not silently drop it.
|
||||
func TestARuleWithNoPortIsLeftAlone(t *testing.T) {
|
||||
m := aRoutedWeb()
|
||||
m.ContributesMany = map[string]map[string]map[string]any{
|
||||
"route": {"refused": {"label": "app", "path": "/internal", "deny": true}},
|
||||
}
|
||||
r := Resolution{Node: "anchor", Modules: []Manifest{m},
|
||||
PublicDomain: "example.test", At: "anchor.internal"}
|
||||
given, err := r.contributions(reachSet(ReachInternal), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var sawDeny bool
|
||||
for _, c := range given["route"] {
|
||||
if deny, _ := c.Values["deny"].(bool); deny {
|
||||
sawDeny = true
|
||||
// It keeps both, because it named no endpoint to be narrowed by.
|
||||
if c.Values["name"] == nil || c.Values["internal-name"] == nil {
|
||||
t.Fatalf("the path rule lost a name it shadows: %v", c.Values)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sawDeny {
|
||||
t.Fatal("the path rule was dropped")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user